package networking

import (
	"context"
	"encoding/json"
	"fmt"
	"net/netip"
	"os"
	"strconv"
	"strings"

	"nadir/internal/oscmd"

	"github.com/danielgtaylor/huma/v2"
)

// Reads go through `ip -j` (JSON) and /etc/resolv.conf, which behave the same
// regardless of which backend manages the interfaces - so reads need no backend
// detection.

var resolvConf = "/etc/resolv.conf"

type Interface struct {
	Name  string   `json:"name" example:"eth0"`
	State string   `json:"state" example:"up" doc:"operstate: up / down / unknown"`
	MAC   string   `json:"mac" example:"52:54:00:12:34:56"`
	MTU   int      `json:"mtu" example:"1500"`
	IPv4  []string `json:"ipv4" example:"[\"192.168.1.10/24\"]" doc:"IPv4 addresses in CIDR form"`
	IPv6  []string `json:"ipv6" example:"[\"fe80::1/64\"]" doc:"IPv6 addresses in CIDR form"`
}

type ListInterfacesOutput struct {
	Body struct {
		Interfaces []Interface `json:"interfaces"`
	}
}

type RouteEntry struct {
	Destination string `json:"destination" example:"default" doc:"Destination network, or \"default\""`
	Gateway     string `json:"gateway,omitempty" example:"192.168.1.1"`
	Interface   string `json:"interface" example:"eth0"`
	Source      string `json:"source,omitempty" example:"192.168.1.10" doc:"Preferred source address"`
	Metric      int    `json:"metric,omitempty" example:"100"`
}

type ListRoutesOutput struct {
	Body struct {
		Routes []RouteEntry `json:"routes"`
	}
}

type DNSOutput struct {
	Body struct {
		Servers []string `json:"servers" example:"[\"1.1.1.1\"]" doc:"Nameservers from /etc/resolv.conf"`
	}
}

// GetInterfaceConfigInput carries the path param; matches the PUT endpoint's
// IfacePathInput so the frontend can use the same path for both verbs.
type GetInterfaceConfigInput struct {
	Name string `path:"name" example:"eth0" doc:"Interface name"`
}

// GetInterfaceConfigOutput returns the same IfaceConfig shape that PUT
// accepts, so the form can be pre-filled directly from this response.
type GetInterfaceConfigOutput struct {
	Body IfaceConfig
}

