package main

import (
	"bytes"
	"crypto/sha1"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"text/template"
	"time"

	cfsslconfig "github.com/cloudflare/cfssl/config"
	"github.com/cloudflare/cfssl/csr"
	restful "github.com/emicklei/go-restful"
	yaml "gopkg.in/yaml.v2"

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

var cmdlineParam = restful.QueryParameter("cmdline", "Linux kernel cmdline addition")

type renderContext struct {
	Host      *localconfig.Host
	SSLConfig *cfsslconfig.Config

	// Linux kernel extra cmdline
	CmdLine string `yaml:"-"`
}

func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string,
	create func(out io.Writer, ctx *renderContext) error) (err error) {

	ctx.CmdLine = r.URL.Query().Get(cmdlineParam.Data().Name)

	if ctx.CmdLine != "" {
		what = what + "?cmdline=" + url.QueryEscape(ctx.CmdLine)
	}

	// get it or create it
	outfile, err := os.CreateTemp("/tmp", "dls."+what+".")
	if err != nil {
		return
	}

	defer os.Remove(outfile.Name())

	log.Printf("building %s for %q", what, ctx.Host.Name)
	err = create(outfile, ctx)
	if err != nil {
		return
	}

	// serve it
	log.Printf("sending %s for %q", what, ctx.Host.Name)

	outfile.Seek(0, io.SeekStart)
	io.Copy(w, outfile)

	return
}

func sslConfigFromLocalConfig(cfg *localconfig.Config) (sslCfg *cfsslconfig.Config, err error) {
	if len(cfg.SSLConfig) == 0 {
		sslCfg = &cfsslconfig.Config{}
	} else {
		sslCfg, err = cfsslconfig.LoadConfig([]byte(cfg.SSLConfig))
		if err != nil {
			return
		}
	}
	return
}

func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
	sslCfg, err := sslConfigFromLocalConfig(cfg)
	if err != nil {
		return
	}

	return &renderContext{
		Host:      host,
		SSLConfig: sslCfg,
	}, nil
}

func (ctx *renderContext) Config() (ba []byte, cfg *config.Config, err error) {
	ba, err = ctx.render(ctx.Host.Config)
	if err != nil {
		return
	}

	// log.Print("rendered config:\n", string(ba))

	cfg = &config.Config{}
	if err = yaml.Unmarshal(ba, cfg); err != nil {
		return
	}

	return
}

func (ctx *renderContext) BootstrapConfig() (ba []byte, err error) {
	return ctx.render(ctx.Host.BootstrapConfig)
}

func (ctx *renderContext) render(templateText string) (ba []byte, err error) {
	tmpl, err := template.New(ctx.Host.Name + "/config").
		Funcs(ctx.TemplateFuncs()).
		Parse(templateText)

	if err != nil {
		return
	}

	buf := bytes.NewBuffer(make([]byte, 0, 4096))
	if err = tmpl.Execute(buf, nil); err != nil {
		return
	}

	ba = buf.Bytes()
	return
}

func distFilePath(path ...string) string {
	return filepath.Join(append([]string{*dataDir, "dist"}, path...)...)
}

func (ctx *renderContext) Tag() (string, error) {
	h := sha256.New()

	_, cfg, err := ctx.Config()
	if err != nil {
		return "", err
	}

	enc := yaml.NewEncoder(h)

	for _, o := range []any{cfg, ctx} {
		if err := enc.Encode(o); err != nil {
			return "", err
		}
	}

	return hex.EncodeToString(h.Sum(nil)), nil
}

