package main

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/certificates/authority"
	"github.com/smallstep/certificates/ca"
	"github.com/smallstep/cli/config"
	"github.com/smallstep/cli/crypto/randutil"
	"github.com/smallstep/cli/jose"
	"github.com/smallstep/cli/token"
	"github.com/smallstep/cli/token/provision"
)

const (
	tokenLifetime = 5 * time.Minute
)

// Provisioner is an authorized entity that can sign tokens necessary for
// signature requests.
type Provisioner interface {
	Name() string
	Kid() string
	Token(subject string) (string, error)
}

type provisioner struct {
	name          string
	kid           string
	caURL         string
	caRoot        string
	jwk           *jose.JSONWebKey
	tokenLifetime time.Duration
}

// Name returns the provisioner's name.
func (p *provisioner) Name() string {
	return p.name
}

// Kid returns the provisioners key ID.
func (p *provisioner) Kid() string {
	return p.kid
}

// Token generates a bootstrap token for a subject.
func (p *provisioner) Token(subject string) (string, error) {
	// A random jwt id will be used to identify duplicated tokens
	jwtID, err := randutil.Hex(64) // 256 bits
	if err != nil {
		return "", err
	}

	notBefore := time.Now()
	notAfter := notBefore.Add(tokenLifetime)
	signURL := fmt.Sprintf("%v/1.0/sign", p.caURL)

	tokOptions := []token.Options{
		token.WithJWTID(jwtID),
		token.WithKid(p.kid),
		token.WithIssuer(p.name),
		token.WithAudience(signURL),
		token.WithValidity(notBefore, notAfter),
		token.WithRootCA(p.caRoot),
	}

	tok, err := provision.New(subject, tokOptions...)
	if err != nil {
		return "", err
	}

	return tok.SignedString(p.jwk.Algorithm, p.jwk.Key)
}

func decryptProvisionerJWK(encryptedKey, passFile string) (*jose.JSONWebKey, error) {
	decrypted, err := jose.Decrypt("", []byte(encryptedKey), jose.WithPasswordFile(passFile))
	if err != nil {
		return nil, err
	}

	jwk := new(jose.JSONWebKey)
	if err := json.Unmarshal(decrypted, jwk); err != nil {
		return nil, errors.Wrap(err, "error unmarshalling provisioning key")
	}
	return jwk, nil
}

// loadProvisionerJWKByKid retrieves a provisioner key from the CA by key ID and
// decrypts it using the specified password file.
func loadProvisionerJWKByKid(kid, caURL, caRoot, passFile string) (*jose.JSONWebKey, error) {
	encrypted, err := getProvisionerKey(caURL, caRoot, kid)
	if err != nil {
		return nil, err
	}

	return decryptProvisionerJWK(encrypted, passFile)
}

// loadProvisionerJWKByName retrieves the list of provisioners and encrypted key then
// returns the key of the first provisioner with a matching name that can be successfully
// decrypted with the specified password file.
func loadProvisionerJWKByName(name, caURL, caRoot, passFile string) (key *jose.JSONWebKey, err error) {
	provisioners, err := getProvisioners(caURL, caRoot)
	if err != nil {
		err = errors.Wrap(err, "error getting the provisioners")
		return
	}

	for _, provisioner := range provisioners {
		if provisioner.Name == name {
			key, err = decryptProvisionerJWK(provisioner.EncryptedKey, passFile)
			if err == nil {
				return
			}
		}
	}
	return nil, errors.Errorf("provisioner '%s' not found (or your password is wrong)", name)
}

// NewProvisioner loads and decrypts key material from the CA for the named
// provisioner. The key identified by `kid` will be used if specified. If `kid`
// is the empty string we'll use the first key for the named provisioner that
// decrypts using `passFile`.
func NewProvisioner(name, kid, caURL, caRoot, passFile string) (Provisioner, error) {
	var jwk *jose.JSONWebKey
	var err error
	if kid != "" {
		jwk, err = loadProvisionerJWKByKid(kid, caURL, caRoot, passFile)
	} else {
		jwk, err = loadProvisionerJWKByName(name, caURL, caRoot, passFile)
	}
	if err != nil {
		return nil, err
	}

	return &provisioner{
		name:          name,
		kid:           jwk.KeyID,
		caURL:         caURL,
		caRoot:        caRoot,
		jwk:           jwk,
		tokenLifetime: tokenLifetime,
	}, nil
}

// getRootCAPath returns the path where the root CA is stored based on the
// STEPPATH environment variable.
func getRootCAPath() string {
	return filepath.Join(config.StepPath(), "certs", "root_ca.crt")
}

// getProvisioners returns the map of provisioners on the given CA.
func getProvisioners(caURL, rootFile string) ([]*authority.Provisioner, error) {
	if len(rootFile) == 0 {
		rootFile = getRootCAPath()
	}
	client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))
	if err != nil {
		return nil, err
	}
	cursor := ""
	provisioners := []*authority.Provisioner{}
	for {
		resp, err := client.Provisioners(ca.WithProvisionerCursor(cursor), ca.WithProvisionerLimit(100))
		if err != nil {
			return nil, err
		}
		provisioners = append(provisioners, resp.Provisioners...)
		if resp.NextCursor == "" {
			return provisioners, nil
		}
		cursor = resp.NextCursor
	}
}

// getProvisionerKey returns the encrypted provisioner key with the for the
// given kid.
func getProvisionerKey(caURL, rootFile, kid string) (string, error) {
	if len(rootFile) == 0 {
		rootFile = getRootCAPath()
	}
	client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))
	if err != nil {
		return "", err
	}
	resp, err := client.ProvisionerKey(kid)
	if err != nil {
		return "", err
	}
	return resp.Key, nil
}
