package main

import (
	"crypto/ecdsa"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"log"
	"math"
	"mime/multipart"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"regexp"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"github.com/spf13/viper"
)

// CertificateInfo contains all data related to a certificate (file)
type CertificateInfo struct {
	IsRoot          bool
	IsFirst         bool
	KeyTypes        map[string]string
	KeyType         string
	CreateType      string
	IsRootGenerated bool
	RootSubject     string
	RootEnddate     string
	NumDays         int

	Country      string
	Organization string
	CommonName   string

	ImportFile    multipart.File
	ImportHandler *multipart.FileHeader
	ImportPwd     string

	Key         string
	Passphrase  string
	Certificate string
	CRL         string

	/*
		KeyFromHSM     bool
		HSMInfo        HSMInfo
		HSMKeys        map[string]string
		HSMKey         string
		HSMLabel       string
		StoreCertOnHSM bool
	*/

	RequestBase string
	Errors      map[string]string
}

// Initialize the CertificateInfo and set the list of available key types
func (ci *CertificateInfo) Initialize() {
	ci.Errors = make(map[string]string)

	ci.KeyTypes = make(map[string]string)
	ci.KeyTypes["rsa4096"] = "RSA-4096"
	ci.KeyTypes["rsa2048"] = "RSA-2048"
	ci.KeyTypes["ecdsa384"] = "ECDSA-384"
	ci.KeyTypes["ecdsa256"] = "ECDSA-256"

	ci.KeyType = "rsa4096"

	// ci.HSMKeys = make(map[string]string)
	// ci.StoreCertOnHSM = true
}

// ValidateGenerate that the CertificateInfo contains valid and all required data for generating a cert
func (ci *CertificateInfo) ValidateGenerate() {
	if strings.TrimSpace(ci.KeyType) == "" || strings.TrimSpace(ci.KeyTypes[ci.KeyType]) == "" {
		ci.Errors["KeyType"] = "Please select a key type/size"
	}
	if strings.TrimSpace(ci.Country) == "" || len(ci.Country) < 2 {
		ci.Errors["Country"] = "Please enter a valid 2-character country code"
	}
	if strings.TrimSpace(ci.Organization) == "" {
		ci.Errors["Organization"] = "Please enter an organization name"
	}
	if strings.TrimSpace(ci.CommonName) == "" {
		ci.Errors["CommonName"] = "Please enter a common name"
	}
}

// Validate that the CertificateInfo contains valid and all required data
func (ci *CertificateInfo) Validate() bool {
	ci.Errors = make(map[string]string)

	if ci.CreateType == "generate" {
		ci.ValidateGenerate()
	}

	if (ci.CreateType == "import") && (ci.ImportHandler != nil) {
		ext := ci.ImportHandler.Filename[len(ci.ImportHandler.Filename)-4:]
		if (ci.ImportHandler.Size == 0) || (ext != ".zip" && ext != ".pfx") {
			ci.Errors["Import"] = "Please provide a bundle (.pfx or .zip) with a key and certificate"
		}
	}

	if ci.CreateType == "upload" {
		if strings.TrimSpace(ci.Key) == "" {
			ci.Errors["Key"] = "Please provide a PEM-encoded key"
		}
		if strings.TrimSpace(ci.Certificate) == "" {
			ci.Errors["Certificate"] = "Please provide a PEM-encoded certificate"
		}
	}

	return len(ci.Errors) == 0
}

func reportError(param interface{}) error {
	lines := strings.Split(string(debug.Stack()), "\n")
	if len(lines) >= 5 {
		lines = append(lines[:0], lines[5:]...)
	}

	stop := len(lines)
	for i := 0; i < len(lines); i++ {
		if strings.Contains(lines[i], ".ServeHTTP(") {
			stop = i
			break
		}
	}
	lines = lines[:stop]
	lines = append(lines, "...")

	fmt.Println(strings.Join(lines, "\n"))

	res := errors.New("error: see LabCA logs for details")
	switch v := param.(type) {
	case error:
		res = errors.New("Error (" + v.Error() + ")! See LabCA logs for details")
	case []byte:
		res = errors.New("Error (" + string(v) + ")! See LabCA logs for details")
	default:
		fmt.Printf("unexpected type %T", v)
	}

	return res
}

func ceremonyConfig(path string, rewrites map[string]string) (string, error) {
	tmplBytes, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	tmp, err := os.CreateTemp(os.TempDir(), "ceremony-config")
	if err != nil {
		return "", err
	}
	defer func() { _ = tmp.Close() }()
	tmpl, err := template.New("config").Parse(string(tmplBytes))
	if err != nil {
		return "", err
	}
	err = tmpl.Execute(tmp, rewrites)
	if err != nil {
		return "", err
	}
	return tmp.Name(), nil
}

func waitForFile(filePath string) error {
	start := time.Now()
	for {
		if _, err := os.Stat(filePath); err == nil {
			return nil // File found
		} else if !os.IsNotExist(err) {
			return fmt.Errorf("error checking file: %v", err) // Unexpected error
		}

		// Check if the timeout has been reached
		if time.Since(start) > 2*time.Minute {
			return fmt.Errorf("timeout reached while waiting for file")
		}

		// Sleep for a short interval before checking again
		time.Sleep(5 * time.Second)
	}
}

