package main

import (
	"crypto"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"log"
	"net"

	"github.com/cloudflare/cfssl/config"
	"github.com/cloudflare/cfssl/csr"
	"github.com/cloudflare/cfssl/helpers"
	"github.com/cloudflare/cfssl/initca"
	"github.com/cloudflare/cfssl/signer"
	"github.com/cloudflare/cfssl/signer/local"
	"k8s.io/apimachinery/pkg/util/validation"
)

type CA struct {
	Key  []byte
	Cert []byte

	Signed map[string]*KeyCert
}

func (ca *CA) Init() (err error) {
	req := ca.newReq()

	cert, _, key, err := initca.New(req)
	if err != nil {
		err = fmt.Errorf("initca: %w", err)
		return
	}

	ca.Key = key
	ca.Cert = cert

	return
}

func (ca *CA) RenewCert() (err error) {
	var signer crypto.Signer
	signer, err = helpers.ParsePrivateKeyPEM(ca.Key)
	if err != nil {
		return
	}

	newCert, _, err := initca.NewFromSigner(ca.newReq(), signer)
	if err != nil {
		return
	}

	ca.Cert = newCert

	return
}

func (_ CA) newReq() *csr.CertificateRequest {
	return &csr.CertificateRequest{
		CN: "Direktil Local Server",
		KeyRequest: &csr.KeyRequest{
			A: "ecdsa",
			S: 521, // 256, 384, 521
		},
		Names: []csr.Name{
			{
				O: "novit.io",
			},
		},
	}
}

func (ca CA) ParseKey() (key crypto.Signer, err error) {
	return helpers.ParsePrivateKeyPEM(ca.Key)
}
func (ca CA) ParseCert() (cert *x509.Certificate, err error) {
	block, _ := pem.Decode(ca.Cert)
	if block == nil {
		err = errors.New("no PEM block found")
		return
	}
	return x509.ParseCertificate(block.Bytes)
}

func (ca CA) Signer(policy *config.Signing) (result *local.Signer, err error) {
	caCert, err := ca.ParseCert()
	if err != nil {
		return
	}

	caKey, err := ca.ParseKey()
	if err != nil {
		return
	}

	return local.NewSigner(caKey, caCert, signer.DefaultSigAlgo(caKey), policy)
}

func getUsableKeyCert(cluster, caName, name, profile, label string, req *csr.CertificateRequest, cfg *config.Config) (kc KeyCert, err error) {
	log := log.New(log.Default().Writer(), cluster+": CA "+caName+": ", log.Flags()|log.Lmsgprefix)

	ca, err := getUsableClusterCA(cluster, caName)
	if err != nil {
		return
	}

	for _, host := range req.Hosts {
		if ip := net.ParseIP(host); ip != nil {
			// valid IP (v4 or v6)
			continue
		}

		if host == "*" {
			continue
		}

		if errs := validation.IsDNS1123Subdomain(host); len(errs) == 0 {
			continue
		}
		if errs := validation.IsWildcardDNS1123Subdomain(host); len(errs) == 0 {
			continue
		}

		err = fmt.Errorf("%q is not an IP or FQDN", host)
		return
	}

	if req.CA != nil {
		err = errors.New("no CA section allowed here")
		return
	}

	rh := hash(req)

	key := cluster + "/" + caName + "/" + name

	kc, found, err := clusterCASignedKeys.Get(key)
	if err != nil {
		return
	}

	if found {
		if rh == kc.ReqHash {
			err = func() (err error) {
				err = checkCertUsable(kc.Cert)
				if err != nil {
					return
				}

				pool := x509.NewCertPool()
				if !pool.AppendCertsFromPEM(ca.Cert) {
					panic("unexpected invalid CA certificate at this point")
				}

				certBlock, _ := pem.Decode(kc.Cert)
				cert, err := x509.ParseCertificate(certBlock.Bytes)
				if err != nil {
					return
				}

				_, err = cert.Verify(x509.VerifyOptions{Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}})
				return
			}()
			if err == nil {
				return // all good, no need to create or renew
			}

			log.Print("regenerating certificate: ", err)

		} else {
			log.Printf("CSR changed for %s: hash=%q previous=%q", name, rh, kc.ReqHash)
		}
	} else {
		log.Print("new CSR for ", name)
	}

	sgr, err := ca.Signer(cfg.Signing)
	if err != nil {
		return
	}

	generator := &csr.Generator{Validator: func(_ *csr.CertificateRequest) error { return nil }}

	csr, tlsKey, err := generator.ProcessRequest(req)
	if err != nil {
		return
	}

	signReq := signer.SignRequest{
		Request: string(csr),
		Profile: profile,
		Label:   label,
	}

	cert, err := sgr.Sign(signReq)
	if err != nil {
		return
	}

	kc = KeyCert{
		Key:     tlsKey,
		Cert:    cert,
		ReqHash: rh,
	}

	err = clusterCASignedKeys.Put(key, kc)

	return
}
