package main

import (
	"encoding/json"
	"fmt"
	"strings"
	"text/template"
	"time"

	"github.com/Masterminds/sprig/v3"
	"github.com/miekg/dns"
)

const coreDNSConfigTmpl = `
{{ .zone }}:53 {
	file {{ .dbFile }} {
		reload 1s
	}
	errors
	{{- if .dnsSecBasename }}
	dnssec {
		key file {{ .dnsSecBasename}}
	}
	{{- end }}
	log
	health {
		lameduck 5s
	}
	ready
	cache 30
	loop
	reload
	loadbalance
}
`

const recordsDBTmpl = `
{{- $zone := .zone }}
{{ $zone }}.   IN SOA ns1.{{ $zone }}. hostmaster.{{ $zone }}. {{ .nowUnix }} 7200 3600 1209600 3600
{{- range $i, $ns := .nameserverIP }}
ns{{ add1 $i }}.{{ $zone }}. 10800 IN A {{ $ns }}
{{- end }}
ns.p.{{ $zone }}.	10800	IN	A	100.100.100.100
devices.p.{{ $zone }}.	10800	IN	NS	ns.p.{{ $zone }}
{{- range .publicIP }}
{{ $zone }}. 10800 IN A {{ . }}
*.{{ $zone }}. 10800 IN A {{ . }}
*.*.{{ $zone }}. 10800 IN A {{ . }}
{{- end }}
*.p.{{ $zone }}. 10800 IN A {{ .privateIP }}
`

func NewStore(fs FS, config string, db string, zone string, publicIP []string, privateIP string, nameserverIP []string) (RecordStore, string, error) {
	dnsSec, err := getDNSSecKey(fs, zone)
	if err != nil {
		return nil, "", err
	}
	if err := fs.Write(dnsSec.Basename+".key", string(dnsSec.Key)); err != nil {
		return nil, "", err
	}
	if err := fs.Write(dnsSec.Basename+".private", string(dnsSec.Private)); err != nil {
		return nil, "", err
	}
	if err := executeTemplate(fs, config, coreDNSConfigTmpl, map[string]any{
		"zone":           zone,
		"dbFile":         fs.AbsolutePath(db),
		"dnsSecBasename": fs.AbsolutePath(dnsSec.Basename),
	}); err != nil {
		return nil, "", err
	}
	ok, err := fs.Exists(db)
	if err != nil {
		return nil, "", err
	}
	if !ok {
		if err := executeTemplate(fs, db, recordsDBTmpl, map[string]any{
			"zone":         zone,
			"publicIP":     publicIP,
			"privateIP":    privateIP,
			"nameserverIP": nameserverIP,
			"nowUnix":      NowUnix(),
		}); err != nil {
			return nil, "", err
		}
	}
	return &fsRecordStore{zone, publicIP, fs, db}, string(dnsSec.DS), nil
}

func getDNSSecKey(fs FS, zone string) (DNSSecKey, error) {
	const configFile = "dns-sec-key.json"
	ok, err := fs.Exists(configFile)
	if err != nil {
		return DNSSecKey{}, err
	}
	if ok {
		d, err := fs.Read(configFile)
		if err != nil {
			return DNSSecKey{}, err
		}
		var k DNSSecKey
		if err := json.Unmarshal([]byte(d), &k); err != nil {
			return DNSSecKey{}, err
		}
		return k, nil
	}
	k, err := newDNSSecKey(zone)
	if err != nil {
		return DNSSecKey{}, err
	}
	d, err := json.MarshalIndent(k, "", "\t")
	if err != nil {
		return DNSSecKey{}, err
	}
	if err := fs.Write(configFile, string(d)); err != nil {
		return DNSSecKey{}, err
	}
	return k, nil
}

type DNSSecKey struct {
	Basename string `json:"basename,omitempty"`
	Key      []byte `json:"key,omitempty"`
	Private  []byte `json:"private,omitempty"`
	DS       []byte `json:"ds,omitempty"`
}

func newDNSSecKey(zone string) (DNSSecKey, error) {
	key := &dns.DNSKEY{
		Hdr:       dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
		Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
	}
	priv, err := key.Generate(256)
	if err != nil {
		return DNSSecKey{}, err
	}
	return DNSSecKey{
		Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
		Key:      []byte(key.String()),
		Private:  []byte(key.PrivateKeyString(priv)),
		DS:       []byte(key.ToDS(dns.SHA256).String()),
	}, nil
}

// TODO(gio): not going to work in 15 years?
// TODO(gio): remove 10 *
func NowUnix() uint32 {
	return 10 * uint32(time.Now().Unix())
}

func executeTemplate(fs FS, path string, contents string, values map[string]any) error {
	tmpl, err := template.New("tmpl").Funcs(sprig.TxtFuncMap()).Parse(contents)
	if err != nil {
		return err
	}
	var d strings.Builder
	if err := tmpl.Execute(&d, values); err != nil {
		return err
	}
	return fs.Write(path, strings.TrimSpace(d.String())+"\n")
}
