package lanresolver

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"
	"time"
)

// Loop periodically reconciles the split-horizon resolver for every guest the agent has provisioned.
// It is the IP-freshness mechanism: the guest stays DHCP/ephemeral, so its LAN IP can move (lease
// renewal, MAC reset); the loop re-discovers it each tick and updates the resolver only on change.
// Like the storage watchdog / PBS verify loop, it owns its own cadence and is not journaled/gated.
type Loop struct {
	mgr      *Manager
	stateDir string // agent state dir; provisioned guests live under <stateDir>/guests/<vmid>/
	interval time.Duration
	logger   *slog.Logger
}

// NewLoop builds the reconcile loop. stateDir is the agent state dir (default /var/lib/felhom-agent).
func NewLoop(mgr *Manager, stateDir string, interval time.Duration, logger *slog.Logger) *Loop {
	if interval <= 0 {
		interval = 5 * time.Minute
	}
	return &Loop{mgr: mgr, stateDir: stateDir, interval: interval, logger: logger}
}

// Run ensures dnsmasq + the base config once, then reconciles all guests immediately and every
// interval until ctx is cancelled.
func (l *Loop) Run(ctx context.Context) error {
	if err := l.mgr.EnsureDnsmasq(ctx); err != nil {
		// Non-fatal: log and keep trying on the ticker (a transient apt/systemd hiccup must not kill
		// the loop). The host's own resolution is unaffected (we never touch /etc/resolv.conf).
		l.logger.Warn("lanresolver: EnsureDnsmasq failed — will retry", "err", err)
	}
	l.reconcileAll(ctx)
	t := time.NewTicker(l.interval)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-t.C:
			l.reconcileAll(ctx)
		}
	}
}

func (l *Loop) reconcileAll(ctx context.Context) {
	guests, err := l.listGuests()
	if err != nil {
		l.logger.Warn("lanresolver: cannot list provisioned guests", "err", err)
		return
	}
	for _, g := range guests {
		if err := l.mgr.ReconcileGuest(ctx, g.VMID, g.CustomerID); err != nil {
			l.logger.Warn("lanresolver: reconcile failed", "vmid", g.VMID, "err", err)
		}
	}
}

type guestRef struct {
	VMID       int
	CustomerID string
}

// listGuests enumerates the provisioned guests under <stateDir>/guests/<vmid>/bootstrap/bootstrap.json,
// reading customer.id from each bootstrap. Dirs without a readable bootstrap are skipped.
func (l *Loop) listGuests() ([]guestRef, error) {
	root := filepath.Join(l.stateDir, "guests")
	entries, err := os.ReadDir(root)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil // no guests provisioned yet
		}
		return nil, err
	}
	var out []guestRef
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		vmid, err := strconv.Atoi(e.Name())
		if err != nil {
			continue
		}
		cid := readCustomerID(filepath.Join(root, e.Name(), "bootstrap", "bootstrap.json"))
		out = append(out, guestRef{VMID: vmid, CustomerID: cid})
	}
	return out, nil
}

// CustomerID returns a provisioned guest's customer id from its bootstrap (best-effort "").
func CustomerID(stateDir string, vmid int) string {
	return readCustomerID(filepath.Join(stateDir, "guests", strconv.Itoa(vmid), "bootstrap", "bootstrap.json"))
}

func readCustomerID(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	var b struct {
		Customer struct {
			ID string `json:"id"`
		} `json:"customer"`
	}
	if err := json.Unmarshal(data, &b); err != nil {
		return ""
	}
	return b.Customer.ID
}

// DeriveHostIP extracts the host LAN IP from the local-API listen address (host:port). Returns "" if
// it can't be parsed (caller then requires an explicit config value).
func DeriveHostIP(listenAddr string) string {
	h, _, err := splitHostPort(listenAddr)
	if err != nil || h == "" || h == "0.0.0.0" {
		return ""
	}
	return h
}

// splitHostPort is net.SplitHostPort wrapped to avoid importing net in tests that don't need it.
func splitHostPort(s string) (string, string, error) {
	i := lastIndexByte(s, ':')
	if i < 0 {
		return "", "", fmt.Errorf("missing port in %q", s)
	}
	return s[:i], s[i+1:], nil
}

func lastIndexByte(s string, b byte) int {
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == b {
			return i
		}
	}
	return -1
}