func (ci *CertificateInfo) CeremonyRoot(seqnr string, use_existing_key bool) (string, error) {
	keytype := "rsa"
	keyparam := strings.ReplaceAll(ci.KeyType, "rsa", "")
	algo := "SHA256WithRSA"
	if strings.HasPrefix(ci.KeyType, "ecdsa") {
		keytype = "ecdsa"
		len := strings.ReplaceAll(ci.KeyType, "ecdsa", "")
		keyparam = "P-" + len
		algo = "ECDSAWithSHA" + len
	}

	notbefore := time.Now().Add(-1 * time.Second)
	notafter := notbefore.AddDate(0, 0, ci.NumDays).Add(-1 * time.Second)

	cfg := &HSMConfig{}
	cfg.Initialize("root", seqnr)
	if err := cfg.CreateSlot(); err != nil {
		return "", fmt.Errorf("failed to create root slot: %s", err.Error())
	}

	certFileName := fmt.Sprintf("%sroot-%s-cert.pem", CERT_FILES_PATH, seqnr)
	cb := renameBackup(certFileName)
	var pb BackupResult
	if !use_existing_key {
		pb = renameBackup(fmt.Sprintf("%sroot-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
	}

	ceremonyCfg, err := ceremonyConfig("templates/cert-ceremonies/root.yaml", map[string]string{
		"Module":        cfg.Module,
		"UserPIN":       cfg.UserPIN,
		"SlotID":        cfg.SlotID,
		"Label":         cfg.Label,
		"Path":          CERT_FILES_PATH,
		"KeyType":       keytype,
		"KeyParam":      keyparam,
		"Extractable":   strconv.FormatBool(true), // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
		"SeqNr":         seqnr,
		"SignAlgorithm": algo,
		"CommonName":    ci.CommonName,
		"OrgName":       ci.Organization,
		"Country":       ci.Country,
		"NotBefore":     notbefore.UTC().Format("2006-01-02 15:04:05"),
		"NotAfter":      notafter.UTC().Format("2006-01-02 15:04:05"),
		"Renewal":       strconv.FormatBool(use_existing_key),
	})
	if err != nil {
		ci.Errors["Generate"] = "error preparing for root ceremony, see logs for details"
		cb.Restore()
		if !use_existing_key {
			pb.Restore()
		}
		return "", fmt.Errorf("could not fill root ceremony template: %s", err.Error())
	}
	defer func() { _ = os.Remove(ceremonyCfg) }()

	err = waitForFile("/opt/boulder/bin/ceremony")
	if err != nil {
		return "", fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
	}

	if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + ceremonyCfg); err != nil {
		ci.Errors["Generate"] = "failed to execute root ceremony, see logs for details"
		cb.Restore()
		if !use_existing_key {
			pb.Restore()
		}
		return "", err
	}

	cb.Remove()
	if !use_existing_key {
		pb.Remove()
	}
	return certFileName, nil
}

func (ci *CertificateInfo) CeremonyIssuer(seqnr, rootseqnr string, use_existing_key bool) (string, error) {
	fqdn := viper.GetString("labca.fqdn")

	keytype := "rsa"
	keyparam := strings.ReplaceAll(ci.KeyType, "rsa", "")
	algo := "SHA256WithRSA"
	if strings.HasPrefix(ci.KeyType, "ecdsa") {
		keytype = "ecdsa"
		len := strings.ReplaceAll(ci.KeyType, "ecdsa", "")
		keyparam = "P-" + len
		algo = "ECDSAWithSHA" + len
	}

	notbefore := time.Now().Add(-1 * time.Second)
	notafter := notbefore.AddDate(0, 0, ci.NumDays).Add(-1 * time.Second)

	cfg := &HSMConfig{}
	cfg.Initialize("issuer", seqnr)
	if err := cfg.CreateSlot(); err != nil {
		return "", fmt.Errorf("failed to create issuer slot: %s", err.Error())
	}

	if !use_existing_key {
		pb := renameBackup(fmt.Sprintf("%sissuer-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
		jb := renameBackup(fmt.Sprintf("%sissuer-%s.pkcs11.json", CERT_FILES_PATH, seqnr))

		keyCfg, err := ceremonyConfig("templates/cert-ceremonies/issuer-key.yaml", map[string]string{
			"Module":      cfg.Module,
			"UserPIN":     cfg.UserPIN,
			"SlotID":      cfg.SlotID,
			"Label":       cfg.Label,
			"Path":        CERT_FILES_PATH,
			"KeyType":     keytype,
			"KeyParam":    keyparam,
			"Extractable": strconv.FormatBool(true), // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
			"SeqNr":       seqnr,
		})
		if err != nil {
			ci.Errors["Generate"] = "error preparing for issuer key ceremony, see logs for details"
			pb.Restore()
			jb.Restore()
			return "", fmt.Errorf("could not fill issuer key ceremony template: %s", err.Error())
		}
		defer func() { _ = os.Remove(keyCfg) }()

		err = waitForFile("/opt/boulder/bin/ceremony")
		if err != nil {
			return "", fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
		}

		if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + keyCfg); err != nil {
			ci.Errors["Generate"] = "failed to execute issuer key ceremony, see logs for details"
			pb.Restore()
			jb.Restore()
			return "", err
		}

		pb.Remove()
		jb.Remove()
	}

	cfg = &HSMConfig{}
	cfg.Initialize("root", rootseqnr)
	if err := cfg.CreateSlot(); err != nil {
		return "", fmt.Errorf("failed to get root slot: %s", err.Error())
	}

	certFileName := fmt.Sprintf("%sissuer-%s-cert.pem", CERT_FILES_PATH, seqnr)
	cb := renameBackup(certFileName)

	ceremonyCfg, err := ceremonyConfig("templates/cert-ceremonies/issuer-cert.yaml", map[string]string{
		"Module":        cfg.Module,
		"UserPIN":       cfg.UserPIN,
		"RootSlotID":    cfg.SlotID,
		"RootLabel":     cfg.Label,
		"Path":          CERT_FILES_PATH,
		"SeqNr":         seqnr,
		"RootSeqNr":     rootseqnr,
		"SignAlgorithm": algo,
		"CommonName":    ci.CommonName,
		"OrgName":       ci.Organization,
		"Country":       ci.Country,
		"NotBefore":     notbefore.UTC().Format("2006-01-02 15:04:05"),
		"NotAfter":      notafter.UTC().Format("2006-01-02 15:04:05"),
		"CrlUrl":        fmt.Sprintf("http://%s/crl/root-%s-crl.pem", fqdn, rootseqnr),
		"IssuerUrl":     fmt.Sprintf("http://%s/certs/root-%s-cert.pem", fqdn, rootseqnr),
	})
	if err != nil {
		ci.Errors["Generate"] = "error preparing for issuer cert ceremony, see logs for details"
		cb.Restore()
		return "", fmt.Errorf("could not fill issuer cert ceremony template: %s", err.Error())
	}
	defer func() { _ = os.Remove(ceremonyCfg) }()

	err = waitForFile("/opt/boulder/bin/ceremony")
	if err != nil {
		return "", fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
	}

	if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + ceremonyCfg); err != nil {
		ci.Errors["Generate"] = "failed to execute issuer cert ceremony, see logs for details"
		cb.Restore()
		return "", err
	}

	cb.Remove()
	return certFileName, nil
}

func readCertificate(filename string) (*x509.Certificate, error) {
	read, err := os.ReadFile(filename)
	if err != nil {
		fmt.Println(err)
		return nil, errors.New("could not read '" + filename + "': " + err.Error())
	}
	block, _ := pem.Decode(read)
	if block == nil || block.Type != "CERTIFICATE" {
		fmt.Println(block)
		return nil, errors.New("failed to decode PEM block containing certificate")
	}
	crt, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return nil, err
	}

	return crt, nil
}

