package dhcp

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"sync"
)

const (
	ConfigFile = "/etc/dnsmasq.d/alpine-router-dhcp.conf"
	StateFile  = "/var/lib/alpine-router/dhcp.json"
)

// Pool describes a DHCP pool tied to one interface/subnet.
type Pool struct {
	Interface  string   `json:"interface"`
	Enabled    bool     `json:"enabled"`
	Subnet     string   `json:"subnet"`      // e.g. "172.16.54.0"
	Netmask    string   `json:"netmask"`     // e.g. "255.255.255.0"
	RangeStart string   `json:"range_start"` // e.g. "172.16.54.100"
	RangeEnd   string   `json:"range_end"`   // e.g. "172.16.54.200"
	Router     string   `json:"router"`      // option routers (advertised gateway)
	DNS        []string `json:"dns"`         // option domain-name-servers
	LeaseTime  int      `json:"lease_time"`  // seconds, 0 → 86400
}

// Config is the top-level DHCP configuration persisted on disk.
type Config struct {
	Enabled bool   `json:"enabled"`
	Pools   []Pool `json:"pools"`
}

// StaticBinding represents a DHCP host reservation (MAC → fixed IP).
type StaticBinding struct {
	MAC  string `json:"mac"`
	Host string `json:"host"`
	IP   string `json:"ip"`
}

var mu sync.Mutex

// IsInstalled reports whether dnsmasq binary is available.
func IsInstalled() bool {
	_, err := exec.LookPath("dnsmasq")
	return err == nil
}

// Load reads the config from the state file.
// Returns an empty config if the file does not exist yet.
func Load() (*Config, error) {
	mu.Lock()
	defer mu.Unlock()

	data, err := os.ReadFile(StateFile)
	if err != nil {
		if os.IsNotExist(err) {
			return &Config{Pools: []Pool{}}, nil
		}
		return nil, fmt.Errorf("read state: %w", err)
	}
	var cfg Config
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse state: %w", err)
	}
	if cfg.Pools == nil {
		cfg.Pools = []Pool{}
	}
	return &cfg, nil
}

// Save writes the config to the state file (JSON, not dnsmasq.conf).
func Save(cfg *Config) error {
	mu.Lock()
	defer mu.Unlock()

	if err := os.MkdirAll("/var/lib/alpine-router", 0755); err != nil {
		return fmt.Errorf("mkdir state dir: %w", err)
	}
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(StateFile, data, 0644)
}

// WriteConfigs generates /etc/dnsmasq.d/alpine-router-dhcp.conf.
// dnsmasq is used in DHCP-only mode (port=0 disables DNS resolver).
func WriteConfigs(cfg *Config) error {
	if err := os.MkdirAll("/etc/dnsmasq.d", 0755); err != nil {
		return fmt.Errorf("mkdir /etc/dnsmasq.d: %w", err)
	}

	var sb strings.Builder
	sb.WriteString("# Generated by alpine-router — do not edit manually\n")
	sb.WriteString("# dnsmasq running in DHCP-only mode (DNS disabled)\n\n")
	sb.WriteString("port=0\n")          // disable DNS
	sb.WriteString("bind-interfaces\n") // only listen on specified interfaces
	sb.WriteString("\n")

	for _, pool := range cfg.Pools {
		if !pool.Enabled {
			continue
		}

		leaseTime := pool.LeaseTime
		if leaseTime <= 0 {
			leaseTime = 86400
		}

		tag := pool.Interface // use interface name as tag for option scoping

		fmt.Fprintf(&sb, "# Pool: %s\n", pool.Interface)
		fmt.Fprintf(&sb, "interface=%s\n", pool.Interface)

		if pool.RangeStart != "" && pool.RangeEnd != "" {
			fmt.Fprintf(&sb, "dhcp-range=set:%s,%s,%s,%s,%ds\n",
				tag, pool.RangeStart, pool.RangeEnd, pool.Netmask, leaseTime)
		}

		if pool.Router != "" {
			fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:router,%s\n", tag, pool.Router)
		}

		if len(pool.DNS) > 0 {
			fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:dns-server,%s\n",
				tag, strings.Join(pool.DNS, ","))
		}

		sb.WriteString("\n")
	}

	return os.WriteFile(ConfigFile, []byte(sb.String()), 0644)
}

// WriteConfigsWithBindings generates dnsmasq config including static host reservations.
func WriteConfigsWithBindings(cfg *Config, bindings []StaticBinding) error {
	if err := os.MkdirAll("/etc/dnsmasq.d", 0755); err != nil {
		return fmt.Errorf("mkdir /etc/dnsmasq.d: %w", err)
	}

	var sb strings.Builder
	sb.WriteString("# Generated by alpine-router — do not edit manually\n")
	sb.WriteString("# dnsmasq running in DHCP-only mode (DNS disabled)\n\n")
	sb.WriteString("port=0\n")
	sb.WriteString("bind-interfaces\n")
	sb.WriteString("\n")

	for _, pool := range cfg.Pools {
		if !pool.Enabled {
			continue
		}

		leaseTime := pool.LeaseTime
		if leaseTime <= 0 {
			leaseTime = 86400
		}

		tag := pool.Interface

		fmt.Fprintf(&sb, "# Pool: %s\n", pool.Interface)
		fmt.Fprintf(&sb, "interface=%s\n", pool.Interface)

		if pool.RangeStart != "" && pool.RangeEnd != "" {
			fmt.Fprintf(&sb, "dhcp-range=set:%s,%s,%s,%s,%ds\n",
				tag, pool.RangeStart, pool.RangeEnd, pool.Netmask, leaseTime)
		}

		if pool.Router != "" {
			fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:router,%s\n", tag, pool.Router)
		}

		if len(pool.DNS) > 0 {
			fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:dns-server,%s\n",
				tag, strings.Join(pool.DNS, ","))
		}

		sb.WriteString("\n")
	}

	if len(bindings) > 0 {
		sb.WriteString("# Static host reservations\n")
		for _, b := range bindings {
			if b.MAC == "" || b.IP == "" {
				continue
			}
			if b.Host != "" {
				fmt.Fprintf(&sb, "dhcp-host=%s,%s,%s,infinite\n", b.MAC, b.IP, b.Host)
			} else {
				fmt.Fprintf(&sb, "dhcp-host=%s,%s,infinite\n", b.MAC, b.IP)
			}
		}
		sb.WriteString("\n")
	}

	return os.WriteFile(ConfigFile, []byte(sb.String()), 0644)
}

// ServiceStatus returns true if dnsmasq is running.
func ServiceStatus() bool {
	return exec.Command("rc-service", "dnsmasq", "status").Run() == nil
}

// ServiceRestart restarts the dnsmasq service.
func ServiceRestart() error {
	out, err := exec.Command("rc-service", "dnsmasq", "restart").CombinedOutput()
	if err != nil {
		return fmt.Errorf("restart dnsmasq: %s", strings.TrimSpace(string(out)))
	}
	return nil
}

// ServiceStop stops the dnsmasq service.
func ServiceStop() error {
	out, err := exec.Command("rc-service", "dnsmasq", "stop").CombinedOutput()
	if err != nil {
		return fmt.Errorf("stop dnsmasq: %s", strings.TrimSpace(string(out)))
	}
	return nil
}
