package didx509resolver

import (
	"crypto/sha256"
	"crypto/sha512"
	"crypto/x509"
	"encoding/asn1"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"

	"github.com/lestrrat-go/jwx/jwk"
)

func VerifyCertificateChain(chain []*x509.Certificate, trustedRoots []*x509.Certificate, ignoreTime bool) ([][]*x509.Certificate, error) {
	roots := x509.NewCertPool()
	for _, cert := range trustedRoots {
		roots.AddCert(cert)
	}

	intermediates := x509.NewCertPool()

	for _, c := range chain[1 : len(chain)-1] {
		intermediates.AddCert(c)
	}

	opts := x509.VerifyOptions{
		Roots:         roots,
		Intermediates: intermediates,
		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
		CurrentTime:   chain[0].NotAfter, // TODO: Figure out how to disable expiry checks?
	}

	return chain[0].Verify(opts)
}

func checkFingerprint(chain []*x509.Certificate, caFingerprintAlg, caFingerprint string) error {

	expectedCaFingerprints := make(map[string]struct{})
	for _, cert := range chain[1:] {
		var n struct{}
		var hash []byte
		switch caFingerprintAlg {
		case "sha256":
			x := sha256.Sum256(cert.Raw)
			hash = x[:]
		case "sha384":
			x := sha512.Sum384(cert.Raw)
			hash = x[:]
		case "sha512":
			x := sha512.Sum512(cert.Raw)
			hash = x[:]
		default:
			return errors.New("unsupported hash algorithm")
		}
		hashStr := base64.RawURLEncoding.EncodeToString(hash)
		expectedCaFingerprints[hashStr] = n
	}

	if _, found := expectedCaFingerprints[caFingerprint]; !found {
		return errors.New("unexpected certificate fingerprint")
	}

	return nil
}

func oidFromString(s string) (*asn1.ObjectIdentifier, error) {
	tokens := strings.Split(s, ".")
	var ints []int
	for _, x := range tokens {
		i, err := strconv.Atoi(x)
		if err != nil {
			return nil, errors.New("invalid OID")
		}
		ints = append(ints, i)
	}
	result := asn1.ObjectIdentifier(ints)
	return &result, nil
}

func checkHasSan(sanType string, value string, cert *x509.Certificate) error {
	switch sanType {
	case "dns":
		for _, name := range cert.DNSNames {
			if name == value {
				return nil
			}
		}
	case "email":
		for _, email := range cert.EmailAddresses {
			if email == value {
				return nil
			}
		}
	case "ipaddress":
		for _, ip := range cert.IPAddresses {
			if ip.String() == value {
				return nil
			}
		}
	case "uri":
		for _, uri := range cert.URIs {
			if uri.String() == value {
				return nil
			}
		}
	default:
		return fmt.Errorf("unknown SAN type: %s", sanType)
	}
	return fmt.Errorf("SAN not found: %s", value)
}

// The x509 package/module doesn't export these.
// they are derived from https://www.rfc-editor.org/rfc/rfc5280 RFC 5280, 4.2.1.12  Extended Key Usage
// and can never change

var (
	oidExtKeyUsageAny                            = asn1.ObjectIdentifier{2, 5, 29, 37, 0}
	oidExtKeyUsageServerAuth                     = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1}
	oidExtKeyUsageClientAuth                     = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2}
	oidExtKeyUsageCodeSigning                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3}
	oidExtKeyUsageEmailProtection                = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4}
	oidExtKeyUsageIPSECEndSystem                 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}
	oidExtKeyUsageIPSECTunnel                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}
	oidExtKeyUsageIPSECUser                      = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}
	oidExtKeyUsageTimeStamping                   = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8}
	oidExtKeyUsageOCSPSigning                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9}
	oidExtKeyUsageMicrosoftServerGatedCrypto     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}
	oidExtKeyUsageNetscapeServerGatedCrypto      = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}
	oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}
	oidExtKeyUsageMicrosoftKernelCodeSigning     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}
)

var extKeyUsageOIDs = []struct {
	extKeyUsage x509.ExtKeyUsage
	oid         asn1.ObjectIdentifier
}{
	{x509.ExtKeyUsageAny, oidExtKeyUsageAny},
	{x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth},
	{x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth},
	{x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning},
	{x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection},
	{x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem},
	{x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel},
	{x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser},
	{x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping},
	{x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning},
	{x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto},
	{x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto},
	{x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning},
	{x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning},
}

