package networking

import (
	"context"
	"fmt"
	"net/netip"
	"os"
	"path/filepath"
	"strings"

	"nadir/internal/oscmd"
)

// networkdBackend implements backend via systemd-networkd. Configuration is done
// by writing .network files under networkdDir. Each managed interface gets its
// own file named "90-nadir-<iface>.network" — the 90 prefix puts nadir's config
// after most distro defaults, and the "nadir-" infix ensures we never clobber
// distro-provided files.
type networkdBackend struct{}

var networkdDir = "/etc/systemd/network"

func (b *networkdBackend) Name() string { return "networkd" }

// networkdFile returns the path for the nadir-managed .network file for iface.
// iface is already validated by validateIface to prevent path traversal.
func networkdFile(iface string) string {
	return filepath.Join(networkdDir, "90-nadir-"+iface+".network")
}

func (b *networkdBackend) Snapshot(ctx context.Context, iface string) (IfaceConfig, error) {
	path := networkdFile(iface)
	data, err := os.ReadFile(path)
	if err != nil {
		// No nadir-managed file exists. Fall back to live ip output and assume
		// DHCP — the safest rollback assumption (reverts to "whatever the
		// system was doing before nadir touched it").
		return snapshotFromIP(ctx, iface)
	}
	return parseNetworkdFile(string(data)), nil
}

// snapshotFromIP captures the current state from ip -j, assuming DHCP as the
// method since there's no way to tell from live output.
func snapshotFromIP(ctx context.Context, iface string) (IfaceConfig, error) {
	cfg := IfaceConfig{Method: "dhcp"}

	// Grab addresses.
	out, err := oscmd.RunContext(ctx, "ip", "-j", "addr", "show", "--", iface)
	if err != nil {
		return cfg, nil // interface may not exist yet; DHCP fallback is fine
	}
	ifaces, err := parseInterfaces(out)
	if err != nil || len(ifaces) == 0 {
		return cfg, nil
	}

	// If there are IPv4 addresses, capture the first one.
	if len(ifaces[0].IPv4) > 0 {
		addr, prefix := splitCIDR(ifaces[0].IPv4[0])
		cfg.Method = "static"
		cfg.Address = addr
		cfg.Prefix = prefix
	}

	// Capture a global IPv6 address if present, skipping link-local (fe80::/10),
	// so a rollback restores the interface's real IPv6 state.
	cfg.IPv6 = &IPv6Config{Method: "auto"}
	for _, c := range ifaces[0].IPv6 {
		addr, prefix := splitCIDR(c)
		if ip, err := netip.ParseAddr(addr); err == nil && !ip.IsLinkLocalUnicast() {
			cfg.IPv6 = &IPv6Config{Method: "static", Address: addr, Prefix: prefix}
			break
		}
	}

	// Grab the default gateway for this interface.
	routeOut, err := oscmd.RunContext(ctx, "ip", "-j", "route", "show", "dev", "--", iface)
	if err == nil {
		routes, _ := parseRoutes(routeOut)
		for _, r := range routes {
			if r.Destination == "default" && r.Gateway != "" {
				cfg.Gateway = r.Gateway
				break
			}
		}
	}

	// Grab DNS from /etc/resolv.conf
	if data, err := os.ReadFile(resolvConf); err == nil {
		cfg.DNS = parseResolv(string(data))
	}

	return cfg, nil
}

// parseNetworkdFile extracts IfaceConfig from a systemd .network file.
func parseNetworkdFile(content string) IfaceConfig {
	cfg := IfaceConfig{Method: "dhcp"}
	section := ""

	for line := range strings.SplitSeq(content, "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
			section = line
			continue
		}

		key, val, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		key = strings.TrimSpace(key)
		val = strings.TrimSpace(val)

		if section == "[Network]" {
			switch key {
			case "DHCP":
				if val == "yes" || val == "ipv4" {
					cfg.Method = "dhcp"
				}
			case "Address":
				addr, prefix := splitCIDR(val)
				if ip, err := netip.ParseAddr(addr); err == nil && !ip.Is4() {
					g := v6(&cfg)
					g.Method = "static"
					g.Address = addr
					g.Prefix = prefix
				} else if addr != "" {
					cfg.Method = "static"
					cfg.Address = addr
					cfg.Prefix = prefix
				}
			case "Gateway":
				if ip, err := netip.ParseAddr(val); err == nil && !ip.Is4() {
					v6(&cfg).Gateway = val
				} else {
					cfg.Gateway = val
				}
			case "DNS":
				for s := range strings.FieldsSeq(val) {
					cfg.DNS = append(cfg.DNS, s)
				}
			case "IPv6AcceptRA":
				if val == "yes" {
					if g := v6(&cfg); g.Method != "static" {
						g.Method = "auto"
					}
				}
			case "LinkLocalAddressing":
				if val == "no" {
					v6(&cfg).Method = "ignore"
				}
			}
		}
	}

	// Parse [Route] sections separately.
	cfg.Routes = parseNetworkdRoutes(content)
	return cfg
}

