package policy

import (
	"crypto/x509"
	"errors"
	"fmt"

	"golang.org/x/crypto/ssh"
)

// Engine is a container for multiple policies.
type Engine struct {
	x509Policy    X509Policy
	sshUserPolicy UserPolicy
	sshHostPolicy HostPolicy
}

// New returns a new Engine using Options.
func New(options *Options) (*Engine, error) {
	// if no options provided, return early
	if options == nil {
		//nolint:nilnil // legacy
		return nil, nil
	}

	var (
		x509Policy    X509Policy
		sshHostPolicy HostPolicy
		sshUserPolicy UserPolicy
		err           error
	)

	// initialize the x509 allow/deny policy engine
	if x509Policy, err = NewX509PolicyEngine(options.GetX509Options()); err != nil {
		return nil, err
	}

	// initialize the SSH allow/deny policy engine for host certificates
	if sshHostPolicy, err = NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {
		return nil, err
	}

	// initialize the SSH allow/deny policy engine for user certificates
	if sshUserPolicy, err = NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {
		return nil, err
	}

	return &Engine{
		x509Policy:    x509Policy,
		sshHostPolicy: sshHostPolicy,
		sshUserPolicy: sshUserPolicy,
	}, nil
}

// IsX509CertificateAllowed evaluates an X.509 certificate against
// the X.509 policy (if available) and returns an error if one of the
// names in the certificate is not allowed.
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
	// return early if there's no policy to evaluate
	if e == nil || e.x509Policy == nil {
		return nil
	}

	// return result of X.509 policy evaluation
	return e.x509Policy.IsX509CertificateAllowed(cert)
}

// AreSANsAllowed evaluates the slice of SANs against the X.509 policy
// (if available) and returns an error if one of the SANs is not allowed.
func (e *Engine) AreSANsAllowed(sans []string) error {
	// return early if there's no policy to evaluate
	if e == nil || e.x509Policy == nil {
		return nil
	}

	// return result of X.509 policy evaluation
	return e.x509Policy.AreSANsAllowed(sans)
}

// IsSSHCertificateAllowed evaluates an SSH certificate against the
// user or host policy (if configured) and returns an error if one of the
// principals in the certificate is not allowed.
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
	// return early if there's no policy to evaluate
	if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
		return nil
	}

	switch cert.CertType {
	case ssh.HostCert:
		// when no host policy engine is configured, but a user policy engine is
		// configured, the host certificate is denied.
		if e.sshHostPolicy == nil && e.sshUserPolicy != nil {
			return errors.New("authority not allowed to sign SSH host certificates when SSH user certificate policy is active")
		}

		// return result of SSH host policy evaluation
		return e.sshHostPolicy.IsSSHCertificateAllowed(cert)
	case ssh.UserCert:
		// 	when no user policy engine is configured, but a host policy engine is
		// 	configured, the user certificate is denied.
		if e.sshUserPolicy == nil && e.sshHostPolicy != nil {
			return errors.New("authority not allowed to sign SSH user certificates when SSH host certificate policy is active")
		}

		// return result of SSH user policy evaluation
		return e.sshUserPolicy.IsSSHCertificateAllowed(cert)
	default:
		return fmt.Errorf("unexpected SSH certificate type %q", cert.CertType)
	}
}
