package softkms

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/rsa"
	"crypto/x509"
	"fmt"
	"os"
	"syscall"

	"github.com/pkg/errors"

	"go.step.sm/crypto/keyutil"
	"go.step.sm/crypto/kms/apiv1"
	"go.step.sm/crypto/kms/uri"
	"go.step.sm/crypto/pemutil"
	"go.step.sm/crypto/x25519"
)

type algorithmAttributes struct {
	Type  string
	Curve string
}

// Scheme is the scheme used in uris, the string "softkms".
const Scheme = string(apiv1.SoftKMS)

// DefaultRSAKeySize is the default size for RSA keys.
const DefaultRSAKeySize = 3072

var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{
	apiv1.UnspecifiedSignAlgorithm: {"EC", "P-256"},
	apiv1.SHA256WithRSA:            {"RSA", ""},
	apiv1.SHA384WithRSA:            {"RSA", ""},
	apiv1.SHA512WithRSA:            {"RSA", ""},
	apiv1.SHA256WithRSAPSS:         {"RSA", ""},
	apiv1.SHA384WithRSAPSS:         {"RSA", ""},
	apiv1.SHA512WithRSAPSS:         {"RSA", ""},
	apiv1.ECDSAWithSHA256:          {"EC", "P-256"},
	apiv1.ECDSAWithSHA384:          {"EC", "P-384"},
	apiv1.ECDSAWithSHA512:          {"EC", "P-521"},
	apiv1.PureEd25519:              {"OKP", "Ed25519"},
}

// generateKey is used for testing purposes.
var generateKey = func(kty, crv string, size int) (interface{}, interface{}, error) {
	if kty == "RSA" && size == 0 {
		size = DefaultRSAKeySize
	}
	return keyutil.GenerateKeyPair(kty, crv, size)
}

// SoftKMS is a key manager that uses keys stored in disk.
type SoftKMS struct{}

// New returns a new SoftKMS.
func New(_ context.Context, _ apiv1.Options) (*SoftKMS, error) {
	return &SoftKMS{}, nil
}

func init() {
	apiv1.Register(apiv1.SoftKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
		return New(ctx, opts)
	})
}

// Close is a noop that just returns nil.
func (k *SoftKMS) Close() error {
	return nil
}

// CreateSigner returns a new signer configured with the given signing key.
func (k *SoftKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
	var opts []pemutil.Options
	if req.Password != nil {
		opts = append(opts, pemutil.WithPassword(req.Password))
	} else if req.PasswordPrompter != nil {
		opts = append(opts, pemutil.WithPasswordPrompt("Please enter the password to decrypt the signing key", pemutil.PasswordPrompter(req.PasswordPrompter)))
	}

	switch {
	case req.Signer != nil:
		return req.Signer, nil
	case len(req.SigningKeyPEM) != 0:
		v, err := pemutil.ParseKey(req.SigningKeyPEM, opts...)
		if err != nil {
			return nil, err
		}
		sig, ok := v.(crypto.Signer)
		if !ok {
			return nil, errors.New("signingKeyPEM is not a crypto.Signer")
		}
		return sig, nil
	case req.SigningKey != "":
		v, err := pemutil.Read(filename(req.SigningKey), opts...)
		if err != nil {
			return nil, toKMSError(err, "key not found")
		}
		sig, ok := v.(crypto.Signer)
		if !ok {
			return nil, errors.New("signingKey is not a crypto.Signer")
		}
		return sig, nil
	default:
		return nil, errors.New("failed to load softKMS: please define signingKeyPEM or signingKey")
	}
}

// CreateKey generates a new key using Golang crypto and returns both public and
// private key.
func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
	v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm]
	if !ok {
		return nil, errors.Errorf("softKMS does not support signature algorithm '%s'", req.SignatureAlgorithm)
	}

	pub, priv, err := generateKey(v.Type, v.Curve, req.Bits)
	if err != nil {
		return nil, err
	}
	signer, ok := priv.(crypto.Signer)
	if !ok {
		return nil, errors.Errorf("softKMS createKey result is not a crypto.Signer: type %T", priv)
	}

	name := filename(req.Name)
	return &apiv1.CreateKeyResponse{
		Name:       name,
		PublicKey:  pub,
		PrivateKey: priv,
		CreateSignerRequest: apiv1.CreateSignerRequest{
			Signer:     signer,
			SigningKey: name,
		},
	}, nil
}

