package main

import (
	"crypto"
	"crypto/rand"
	"crypto/x509"
	"encoding/base32"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"path"
	"strconv"
	"strings"

	cfsslconfig "github.com/cloudflare/cfssl/config"
	"github.com/cloudflare/cfssl/csr"
	"github.com/cloudflare/cfssl/helpers"
	yaml "gopkg.in/yaml.v2"

	"novit.tech/direktil/pkg/bootstrapconfig"
	"novit.tech/direktil/pkg/config"
)

func templateFuncs(sslCfg *cfsslconfig.Config) map[string]any {
	getKey := func(cluster, caName string) (key crypto.Signer, err error) {
		ca, err := getUsableClusterCA(cluster, caName)
		if err != nil {
			return
		}
		key, err = helpers.ParsePrivateKeyPEM(ca.Key)
		return
	}
	getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc KeyCert, err error) {
		certReq := &csr.CertificateRequest{
			KeyRequest: csr.NewKeyRequest(),
		}

		err = json.Unmarshal([]byte(reqJson), certReq)
		if err != nil {
			log.Print("CSR unmarshal failed on: ", reqJson)
			return
		}

		return getUsableKeyCert(cluster, caName, name, profile, label, certReq, sslCfg)
	}

	hash := func(plain, seed []byte, hashAlg string) (hashed string, err error) {
		switch hashAlg {
		case "sha512crypt":
			return sha512crypt(plain, seed)

		case "bootstrap":
			return bootstrapconfig.JoinSeedAndHash(seed, bootstrapconfig.PasswordHashFromSeed(seed, plain)), nil

		default:
			return "", fmt.Errorf("unknown hash alg: %q", hashAlg)
		}
	}

	return map[string]any{
		"quote": strconv.Quote,
		"yaml":  asYaml,
		"indent": func(s, indent string) string {
			buf := new(strings.Builder)
			for _, line := range strings.Split(s, "\n") {
				buf.WriteString(indent + line + "\n")
			}
			return buf.String()
		},

		"password": func(cluster, name, hashAlg string) (password string, err error) {
			key := cluster + "/" + name

			seed, err := seeds.GetOrCreate(key, func() (seed []byte, err error) {
				seed = make([]byte, 16)
				_, err = rand.Read(seed)
				return
			})

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

			password, err = clusterPasswords.GetOrCreate(key, func() (password string, err error) {
				raw := make([]byte, 10)
				_, err = rand.Read(raw)
				if err != nil {
					return "", fmt.Errorf("failed to generate password: %w", err)
				}

				password = strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(raw))
				return
			})
			if err != nil {
				return
			}

			return hash([]byte(password), seed, hashAlg)
		},

		"token": getOrCreateClusterToken,

		"ca_key": func(cluster, name string) (s string, err error) {
			ca, err := getUsableClusterCA(cluster, name)
			if err != nil {
				return
			}

			s = string(ca.Key)
			return
		},

		"ca_crt": func(cluster, name string) (s string, err error) {
			ca, err := getCurrentClusterCA(cluster, name)
			if err != nil {
				return
			}

			s = string(ca.Cert)
			return
		},

		"ca_crt_all": func(cluster, name string) (s string, err error) {
			ca, err := getUsableClusterCA(cluster, name)
			if err != nil {
				return
			}

			extra, err := caExtraCerts(cluster, name)
			if err != nil {
				return
			}

			s = string(ca.Cert) + extra
			return
		},

		"ca_dir": func(cluster, name string) (s string, err error) {
			ca, err := getUsableClusterCA(cluster, name)
			if err != nil {
				return
			}

			extra, err := caExtraCerts(cluster, name)
			if err != nil {
				return
			}

			dir := "/etc/tls-ca/" + name

			return asYaml([]config.FileDef{
				{
					Path:    path.Join(dir, "ca.crt"),
					Mode:    0644,
					Content: string(ca.Cert) + extra,
				},
				{
					Path:    path.Join(dir, "ca.key"),
					Mode:    0600,
					Content: string(ca.Key),
				},
			})
		},

		"tls_key": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
			kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
			if err != nil {
				return
			}

			s = string(kc.Key)
			return
		},

		"tls_pubkey": func(cluster, caName string) (s string, err error) {
			priv, err := getKey(cluster, caName)
			if err != nil {
				return
			}

			ba, err := x509.MarshalPKIXPublicKey(priv.Public())
			if err != nil {
				err = fmt.Errorf("marshal public key failed: %w", err)
				return
			}

			s = base64.StdEncoding.EncodeToString(ba)
			return
		},

		"tls_crt": func(cluster, caName, name, profile, label, reqJson string) (s string, err error) {
			kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
			if err != nil {
				return
			}

			s = string(kc.Cert)
			return
		},
	}
}

func asYaml(v any) (string, error) {
	ba, err := yaml.Marshal(v)
	if err != nil {
		return "", err
	}

	return string(ba), nil
}
