package identity

import (
	"bytes"
	"crypto"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/certificates/api"
	"go.step.sm/cli-utils/step"
	"go.step.sm/crypto/pemutil"
)

// Type represents the different types of identity files.
type Type string

// Disabled represents a disabled identity type
const Disabled Type = ""

// MutualTLS represents the identity using mTLS.
const MutualTLS Type = "mTLS"

// TunnelTLS represents an identity using a (m)TLS tunnel.
//
// TunnelTLS can be optionally configured with client certificates and a root
// file with the CAs to trust. By default it will use the system truststore
// instead of the CA truststore.
const TunnelTLS Type = "tTLS"

// DefaultLeeway is the duration for matching not before claims.
const DefaultLeeway = 1 * time.Minute

var (
	identityDir = step.IdentityPath
	configDir   = step.ConfigPath

	// IdentityFile contains a pointer to a function that outputs the location of
	// the identity file.
	IdentityFile = step.IdentityFile

	// DefaultsFile contains a prointer a function that outputs the location of the
	// defaults configuration file.
	DefaultsFile = step.DefaultsFile
)

// Identity represents the identity file that can be used to authenticate with
// the CA.
type Identity struct {
	Type        string `json:"type"`
	Certificate string `json:"crt"`
	Key         string `json:"key"`

	// Host is the tunnel host for a TunnelTLS (tTLS) identity.
	Host string `json:"host,omitempty"`
	// Root is the CA bundle of root CAs used in TunnelTLS to trust the
	// certificate of the host.
	Root string `json:"root,omitempty"`
}

// LoadIdentity loads an identity present in the given filename.
func LoadIdentity(filename string) (*Identity, error) {
	b, err := os.ReadFile(filename)
	if err != nil {
		return nil, errors.Wrapf(err, "error reading %s", filename)
	}
	identity := new(Identity)
	if err := json.Unmarshal(b, &identity); err != nil {
		return nil, errors.Wrapf(err, "error unmarshaling %s", filename)
	}
	return identity, nil
}

// LoadDefaultIdentity loads the default identity.
func LoadDefaultIdentity() (*Identity, error) {
	return LoadIdentity(IdentityFile())
}

// WriteDefaultIdentity writes the given certificates and key and the
// identity.json pointing to the new files.
func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error {
	if err := os.MkdirAll(configDir(), 0700); err != nil {
		return errors.Wrap(err, "error creating config directory")
	}

	identityDir := identityDir()
	if err := os.MkdirAll(identityDir, 0700); err != nil {
		return errors.Wrap(err, "error creating identity directory")
	}

	certFilename := filepath.Join(identityDir, "identity.crt")
	keyFilename := filepath.Join(identityDir, "identity_key")

	// Write certificate
	if err := writeCertificate(certFilename, certChain); err != nil {
		return err
	}

	// Write key
	buf := new(bytes.Buffer)
	block, err := pemutil.Serialize(key)
	if err != nil {
		return err
	}
	if err := pem.Encode(buf, block); err != nil {
		return errors.Wrap(err, "error encoding identity key")
	}
	if err := os.WriteFile(keyFilename, buf.Bytes(), 0600); err != nil {
		return errors.Wrap(err, "error writing identity certificate")
	}

	// Write identity.json
	buf.Reset()
	enc := json.NewEncoder(buf)
	enc.SetIndent("", "   ")
	if err := enc.Encode(Identity{
		Type:        string(MutualTLS),
		Certificate: certFilename,
		Key:         keyFilename,
	}); err != nil {
		return errors.Wrap(err, "error writing identity json")
	}
	if err := os.WriteFile(IdentityFile(), buf.Bytes(), 0600); err != nil {
		return errors.Wrap(err, "error writing identity certificate")
	}

	return nil
}

// WriteIdentityCertificate writes the identity certificate to disk.
func WriteIdentityCertificate(certChain []api.Certificate) error {
	filename := filepath.Join(identityDir(), "identity.crt")
	return writeCertificate(filename, certChain)
}

// writeCertificate writes the given certificate on disk.
func writeCertificate(filename string, certChain []api.Certificate) error {
	buf := new(bytes.Buffer)
	for _, crt := range certChain {
		block := &pem.Block{
			Type:  "CERTIFICATE",
			Bytes: crt.Raw,
		}
		if err := pem.Encode(buf, block); err != nil {
			return errors.Wrap(err, "error encoding certificate")
		}
	}

	if err := os.WriteFile(filename, buf.Bytes(), 0600); err != nil {
		return errors.Wrap(err, "error writing certificate")
	}

	return nil
}

// Kind returns the type for the given identity.
func (i *Identity) Kind() Type {
	switch strings.ToLower(i.Type) {
	case "":
		return Disabled
	case "mtls":
		return MutualTLS
	case "ttls":
		return TunnelTLS
	default:
		return Type(i.Type)
	}
}