func OidFromExtKeyUsage(eku x509.ExtKeyUsage) (oid asn1.ObjectIdentifier, ok bool) {
	for _, pair := range extKeyUsageOIDs {
		if eku == pair.extKeyUsage {
			return pair.oid, true
		}
	}
	return
}

// did:x509 examples
//
//      did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:ST:California:L:San%20Francisco:O:GitHub%2C%20Inc.
//      did:x509:0:sha256:I5ni_nuWegx4NiLaeGabiz36bDUhDDiHEFl8HXMA_4o::subject:CN:Test%20Leaf%20%28DO%20NOT%20TRUST%20
//
// see https://github.com/microsoft/did-x509/blob/main/specification.md and https://www.w3.org/TR/2022/REC-did-core-20220719/

// check that the did "did" matches the public cert chain "chain"
func verifyDid(chain []*x509.Certificate, did string) error {
	var topTokens = strings.Split(did, "::")

	if len(topTokens) <= 1 {
		return errors.New("invalid DID string")
	}

	var pretokens = strings.Split(topTokens[0], ":")

	if len(pretokens) < 5 || pretokens[0] != "did" || pretokens[1] != "x509" {
		return errors.New("unsupported method/prefix")
	}

	if pretokens[2] != "0" {
		return errors.New("unsupported did:x509 version")
	}

	caFingerprintAlg := pretokens[3]
	caFingerprint := pretokens[4]

	policies := topTokens[1:]

	if len(chain) < 2 {
		return errors.New("certificate chain too short")
	}

	err := checkFingerprint(chain, caFingerprintAlg, caFingerprint)
	if err != nil {
		return err
	}

	for _, policy := range policies {
		parts := strings.Split(policy, ":")

		if len(parts) < 2 {
			return errors.New("invalid policy")
		}

		policyName, args := parts[0], parts[1:]
		switch policyName {
		case "subject":
			if len(args) == 0 || len(args)%2 != 0 {
				return errors.New("key-value pairs required")
			}

			if len(args) < 2 {
				return errors.New("at least one key-value pair is required")
			}

			// Walk the x509 subject description (a list of key:value pairs like "CN:ContainerPlat" saying the subject common
			// name is ContainerPlat) extracting the various fields and checking they do not occur more than once.
			var seenFields []string
			for i := 0; i < len(args); i += 2 {
				k := strings.ToUpper(args[i])
				v, err := url.QueryUnescape(args[i+1])

				if err != nil {
					return fmt.Errorf("urlUnescape failed: %w", err)
				}

				for _, sk := range seenFields {
					if sk == k {
						return fmt.Errorf("duplicate field '%s'", k)
					}
				}
				seenFields = append(seenFields, k)

				leaf := chain[0]
				var fieldValues []string
				switch k {
				case "C":
					fieldValues = leaf.Subject.Country
				case "O":
					fieldValues = leaf.Subject.Organization
				case "OU":
					fieldValues = leaf.Subject.OrganizationalUnit
				case "L":
					fieldValues = leaf.Subject.Locality
				case "S":
					fieldValues = leaf.Subject.Province
				case "STREET":
					fieldValues = leaf.Subject.StreetAddress
				case "POSTALCODE":
					fieldValues = leaf.Subject.PostalCode
				case "SERIALNUMBER":
					fieldValues = []string{leaf.Subject.SerialNumber}
				case "CN":
					fieldValues = []string{leaf.Subject.CommonName}
				default:
					for _, aav := range leaf.Subject.Names {
						if aav.Type.String() == k {
							fieldValues = []string{aav.Value.(string)}
							break
						}
					}
					if len(fieldValues) == 0 {
						return fmt.Errorf("unsupported subject key: %s", k)
					}
				}
				found := false
				for _, fv := range fieldValues {
					if fv == v {
						found = true
						break
					}
				}
				if !found {
					return fmt.Errorf("invalid subject value: %s=%s", k, v)
				}
			}
		case "san":
			if len(args) != 2 {
				return fmt.Errorf("exactly one SAN type and value required")
			}
			sanType := args[0]
			sanValue, err := url.QueryUnescape(args[1])
			if err != nil {
				return fmt.Errorf("url.QueryUnescape failed: %w", err)
			}
			err = checkHasSan(sanType, sanValue, chain[0])
			if err != nil {
				return err
			}
		case "eku":
			if len(args) != 1 {
				return errors.New("exactly one EKU required")
			}

			ekuOid, err := oidFromString(args[0])
			if err != nil {
				return fmt.Errorf("oidFromString failed: %w", err)
			}

			if len(chain[0].UnknownExtKeyUsage) == 0 {
				return errors.New("no EKU extension in certificate")
			}

			foundEku := false
			for _, certEku := range chain[0].ExtKeyUsage {
				certEkuOid, ok := OidFromExtKeyUsage(certEku)
				if ok && certEkuOid.Equal(*ekuOid) {
					foundEku = true
					break
				}
			}
			for _, certEkuOid := range chain[0].UnknownExtKeyUsage {
				if certEkuOid.Equal(*ekuOid) {
					foundEku = true
					break
				}
			}

			if !foundEku {
				return fmt.Errorf("EKU not found: %s", ekuOid)
			}
		case "fulcio-issuer":
			if len(args) != 1 {
				return errors.New("excessive arguments to fulcio-issuer")
			}
			decodedArg, err := url.QueryUnescape(args[0])
			if err != nil {
				return fmt.Errorf("urlUnescape failed: %w", err)
			}
			fulcioIssuer := "https://" + decodedArg
			fulcioIssuerOid, err := oidFromString("1.3.6.1.4.1.57264.1.1")
			if err != nil {
				return fmt.Errorf("oidFromString failed: %w", err)
			}
			found := false
			for _, ext := range chain[0].Extensions {
				if ext.Id.Equal(*fulcioIssuerOid) {
					if string(ext.Value) == fulcioIssuer {
						found = true
						break
					}
				}
			}
			if !found {
				return fmt.Errorf("invalid fulcio-issuer: %s", fulcioIssuer)
			}
		default:
			return fmt.Errorf("unsupported did:x509 policy name '%s'", policyName)
		}
	}

	return nil
}

