// Package dnsmasq manages the process lifecycle of the dnsmasq(8) DHCP server.
package dnsmasq

import (
	"bufio"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"
)

// Process is a handle for a dnsmasq(8) process.
type Process struct {
	killed  bool          // whether Kill was called
	done    chan struct{} // closed when Done() is called
	wait    chan struct{} // closed when Wait() returns
	dnsmasq *exec.Cmd

	mu      sync.Mutex
	actions []string
}

var dhcpActionRe = regexp.MustCompile(` (DHCP[^(]+\(.*)$`)

// Run starts a dnsmasq(8) process and returns a handle to it.
func Run(t *testing.T, args ...string) *Process {
	const ns = "ns0" // TODO: configurable?

	ready, err := ioutil.TempFile("", "router7")
	if err != nil {
		t.Fatal(err)
	}
	ready.Close()                 // dnsmasq will re-create the file anyway
	defer os.Remove(ready.Name()) // dnsmasq does not clean up its pid file

	// iface, err := net.InterfaceByName("veth0a")
	// if err != nil {
	// 	t.Fatal(err)
	// }

	p := &Process{
		wait: make(chan struct{}),
	}

	p.dnsmasq = exec.Command("ip", "netns", "exec", ns, "dnsmasq",
		"--keep-in-foreground", // cannot use --no-daemon because we need --pid-file
		"--log-facility=-",     // log to stderr
		"--pid-file="+ready.Name(),
		"--bind-interfaces",
		"--interface=veth0b",
		"--dhcp-range=192.168.23.2,192.168.23.10",
		"--dhcp-range=::1,::10,constructor:veth0b",
		"--dhcp-authoritative", // eliminate timeouts
		"--no-ping",            // disable ICMP confirmation of unused addresses to eliminate tedious timeout
		"--leasefile-ro",       // do not create a lease database
	)

	p.dnsmasq.Stdout = os.Stdout
	stderr := make(chan string)
	r, w := io.Pipe()
	scanner := bufio.NewScanner(r)
	go func() {
		for scanner.Scan() {
			stderr <- scanner.Text()
		}
		close(stderr)
	}()
	p.dnsmasq.Stderr = w
	//mac := iface.HardwareAddr.String()
	go func() {
		for line := range stderr {
			fmt.Printf("dnsmasq log line: %s\n", line)
			if !strings.HasPrefix(line, "dnsmasq-dhcp") {
				continue
			}
			// if !strings.Contains(line, mac) {
			// 	continue
			// }
			matches := dhcpActionRe.FindStringSubmatch(line)
			if matches == nil {
				continue
			}
			p.mu.Lock()
			p.actions = append(p.actions, matches[1])
			p.mu.Unlock()
		}
	}()
	if err := p.dnsmasq.Start(); err != nil {
		t.Fatal(err)
	}

	p.done = make(chan struct{})
	go func() {
		err := p.dnsmasq.Wait()
		close(p.wait)
		select {
		case <-p.done:
			return // test done, any errors are from our Kill()
		default:
			t.Fatalf("dnsmasq exited prematurely: %v", err)
		}
	}()

	// TODO(later): use inotify instead of polling
	// Wait for dnsmasq to write its process id, at which point it is already
	// listening for requests.
	for {
		b, err := ioutil.ReadFile(ready.Name())
		if err != nil {
			t.Fatal(err)
		}
		if strings.TrimSpace(string(b)) == strconv.Itoa(p.dnsmasq.Process.Pid) {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	return p
}

// Kill shuts down the dnsmasq(8) process and returns once waitpid returns.
func (p *Process) Kill() {
	if p.killed {
		return
	}
	p.killed = true
	close(p.done)
	p.dnsmasq.Process.Kill()
	<-p.wait
}

// Actions returns a string slice of dnsmasq(8) actions (as per its stderr log)
// received up until now. Use Kill before calling Actions to force a log flush.
func (p *Process) Actions() []string {
	p.mu.Lock()
	defer p.mu.Unlock()
	result := make([]string, len(p.actions))
	copy(result, p.actions)
	return result
}