// Validate validates the identity object.
func (i *Identity) Validate() error {
	switch i.Kind() {
	case Disabled:
		return nil
	case MutualTLS:
		if i.Certificate == "" {
			return errors.New("identity.crt cannot be empty")
		}
		if i.Key == "" {
			return errors.New("identity.key cannot be empty")
		}
		if err := fileExists(i.Certificate); err != nil {
			return err
		}
		return fileExists(i.Key)
	case TunnelTLS:
		if i.Host == "" {
			return errors.New("tunnel.host cannot be empty")
		}
		if i.Certificate != "" {
			if err := fileExists(i.Certificate); err != nil {
				return err
			}
			if i.Key == "" {
				return errors.New("tunnel.key cannot be empty")
			}
			if err := fileExists(i.Key); err != nil {
				return err
			}
		}
		if i.Root != "" {
			if err := fileExists(i.Root); err != nil {
				return err
			}
		}
		return nil
	default:
		return errors.Errorf("unsupported identity type %s", i.Type)
	}
}

// TLSCertificate returns a tls.Certificate for the identity.
func (i *Identity) TLSCertificate() (tls.Certificate, error) {
	fail := func(err error) (tls.Certificate, error) { return tls.Certificate{}, err }
	switch i.Kind() {
	case Disabled:
		return tls.Certificate{}, nil
	case MutualTLS, TunnelTLS:
		crt, err := tls.LoadX509KeyPair(i.Certificate, i.Key)
		if err != nil {
			return fail(errors.Wrap(err, "error creating identity certificate"))
		}

		// Check if certificate is expired.
		x509Cert, err := x509.ParseCertificate(crt.Certificate[0])
		if err != nil {
			return fail(errors.Wrap(err, "error creating identity certificate"))
		}
		now := time.Now().Truncate(time.Second)
		if now.Add(DefaultLeeway).Before(x509Cert.NotBefore) {
			return fail(errors.New("certificate is not yet valid"))
		}
		if now.After(x509Cert.NotAfter) {
			return fail(errors.New("certificate is already expired"))
		}
		return crt, nil
	default:
		return fail(errors.Errorf("unsupported identity type %s", i.Type))
	}
}

// GetClientCertificateFunc returns a method that can be used as the
// GetClientCertificate property in a tls.Config.
func (i *Identity) GetClientCertificateFunc() func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
	return func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
		crt, err := tls.LoadX509KeyPair(i.Certificate, i.Key)
		if err != nil {
			return nil, errors.Wrap(err, "error loading identity certificate")
		}
		return &crt, nil
	}
}

// GetCertPool returns a x509.CertPool if the identity defines a custom root.
func (i *Identity) GetCertPool() (*x509.CertPool, error) {
	if i.Root == "" {
		return nil, nil
	}
	b, err := os.ReadFile(i.Root)
	if err != nil {
		return nil, errors.Wrap(err, "error reading identity root")
	}
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(b) {
		return nil, errors.Errorf("error pasing identity root: %s does not contain any certificate", i.Root)
	}
	return pool, nil
}

// Renewer is that interface that a renew client must implement.
type Renewer interface {
	GetRootCAs() *x509.CertPool
	Renew(tr http.RoundTripper) (*api.SignResponse, error)
}

// Renew renews the current identity certificate using a client with a renew
// method.
func (i *Identity) Renew(client Renewer) error {
	switch i.Kind() {
	case Disabled:
		return nil
	case MutualTLS, TunnelTLS:
		cert, err := i.TLSCertificate()
		if err != nil {
			return err
		}

		tr := http.DefaultTransport.(*http.Transport).Clone()
		tr.TLSClientConfig = &tls.Config{
			Certificates:             []tls.Certificate{cert},
			RootCAs:                  client.GetRootCAs(),
			PreferServerCipherSuites: true,
		}

		sign, err := client.Renew(tr)
		if err != nil {
			return err
		}

		if sign.CertChainPEM == nil || len(sign.CertChainPEM) == 0 {
			sign.CertChainPEM = []api.Certificate{sign.ServerPEM, sign.CaPEM}
		}

		// Write certificate
		buf := new(bytes.Buffer)
		for _, crt := range sign.CertChainPEM {
			block := &pem.Block{
				Type:  "CERTIFICATE",
				Bytes: crt.Raw,
			}
			if err := pem.Encode(buf, block); err != nil {
				return errors.Wrap(err, "error encoding identity certificate")
			}
		}
		certFilename := filepath.Join(identityDir(), "identity.crt")
		if err := os.WriteFile(certFilename, buf.Bytes(), 0600); err != nil {
			return errors.Wrap(err, "error writing identity certificate")
		}

		return nil
	default:
		return errors.Errorf("unsupported identity type %s", i.Type)
	}
}

func fileExists(filename string) error {
	info, err := os.Stat(filename)
	if err != nil {
		return errors.Wrapf(err, "error reading %s", filename)
	}
	if info.IsDir() {
		return errors.Errorf("error reading %s: file is a directory", filename)
	}
	return nil
}
