package authority

import (
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/cli/crypto/x509util"

	jose "gopkg.in/square/go-jose.v2"
)

// ProvisionerClaims so that individual provisioners can override global claims.
type ProvisionerClaims struct {
	globalClaims   *ProvisionerClaims
	MinTLSDur      *Duration `json:"minTLSCertDuration,omitempty"`
	MaxTLSDur      *Duration `json:"maxTLSCertDuration,omitempty"`
	DefaultTLSDur  *Duration `json:"defaultTLSCertDuration,omitempty"`
	DisableRenewal *bool     `json:"disableRenewal,omitempty"`
}

// Init initializes and validates the individual provisioner claims.
func (pc *ProvisionerClaims) Init(global *ProvisionerClaims) (*ProvisionerClaims, error) {
	if pc == nil {
		pc = &ProvisionerClaims{}
	}
	pc.globalClaims = global
	err := pc.Validate()
	return pc, err
}

// DefaultTLSCertDuration returns the default TLS cert duration for the
// provisioner. If the default is not set within the provisioner, then the global
// default from the authority configuration will be used.
func (pc *ProvisionerClaims) DefaultTLSCertDuration() time.Duration {
	if pc.DefaultTLSDur == nil || pc.DefaultTLSDur.Duration == 0 {
		return pc.globalClaims.DefaultTLSCertDuration()
	}
	return pc.DefaultTLSDur.Duration
}

// MinTLSCertDuration returns the minimum TLS cert duration for the provisioner.
// If the minimum is not set within the provisioner, then the global
// minimum from the authority configuration will be used.
func (pc *ProvisionerClaims) MinTLSCertDuration() time.Duration {
	if pc.MinTLSDur == nil || pc.MinTLSDur.Duration == 0 {
		return pc.globalClaims.MinTLSCertDuration()
	}
	return pc.MinTLSDur.Duration
}

// MaxTLSCertDuration returns the maximum TLS cert duration for the provisioner.
// If the maximum is not set within the provisioner, then the global
// maximum from the authority configuration will be used.
func (pc *ProvisionerClaims) MaxTLSCertDuration() time.Duration {
	if pc.MaxTLSDur == nil || pc.MaxTLSDur.Duration == 0 {
		return pc.globalClaims.MaxTLSCertDuration()
	}
	return pc.MaxTLSDur.Duration
}

// IsDisableRenewal returns if the renewal flow is disabled for the
// provisioner. If the property is not set within the provisioner, then the
// global value from the authority configuration will be used.
func (pc *ProvisionerClaims) IsDisableRenewal() bool {
	if pc.DisableRenewal == nil {
		return pc.globalClaims.IsDisableRenewal()
	}
	return *pc.DisableRenewal
}

// Validate validates and modifies the Claims with default values.
func (pc *ProvisionerClaims) Validate() error {
	var (
		min = pc.MinTLSCertDuration()
		max = pc.MaxTLSCertDuration()
		def = pc.DefaultTLSCertDuration()
	)
	switch {
	case min == 0:
		return errors.Errorf("claims: MinTLSCertDuration cannot be empty")
	case max == 0:
		return errors.Errorf("claims: MaxTLSCertDuration cannot be empty")
	case def == 0:
		return errors.Errorf("claims: DefaultTLSCertDuration cannot be empty")
	case max < min:
		return errors.Errorf("claims: MaxCertDuration cannot be less "+
			"than MinCertDuration: MaxCertDuration - %v, MinCertDuration - %v", max, min)
	case def < min:
		return errors.Errorf("claims: DefaultCertDuration cannot be less than MinCertDuration: DefaultCertDuration - %v, MinCertDuration - %v", def, min)
	case max < def:
		return errors.Errorf("claims: MaxCertDuration cannot be less than DefaultCertDuration: MaxCertDuration - %v, DefaultCertDuration - %v", max, def)
	default:
		return nil
	}
}

// Provisioner - authorized entity that can sign tokens necessary for signature requests.
type Provisioner struct {
	Name         string             `json:"name,omitempty"`
	Type         string             `json:"type,omitempty"`
	Key          *jose.JSONWebKey   `json:"key,omitempty"`
	EncryptedKey string             `json:"encryptedKey,omitempty"`
	Claims       *ProvisionerClaims `json:"claims,omitempty"`
}

// Init initializes and validates a the fields of Provisioner type.
func (p *Provisioner) Init(global *ProvisionerClaims) error {
	switch {
	case p.Name == "":
		return errors.New("provisioner name cannot be empty")

	case p.Type == "":
		return errors.New("provisioner type cannot be empty")

	case p.Key == nil:
		return errors.New("provisioner key cannot be empty")
	}

	var err error
	p.Claims, err = p.Claims.Init(global)
	return err
}

// getTLSApps returns a list of modifiers and validators that will be applied to
// the certificate.
func (p *Provisioner) getTLSApps(so SignOptions) ([]x509util.WithOption, []certClaim, error) {
	c := p.Claims
	return []x509util.WithOption{
			x509util.WithNotBeforeAfterDuration(so.NotBefore,
				so.NotAfter, c.DefaultTLSCertDuration()),
			withProvisionerOID(p.Name, p.Key.KeyID),
		}, []certClaim{
			&certTemporalClaim{
				min: c.MinTLSCertDuration(),
				max: c.MaxTLSCertDuration(),
			},
		}, nil
}

// ID returns the provisioner identifier. The name and credential id should
// uniquely identify any provisioner.
func (p *Provisioner) ID() string {
	return p.Name + ":" + p.Key.KeyID
}
