package networking

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"nadir/internal/oscmd"
)

// nmcliBackend implements backend via NetworkManager's nmcli CLI.
type nmcliBackend struct{}

func (b *nmcliBackend) Name() string { return "nmcli" }

// connForIface resolves a network interface name to the NM connection name
// that owns it. Returns an error if the interface has no active connection.
//
// nmcli -t uses ':' as the field separator. Connection names can contain colons
// (e.g. "VLAN:100"), but Linux device names cannot, so we split on the last
// colon to get the device and treat everything before it as the connection name.
func connForIface(ctx context.Context, iface string) (string, error) {
	out, err := oscmd.RunContext(ctx, "nmcli", "-t", "-f", "NAME,DEVICE", "con", "show", "--active")
	if err != nil {
		return "", fmt.Errorf("nmcli con show: %w", err)
	}
	for line := range strings.SplitSeq(out, "\n") {
		// Split on the last colon: "conn:name:eth0" → ("conn:name", "eth0")
		idx := strings.LastIndex(line, ":")
		if idx < 0 {
			continue
		}
		name, dev := line[:idx], line[idx+1:]
		if dev == iface {
			return name, nil
		}
	}
	return "", fmt.Errorf("no active NM connection found for interface %s", iface)
}

func (b *nmcliBackend) Snapshot(ctx context.Context, iface string) (IfaceConfig, error) {
	conn, err := connForIface(ctx, iface)
	if err != nil {
		// No managed connection → assume DHCP (safest rollback assumption).
		return IfaceConfig{Method: "dhcp"}, nil
	}

	// nmcli's `con show <NAME>` parser does NOT honor `--` as an end-of-options
	// separator; passing it makes nmcli look for a connection literally named
	// "--" and fail. `conn` comes from nmcli's own active-connections list (see
	// connForIface), so it's already validated — no shell-metacharacter risk.
	// Same applies to con up / con down / con modify below.
	out, err := oscmd.RunContext(ctx, "nmcli", "-t", "-f",
		"ipv4.method,ipv4.addresses,ipv4.gateway,ipv4.dns,ipv4.routes,ipv6.method,ipv6.addresses,ipv6.gateway",
		"con", "show", conn)
	if err != nil {
		// nmcli can refuse the read (connection state odd, permission, terse-mode
		// quirks). Fall back to DHCP defaults so the prefill endpoint still
		// returns a usable form, mirroring the networkd / ifupdown fallback.
		return IfaceConfig{Method: "dhcp"}, nil
	}

	return parseNmcliSnapshot(out), nil
}

// parseNmcliSnapshot parses the terse output of `nmcli -t -f ... con show`.
// Fields are colon-separated key:value lines. Multi-valued fields (addresses,
// dns, routes) use comma separation within the value.
func parseNmcliSnapshot(out string) IfaceConfig {
	cfg := IfaceConfig{Method: "dhcp"}
	for line := range strings.SplitSeq(out, "\n") {
		key, val, ok := strings.Cut(line, ":")
		if !ok || val == "" || val == "--" {
			continue
		}
		switch key {
		case "ipv4.method":
			if val == "manual" {
				cfg.Method = "static"
			} else {
				cfg.Method = "dhcp"
			}
		case "ipv4.addresses":
			// "192.168.1.10/24" or "192.168.1.10/24, 10.0.0.1/8"
			for _, part := range splitTrim(val, ",") {
				addr, prefix := splitCIDR(part)
				if addr != "" {
					cfg.Address = addr
					cfg.Prefix = prefix
				}
			}
		case "ipv4.gateway":
			cfg.Gateway = val
		case "ipv4.dns":
			cfg.DNS = append(cfg.DNS, splitTrim(val, ",")...)
		case "ipv4.routes":
			// "dst=10.0.0.0/24, nh=192.168.1.1; dst=default, nh=192.168.1.1"
			// or simpler "{ dst = 10.0.0.0/24, nh = 192.168.1.1 }"
			cfg.Routes = parseNmcliRoutes(val)
		case "ipv6.method":
			v6(&cfg).Method = nmcliV6Method(val)
		case "ipv6.addresses":
			for _, part := range splitTrim(val, ",") {
				if addr, prefix := splitCIDR(part); addr != "" {
					v6(&cfg).Address = addr
					v6(&cfg).Prefix = prefix
				}
			}
		case "ipv6.gateway":
			v6(&cfg).Gateway = val
		}
	}
	return cfg
}

// v6 lazily allocates the IPv6 block so a snapshot captures the live IPv6 state
// (defaulting to "auto") even when only some ipv6.* fields are present.
func v6(cfg *IfaceConfig) *IPv6Config {
	if cfg.IPv6 == nil {
		cfg.IPv6 = &IPv6Config{Method: "auto"}
	}
	return cfg.IPv6
}

// nmcliV6Method maps nmcli's ipv6.method to our vocabulary.
func nmcliV6Method(val string) string {
	switch val {
	case "manual":
		return "static"
	case "ignore", "disabled", "link-local":
		return "ignore"
	default:
		return "auto"
	}
}