func (ci *CertificateInfo) CeremonyRootCRL(seqnr string) error {
	now := time.Now()

	if viper.Get("crl_root_days") == nil || viper.Get("crl_root_days") == "" {
		viper.Set("crl_root_days", 365)
		_ = viper.WriteConfig()
	}
	crlint, err := time.ParseDuration(fmt.Sprintf("%dh", viper.GetInt("crl_root_days")*24-1))
	if err != nil {
		crlint, _ = time.ParseDuration("8759h") // 365 days - 1 hour
	}

	cert, err := readCertificate(fmt.Sprintf("%sroot-%s-cert.pem", CERT_FILES_PATH, seqnr))
	if err != nil {
		return err
	}

	thisupdate := now
	if thisupdate.Before(cert.NotBefore) {
		thisupdate = cert.NotBefore.Add(1 * time.Second)
	}

	nextupdate := now.Add(crlint)
	maxNext := cert.NotAfter.Add(-1 * time.Second)
	if nextupdate.After(maxNext) {
		nextupdate = maxNext
	}
	if nextupdate.Sub(thisupdate) > time.Hour*24*365 {
		nextupdate = thisupdate.Add(time.Hour * 24 * 365).Add(-1 * time.Second)
	}

	crlnumber := fmt.Sprintf("%02d%03d%s", now.Year()-2000, now.YearDay(), seqnr)

	cb := renameBackup(fmt.Sprintf("%sroot-%s-crl.pem", CERT_FILES_PATH, seqnr))

	cfg := &HSMConfig{}
	cfg.Initialize("root", seqnr)
	if err := cfg.CreateSlot(); err != nil {
		return fmt.Errorf("failed to get root slot: %s", err.Error())
	}

	keyCfg, err := ceremonyConfig("templates/cert-ceremonies/root-crl.yaml", map[string]string{
		"Module":     cfg.Module,
		"UserPIN":    cfg.UserPIN,
		"RootSlotID": cfg.SlotID,
		"RootLabel":  cfg.Label,
		"Path":       CERT_FILES_PATH,
		"RootSeqNr":  seqnr,
		"ThisUpdate": thisupdate.UTC().Format("2006-01-02 15:04:05"),
		"NextUpdate": nextupdate.UTC().Format("2006-01-02 15:04:05"),
		"CrlNumber":  crlnumber,
	})
	if err != nil {
		ci.Errors["CRL"] = "error preparing for root crl ceremony, see logs for details"
		cb.Restore()
		return fmt.Errorf("could not fill root crl ceremony template: %s", err.Error())
	}
	defer func() { _ = os.Remove(keyCfg) }()

	err = waitForFile("/opt/boulder/bin/ceremony")
	if err != nil {
		return fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
	}

	if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + keyCfg); err != nil {
		ci.Errors["CRL"] = "failed to execute root crl ceremony, see logs for details"
		cb.Restore()
		return err
	}

	cb.Remove()
	return nil
}