func registerReads(api huma.API, m *Module) {
	huma.Register(api, huma.Operation{
		OperationID: "networking-get-interface",
		Method:      "GET",
		Path:        "/api/networking/interfaces/{name}",
		Summary:     "Get an interface's current configuration",
		Description: "Returns the IfaceConfig the backend currently has for this " +
			"interface (method, address/prefix, gateway, DNS, IPv6). Same schema as " +
			"PUT /api/networking/interfaces/{name}, so the frontend can prefill an " +
			"edit form from this response directly. Returns 501 when no backend was " +
			"detected (nmcli / networkd / ifupdown).",
		Tags:     []string{tagNetworking},
		Metadata: op("read"),
		Errors:   readErrors,
	}, func(ctx context.Context, in *GetInterfaceConfigInput) (*GetInterfaceConfigOutput, error) {
		if m.be == nil {
			return nil, huma.Error501NotImplemented("", errNoBackend)
		}
		if err := validateIface(in.Name); err != nil {
			return nil, err
		}
		cfg, err := m.be.Snapshot(ctx, in.Name)
		if err != nil {
			return nil, huma.Error500InternalServerError("snapshot failed", err)
		}
		augmentWithLiveState(ctx, in.Name, &cfg)
		return &GetInterfaceConfigOutput{Body: cfg}, nil
	})


	huma.Register(api, huma.Operation{
		OperationID: "networking-list-interfaces",
		Method:      "GET",
		Path:        "/api/networking/interfaces",
		Summary:     "List network interfaces",
		Description: "Returns every interface with its state, MAC, MTU and addresses, via `ip -j addr`.",
		Tags:        []string{tagNetworking},
		Metadata:    op("read"),
		Errors:      readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListInterfacesOutput, error) {
		out, err := oscmd.RunContext(ctx, "ip", "-j", "addr")
		if err != nil {
			return nil, huma.Error500InternalServerError("ip addr failed", err)
		}
		ifaces, err := parseInterfaces(out)
		if err != nil {
			return nil, huma.Error500InternalServerError("parse ip addr failed", err)
		}
		res := &ListInterfacesOutput{}
		res.Body.Interfaces = ifaces
		return res, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "networking-list-routes",
		Method:      "GET",
		Path:        "/api/networking/routes",
		Summary:     "List the IPv4 route table",
		Description: "Returns the kernel IPv4 route table via `ip -j route`.",
		Tags:        []string{tagNetworking},
		Metadata:    op("read"),
		Errors:      readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListRoutesOutput, error) {
		out, err := oscmd.RunContext(ctx, "ip", "-j", "route")
		if err != nil {
			return nil, huma.Error500InternalServerError("ip route failed", err)
		}
		routes, err := parseRoutes(out)
		if err != nil {
			return nil, huma.Error500InternalServerError("parse ip route failed", err)
		}
		res := &ListRoutesOutput{}
		res.Body.Routes = routes
		return res, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "networking-get-dns",
		Method:      "GET",
		Path:        "/api/networking/dns",
		Summary:     "Get configured DNS servers",
		Description: "Returns the nameservers listed in /etc/resolv.conf. DNS is set " +
			"per-interface as part of the interface config (PUT /api/networking/interfaces/{name}), " +
			"so there is no standalone DNS write endpoint.",
		Tags:     []string{tagNetworking},
		Metadata: op("read"),
		Errors:   readErrors,
	}, func(ctx context.Context, _ *struct{}) (*DNSOutput, error) {
		data, err := os.ReadFile(resolvConf)
		if err != nil {
			return nil, huma.Error500InternalServerError("DNS config lookup failed", err)
		}
		res := &DNSOutput{}
		res.Body.Servers = parseResolv(string(data))
		return res, nil
	})
}

// --- parsers (pure, tested) --------------------------------------------------

// ipAddr mirrors the fields we use from one `ip -j addr` element.
type ipAddr struct {
	Name      string `json:"ifname"`
	MAC       string `json:"address"`
	MTU       int    `json:"mtu"`
	OperState string `json:"operstate"`
	AddrInfo  []struct {
		Family string `json:"family"` // "inet" / "inet6"
		Local  string `json:"local"`
		Prefix int    `json:"prefixlen"`
	} `json:"addr_info"`
}

func parseInterfaces(jsonOut string) ([]Interface, error) {
	var raw []ipAddr
	if err := json.Unmarshal([]byte(jsonOut), &raw); err != nil {
		return nil, err
	}
	ifaces := make([]Interface, 0, len(raw))
	for _, r := range raw {
		iface := Interface{
			Name:  r.Name,
			State: strings.ToLower(r.OperState),
			MAC:   r.MAC,
			MTU:   r.MTU,
			IPv4:  []string{},
			IPv6:  []string{},
		}
		for _, a := range r.AddrInfo {
			cidr := a.Local + "/" + strconv.Itoa(a.Prefix)
			if a.Family == "inet6" {
				iface.IPv6 = append(iface.IPv6, cidr)
			} else {
				iface.IPv4 = append(iface.IPv4, cidr)
			}
		}
		ifaces = append(ifaces, iface)
	}
	return ifaces, nil
}

// ipRoute mirrors the fields we use from one `ip -j route` element.
type ipRoute struct {
	Dst     string `json:"dst"`
	Gateway string `json:"gateway"`
	Dev     string `json:"dev"`
	PrefSrc string `json:"prefsrc"`
	Metric  int    `json:"metric"`
}

func parseRoutes(jsonOut string) ([]RouteEntry, error) {
	var raw []ipRoute
	if err := json.Unmarshal([]byte(jsonOut), &raw); err != nil {
		return nil, err
	}
	routes := make([]RouteEntry, 0, len(raw))
	for _, r := range raw {
		routes = append(routes, RouteEntry{
			Destination: r.Dst,
			Gateway:     r.Gateway,
			Interface:   r.Dev,
			Source:      r.PrefSrc,
			Metric:      r.Metric,
		})
	}
	return routes, nil
}

