package gitsshkey

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"strings"

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

type Algorithm string

const (
	// AlgorithmEd25519 is the Edwards-curve Digital Signature Algorithm using Curve25519
	AlgorithmEd25519 Algorithm = "ed25519"
	// AlgorithmECDSA is the Digital Signature Algorithm (DSA) using NIST Elliptic Curve
	AlgorithmECDSA Algorithm = "ecdsa"
	// AlgorithmRSA4096 is the venerable Rivest-Shamir-Adleman algorithm
	// and creates a key with a fixed size of 4096-bit.
	AlgorithmRSA4096 Algorithm = "rsa4096"
)

// ParseAlgorithm returns a valid Algorithm or error if input is not a valid.
func ParseAlgorithm(t string) (Algorithm, error) {
	ok := []string{
		string(AlgorithmEd25519),
		string(AlgorithmECDSA),
		string(AlgorithmRSA4096),
	}

	for _, a := range ok {
		if strings.EqualFold(a, t) {
			return Algorithm(a), nil
		}
	}

	return "", xerrors.Errorf(`invalid key type: %s, must be one of: %s`, t, strings.Join(ok, ","))
}

// Generate creates a private key in the OpenSSH PEM format and public key in
// the authorized key format.
func Generate(algo Algorithm) (privateKey string, publicKey string, err error) {
	switch algo {
	case AlgorithmEd25519:
		return ed25519KeyGen()
	case AlgorithmECDSA:
		return ecdsaKeyGen()
	case AlgorithmRSA4096:
		return rsa4096KeyGen()
	default:
		return "", "", xerrors.Errorf("invalid algorithm: %s", algo)
	}
}

// ed25519KeyGen returns an ED25519-based SSH private key.
func ed25519KeyGen() (privateKey string, publicKey string, err error) {
	_, privateKeyRaw, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return "", "", xerrors.Errorf("generate ed25519 private key: %w", err)
	}

	// NOTE: as of the time of writing, x/crypto/ssh is unable to marshal an ED25519 private key
	// into the format expected by OpenSSH. See: https://github.com/golang/go/issues/37132
	// Until this support is added, using a third-party implementation.
	byt, err := MarshalED25519PrivateKey(privateKeyRaw)
	if err != nil {
		return "", "", xerrors.Errorf("marshal ed25519 private key: %w", err)
	}

	return generateKeys(pem.Block{
		Type:  "OPENSSH PRIVATE KEY",
		Bytes: byt,
	}, privateKeyRaw)
}

// ecdsaKeyGen returns an ECDSA-based SSH private key.
func ecdsaKeyGen() (privateKey string, publicKey string, err error) {
	privateKeyRaw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return "", "", xerrors.Errorf("generate ecdsa private key: %w", err)
	}
	byt, err := x509.MarshalECPrivateKey(privateKeyRaw)
	if err != nil {
		return "", "", xerrors.Errorf("marshal private key: %w", err)
	}

	return generateKeys(pem.Block{
		Type:  "EC PRIVATE KEY",
		Bytes: byt,
	}, privateKeyRaw)
}

// rsaKeyGen returns an RSA-based SSH private key of size 4096.
//
// Administrators may configure this for SSH key compatibility with Azure DevOps.
func rsa4096KeyGen() (privateKey string, publicKey string, err error) {
	privateKeyRaw, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		return "", "", xerrors.Errorf("generate RSA4096 private key: %w", err)
	}

	return generateKeys(pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privateKeyRaw),
	}, privateKeyRaw)
}

func generateKeys(block pem.Block, cp crypto.Signer) (privateKey string, publicKey string, err error) {
	pkBytes := pem.EncodeToMemory(&block)
	privateKey = string(pkBytes)

	publicKeyRaw := cp.Public()
	p, err := ssh.NewPublicKey(publicKeyRaw)
	if err != nil {
		return "", "", err
	}
	publicKey = string(ssh.MarshalAuthorizedKey(p))

	return privateKey, publicKey, nil
}