// Generate a key and certificate file for the data from this CertificateInfo
func (ci *CertificateInfo) Generate(certBase string) error {
	var err error
	if ci.IsRoot {
		_, err = ci.CeremonyRoot("01", false)

		viper.Set("crl_root_days", ci.NumDays)
		_ = viper.WriteConfig()
	} else {
		_, err = ci.CeremonyIssuer("01", "01", false)
	}

	if err != nil {
		log.Printf("failed to create certificate: %s", err.Error())
		return errors.New("failed to create certificate, see logs for details")
	}

	if !ci.IsRoot {
		// Create CRLs stating that the intermediates are not revoked.
		err = ci.CeremonyRootCRL("01")

		if err != nil {
			log.Printf("failed to create crl: %s", err.Error())
			return errors.New("failed to create crl, see logs for details")
		}
	}

	return nil
}

// ImportPkcs12 imports an uploaded PKCS#12 / PFX file
func (ci *CertificateInfo) ImportPkcs12(tmpFile string, tmpKey string, tmpCert string) error {
	if ci.IsRoot {
		if (strings.Index(ci.ImportHandler.Filename, "labca-root-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) {
			fmt.Printf("WARNING: importing root from .pfx file but name is %s\n", ci.ImportHandler.Filename)
		}
	} else {
		if (strings.Index(ci.ImportHandler.Filename, "labca-issuer-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0) {
			fmt.Printf("WARNING: importing issuer from .pfx file but name is %s\n", ci.ImportHandler.Filename)
		}
	}

	pwd := "pass:dummy"
	if ci.ImportPwd != "" {
		pwd = "pass:" + strings.ReplaceAll(ci.ImportPwd, " ", "\\\\")
	}

	if out, err := exeCmd("openssl pkcs12 -in " + strings.ReplaceAll(tmpFile, " ", "\\\\") + " -password " + pwd + " -nocerts -nodes -out " + tmpKey); err != nil {
		if strings.Contains(string(out), "invalid password") {
			return errors.New("incorrect password")
		}

		return reportError(err)
	}
	if out, err := exeCmd("openssl pkcs12 -in " + strings.ReplaceAll(tmpFile, " ", "\\\\") + " -password " + pwd + " -nokeys -out " + tmpCert); err != nil {
		if strings.Contains(string(out), "invalid password") {
			return errors.New("incorrect password")
		}

		return reportError(err)
	}

	return nil
}

// ImportZip imports an uploaded ZIP file
func (ci *CertificateInfo) ImportZip(tmpFile string, tmpDir string) error {
	if ci.IsRoot {
		if (strings.Index(ci.ImportHandler.Filename, "labca-root-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_certificates") != 0) {
			fmt.Printf("WARNING: importing root from .zip file but name is %s\n", ci.ImportHandler.Filename)
		}
	} else {
		if (strings.Index(ci.ImportHandler.Filename, "labca-issuer-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0) {
			fmt.Printf("WARNING: importing issuer from .zip file but name is %s\n", ci.ImportHandler.Filename)
		}
	}

	cmd := "unzip -j"
	if ci.ImportPwd != "" {
		cmd = cmd + " -P " + strings.ReplaceAll(ci.ImportPwd, " ", "\\\\")
	} else {
		cmd = cmd + " -P dummy"
	}
	cmd = cmd + " " + strings.ReplaceAll(tmpFile, " ", "\\\\") + " -d " + tmpDir

	if _, err := exeCmd(cmd); err != nil {
		if err.Error() == "exit status 82" {
			return errors.New("incorrect password")
		}

		return reportError(err)
	}

	return nil
}

// Import a certificate and key file
func (ci *CertificateInfo) Import(tmpDir string, tmpKey string, tmpCert string) error {
	tmpFile := filepath.Join(tmpDir, ci.ImportHandler.Filename)

	f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		return err
	}
	defer func() { _ = f.Close() }()

	_, _ = io.Copy(f, ci.ImportFile)

	contentType := ci.ImportHandler.Header.Get("Content-Type")
	switch contentType {
	case "application/x-pkcs12":
		err := ci.ImportPkcs12(tmpFile, tmpKey, tmpCert)
		if err != nil {
			return err
		}
	case "application/zip", "application/x-zip-compressed":
		err := ci.ImportZip(tmpFile, tmpDir)
		if err != nil {
			return err
		}
	default:
		return errors.New("Content Type '" + contentType + "' not supported!")
	}

	return nil
}

// Upload a certificate and key file
func (ci *CertificateInfo) Upload(tmpKey string, tmpCert string) error {
	if ci.Key != "" {
		if err := os.WriteFile(tmpKey, []byte(ci.Key), 0644); err != nil {
			return err
		}

		pwd := "pass:dummy"
		if ci.Passphrase != "" {
			pwd = "pass:" + strings.ReplaceAll(ci.Passphrase, " ", "\\\\")
		}

		if out, err := exeCmd("openssl pkey -passin " + pwd + " -in " + tmpKey + " -out " + tmpKey + "-out"); err != nil {
			if strings.Contains(string(out), ":bad decrypt:") {
				return errors.New("incorrect password")
			}

			return reportError(err)
		}

		if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
			return reportError(err)
		}
	}

	if err := os.WriteFile(tmpCert, []byte(ci.Certificate), 0644); err != nil {
		return err
	}

	if out, err := exeCmd("openssl x509 -in " + tmpCert + " -out " + tmpCert + "-out"); err != nil {
		return reportError(out)
	}

	if _, err := exeCmd("mv " + tmpCert + "-out " + tmpCert); err != nil {
		return reportError(err)
	}

	return nil
}

func parseSubjectDn(subject string) map[string]string {
	trackerResultMap := map[string]string{"C=": "", "C =": "", "O=": "", "O =": "", "CN=": "", "CN =": "", "OU=": "", "OU =": ""}

	for tracker := range trackerResultMap {
		index := strings.Index(subject, tracker)

		if index < 0 {
			continue
		}

		var res string
		// track quotes for delimited fields so we know not to split on the comma
		quoteCount := 0

		for i := index + len(tracker); i < len(subject); i++ {
			char := subject[i]

			// if ", we need to count and delimit
			if char == 34 {
				quoteCount++
				if quoteCount == 2 {
					break
				} else {
					continue
				}
			}

			// comma, lets stop here but only if we don't have quotes
			if char == 44 && quoteCount == 0 {
				break
			}

			// add this individual char
			res += string(rune(char))
		}

		trackerResultMap[strings.TrimSpace(strings.TrimSuffix(tracker, "="))] = strings.TrimSpace(strings.TrimPrefix(res, "="))
	}

	for k, v := range trackerResultMap {
		if len(v) == 0 {
			delete(trackerResultMap, k)
		}
	}

	return trackerResultMap
}

// VerifyCerts verifies the root and the issuer certificates
func (ci *CertificateInfo) VerifyCerts(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
	var rootSubject string
	if (rootCert != "") && (rootKey != "") {
		r, err := exeCmd("openssl x509 -noout -subject -in " + rootCert)
		if err != nil {
			return reportError(err)
		}

		rootSubject = string(r[0 : len(r)-1])
		fmt.Printf("Import root with subject '%s'\n", rootSubject)

		subjectMap := parseSubjectDn(rootSubject)
		if val, ok := subjectMap["C"]; ok {
			ci.Country = val
		}
		if val, ok := subjectMap["O"]; ok {
			ci.Organization = val
		}
		if val, ok := subjectMap["CN"]; ok {
			ci.CommonName = val
		}

		keyFileExists := true
		if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
			keyFileExists = false
		}
		if keyFileExists {
			_, err = exeCmd("openssl pkey -noout -in " + rootKey)
			if err != nil {
				return reportError(err)
			}

			fmt.Println("Import root key")
		}
	}

	if (issuerCert != "") && (issuerKey != "") {
		r, err := exeCmd("openssl x509 -noout -subject -in " + issuerCert)
		if err != nil {
			return reportError(err)
		}

		fmt.Printf("Import issuer with subject '%s'\n", string(r[0:len(r)-1]))

		r, err = exeCmd("openssl x509 -noout -issuer -in " + issuerCert)
		if err != nil {
			return reportError(err)
		}

		issuerIssuer := string(r[0 : len(r)-1])
		fmt.Printf("Issuer certificate issued by CA '%s'\n", issuerIssuer)

		if rootSubject == "" {
			r, err := exeCmd("openssl x509 -noout -subject -in " + CERT_FILES_PATH + "root-01-cert.pem")
			if err != nil {
				return reportError(err)
			}

			rootSubject = string(r[0 : len(r)-1])
		}

		issuerIssuer = strings.ReplaceAll(issuerIssuer, "issuer=", "")
		rootSubject = strings.ReplaceAll(rootSubject, "subject=", "")
		if issuerIssuer != rootSubject {
			return errors.New("issuer not issued by our Root CA")
		}

		_, err = exeCmd("openssl verify -CAfile " + CERT_FILES_PATH + "root-01-cert.pem " + issuerCert)
		if err != nil {
			return errors.New("could not verify that issuer was issued by our Root CA")
		}

		_, err = exeCmd("openssl pkey -noout -in " + issuerKey)
		if err != nil {
			return reportError(err)
		}

		fmt.Println("Import issuer key")
	}

	return nil
}

