package networking

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

	"nadir/internal/oscmd"
)

// ifupdownBackend implements backend via classic Debian ifupdown. Configuration
// is done by writing stanza files under interfacesDDir. Each managed interface
// gets "nadir-<iface>" so we never touch /etc/network/interfaces itself.
//
// Prerequisite: /etc/network/interfaces must contain
//
//	source /etc/network/interfaces.d/*
//
// for these stanzas to take effect. If missing, Apply checks for this and adds
// the source directive (same auto-provision pattern as the PAM service).
type ifupdownBackend struct{}

var (
	interfacesFile = "/etc/network/interfaces"
	interfacesDDir = "/etc/network/interfaces.d"
)

func (b *ifupdownBackend) Name() string { return "ifupdown" }

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

func (b *ifupdownBackend) Snapshot(ctx context.Context, iface string) (IfaceConfig, error) {
	path := ifupdownFile(iface)
	data, err := os.ReadFile(path)
	if err != nil {
		// No nadir-managed stanza → fall back to live ip output, assume DHCP.
		return snapshotFromIP(ctx, iface)
	}
	return parseIfupdownStanza(string(data)), nil
}

// parseIfupdownStanza extracts IfaceConfig from an ifupdown stanza file. A file
// may hold both an "inet" (IPv4) and an "inet6" (IPv6) stanza for the interface;
// family tracks which one the indented keys below belong to.
func parseIfupdownStanza(content string) IfaceConfig {
	cfg := IfaceConfig{Method: "dhcp"}
	family := "inet"

	for line := range strings.SplitSeq(content, "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}

		switch fields[0] {
		case "iface":
			// "iface eth0 inet static" / "iface eth0 inet6 auto"
			if len(fields) >= 4 {
				family = fields[2]
				switch family {
				case "inet":
					if fields[3] == "static" {
						cfg.Method = "static"
					}
				case "inet6":
					switch fields[3] {
					case "static":
						v6(&cfg).Method = "static"
					default: // auto / dhcp / manual → treat as autoconf
						v6(&cfg).Method = "auto"
					}
				}
			}
		case "address":
			addr, prefix := splitCIDR(fields[1])
			if family == "inet6" {
				g := v6(&cfg)
				g.Address = addr
				if prefix > 0 {
					g.Prefix = prefix
				}
			} else if addr != "" {
				cfg.Address = addr
				if prefix > 0 {
					cfg.Prefix = prefix
				}
			}
		case "gateway":
			if family == "inet6" {
				v6(&cfg).Gateway = fields[1]
			} else {
				cfg.Gateway = fields[1]
			}
		case "dns-nameservers":
			cfg.DNS = append(cfg.DNS, fields[1:]...)
		case "up":
			// "up ip route add 10.0.0.0/24 via 192.168.1.1"
			r := parseUpRoute(fields[1:])
			if r.Destination != "" {
				cfg.Routes = append(cfg.Routes, r)
			}
		}
	}
	return cfg
}

// parseUpRoute extracts a Route from "ip route add <dst> via <gw>" post-up commands.
func parseUpRoute(args []string) Route {
	// Expected: ["ip", "route", "add", "10.0.0.0/24", "via", "192.168.1.1"]
	if len(args) < 6 || args[0] != "ip" || args[1] != "route" || args[2] != "add" {
		return Route{}
	}
	r := Route{Destination: args[3]}
	for i, a := range args {
		if a == "via" && i+1 < len(args) {
			r.Gateway = args[i+1]
			break
		}
	}
	return r
}

func (b *ifupdownBackend) Apply(ctx context.Context, iface string, cfg IfaceConfig) error {
	// Ensure interfaces.d exists and is sourced.
	if err := ensureSourceDirective(); err != nil {
		return err
	}

	content := renderIfupdownStanza(iface, cfg)
	path := ifupdownFile(iface)

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

	// Bring the interface down and back up with the new config.
	// ifdown may fail if the interface wasn't previously managed — ignore that.
	_, _ = oscmd.RunContext(ctx, "ifdown", "--force", "--", iface)
	if _, err := oscmd.RunContext(ctx, "ifup", "--", iface); err != nil {
		return fmt.Errorf("ifup %s: %w", iface, err)
	}
	return nil
}

// ensureSourceDirective checks that /etc/network/interfaces contains a source
// line for interfaces.d. If not, appends one (same auto-provision pattern as
// the PAM service file).
func ensureSourceDirective() error {
	data, err := os.ReadFile(interfacesFile)
	if err != nil {
		// If the file doesn't exist at all, that's a broken system; don't create it.
		return fmt.Errorf("read %s: %w", interfacesFile, err)
	}

	content := string(data)
	// Check for any source line pointing at interfaces.d.
	for line := range strings.SplitSeq(content, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "source") && strings.Contains(line, "interfaces.d") {
			return nil // already present
		}
	}

	// Append the source directive.
	addition := "\n# Added by nadir to pick up per-interface stanzas.\nsource " + interfacesDDir + "/*\n"
	if err := os.WriteFile(interfacesFile, []byte(content+addition), 0644); err != nil {
		return fmt.Errorf("append source directive to %s: %w", interfacesFile, err)
	}
	return nil
}

// renderIfupdownStanza builds the content for an ifupdown stanza file.
func renderIfupdownStanza(iface string, cfg IfaceConfig) string {
	var b strings.Builder
	b.WriteString("# Managed by nadir — do not edit manually.\n")

	switch cfg.Method {
	case "dhcp":
		fmt.Fprintf(&b, "auto %s\n", iface)
		fmt.Fprintf(&b, "iface %s inet dhcp\n", iface)
	case "static":
		fmt.Fprintf(&b, "auto %s\n", iface)
		fmt.Fprintf(&b, "iface %s inet static\n", iface)
		fmt.Fprintf(&b, "    address %s/%d\n", cfg.Address, cfg.Prefix)
		if cfg.Gateway != "" {
			fmt.Fprintf(&b, "    gateway %s\n", cfg.Gateway)
		}
	}

	if len(cfg.DNS) > 0 {
		fmt.Fprintf(&b, "    dns-nameservers %s\n", strings.Join(cfg.DNS, " "))
	}

	for _, r := range cfg.Routes {
		fmt.Fprintf(&b, "    up ip route add %s via %s\n", r.Destination, r.Gateway)
		fmt.Fprintf(&b, "    down ip route del %s via %s\n", r.Destination, r.Gateway)
	}

	// IPv6 goes in its own inet6 stanza (the "auto eth0" above covers both).
	if cfg.IPv6 != nil {
		switch cfg.IPv6.Method {
		case "auto":
			fmt.Fprintf(&b, "iface %s inet6 auto\n", iface)
		case "static":
			fmt.Fprintf(&b, "iface %s inet6 static\n", iface)
			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)
			}
		case "ignore":
			// no inet6 stanza
		}
	}

	return b.String()
}

func (b *ifupdownBackend) SetLinkUp(ctx context.Context, iface string) error {
	_, err := oscmd.RunContext(ctx, "ifup", "--", iface)
	return err
}

func (b *ifupdownBackend) SetLinkDown(ctx context.Context, iface string) error {
	_, err := oscmd.RunContext(ctx, "ifdown", "--", iface)
	return err
}