// parseNmcliRoutes parses route entries from nmcli terse output.
func parseNmcliRoutes(val string) []Route {
	var routes []Route
	// Routes are semicolon-separated, each containing "dst=..., nh=..."
	for _, entry := range splitTrim(val, ";") {
		entry = strings.Trim(entry, "{}")
		var r Route
		for _, part := range splitTrim(entry, ",") {
			k, v, ok := strings.Cut(part, "=")
			if !ok {
				continue
			}
			switch strings.TrimSpace(k) {
			case "dst":
				r.Destination = strings.TrimSpace(v)
			case "nh":
				r.Gateway = strings.TrimSpace(v)
			}
		}
		if r.Destination != "" && r.Gateway != "" {
			routes = append(routes, r)
		}
	}
	return routes
}

func (b *nmcliBackend) Apply(ctx context.Context, iface string, cfg IfaceConfig) error {
	conn, err := connForIface(ctx, iface)
	if err != nil {
		return fmt.Errorf("cannot apply: %w", err)
	}

	// conn comes from nmcli's own active list (connForIface), not user input.
	// nmcli's con subcommands don't honor "--" as an end-of-options separator.
	args := []string{"con", "modify", conn}

	switch cfg.Method {
	case "static":
		cidr := fmt.Sprintf("%s/%d", cfg.Address, cfg.Prefix)
		args = append(args,
			"ipv4.method", "manual",
			"ipv4.addresses", cidr,
		)
		if cfg.Gateway != "" {
			args = append(args, "ipv4.gateway", cfg.Gateway)
		} else {
			args = append(args, "ipv4.gateway", "")
		}
	case "dhcp":
		args = append(args,
			"ipv4.method", "auto",
			"ipv4.addresses", "",
			"ipv4.gateway", "",
		)
	}

	// DNS: set or clear.
	if len(cfg.DNS) > 0 {
		args = append(args, "ipv4.dns", strings.Join(cfg.DNS, ","))
	} else {
		args = append(args, "ipv4.dns", "")
	}

	// Routes: set or clear.
	if len(cfg.Routes) > 0 {
		var routeStrs []string
		for _, r := range cfg.Routes {
			routeStrs = append(routeStrs, r.Destination+" "+r.Gateway)
		}
		args = append(args, "ipv4.routes", strings.Join(routeStrs, ","))
	} else {
		args = append(args, "ipv4.routes", "")
	}

	// IPv6: only touched when the request includes an ipv6 block.
	if cfg.IPv6 != nil {
		switch cfg.IPv6.Method {
		case "static":
			args = append(args,
				"ipv6.method", "manual",
				"ipv6.addresses", fmt.Sprintf("%s/%d", cfg.IPv6.Address, cfg.IPv6.Prefix),
				"ipv6.gateway", cfg.IPv6.Gateway, // "" clears it
			)
		case "auto":
			args = append(args, "ipv6.method", "auto", "ipv6.addresses", "", "ipv6.gateway", "")
		case "ignore":
			args = append(args, "ipv6.method", "ignore", "ipv6.addresses", "", "ipv6.gateway", "")
		}
	}

	if _, err := oscmd.RunContext(ctx, "nmcli", args...); err != nil {
		return fmt.Errorf("nmcli con modify: %w", err)
	}

	// Bring the connection up to apply changes.
	if _, err := oscmd.RunContext(ctx, "nmcli", "con", "up", conn); err != nil {
		return fmt.Errorf("nmcli con up: %w", err)
	}
	return nil
}

func (b *nmcliBackend) SetLinkUp(ctx context.Context, iface string) error {
	conn, err := connForIface(ctx, iface)
	if err != nil {
		// No NM connection - fall back to `ip link`.
		_, err = oscmd.RunContext(ctx, "ip", "link", "set", "--", iface, "up")
		return err
	}
	_, err = oscmd.RunContext(ctx, "nmcli", "con", "up", conn)
	return err
}

func (b *nmcliBackend) SetLinkDown(ctx context.Context, iface string) error {
	conn, err := connForIface(ctx, iface)
	if err != nil {
		_, err = oscmd.RunContext(ctx, "ip", "link", "set", "--", iface, "down")
		return err
	}
	_, err = oscmd.RunContext(ctx, "nmcli", "con", "down", conn)
	return err
}

// --- helpers -----------------------------------------------------------------

// splitTrim splits s on sep and returns the trimmed, non-empty segments.
func splitTrim(s, sep string) []string {
	var out []string
	for part := range strings.SplitSeq(s, sep) {
		if p := strings.TrimSpace(part); p != "" {
			out = append(out, p)
		}
	}
	return out
}

// splitCIDR splits "192.168.1.10/24" into ("192.168.1.10", 24). A missing or
// malformed prefix yields 0.
func splitCIDR(cidr string) (string, int) {
	addr, prefixStr, ok := strings.Cut(cidr, "/")
	if !ok {
		return addr, 0
	}
	prefix, _ := strconv.Atoi(prefixStr)
	return addr, prefix
}
