package main

import (
	"bytes"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"log"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/cloudflare/cfssl/config"
	"github.com/cloudflare/cfssl/csr"
	"github.com/cloudflare/cfssl/helpers"
	"github.com/cloudflare/cfssl/signer"
	"github.com/cloudflare/cfssl/signer/local"
	restful "github.com/emicklei/go-restful"

	"novit.tech/direktil/local-server/pkg/mime"
	"novit.tech/direktil/pkg/localconfig"
)

var clusterSecretKVs = []string{}

func newClusterSecretKV[T any](name string) KVSecrets[T] {
	clusterSecretKVs = append(clusterSecretKVs, name)
	return KVSecrets[T]{"clusters/" + name}
}

func wsListClusters(req *restful.Request, resp *restful.Response) {
	cfg := wsReadConfig(resp)
	if cfg == nil {
		return
	}

	clusterNames := make([]string, len(cfg.Clusters))
	for i, cluster := range cfg.Clusters {
		clusterNames[i] = cluster.Name
	}

	resp.WriteEntity(clusterNames)
}

func wsReadCluster(req *restful.Request, resp *restful.Response) (cluster *localconfig.Cluster) {
	clusterName := req.PathParameter("cluster-name")

	cfg := wsReadConfig(resp)
	if cfg == nil {
		return
	}

	cluster = cfg.Cluster(clusterName)
	if cluster == nil {
		wsNotFound(resp)
		return
	}

	return
}

func wsCluster(req *restful.Request, resp *restful.Response) {
	cluster := wsReadCluster(req, resp)
	if cluster == nil {
		return
	}

	resp.WriteEntity(cluster)
}

func wsClusterAddons(req *restful.Request, resp *restful.Response) {
	cluster := wsReadCluster(req, resp)
	if cluster == nil {
		return
	}

	if len(cluster.Addons) == 0 {
		log.Printf("cluster %q has no addons defined", cluster.Name)
		wsNotFound(resp)
		return
	}

	cfg := wsReadConfig(resp)
	if cfg == nil {
		return
	}

	sslCfg, err := sslConfigFromLocalConfig(cfg)
	if err != nil {
		wsError(resp, err)
		return
	}

	wsRender(resp, sslCfg, cluster.Addons, cluster)
}

func wsClusterCACert(req *restful.Request, resp *restful.Response) {
	clusterName := req.PathParameter("cluster-name")
	caName := req.PathParameter("ca-name")

	ca, found, err := clusterCAs.Get(clusterName + "/" + caName)
	if err != nil {
		wsError(resp, err)
		return
	}
	if !found {
		wsNotFound(resp)
		return
	}

	resp.Header().Set("Content-Type", mime.CERT)
	resp.Write(ca.Cert)
}

func wsClusterSignedCert(req *restful.Request, resp *restful.Response) {
	clusterName := req.PathParameter("cluster-name")
	caName := req.PathParameter("ca-name")
	name := req.QueryParameter("name")

	kc, found, err := clusterCASignedKeys.Get(clusterName + "/" + caName + "/" + name)
	if err != nil {
		wsError(resp, err)
		return
	}
	if !found {
		wsNotFound(resp)
		return
	}

	resp.AddHeader("Content-Type", mime.CERT)
	resp.AddHeader("Content-Disposition", "attachment; filename="+strconv.Quote(clusterName+"_"+caName+"_"+url.PathEscape(name)+".crt"))
	resp.Write(kc.Cert)
}

type SSHSignReq struct {
	PubKey    string
	Principal string
	Validity  string
	Options   []string
}

func wsClusterSSHUserCAPubKey(req *restful.Request, resp *restful.Response) {
	clusterName := req.PathParameter("cluster-name")

	pubkey, err := sshCAPubKey(clusterName)
	if err != nil {
		wsError(resp, err)
		return
	}

	resp.Write(pubkey)
}

func wsClusterSSHUserCASign(req *restful.Request, resp *restful.Response) {
	clusterName := req.PathParameter("cluster-name")

	signReq := SSHSignReq{}
	err := req.ReadEntity(&signReq)
	if err != nil {
		wsError(resp, err)
		return
	}

	now := time.Now().Truncate(time.Second)
	notBefore, notAfter, err := parseCertDurationRange(signReq.Validity, now)
	if err != nil {
		wsError(resp, fmt.Errorf("invalid validity: %w", err))
		return
	}

	const sshTimestamp = "20060102150405Z"

	validity := notBefore.Format(sshTimestamp) + ":"
	if notAfter.IsZero() {
		validity += "forever"
	} else {
		validity += notAfter.Format(sshTimestamp)
	}

	log.Printf("sign ssh public key, validity %s -> %s", signReq.Validity, validity)

	cert, err := sshCASign(clusterName, []byte(signReq.PubKey), signReq.Principal, validity, signReq.Options...)
	if err != nil {
		wsError(resp, err)
		return
	}

	resp.Write(cert)
}