func createDidDocument(did string, chain []*x509.Certificate) (string, error) {
	format := `
{
	"@context": "https://www.w3.org/ns/did/v1",
	"id": "%s",
	"verificationMethod": [{
		"id": "%[1]s#key-1",
		"type": "JsonWebKey2020",
		"controller": "%[1]s",
		"publicKeyJwk": %s
	}]
	%s
	%s
}`

	includeAssertionMethod := chain[0].KeyUsage == 0 || (chain[0].KeyUsage&x509.KeyUsageDigitalSignature) != 0
	includeKeyAgreement := chain[0].KeyUsage == 0 || (chain[0].KeyUsage&x509.KeyUsageKeyAgreement) != 0

	if !includeAssertionMethod && !includeKeyAgreement {
		return "", errors.New("leaf certificate key usage must include digital signature or key agreement")
	}

	am := ""
	ka := ""
	if includeAssertionMethod {
		am = fmt.Sprintf(",\"assertionMethod\": \"%s#key-1\"", did)
	}
	if includeKeyAgreement {
		ka = fmt.Sprintf(",\"keyAgreement\": \"%s#key-1\"", did)
	}

	leaf, err := jwk.New(chain[0].PublicKey)
	if err != nil {
		return "", err
	}
	jleaf, err := json.Marshal(leaf)
	if err != nil {
		return "", err
	}
	doc := fmt.Sprintf(format, did, jleaf, am, ka)
	return doc, nil
}

func parsePemChain(chainPem string) ([]*x509.Certificate, error) {
	var chain = []*x509.Certificate{}

	bs := []byte(chainPem)
	for block, rest := pem.Decode(bs); block != nil; block, rest = pem.Decode(rest) {
		if block.Type == "CERTIFICATE" {
			cert, err := x509.ParseCertificate(block.Bytes)
			if err != nil {
				return []*x509.Certificate{}, fmt.Errorf("certificate parser failed: %w", err)
			}
			chain = append(chain, cert)
		}
	}

	return chain, nil
}

func Resolve(chainPem string, did string, ignoreTime bool) (string, error) {
	chain, err := parsePemChain(chainPem)

	if err != nil {
		return "", err
	}

	if len(chain) == 0 {
		return "", errors.New("no certificate chain")
	}

	// The last certificate in the chain is assumed to be the trusted root.
	roots := []*x509.Certificate{chain[len(chain)-1]}

	chains, err := VerifyCertificateChain(chain, roots, ignoreTime)

	if err != nil {
		return "", fmt.Errorf("certificate chain verification failed: %w", err)
	}

	for _, chain := range chains {
		err = verifyDid(chain, did)
		if err != nil {
			return "", fmt.Errorf("DID verification failed: %w", err)
		}
	}

	doc, err := createDidDocument(did, chain)

	if err != nil {
		return "", fmt.Errorf("DID document creation failed: %w", err)
	}

	return doc, nil
}