// ImportFiles moves certificate files to their final location and imports the keys into the HSM
func (ci *CertificateInfo) ImportFiles(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
	if rootKey != "" {
		keyFileExists := true
		if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
			keyFileExists = false
		}
		if keyFileExists {
			rootseqnr := "01"
			cfg := &HSMConfig{}
			cfg.Initialize("root", rootseqnr)
			if err := cfg.CreateSlot(); err != nil {
				return fmt.Errorf("failed to create root slot: %s", err.Error())
			}

			pubKey, err := cfg.ImportKeyCert(rootKey, rootCert)
			if err != nil {
				return fmt.Errorf("failed to import root key: %s", err.Error())
			}

			var pubKeyBytes []byte
			if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
				pk := pubKey.(rsa.PublicKey)
				pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
			} else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
				pk := pubKey.(ecdsa.PublicKey)
				pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
			} else {
				return fmt.Errorf("unknown private key type: %s", reflect.TypeOf(pubKey).String())
			}
			if err != nil {
				return fmt.Errorf("failed to marshal root pubkey: %s", err.Error())
			}
			file, err := os.Create(fmt.Sprintf("%sroot-%s-pubkey.pem", CERT_FILES_PATH, rootseqnr))
			if err != nil {
				return fmt.Errorf("failed to create root pubkey file: %s", err.Error())
			}
			defer func() { _ = file.Close() }()
			if err := pem.Encode(file, &pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyBytes}); err != nil {
				return fmt.Errorf("failed to write root pubkey: %s", err.Error())
			}
		}
	}
	if rootCert != "" {
		if _, err := exeCmd("mv " + rootCert + " " + path); err != nil {
			return reportError(err)
		}
	}

	if issuerKey != "" {
		seqnr := "01"
		cfg := &HSMConfig{}
		cfg.Initialize("issuer", seqnr)
		if err := cfg.CreateSlot(); err != nil {
			return fmt.Errorf("failed to create issuer slot: %s", err.Error())
		}

		pubKey, err := cfg.ImportKeyCert(issuerKey, issuerCert)
		if err != nil {
			return reportError(err)
		}

		var pubKeyBytes []byte
		if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
			pk := pubKey.(rsa.PublicKey)
			pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
		} else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
			pk := pubKey.(ecdsa.PublicKey)
			pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
		} else {
			return fmt.Errorf("unknown private key type: %s", reflect.TypeOf(pubKey).String())
		}
		if err != nil {
			return fmt.Errorf("failed to marshal issuer pubkey: %s", err.Error())
		}
		file, err := os.Create(fmt.Sprintf("%sissuer-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
		if err != nil {
			return fmt.Errorf("failed to create issuer pubkey file: %s", err.Error())
		}
		defer func() { _ = file.Close() }()
		if err := pem.Encode(file, &pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyBytes}); err != nil {
			return fmt.Errorf("failed to write issuer pubkey: %s", err.Error())
		}
	}
	if issuerCert != "" {
		if _, err := exeCmd("mv " + issuerCert + " " + path); err != nil {
			return reportError(err)
		}
	}

	return nil
}