type KubeSignReq struct {
	CSR      string
	User     string
	Group    string
	Validity string
}

func wsClusterKubeCASign(req *restful.Request, resp *restful.Response) {
	clusterName := req.PathParameter("cluster-name")

	signReq := KubeSignReq{}
	err := req.ReadEntity(&signReq)
	if err != nil {
		wsError(resp, err)
		return
	}

	now := time.Now().Truncate(time.Second)
	notBefore, notAfter, err := parseCertDurationRange(signReq.Validity, now)
	if err != nil {
		wsError(resp, fmt.Errorf("invalid validity: %w", err))
		return
	}

	var names []csr.Name
	if signReq.Group != "" {
		names = []csr.Name{{O: signReq.Group}}
	}

	ca, err := getUsableClusterCA(clusterName, "cluster")
	if err != nil {
		wsError(resp, fmt.Errorf("get cluster CA failed: %w", err))
		return
	}

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

	caCerts, err := helpers.ParseCertificatesPEM(ca.Cert)
	if err != nil {
		wsError(resp, err)
		return
	}

	signingProfile := &config.Signing{
		Default: &config.SigningProfile{
			Usage:  []string{"client auth"},
			Expiry: notAfter.Sub(now),
		},
	}

	csr := signer.SignRequest{
		Request: signReq.CSR,
		Subject: &signer.Subject{
			CN:    signReq.User,
			Names: names,
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,
	}

	var certPEMs [][]byte
	for _, caCert := range caCerts {
		if caCert.NotAfter.Before(now) {
			continue
		}

		caSigner, err := local.NewSigner(caKey, caCert, signer.DefaultSigAlgo(caKey), signingProfile)
		if err != nil {
			wsError(resp, err)
			return
		}

		cert, err := caSigner.Sign(csr)
		if err != nil {
			wsError(resp, err)
			return
		}

		certPEMs = append(certPEMs, cert)
	}

	resp.Write(bytes.Join(certPEMs, nil))
}

func parseCertDurationRange(d string, now time.Time) (notBefore, notAfter time.Time, err error) {
	if d == "" {
		return
	}

	d1, d2, ok := strings.Cut(d, ":")

	if ok {
		notBefore, err = parseCertDuration(d1, now)
		if err != nil {
			return
		}
		notAfter, err = parseCertDuration(d2, now)
	} else {
		notAfter, err = parseCertDuration(d, now)
	}

	if err != nil {
		return
	}

	if notBefore.IsZero() {
		notBefore = now.Add(-5 * time.Minute)
	}

	return
}

var durRegex = regexp.MustCompile("^([+-]?)([0-9]+)([yMdwhms])")

func parseCertDuration(d string, now time.Time) (t time.Time, err error) {
	if d == "" {
		return
	}

	direction := 1
	t = now

	for d != "" {
		match := durRegex.FindStringSubmatch(d)
		if match == nil {
			t = time.Time{}
			err = errors.New("invalid duration: " + strconv.Quote(d))
			return
		}

		d = d[len(match[0]):]

		switch match[1] {
		case "+":
			direction = 1
		case "-":
			direction = -1
		}

		qty, _ := strconv.Atoi(match[2])
		unit := match[3]

		switch unit {
		case "y":
			t = t.AddDate(qty*direction, 0, 0)
		case "M":
			t = t.AddDate(0, qty*direction, 0)
		case "d":
			t = t.AddDate(0, 0, qty*direction)
		case "w":
			t = t.AddDate(0, 0, 7*qty*direction)
		case "h":
			t = t.Add(time.Duration(qty*direction) * time.Hour)
		case "m":
			t = t.Add(time.Duration(qty*direction) * time.Minute)
		case "s":
			t = t.Add(time.Duration(qty*direction) * time.Second)
		}
	}

	return
}

type KubeCSRParseReq struct {
	CSR string `json:"csr"`
}

type KubeCSRParseResp struct {
	User  string `json:"user,omitempty"`
	Group string `json:"group,omitempty"`
}

func wsParseKubeCSR(req *restful.Request, resp *restful.Response) {
	var r KubeCSRParseReq
	if err := req.ReadEntity(&r); err != nil {
		wsError(resp, err)
		return
	}

	block, _ := pem.Decode([]byte(r.CSR))
	if block == nil || block.Type != "CERTIFICATE REQUEST" {
		wsError(resp, fmt.Errorf("invalid CSR PEM"))
		return
	}

	cr, err := x509.ParseCertificateRequest(block.Bytes)
	if err != nil {
		wsError(resp, fmt.Errorf("parse CSR: %w", err))
		return
	}

	out := KubeCSRParseResp{User: cr.Subject.CommonName}
	if len(cr.Subject.Organization) > 0 {
		out.Group = cr.Subject.Organization[0]
	}

	resp.WriteAsJson(out)
}
