//////////////////////////////////////////////////////////////////////////
// DN42 Registry API Server
//////////////////////////////////////////////////////////////////////////

package main

//////////////////////////////////////////////////////////////////////////

import (
	"fmt"
	"github.com/gorilla/mux"
	log "github.com/sirupsen/logrus"
	//	"math/big"
	"net/http"
	"strings"
	"time"
)

//////////////////////////////////////////////////////////////////////////
// register the api

func init() {
	EventBus.Listen("APIEndpoint", InitDNSAPI)
	EventBus.Listen("RegistryUpdate", DNSUpdate)
}

//////////////////////////////////////////////////////////////////////////
// data model

// very simple DNS record data
type DNSRecord struct {
	Name    string
	Type    string
	Content string
	Comment string `json:",omitempty"`
}

type DNSZone struct {
	Records   []*DNSRecord
	Commit    string
	Generated time.Time
}

var DNSRootZone *DNSZone

//////////////////////////////////////////////////////////////////////////
// fixed set of authoritative zones

var DNSRootAuthZones = map[string]string{
	"dn42":                    "domain/dn42",
	"recursive-servers.dn42":  "domain/recursive-servers.dn42",
	"delegation-servers.dn42": "domain/delegation-servers.dn42",
	"d.f.ip6.arpa":            "inet6num/fd00::_8",
	"20.172.in-addr.arpa":     "inetnum/172.20.0.0_16",
	"21.172.in-addr.arpa":     "inetnum/172.21.0.0_16",
	"22.172.in-addr.arpa":     "inetnum/172.22.0.0_16",
	"23.172.in-addr.arpa":     "inetnum/172.23.0.0_16",
	"31.172.in-addr.arpa":     "inetnum/172.31.0.0_16",
	"10.in-addr.arpa":         "inetnum/10.0.0.0_8",
}

//////////////////////////////////////////////////////////////////////////
// called from main to initialise the API routing

func InitDNSAPI(params ...interface{}) {

	router := params[0].(*mux.Router)

	s := router.
		Methods("GET").
		PathPrefix("/dns").
		Subrouter()

	s.HandleFunc("/root-zone", dnsRZoneHandler)

	log.Info("DNS API installed")
}

//////////////////////////////////////////////////////////////////////////
// api handlers

// return records that should be included in a DN42 root zone
func dnsRZoneHandler(w http.ResponseWriter, r *http.Request) {

	var format []string
	query := r.URL.Query()
	format = query["format"]
	if format == nil || len(format) != 1 {
		format = []string{"json"}
	}

	switch format[0] {
	case "bind":
		DNSRootZone.WriteBindFormat(w)

	case "json":
		ResponseJSON(w, DNSRootZone)

	default:
		ResponseJSON(w, DNSRootZone)
	}
}

//////////////////////////////////////////////////////////////////////////
// called whenever the registry is updated

func DNSUpdate(params ...interface{}) {

	registry := params[0].(*Registry)
	//	path := params[1].(string)

	zone := &DNSZone{
		Generated: time.Now(),
		Commit:    registry.Commit,
	}

	// add zones that are authoritative within DN42
	for name, object := range DNSRootAuthZones {
		zone.AddRecords(registry, name, object, "DN42 Authoritative Zone")
	}

	// search all domain objects and add stub records for each TLD
	rtype := registry.Types["domain"]
	for name, object := range rtype.Objects {
		// domain is a TLD if it doesn't contain a '.'
		if strings.IndexRune(name, '.') == -1 {
			// don't include zones which are authoritative within DN42
			if DNSRootAuthZones[name] == "" {
				zone.AddRecords(registry, name, object.Ref, "Forward Zone")
			}
		}
	}

	DNSRootZone = zone
}

//////////////////////////////////////////////////////////////////////////
// utility function to add a DNS record to a zone

func (zone *DNSZone) AddRecord(name string, t string,
	content string, comment string) {
	record := &DNSRecord{
		Name:    name,
		Type:    t,
		Content: content,
		Comment: comment,
	}
	zone.Records = append(zone.Records, record)
}

//////////////////////////////////////////////////////////////////////////
// add nserver and ds-rdata records from a registry object

func (zone *DNSZone) AddRecords(registry *Registry, name string,
	path string, comment string) {

	// use the registry metadata key index to find the appropriate values
	object := registry.GetObject(path)
	if object == nil {
		log.WithFields(log.Fields{
			"zone": name,
			"path": path,
		}).Error("DNS: unable to find object in registry")
		return
	}

	nserver := object.GetKey("nserver")
	for _, ns := range nserver {
		// check if stub record needs to be added
		fields := strings.Split(ns.RawValue, " ")
		if len(fields) == 2 {
			// add a record for the NS, together with a stub A or AAAA record

			var stubtype string
			if strings.IndexRune(fields[1], ':') == -1 {
				// no : so IPv4
				stubtype = "A"
			} else {
				// has : so IPv6
				stubtype = "AAAA"
			}

			zone.AddRecord(name, "NS", fields[0]+".", comment)
			zone.AddRecord(fields[0], stubtype, fields[1], comment)

		} else {
			// no, just add an NS record as it was presented
			zone.AddRecord(name, "NS", ns.RawValue+".", comment)
		}

	}

	dsrdata := object.GetKey("ds-rdata")
	for _, ds := range dsrdata {
		zone.AddRecord(name, "DS", ds.RawValue, comment)
	}

}

//////////////////////////////////////////////////////////////////////////
// Functions for outputting zone records in different formats

func (r *DNSRecord) ToBindString() string {
	var comment string
	if r.Comment == "" {
		comment = ""
	} else {
		comment = "\t; " + r.Comment
	}

	return fmt.Sprintf("%s\tIN\t%s\t%s%s",
		r.Name, r.Type, r.Content, comment,
	)
}

func (zone *DNSZone) WriteBindFormat(w http.ResponseWriter) {

	w.Header().Set("Content-Type", "text/plain")
	w.Header().Set("Access-Control-Allow-Origin", "*")

	// provide a header
	fmt.Fprintf(w, ";; DN42 Root Zone Records\n"+
		";; Commit Reference: %s\n;; Generated: %s\n",
		zone.Commit, zone.Generated)

	// then simply output each record in turn
	for _, record := range zone.Records {
		fmt.Fprintln(w, record.ToBindString())
	}

}

//////////////////////////////////////////////////////////////////////////
// end of code