// Extract key and certificate files from a container file
func (ci *CertificateInfo) Extract(certBase string, tmpDir string, wasCSR bool) error {
	var rootCert string
	var rootKey string
	var issuerCert string
	var issuerKey string

	path := CERT_FILES_PATH // TODO !!

	if ci.IsRoot {
		rootCert = filepath.Join(tmpDir, "root-01-cert.pem")
		rootKey = filepath.Join(tmpDir, "root-01-key.pem")

		if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
			altCert := filepath.Join(tmpDir, "root-ca.pem")
			if _, err = os.Stat(altCert); err == nil {
				if _, err := exeCmd("mv " + altCert + " " + rootCert); err != nil {
					return err
				}
			}

			altKey := filepath.Join(tmpDir, "root-ca.key")
			if _, err = os.Stat(altKey); err == nil {
				if _, err := exeCmd("mv " + altKey + " " + rootKey); err != nil {
					return err
				}
			}
		}

		if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
			altCert := filepath.Join(tmpDir, "test-root.pem")
			if _, err = os.Stat(altCert); err == nil {
				if _, err := exeCmd("mv " + altCert + " " + rootCert); err != nil {
					return err
				}
			}

			altKey := filepath.Join(tmpDir, "test-root.key")
			if _, err = os.Stat(altKey); err == nil {
				if _, err := exeCmd("mv " + altKey + " " + rootKey); err != nil {
					return err
				}
			}

			if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
				return errors.New("file does not contain root certificate")
			}
		}
	}

	issuerCert = filepath.Join(tmpDir, "issuer-01-cert.pem")
	issuerKey = filepath.Join(tmpDir, "issuer-01-key.pem")

	if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
		if ci.IsRoot {
			issuerCert = ""
		} else {
			altCert := filepath.Join(tmpDir, "ca-int.pem")
			if _, err = os.Stat(altCert); err == nil {
				if _, err := exeCmd("mv " + altCert + " " + issuerCert); err != nil {
					return err
				}
			}

			if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
				altCert := filepath.Join(tmpDir, "test-ca.pem")
				if _, err = os.Stat(altCert); err == nil {
					if _, err := exeCmd("mv " + altCert + " " + issuerCert); err != nil {
						return err
					}
				}

				if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
					return errors.New("file does not contain issuer certificate")
				}
			}
		}
	}
	if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
		if ci.IsRoot || wasCSR {
			issuerKey = ""
		} else {
			altKey := filepath.Join(tmpDir, "ca-int.key")
			if _, err = os.Stat(altKey); err == nil {
				if _, err := exeCmd("mv " + altKey + " " + issuerKey); err != nil {
					return err
				}
			}

			if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
				altKey := filepath.Join(tmpDir, "test-ca.key")
				if _, err = os.Stat(altKey); err == nil {
					if _, err := exeCmd("mv " + altKey + " " + issuerKey); err != nil {
						return err
					}
				}

				if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
					return errors.New("file does not contain issuer key")
				}
			}
		}
	}

	err := ci.VerifyCerts(path, rootCert, rootKey, issuerCert, issuerKey)
	if err != nil {
		return err
	}

	// All is good now, move files to their permanent location...
	err = ci.ImportFiles(path, rootCert, rootKey, issuerCert, issuerKey)
	if err != nil {
		return err
	}

	// Extract enddate to determine what the default CRL validity should be
	if ci.IsRoot {
		certFile := path + filepath.Base(rootCert)
		read, err := os.ReadFile(certFile)
		if err != nil {
			fmt.Println(err)
			return errors.New("could not read '" + certFile + "': " + err.Error())
		}
		block, _ := pem.Decode(read)
		if block == nil || block.Type != "CERTIFICATE" {
			fmt.Println(block)
			return errors.New("failed to decode PEM block containing certificate")
		}
		crt, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return err
		}
		numDays := time.Until(crt.NotAfter).Hours() / 24
		// TODO: adjust for max root ceremony value...
		viper.Set("crl_root_days", int(math.Ceil(numDays)))
		_ = viper.WriteConfig()

	} else {
		// Create CRLs stating that the intermediates are not revoked.
		err = ci.CeremonyRootCRL("01")

		if err != nil {
			log.Printf("failed to create crl: %s", err.Error())
			return errors.New("failed to create crl, see logs for details")
		}
	}

	return nil
}

