package main

import (
	"fmt"
	"github.com/miekg/dns"
	"log"
	"net"
	"time"
)

type updateState struct {
	updateZone  string
	removeNames []dns.RR
	inserts     []dns.RR
}

type Update struct {
	ttl     int
	timeout time.Duration
	retry   time.Duration
	verbose bool

	zone string
	name string

	tsig     map[string]string
	tsigAlgo TSIGAlgorithm
	server   string

	updateChan chan updateState
	doneChan   chan error
}

func (u *Update) Init(name string, zone string, server string) error {
	if name == "" {
		return fmt.Errorf("Missing name")
	} else {
		u.name = dns.Fqdn(name)
	}

	if zone == "" {
		// guess
		if labels := dns.Split(u.name); len(labels) > 1 {
			u.zone = u.name[labels[1]:]
		} else {
			return fmt.Errorf("Missing zone")
		}
	} else {
		u.zone = dns.Fqdn(zone)
	}

	if server == "" {
		if server, err := discoverZoneServer(u.zone); err != nil {
			return fmt.Errorf("Failed to discver server")
		} else {
			log.Printf("discover server=%v", server)

			u.server = net.JoinHostPort(server, "53")
		}
	} else {
		if _, _, err := net.SplitHostPort(server); err == nil {
			u.server = server
		} else {
			u.server = net.JoinHostPort(server, "53")
		}
	}

	return nil
}

func (u *Update) InitTSIG(name string, secret string, algo TSIGAlgorithm) {
	u.tsig = map[string]string{dns.Fqdn(name): secret}
	u.tsigAlgo = algo
}

func (u *Update) buildAddr(ip net.IP) dns.RR {
	if ip4 := ip.To4(); ip4 != nil {
		return &dns.A{
			Hdr: dns.RR_Header{Name: u.name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: uint32(u.ttl)},
			A:   ip4,
		}
	}

	if ip6 := ip.To16(); ip6 != nil {
		return &dns.AAAA{
			Hdr:  dns.RR_Header{Name: u.name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: uint32(u.ttl)},
			AAAA: ip6,
		}
	}

	return nil
}
func (u *Update) buildState(addrs *AddrSet) (state updateState, err error) {
	state.updateZone = u.zone
	state.removeNames = []dns.RR{
		&dns.RR_Header{Name: u.name},
	}

	addrs.Each(func(ip net.IP) {
		state.inserts = append(state.inserts, u.buildAddr(ip))
	})

	return
}
func (u *Update) buildQuery(state updateState) *dns.Msg {
	var msg = new(dns.Msg)

	msg.SetUpdate(state.updateZone)
	msg.RemoveName(state.removeNames)
	msg.Insert(state.inserts)

	if u.tsig != nil {
		for keyName, _ := range u.tsig {
			msg.SetTsig(keyName, string(u.tsigAlgo), TSIG_FUDGE_SECONDS, time.Now().Unix())
		}
	}

	return msg
}

func (u *Update) query(msg *dns.Msg) (*dns.Msg, error) {
	var client = new(dns.Client)

	client.DialTimeout = u.timeout
	client.ReadTimeout = u.timeout
	client.WriteTimeout = u.timeout

	if u.tsig != nil {
		client.TsigSecret = u.tsig
	}

	msg, _, err := client.Exchange(msg, u.server)

	if err != nil {
		return msg, fmt.Errorf("dns:Client.Exchange ... %v: %v", u.server, err)
	}

	if msg.Rcode == dns.RcodeSuccess {
		return msg, nil
	} else {
		return msg, fmt.Errorf("rcode=%v", dns.RcodeToString[msg.Rcode])
	}
}

func (u *Update) update(state updateState) error {
	q := u.buildQuery(state)

	if u.verbose {
		log.Printf("update query:\n%v", q)
	} else {
		log.Printf("update query...")
	}

	r, err := u.query(q)

	if err != nil {
		return err
	}

	if u.verbose {
		log.Printf("update answer:\n%v", r)
	} else {
		log.Printf("update answer")
	}

	return nil
}

func (u *Update) run() {
	var state updateState
	var retry = 0
	var retryTimer = time.NewTimer(time.Duration(0))
	var updateChan = u.updateChan
	var updateError error

	defer func() { u.doneChan <- updateError }()

	for {
		select {
		case updateState, running := <-updateChan:
			if running {
				// Update() called
				state = updateState

			} else if retry > 0 {
				// Done() called, but still waiting for retry...
				updateChan = nil
				continue

			} else {
				// Done() called, no retrys or updates remaining
				return
			}

		case <-retryTimer.C:
			if retry == 0 {
				// spurious timer event..
				continue
			}

			// trigger retry
		}

		if err := u.update(state); err != nil {
			log.Printf("update (retry=%v) error: %v", retry, err)

			updateError = err
			retry++
		} else {
			// success
			updateError = nil
			retry = 0
		}

		if retry == 0 && updateChan == nil {
			// done, no more updates
			return

		} else if retry == 0 {
			// wait for next update
			retryTimer.Stop()

		} else {
			retryTimeout := time.Duration(retry * int(u.retry))

			// wait for next retry
			// TODO: exponential backoff?
			retryTimer.Reset(retryTimeout)

			log.Printf("update retry in %v...", retryTimeout)
		}
	}
}

func (u *Update) Start() {
	u.updateChan = make(chan updateState)

	go u.run()
}

func (u *Update) Update(addrs *AddrSet) error {
	if state, err := u.buildState(addrs); err != nil {
		return err
	} else {
		u.updateChan <- state
	}

	return nil
}

func (u *Update) Done() error {
	u.doneChan = make(chan error)

	close(u.updateChan)

	return <-u.doneChan
}