// GetPublicKey returns the public key from the file passed in the request name.
func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
	v, err := pemutil.Read(filename(req.Name))
	if err != nil {
		return nil, toKMSError(err, "key not found")
	}

	switch vv := v.(type) {
	case *x509.Certificate:
		return vv.PublicKey, nil
	case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, x25519.PublicKey:
		return vv, nil
	case crypto.Signer:
		return vv.Public(), nil
	default:
		return nil, errors.Errorf("unsupported public key type %T", v)
	}
}

// CreateDecrypter creates a new crypto.Decrypter backed by disk/software
func (k *SoftKMS) CreateDecrypter(req *apiv1.CreateDecrypterRequest) (crypto.Decrypter, error) {
	var opts []pemutil.Options
	if req.Password != nil {
		opts = append(opts, pemutil.WithPassword(req.Password))
	} else if req.PasswordPrompter != nil {
		opts = append(opts, pemutil.WithPasswordPrompt("Please enter the password to decrypt the decryption key", pemutil.PasswordPrompter(req.PasswordPrompter)))
	}

	switch {
	case req.Decrypter != nil:
		return req.Decrypter, nil
	case len(req.DecryptionKeyPEM) != 0:
		v, err := pemutil.ParseKey(req.DecryptionKeyPEM, opts...)
		if err != nil {
			return nil, err
		}
		decrypter, ok := v.(crypto.Decrypter)
		if !ok {
			return nil, errors.New("decryptorKeyPEM is not a crypto.Decrypter")
		}
		return decrypter, nil
	case req.DecryptionKey != "":
		v, err := pemutil.Read(filename(req.DecryptionKey), opts...)
		if err != nil {
			return nil, toKMSError(err, "key not found")
		}
		decrypter, ok := v.(crypto.Decrypter)
		if !ok {
			return nil, errors.New("decryptionKey is not a crypto.Decrypter")
		}
		return decrypter, nil
	default:
		return nil, errors.New("failed to load softKMS: please define decryptionKeyPEM or decryptionKey")
	}
}

// LoadCertificate returns a x509.Certificate from the file passed in the
// request name.
func (k *SoftKMS) LoadCertificate(req *apiv1.LoadCertificateRequest) (*x509.Certificate, error) {
	if req.Name == "" {
		return nil, fmt.Errorf("loadCertificateRequest 'name' cannot be empty")
	}

	bundle, err := pemutil.ReadCertificateBundle(filename(req.Name))
	if err != nil {
		return nil, toKMSError(err, "certificate not found")
	}

	return bundle[0], nil
}

// LoadCertificateChain returns a slice of x509.Certificate from the file passed
// in the request name.
func (k *SoftKMS) LoadCertificateChain(req *apiv1.LoadCertificateChainRequest) ([]*x509.Certificate, error) {
	if req.Name == "" {
		return nil, fmt.Errorf("loadCertificateChainRequest 'name' cannot be empty")
	}

	chain, err := pemutil.ReadCertificateBundle(filename(req.Name))
	if err != nil {
		return nil, toKMSError(err, "certificate not found")
	}

	return chain, nil
}

func filename(s string) string {
	if u, err := uri.ParseWithScheme(Scheme, s); err == nil {
		if f := u.Get("path"); f != "" {
			return f
		}
		switch {
		case u.Path != "":
			return u.Path
		default:
			return u.Opaque
		}
	}
	return s
}

func toKMSError(err error, message string) error {
	switch {
	case errors.Is(err, os.ErrNotExist), errors.Is(err, syscall.ENOENT):
		return apiv1.NotFoundError{
			Message: message,
		}
	default:
		return err
	}
}