// Create a new pair of key + certificate files based on the info in CertificateInfo
func (ci *CertificateInfo) Create(certBase string, wasCSR bool) error {
	tmpDir, err := os.MkdirTemp("", "labca")
	if err != nil {
		return err
	}

	defer func() { _ = os.RemoveAll(tmpDir) }()

	var tmpKey string
	var tmpCert string
	if ci.IsRoot {
		tmpKey = filepath.Join(tmpDir, "root-01-key.pem")
		tmpCert = filepath.Join(tmpDir, "root-01-cert.pem")
	} else {
		tmpKey = filepath.Join(tmpDir, "issuer-01-key.pem")
		tmpCert = filepath.Join(tmpDir, "issuer-01-cert.pem")
	}

	switch ci.CreateType {
	case "generate":
		err := ci.Generate(certBase)
		if err != nil {
			return err
		}
	case "import":
		err := ci.Import(tmpDir, tmpKey, tmpCert)
		if err != nil {
			return err
		}
	case "upload":
		err := ci.Upload(tmpKey, tmpCert)
		if err != nil {
			return err
		}
	default:
		return fmt.Errorf("unknown CreateType")
	}

	// This is shared between pfx/zip import and pem text upload
	if ci.CreateType != "generate" {
		err := ci.Extract(certBase, tmpDir, wasCSR)
		if err != nil {
			return err
		}
	}

	return nil
}

func storeRootKey(path, certName, tmpDir, keyData, passphrase string) (bool, string) {
	tmpKey := filepath.Join(tmpDir, certName+".key")

	if err := os.WriteFile(tmpKey, []byte(keyData), 0600); err != nil {
		return false, err.Error()
	}

	if passphrase != "" {
		pwd := "pass:" + strings.ReplaceAll(passphrase, " ", "\\\\")

		if out, err := exeCmd("openssl pkey -passin " + pwd + " -in " + tmpKey + " -out " + tmpKey + "-out"); err != nil {
			if strings.Contains(string(out), ":bad decrypt:") {
				return false, "Incorrect password"
			}

			return false, "Unable to load Root CA key"
		}

		if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
			return false, err.Error()
		}
	}

	modKey, err := exeCmd("openssl rsa -noout -modulus -in " + tmpKey)
	if err != nil {
		return false, "Not a private key"
	}
	modCert, err := exeCmd("openssl x509 -noout -modulus -in " + path + certName + ".pem")
	if err != nil {
		return false, "Unable to load Root CA certificate"
	}
	if string(modKey) != string(modCert) {
		return false, "Key does not match the Root CA certificate"
	}

	if _, err := exeCmd("mv " + tmpKey + " " + path); err != nil {
		return false, err.Error()
	}

	return true, ""
}

func (ci *CertificateInfo) StoreRootKey(path string) bool {
	if ci.Errors == nil {
		ci.Errors = make(map[string]string)
	}
	if strings.TrimSpace(ci.Key) == "" {
		ci.Errors["Modal"] = "Please provide a PEM-encoded key"
		return false
	}

	tmpDir, err := os.MkdirTemp("", "labca")
	if err != nil {
		ci.Errors["Modal"] = err.Error()
		return false
	}

	defer func() { _ = os.RemoveAll(tmpDir) }()

	certBase := "root-01"
	if res, newError := storeRootKey(path, certBase, tmpDir, ci.Key, ci.Passphrase); !res {
		ci.Errors["Modal"] = newError
		return false
	}

	// Create root CRL file now that we have the key
	if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
		fmt.Printf("StoreRootKey: %s\n", err.Error())
		return false
	}

	return true
}