// parseResolv extracts "nameserver X" entries, ignoring comments and other
// directives.
func parseResolv(text string) []string {
	servers := []string{}
	for line := range strings.SplitSeq(text, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
			continue
		}
		if rest, ok := strings.CutPrefix(line, "nameserver"); ok {
			if s := strings.TrimSpace(rest); s != "" {
				servers = append(servers, s)
			}
		}
	}
	return servers
}

func getLiveInterface(ctx context.Context, iface string) (*Interface, error) {
	out, err := oscmd.RunContext(ctx, "ip", "-j", "addr", "show", "--", iface)
	if err != nil {
		out, err = oscmd.RunContext(ctx, "ip", "-j", "addr")
		if err != nil {
			return nil, err
		}
	}
	ifaces, err := parseInterfaces(out)
	if err != nil {
		return nil, err
	}
	for i := range ifaces {
		if ifaces[i].Name == iface {
			return &ifaces[i], nil
		}
	}
	return nil, fmt.Errorf("interface %s not found in ip addr output", iface)
}

func getLiveGateway(ctx context.Context, iface string) string {
	routeOut, err := oscmd.RunContext(ctx, "ip", "-j", "route", "show", "dev", "--", iface)
	if err != nil {
		routeOut, err = oscmd.RunContext(ctx, "ip", "-j", "route")
		if err != nil {
			return ""
		}
	}
	routes, err := parseRoutes(routeOut)
	if err != nil {
		return ""
	}
	for _, r := range routes {
		if r.Destination == "default" && (r.Interface == iface || r.Interface == "") && r.Gateway != "" {
			return r.Gateway
		}
	}
	return ""
}

func getLiveIPv6Gateway(ctx context.Context, iface string) string {
	routeOut, err := oscmd.RunContext(ctx, "ip", "-6", "-j", "route", "show", "dev", "--", iface)
	if err != nil {
		routeOut, err = oscmd.RunContext(ctx, "ip", "-6", "-j", "route")
		if err != nil {
			return ""
		}
	}
	routes, err := parseRoutes(routeOut)
	if err != nil {
		return ""
	}
	for _, r := range routes {
		if r.Destination == "default" && (r.Interface == iface || r.Interface == "") && r.Gateway != "" {
			return r.Gateway
		}
	}
	return ""
}

func augmentWithLiveState(ctx context.Context, iface string, cfg *IfaceConfig) {
	liveIface, err := getLiveInterface(ctx, iface)
	if err != nil {
		return
	}

	// Prefill IPv4 address and prefix if empty
	if cfg.Address == "" && len(liveIface.IPv4) > 0 {
		addr, prefix := splitCIDR(liveIface.IPv4[0])
		if addr != "" {
			cfg.Address = addr
			cfg.Prefix = prefix
		}
	}

	// Prefill Gateway if empty
	if cfg.Gateway == "" {
		cfg.Gateway = getLiveGateway(ctx, iface)
	}

	// Prefill DNS if empty
	if len(cfg.DNS) == 0 {
		if data, err := os.ReadFile(resolvConf); err == nil {
			cfg.DNS = parseResolv(string(data))
		}
	}

	// Prefill IPv6 if present and method is not ignore
	if cfg.IPv6 == nil {
		cfg.IPv6 = &IPv6Config{Method: "auto"}
	}
	if cfg.IPv6.Method != "ignore" {
		// Capture first global IPv6 if address is empty
		if cfg.IPv6.Address == "" {
			for _, c := range liveIface.IPv6 {
				addr, prefix := splitCIDR(c)
				if ip, err := netip.ParseAddr(addr); err == nil && !ip.IsLinkLocalUnicast() {
					cfg.IPv6.Address = addr
					cfg.IPv6.Prefix = prefix
					break
				}
			}
		}
		// Capture IPv6 default gateway if empty
		if cfg.IPv6.Gateway == "" {
			cfg.IPv6.Gateway = getLiveIPv6Gateway(ctx, iface)
		}
	}
}
