package networking

import (
	"net/netip"
	"regexp"

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

// tagNetworking groups every networking-module operation under one OpenAPI tag,
// keeping tags 1:1 with modules.
const tagNetworking = "Networking"

var (
	readErrors  = []int{401, 403, 500}
	writeErrors = []int{400, 401, 403, 409, 500, 501}
)

// ifaceNameRe matches a Linux interface name. Linux caps names at 15 bytes and
// forbids '/' and whitespace; we additionally reject a leading dash so a name
// can never be read as a flag by `ip`/`nmcli`/`ifup`. Every command line also
// passes user-supplied names after a "--" separator.
var ifaceNameRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._@-]{0,14}$`)

func validateIface(name string) error {
	if !ifaceNameRe.MatchString(name) {
		return huma.Error400BadRequest("invalid interface name: " + name)
	}
	return nil
}

// IfaceConfig is the declarative desired state for one interface. A PUT replaces
// the interface's IPv4 configuration with this (and IPv6 too, when the IPv6 block
// is included), across whichever backend is in use. Routes are part of the config
// (declarative add/remove): the client sends the full set it wants.
type IfaceConfig struct {
	Method          string      `json:"method" enum:"static,dhcp" example:"static" doc:"\"static\" for a fixed address, \"dhcp\" for automatic"`
	Address         string      `json:"address,omitempty" example:"192.168.1.10" doc:"IPv4 address (static only)"`
	Prefix          int         `json:"prefix,omitempty" minimum:"0" maximum:"32" example:"24" doc:"Network prefix length (static only)"`
	Gateway         string      `json:"gateway,omitempty" example:"192.168.1.1" doc:"Default gateway (static only, optional)"`
	IPv6            *IPv6Config `json:"ipv6,omitempty" doc:"Optional IPv6 settings. Omit to leave IPv6 untouched; include to manage it."`
	DNS             []string    `json:"dns,omitempty" example:"[\"1.1.1.1\",\"8.8.8.8\"]" doc:"DNS servers for this interface (IPv4 or IPv6)"`
	Routes          []Route     `json:"routes,omitempty" doc:"Static routes to install for this interface"`
	RollbackSeconds int         `json:"rollback_seconds,omitempty" minimum:"0" maximum:"3600" example:"60" doc:"Auto-revert after this many seconds unless confirmed. 0 uses the default (60s)."`
}

// IPv6Config is the optional IPv6 settings for an interface. Method "auto" uses
// SLAAC/router advertisements (the usual default), "static" pins an address, and
// "ignore" disables IPv6 on the interface. DHCPv6 is not modeled.
type IPv6Config struct {
	Method  string `json:"method" enum:"auto,static,ignore" example:"static" doc:"\"auto\" (SLAAC), \"static\", or \"ignore\" (disable IPv6)"`
	Address string `json:"address,omitempty" example:"2001:db8::10" doc:"IPv6 address (static only)"`
	Prefix  int    `json:"prefix,omitempty" minimum:"0" maximum:"128" example:"64" doc:"Prefix length (static only)"`
	Gateway string `json:"gateway,omitempty" example:"2001:db8::1" doc:"IPv6 default gateway (static only, optional)"`
}

// Route is a single static route. Destination is a CIDR (or \"default\").
type Route struct {
	Destination string `json:"destination" example:"10.0.0.0/24" doc:"Destination network in CIDR notation, or \"default\""`
	Gateway     string `json:"gateway" example:"192.168.1.1" doc:"Next-hop gateway"`
}

// validate checks the desired config independently of the backend, so a bad
// request is a 400 before we touch the system. IP/CIDR parsing uses net/netip.
func (c IfaceConfig) validate() error {
	switch c.Method {
	case "static":
		if c.Address == "" {
			return huma.Error400BadRequest("static method requires an address")
		}
		if _, err := netip.ParseAddr(c.Address); err != nil {
			return huma.Error400BadRequest("invalid address: " + c.Address)
		}
		if c.Prefix < 1 || c.Prefix > 32 {
			return huma.Error400BadRequest("prefix must be 1-32")
		}
		if c.Gateway != "" {
			if _, err := netip.ParseAddr(c.Gateway); err != nil {
				return huma.Error400BadRequest("invalid gateway: " + c.Gateway)
			}
		}
	case "dhcp":
		// address/gateway/prefix are ignored; nothing to validate.
	default:
		return huma.Error400BadRequest("method must be \"static\" or \"dhcp\"")
	}
	for _, s := range c.DNS {
		if _, err := netip.ParseAddr(s); err != nil {
			return huma.Error400BadRequest("invalid DNS server: " + s)
		}
	}
	for _, r := range c.Routes {
		if r.Destination != "default" {
			if _, err := netip.ParsePrefix(r.Destination); err != nil {
				return huma.Error400BadRequest("invalid route destination: " + r.Destination)
			}
		}
		if _, err := netip.ParseAddr(r.Gateway); err != nil {
			return huma.Error400BadRequest("invalid route gateway: " + r.Gateway)
		}
	}
	if c.IPv6 != nil {
		if err := c.IPv6.validate(); err != nil {
			return err
		}
	}
	return nil
}

// validate checks an IPv6 block. Static addresses/gateways must parse as IPv6
// (an IPv4 literal here is a client mistake).
func (c IPv6Config) validate() error {
	switch c.Method {
	case "auto", "ignore":
		// no address fields to validate
	case "static":
		addr, err := netip.ParseAddr(c.Address)
		if err != nil || addr.Is4() {
			return huma.Error400BadRequest("ipv6 static requires a valid IPv6 address, got: " + c.Address)
		}
		if c.Prefix < 1 || c.Prefix > 128 {
			return huma.Error400BadRequest("ipv6 prefix must be 1-128")
		}
		if c.Gateway != "" {
			if gw, err := netip.ParseAddr(c.Gateway); err != nil || gw.Is4() {
				return huma.Error400BadRequest("invalid ipv6 gateway: " + c.Gateway)
			}
		}
	default:
		return huma.Error400BadRequest("ipv6 method must be \"auto\", \"static\", or \"ignore\"")
	}
	return nil
}