// parseNetworkdRoutes extracts Route entries from [Route] sections.
func parseNetworkdRoutes(content string) []Route {
	var routes []Route
	var inRoute bool
	var current Route

	for line := range strings.SplitSeq(content, "\n") {
		line = strings.TrimSpace(line)

		if strings.HasPrefix(line, "[") {
			// Flush any pending route when entering a new section.
			if inRoute && (current.Destination != "" || current.Gateway != "") {
				routes = append(routes, current)
			}
			inRoute = line == "[Route]"
			current = Route{}
			continue
		}

		if !inRoute {
			continue
		}

		key, val, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		switch strings.TrimSpace(key) {
		case "Destination":
			current.Destination = strings.TrimSpace(val)
		case "Gateway":
			current.Gateway = strings.TrimSpace(val)
		}
	}
	// Flush last route if we ended inside a [Route] section.
	if inRoute && (current.Destination != "" || current.Gateway != "") {
		routes = append(routes, current)
	}
	return routes
}

func (b *networkdBackend) Apply(ctx context.Context, iface string, cfg IfaceConfig) error {
	content := renderNetworkdFile(iface, cfg)
	path := networkdFile(iface)

	if err := os.MkdirAll(networkdDir, 0755); err != nil {
		return fmt.Errorf("mkdir %s: %w", networkdDir, err)
	}
	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
		return fmt.Errorf("write %s: %w", path, err)
	}

	// Reload networkd and reconfigure the specific interface.
	if _, err := oscmd.RunContext(ctx, "networkctl", "reload"); err != nil {
		return fmt.Errorf("networkctl reload: %w", err)
	}
	if _, err := oscmd.RunContext(ctx, "networkctl", "reconfigure", "--", iface); err != nil {
		return fmt.Errorf("networkctl reconfigure %s: %w", iface, err)
	}
	return nil
}

// renderNetworkdFile builds the INI content for a systemd .network file.
func renderNetworkdFile(iface string, cfg IfaceConfig) string {
	var b strings.Builder

	b.WriteString("# Managed by nadir — do not edit manually.\n")
	b.WriteString("[Match]\n")
	fmt.Fprintf(&b, "Name=%s\n\n", iface)

	b.WriteString("[Network]\n")
	switch cfg.Method {
	case "dhcp":
		b.WriteString("DHCP=yes\n")
	case "static":
		b.WriteString("DHCP=no\n")
		fmt.Fprintf(&b, "Address=%s/%d\n", cfg.Address, cfg.Prefix)
		if cfg.Gateway != "" {
			fmt.Fprintf(&b, "Gateway=%s\n", cfg.Gateway)
		}
	}
	if cfg.IPv6 != nil {
		switch cfg.IPv6.Method {
		case "static":
			fmt.Fprintf(&b, "Address=%s/%d\n", cfg.IPv6.Address, cfg.IPv6.Prefix)
			if cfg.IPv6.Gateway != "" {
				fmt.Fprintf(&b, "Gateway=%s\n", cfg.IPv6.Gateway)
			}
			b.WriteString("IPv6AcceptRA=no\n")
		case "auto":
			b.WriteString("IPv6AcceptRA=yes\n")
		case "ignore":
			b.WriteString("LinkLocalAddressing=no\nIPv6AcceptRA=no\n")
		}
	}

	for _, dns := range cfg.DNS {
		fmt.Fprintf(&b, "DNS=%s\n", dns)
	}

	for _, r := range cfg.Routes {
		b.WriteString("\n[Route]\n")
		fmt.Fprintf(&b, "Destination=%s\n", r.Destination)
		fmt.Fprintf(&b, "Gateway=%s\n", r.Gateway)
	}

	return b.String()
}

func (b *networkdBackend) SetLinkUp(ctx context.Context, iface string) error {
	// Note: ip link set parses DEVICE positionally, so -- is technically ignored
	// by ip but included here for consistency with other oscmd calls.
	_, err := oscmd.RunContext(ctx, "ip", "link", "set", "--", iface, "up")
	return err
}

func (b *networkdBackend) SetLinkDown(ctx context.Context, iface string) error {
	_, err := oscmd.RunContext(ctx, "ip", "link", "set", "--", iface, "down")
	return err
}
