package main

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"path"
	"path/filepath"
	"text/template"

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

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

type renderContext struct {
	Host      *localconfig.Host
	SSLConfig string
}

func renderCtx(w http.ResponseWriter, r *http.Request, ctx *renderContext, what string,
	create func(out io.Writer, ctx *renderContext) error) error {
	log.Printf("sending %s for %q", what, ctx.Host.Name)

	tag, err := ctx.Tag()
	if err != nil {
		return err
	}

	// get it or create it
	content, meta, err := casStore.GetOrCreate(tag, what, func(out io.Writer) error {
		log.Printf("building %s for %q", what, ctx.Host.Name)
		return create(out, ctx)
	})

	if err != nil {
		return err
	}

	// serve it
	http.ServeContent(w, r, what, meta.ModTime(), content)
	return nil
}

var prevSSLConfig = "-"

func newRenderContext(host *localconfig.Host, cfg *localconfig.Config) (ctx *renderContext, err error) {
	if prevSSLConfig != cfg.SSLConfig {
		var sslCfg *cfsslconfig.Config

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

		err = loadSecretData(sslCfg)
		if err != nil {
			return
		}

		prevSSLConfig = cfg.SSLConfig
	}

	return &renderContext{
		SSLConfig: cfg.SSLConfig,
		Host:      host,
	}, nil
}

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

	if err != nil {
		return
	}

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

	if secretData.Changed() {
		err = secretData.Save()
		if err != nil {
			return
		}
	}

	ba = buf.Bytes()

	cfg = &config.Config{}

	if err = yaml.Unmarshal(buf.Bytes(), cfg); err != nil {
		return
	}

	return
}

func (ctx *renderContext) templateFuncs() map[string]interface{} {
	getKeyCert := func(cluster, caName, name, profile, label, reqJson string) (kc *KeyCert, err error) {
		certReq := &csr.CertificateRequest{
			KeyRequest: csr.NewBasicKeyRequest(),
		}

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

		return secretData.KeyCert(cluster, caName, name, profile, label, certReq)
	}

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

		return string(ba), nil
	}

	return map[string]interface{}{
		"token": func(cluster, name string) (s string, err error) {
			return secretData.Token(cluster, name)
		},

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

			s = string(ca.Key)
			return
		},

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

			s = string(ca.Cert)
			return
		},

		"ca_dir": func(cluster, name string) (s string, err error) {
			ca, err := secretData.CA(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),
				},
				{
					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_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
		},

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

			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),
				},
			})
		},
	}
}

func (ctx *renderContext) 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 []interface{}{cfg, ctx} {
		if err := enc.Encode(o); err != nil {
			return "", err
		}
	}

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

func asMap(v interface{}) map[string]interface{} {
	ba, err := yaml.Marshal(v)
	if err != nil {
		panic(err) // shouldn't happen
	}

	result := make(map[string]interface{})

	if err := yaml.Unmarshal(ba, result); err != nil {
		panic(err) // shouldn't happen
	}

	return result
}