package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"math/rand"
	"path"
	"reflect"
	"strconv"
	"strings"

	"github.com/cespare/xxhash"
	yaml "gopkg.in/yaml.v2"

	"novit.tech/direktil/pkg/config"

	"novit.tech/direktil/local-server/pkg/clustersconfig"
)

type renderContext struct {
	Labels      map[string]string
	Annotations map[string]string

	Host    *clustersconfig.Host
	Cluster *clustersconfig.Cluster
	Vars    map[string]any

	BootstrapConfigTemplate *clustersconfig.Template
	ConfigTemplate          *clustersconfig.Template
	StaticPodsTemplate      *clustersconfig.Template

	clusterConfig *clustersconfig.Config
}

func newRenderContext(host *clustersconfig.Host, cfg *clustersconfig.Config) (ctx *renderContext, err error) {
	cluster := cfg.Cluster(host.Cluster)
	if cluster == nil {
		err = fmt.Errorf("no cluster named %q", host.Cluster)
		return
	}

	vars := make(map[string]any)

	for _, oVars := range []map[string]any{
		cluster.Vars,
		host.Vars,
	} {
		mapMerge(vars, oVars)
	}

	return &renderContext{
		Labels:      mergeLabels(cluster.Labels, host.Labels),
		Annotations: mergeLabels(cluster.Annotations, host.Annotations),

		Host:    host,
		Cluster: cluster,
		Vars:    vars,

		BootstrapConfigTemplate: cfg.ConfigTemplate(host.BootstrapConfig),
		ConfigTemplate:          cfg.ConfigTemplate(host.Config),

		clusterConfig: cfg,
	}, nil
}

func mergeLabels(sources ...map[string]string) map[string]string {
	ret := map[string]string{}

	for _, src := range sources {
		for k, v := range src {
			ret[k] = v
		}
	}

	return ret
}

func mapMerge(target, source map[string]interface{}) {
	for k, v := range source {
		target[k] = genericMerge(target[k], v)
	}
}

func genericMerge(target, source interface{}) (result interface{}) {
	srcV := reflect.ValueOf(source)
	tgtV := reflect.ValueOf(target)

	if srcV.Kind() == reflect.Map && tgtV.Kind() == reflect.Map {
		// XXX maybe more specific later
		result = map[interface{}]interface{}{}
		resultV := reflect.ValueOf(result)

		tgtIt := tgtV.MapRange()
		for tgtIt.Next() {
			sv := srcV.MapIndex(tgtIt.Key())
			if sv.Kind() == 0 {
				resultV.SetMapIndex(tgtIt.Key(), tgtIt.Value())
				continue
			}

			merged := genericMerge(tgtIt.Value().Interface(), sv.Interface())
			resultV.SetMapIndex(tgtIt.Key(), reflect.ValueOf(merged))
		}

		srcIt := srcV.MapRange()
		for srcIt.Next() {
			if resultV.MapIndex(srcIt.Key()).Kind() != 0 {
				continue // already done
			}

			resultV.SetMapIndex(srcIt.Key(), srcIt.Value())
		}

		return
	}

	return source
}

func (ctx *renderContext) Name() string {
	switch {
	case ctx.Host != nil:
		return "host:" + ctx.Host.Name
	case ctx.Cluster != nil:
		return "cluster:" + ctx.Cluster.Name
	default:
		return "unknown"
	}
}

func (ctx *renderContext) BootstrapConfig() string {
	if ctx.BootstrapConfigTemplate == nil {
		log.Fatalf("no such (bootstrap) config: %q", ctx.Host.BootstrapConfig)
	}
	return ctx.renderConfig(ctx.BootstrapConfigTemplate)
}

func (ctx *renderContext) Config() string {
	if ctx.ConfigTemplate == nil {
		log.Fatalf("no such config: %q", ctx.Host.Config)
	}
	return ctx.renderConfig(ctx.ConfigTemplate)
}

func (ctx *renderContext) renderConfig(configTemplate *clustersconfig.Template) string {
	buf := new(strings.Builder)
	ctx.renderConfigTo(buf, configTemplate)
	return buf.String()
}

func (ctx *renderContext) renderConfigTo(buf io.Writer, configTemplate *clustersconfig.Template) {
	ctxName := ctx.Name()

	ctxMap := ctx.asMap()

	extraFuncs := ctx.templateFuncs(ctxMap)

	extraFuncs["static_pods_files"] = func(dir string) (string, error) {
		namePods := ctx.renderStaticPods()

		defs := make([]config.FileDef, 0)

		for _, namePod := range namePods {
			name := namePod.Namespace + "_" + namePod.Name

			ba, err := yaml.Marshal(namePod.Pod)
			if err != nil {
				return "", fmt.Errorf("static pod %s: failed to render: %v", name, err)
			}

			defs = append(defs, config.FileDef{
				Path:    path.Join(dir, name+".yaml"),
				Mode:    0640,
				Content: string(ba),
			})
		}

		ba, err := yaml.Marshal(defs)
		return string(ba), err
	}

	extraFuncs["host_ip"] = func() string {
		if ctx.Host.Template {
			return "{{ host_ip }}"
		}
		return ctx.Host.IP
	}
	extraFuncs["host_name"] = func() string {
		if ctx.Host.Template {
			return "{{ host_name }}"
		}
		return ctx.Host.Name
	}
	extraFuncs["machine_id"] = func() string {
		return "{{ machine_id }}"
	}

	extraFuncs["version"] = func() string { return Version }

	if err := configTemplate.Execute(ctxName, "config", buf, ctxMap, extraFuncs); err != nil {
		log.Fatalf("failed to render config %q for host %q: %v", ctx.Host.Config, ctx.Host.Name, err)
	}
}