func (ci *CertificateInfo) StoreCRL(path string) bool {
	if ci.Errors == nil {
		ci.Errors = make(map[string]string)
	}
	if strings.TrimSpace(ci.CRL) == "" {
		fmt.Println("WARNING: no Root CRL file provided - please upload one from the manage page")
		return true
	}

	tmpDir, err := os.MkdirTemp("", "labca")
	if err != nil {
		ci.Errors["Modal"] = err.Error()
		return false
	}

	defer func() { _ = os.RemoveAll(tmpDir) }()

	tmpCRL := filepath.Join(tmpDir, "root-ca.crl")

	if err := os.WriteFile(tmpCRL, []byte(ci.CRL), 0644); err != nil {
		ci.Errors["Modal"] = err.Error()
		return false
	}

	crlIssuer, err := exeCmd("openssl crl -noout -issuer -in " + tmpCRL)
	if err != nil {
		ci.Errors["Modal"] = "Not a CRL file"
		return false
	}
	rootSubj, err := exeCmd("openssl x509 -noout -subject -in " + path + "root-ca.pem")
	if err != nil {
		ci.Errors["Modal"] = "Cannot get Root CA subject"
		return false
	}

	if strings.TrimPrefix(string(crlIssuer), "issuer=") != strings.TrimPrefix(string(rootSubj), "subject=") {
		ci.Errors["Modal"] = "CRL file does not match the Root CA certificate"
		return false
	}

	if _, err := exeCmd("mv " + tmpCRL + " " + path); err != nil {
		ci.Errors["Modal"] = err.Error()
		return false
	}

	return true
}

func renewCertificate(certname string, days int, rootname string, _ string, _ string) error {
	ci := &CertificateInfo{
		IsRoot:  strings.HasPrefix(certname, "root-"),
		NumDays: days,
	}
	ci.Initialize()

	certFile := fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, certname)
	rootCertFile := ""

	if !ci.IsRoot {
		rootCertFile = fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, rootname)
	}

	seqnr := ""
	re := regexp.MustCompile(`-(\d{2})-`)
	match := re.FindStringSubmatch(certFile)
	if len(match) > 1 {
		seqnr = match[1]
	} else {
		return fmt.Errorf("failed to extract sequence number from filename '%s'", certFile)
	}

	rootseqnr := ""
	if !ci.IsRoot {
		match := re.FindStringSubmatch(rootCertFile)
		if len(match) > 1 {
			rootseqnr = match[1]
		} else {
			return fmt.Errorf("failed to extract sequence number from filename '%s'", rootCertFile)
		}
	}

	crt, err := readCertificate(certFile)
	if err != nil {
		return fmt.Errorf("failed to read current certificate: %w", err)
	}

	if crt.PublicKeyAlgorithm == x509.RSA {
		pub := crt.PublicKey.(*rsa.PublicKey)
		if pub.N.BitLen() == 2048 {
			ci.KeyType = "rsa2048"
		}
		if pub.N.BitLen() == 4096 {
			ci.KeyType = "rsa4096"
		}
	}
	if crt.PublicKeyAlgorithm == x509.ECDSA {
		if crt.SignatureAlgorithm == x509.ECDSAWithSHA256 {
			ci.KeyType = "ecdsa256"
		}
		if crt.SignatureAlgorithm == x509.ECDSAWithSHA384 {
			ci.KeyType = "ecdsa384"
		}
	}

	subjectMap := parseSubjectDn(crt.Subject.String())
	if val, ok := subjectMap["C"]; ok {
		ci.Country = val
	}
	if val, ok := subjectMap["O"]; ok {
		ci.Organization = val
	}
	if val, ok := subjectMap["CN"]; ok {
		ci.CommonName = val
	}

	if ci.IsRoot {
		_, err = ci.CeremonyRoot(seqnr, true)

		viper.Set("crl_root_days", ci.NumDays)
		_ = viper.WriteConfig()
	} else {
		_, err = ci.CeremonyIssuer(seqnr, rootseqnr, true)
	}

	if err != nil {
		log.Printf("failed to create certificate: %s", err.Error())
		return errors.New("failed to create certificate, see logs for details")
	}

	if !ci.IsRoot {
		// Create CRLs stating that the intermediates are not revoked.
		err = ci.CeremonyRootCRL(rootseqnr)

		if err != nil {
			log.Printf("failed to create crl: %s", err.Error())
			return errors.New("failed to create crl, see logs for details")
		}
	}

	return nil
}

func locateFileIn(path, name string) string {
	if _, err := os.Stat(path + name); err == nil {
		return path + name
	}
	return ""
}

// TODO: sort out the file naming/locations properly to not need this and be future proof!!
// Most is found in /opt/boulder/ and some in /opt/boulder/labca/
func locateFile(name string) string {

	for _, path := range []string{"", "data/", "data/issuer", "/go/src/labca/data/", "/go/src/labca/data/issuer/", "labca/", "/opt/boulder/", "/opt/boulder/labca/"} {
		if res := locateFileIn(path, name); res != "" {
			return res
		}
	}

	fmt.Printf("WARNING: could not find '%s'!\n", name)
	return ""
}

func exeCmd(cmd string) ([]byte, error) {
	parts := strings.Fields(cmd)
	for i := 0; i < len(parts); i++ {
		parts[i] = strings.ReplaceAll(parts[i], "\\\\", " ")
	}
	head := parts[0]
	parts = parts[1:]

	out, err := exec.Command(head, parts...).CombinedOutput()
	if err != nil {
		fmt.Print(fmt.Sprint(err) + ": " + string(out))
	}
	return out, err
}
