package cert

import (
	"bytes"
	"crypto"
	"crypto/ecdsa"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"strings"
	"time"

	"github.com/pkg/errors"
)

type CertificateInfo struct {
	Algorithm               string    `json:"algorithm"`
	CN                      string    `json:"cn"`
	Fingerprint             string    `json:"certFingerprint"`
	ExpiresAt               time.Time `json:"expiresAt"`
	IssuedAt                time.Time `json:"issuedAt"`
	Issuer                  string    `json:"issuer"`
	KeySize                 int       `json:"keySize"`
	SerialNumber            string    `json:"serialNumber"`
	SubjectAlternativeNames []string  `json:"subjectAlternativeNames"`
	Version                 int       `json:"version"`
}

func matchAndKeySize(publicKey crypto.PublicKey, privateKey crypto.PrivateKey) (string, int, bool) {
	if algo, size, ok := rsaMatchAndKeySize(publicKey, privateKey); ok {
		return algo, size, ok
	}

	pubKey, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		return "", 0, false
	}

	privKey, ok := privateKey.(*ecdsa.PrivateKey)
	if !ok {
		return "", 0, false
	}

	return "ECC", 256, privKey.X.Cmp(pubKey.X) == 0 && privKey.Y.Cmp(privKey.Y) == 0
}

func rsaMatchAndKeySize(publicKey crypto.PublicKey, privateKey crypto.PrivateKey) (string, int, bool) {
	pubKey, ok := publicKey.(*rsa.PublicKey)
	if !ok {
		return "", 0, false
	}

	privKey, ok := privateKey.(*rsa.PrivateKey)
	if !ok {
		return "", 0, false
	}

	return "RSA", len(privKey.N.Bytes()), pubKey.N.Cmp(privKey.N) == 0
}

func Info(pemCerts, pemKey string) (*CertificateInfo, error) {
	block, _ := pem.Decode([]byte(pemKey))
	if block == nil {
		return nil, errors.New("failed to decode key: not valid pem format")
	}

	var key crypto.PrivateKey
	var err error
	if key, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
		if key, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
			if key, err = x509.ParseECPrivateKey(block.Bytes); err != nil {
				return nil, errors.Wrap(err, "failed to parse key: key must be PEM encoded EC, PKCS1, or PKCS8")
			}
		}
	}

	rest := []byte(pemCerts)
	for {
		block, rest = pem.Decode(rest)
		var certInfo CertificateInfo

		if block == nil {
			break
		}

		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, errors.Wrap(err, "failed to parse certificate")
		}

		algo, size, ok := matchAndKeySize(cert.PublicKey, key)
		if !ok {
			continue
		}

		certInfo.Algorithm = algo
		certInfo.Fingerprint = fingerprint(block.Bytes)
		certInfo.CN = cert.Subject.CommonName
		certInfo.ExpiresAt = cert.NotAfter
		certInfo.IssuedAt = cert.NotBefore
		certInfo.Issuer = cert.Issuer.CommonName
		certInfo.KeySize = size
		certInfo.SerialNumber = cert.SerialNumber.String()
		certInfo.Version = cert.Version

		for _, name := range cert.DNSNames {
			certInfo.SubjectAlternativeNames = append(certInfo.SubjectAlternativeNames, name)
		}

		for _, ip := range cert.IPAddresses {
			certInfo.SubjectAlternativeNames = append(certInfo.SubjectAlternativeNames, ip.String())
		}

		return &certInfo, nil
	}

	return nil, fmt.Errorf("failed to find cert that matched private key")
}

func fingerprint(data []byte) string {
	digest := sha1.Sum(data)
	buf := &bytes.Buffer{}
	for i := 0; i < len(digest); i++ {
		if buf.Len() > 0 {
			buf.WriteString(":")
		}
		buf.WriteString(strings.ToUpper(hex.EncodeToString(digest[i : i+1])))
	}
	return buf.String()
}