func (ctx *renderContext) templateFuncs(ctxMap map[string]any) map[string]any {
	cluster := ctx.Cluster.Name

	getKeyCert := func(name, funcName string) (s string, err error) {
		req := ctx.clusterConfig.CSR(name)
		if req == nil {
			err = fmt.Errorf("no certificate request named %q", name)
			return
		}

		if req.CA == "" {
			err = fmt.Errorf("CA not defined in req %q", name)
			return
		}

		buf := &bytes.Buffer{}
		err = req.Execute(ctx.Name(), "req:"+name, buf, ctxMap, nil)
		if err != nil {
			return
		}
		reqJson := cleanJsonObject(buf.String())

		key := name
		if req.PerHost {
			key += "/" + ctx.Host.Name
		}

		switch funcName {
		case "tls_dir":
			// needs the dir name
			dir := "/etc/tls/" + name

			s = fmt.Sprintf("{{ %s %q %q %q %q %q %q %q }}", funcName,
				dir, cluster, req.CA, key, req.Profile, req.Label, reqJson)

		default:
			s = fmt.Sprintf("{{ %s %q %q %q %q %q %q }}", funcName,
				cluster, req.CA, key, req.Profile, req.Label, reqJson)
		}
		return
	}

	funcs := clusterFuncs(ctx.Cluster)
	for k, v := range map[string]any{
		"default": func(value, defaultValue any) any {
			switch v := value.(type) {
			case string:
				if v != "" {
					return v
				}
			case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, float32, float64:
				if v != 0 {
					return v
				}

			default:
				if v != nil {
					return v
				}
			}
			return defaultValue
		},
		"tls_key": func(name string) (string, error) {
			return getKeyCert(name, "tls_key")
		},
		"tls_pubkey": func(name string) string {
			return fmt.Sprintf("{{ tls_pubkey %q %q }}", ctx.Cluster.Name, name)
		},

		"tls_crt": func(name string) (s string, err error) {
			return getKeyCert(name, "tls_crt")
		},

		"tls_dir": func(name string) (s string, err error) {
			return getKeyCert(name, "tls_dir")
		},

		"ssh_user_ca": func(path string) (s string) {
			return fmt.Sprintf("{{ ssh_user_ca   %q %q }}",
				path, cluster)
		},
		"ssh_host_keys": func(dir string) (s string) {
			return fmt.Sprintf("{{ ssh_host_keys %q %q \"\" }}",
				dir, cluster)
		},
		"host_download_token": func() (s string) {
			return "{{ host_download_token }}"
		},
		"asset_download_token": func(args ...string) (s string) {
			argsStr := new(strings.Builder)
			for _, arg := range args {
				argsStr.WriteByte(' ')
				argsStr.WriteString(strconv.Quote(arg))
			}
			return "{{ asset_download_token" + argsStr.String() + " }}"
		},

		"hosts_of_group": func() (hosts []any) {
			hosts = make([]any, 0)

			for _, host := range ctx.clusterConfig.Hosts {
				if host.Cluster == ctx.Cluster.Name && host.Group != ctx.Host.Group {
					continue
				}

				hosts = append(hosts, asMap(host))
			}

			return hosts
		},

		"hosts_of_group_count": func() (count int) {
			for _, host := range ctx.clusterConfig.Hosts {
				if host.Cluster == ctx.Cluster.Name && host.Group == ctx.Host.Group {
					count++
				}
			}
			return
		},

		"shuffled_hosts_by_group": func(group string) (hosts []any) {
			for _, host := range src.Hosts {
				if host.Cluster == ctx.Cluster.Name && host.Group == group {
					hosts = append(hosts, asMap(host))
				}
			}

			if len(hosts) == 0 {
				log.Printf("WARNING: no hosts in group %q", group)
				return
			}

			seed := xxhash.Sum64String(ctx.Host.Name)
			rng := rand.New(rand.NewSource(int64(seed)))
			rng.Shuffle(len(hosts), func(i, j int) { hosts[i], hosts[j] = hosts[j], hosts[i] })

			return
		},
	} {
		funcs[k] = v
	}
	return funcs
}

func (ctx *renderContext) asMap() map[string]interface{} {
	result := asMap(ctx)

	// also expand cluster:
	cluster := result["cluster"].(map[interface{}]interface{})
	cluster["kubernetes_svc_ip"] = ctx.Cluster.KubernetesSvcIP().String()
	cluster["dns_svc_ip"] = ctx.Cluster.DNSSvcIP().String()

	return result
}

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
}