func (ctx *renderContext) TemplateFuncs() map[string]any {
	funcs := templateFuncs(ctx.SSLConfig)

	// FIXME duplicate from cluster-render-context
	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, ctx.SSLConfig)
	}

	for name, method := range map[string]any{
		"base64": func(input string) string {
			return base64.StdEncoding.EncodeToString([]byte(input))
		},

		"host_ip": func() (s string) {
			return ctx.Host.IPs[0]
		},
		"host_name": func() (s string) {
			return ctx.Host.Name
		},
		"machine_id": func() (s string) {
			ba := sha1.Sum([]byte(ctx.Host.ClusterName + "/" + ctx.Host.Name))
			return hex.EncodeToString(ba[:])
		},

		"tls_dir": func(dir, cluster, caName, name, profile, label, reqJson string) (s string, err error) {
			ca, err := getUsableClusterCA(cluster, caName)
			if err != nil {
				return
			}

			reqJson = strings.ReplaceAll(reqJson, "${host_ip}", ctx.Host.IPs[0])
			reqJson = strings.ReplaceAll(reqJson, "${host_name}", ctx.Host.Name)

			kc, err := getKeyCert(cluster, caName, name, profile, label, reqJson)
			if err != nil {
				return
			}

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

		"ssh_user_ca": func(path, cluster string) (s string, err error) {
			userCA, err := sshCAPubKey(cluster)
			return asYaml([]config.FileDef{{
				Path:    path,
				Mode:    0644,
				Content: string(userCA),
			}})
		},
		"ssh_user_ca_pub": func(cluster string) (s string, err error) {
			userCA, err := sshCAPubKey(cluster)
			return string(userCA), err
		},
		"ssh_host_keys": func(dir, cluster, host string) (s string, err error) {
			if host == "" {
				host = ctx.Host.Name
			}
			if host != ctx.Host.Name {
				err = fmt.Errorf("wrong host name")
				return
			}

			pairs, err := getSSHKeyPairs(host)
			if err != nil {
				return
			}

			files := make([]config.FileDef, 0, len(pairs)*2)

			for _, pair := range pairs {
				basePath := path.Join(dir, "ssh_host_"+pair.Type+"_key")
				files = append(files, []config.FileDef{
					{
						Path:    basePath,
						Mode:    0600,
						Content: pair.Private,
					},
					{
						Path:    basePath + ".pub",
						Mode:    0644,
						Content: pair.Public,
					},
				}...)
			}

			return asYaml(files)
		},
		"ssh_host_key": func(type_ string) (s string, err error) {
			pair, err := ctx.sshHostKeyPair(type_)
			if err != nil {
				return
			}
			return pair.Private, nil
		},
		"ssh_host_pubkey": func(type_ string) (s string, err error) {
			pair, err := ctx.sshHostKeyPair(type_)
			if err != nil {
				return
			}
			return pair.Public, nil
		},
		"host_download_token": func() (token string, err error) {
			key := ctx.Host.Name
			token, found, err := hostDownloadTokens.Get(key)
			if err != nil {
				return
			}

			if !found {
				token, err = newToken(32)
				if err != nil {
					return
				}

				err = hostDownloadTokens.Put(key, token)
				if err != nil {
					return
				}
			}

			return
		},
		"asset_download_token": func(asset string, params ...string) (token string, err error) {
			now := time.Now()
			exp := now.Add(24 * time.Hour) // expire in 24h by default
			if len(params) != 0 {
				exp, err = parseCertDuration(params[0], now)
				if err != nil {
					return
				}
			}

			set := DownloadSet{
				Expiry: exp,
				Items: []DownloadSetItem{
					{
						Kind:   "host",
						Name:   ctx.Host.Name,
						Assets: []string{asset},
					},
				},
			}

			privKey, _ := dlsSigningKeys()
			token = set.Signed(privKey)

			return
		},
		"wg_key": func(name string) (key string, err error) {
			return wgKey(name + "/hosts/" + ctx.Host.Name)
		},
		"wg_psk": func(name, peerName string) (key string, err error) {
			a := ctx.Host.Name
			b := peerName
			if a > b {
				a, b = b, a
			}
			return wgKey(name + "/psks/" + a + " " + b)
		},
		"wg_pubkey": func(name, host string) (key string, err error) {
			return wgKey(name + "/hosts/" + host)
		},
	} {
		funcs[name] = method
	}

	return funcs
}

func (ctx *renderContext) sshHostKeyPair(type_ string) (kp SSHKeyPair, err error) {
	pairs, err := getSSHKeyPairs(ctx.Host.Name)
	if err != nil {
		return
	}

	for _, pair := range pairs {
		if pair.Type == type_ {
			return pair, nil
		}
	}

	err = fmt.Errorf("no key pair with type %q", type_)
	return
}
