package networking

import (
	"context"
	"net/netip"
	"os"
	"regexp"
	"strings"
	"sync"

	"nadir/internal/oscmd"

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

// /etc/hosts management ("Host Addresses"). Edits are surgical — upsert/delete a
// single IP's line — so existing comments, ordering and the localhost entries
// are preserved rather than rewritten away.

var hostsFile = "/etc/hosts"

// hostsMu serialises writes to /etc/hosts so concurrent requests don't
// clobber each other's read-modify-write.
var hostsMu sync.Mutex

// hostnameRe matches a single hostname/alias. It forbids whitespace and '#' (so
// an entry can't inject extra fields or a comment) and a leading dash.
var hostnameRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)

// HostEntry is one /etc/hosts mapping: an IP and the names that resolve to it.
type HostEntry struct {
	IP        string   `json:"ip" example:"192.168.1.10" doc:"IPv4 or IPv6 address"`
	Hostnames []string `json:"hostnames" example:"[\"server\",\"server.local\"]" doc:"Names mapped to the address"`
}

type ListHostsOutput struct {
	Body struct {
		Entries []HostEntry `json:"entries"`
	}
}

type HostUpsertInput struct {
	IP   string `path:"ip" example:"192.168.1.10" doc:"IP address to add or update"`
	Body struct {
		Hostnames []string `json:"hostnames" example:"[\"server\",\"server.local\"]" doc:"Names to map to the IP"`
	}
}

type HostDeleteInput struct {
	IP string `path:"ip" example:"192.168.1.10" doc:"IP address whose entry to remove"`
}

func registerHosts(api huma.API) {
	huma.Register(api, huma.Operation{
		OperationID: "networking-list-hosts",
		Method:      "GET",
		Path:        "/api/networking/hosts",
		Summary:     "List /etc/hosts entries",
		Description: "Returns the static host-to-address mappings from /etc/hosts.",
		Tags:        []string{tagNetworking},
		Metadata:    op("read"),
		Errors:      readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListHostsOutput, error) {
		data, err := os.ReadFile(hostsFile)
		if err != nil {
			return nil, huma.Error500InternalServerError("read hosts failed", err)
		}
		res := &ListHostsOutput{}
		res.Body.Entries = parseHosts(string(data))
		return res, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "networking-upsert-host",
		Method:      "PUT",
		Path:        "/api/networking/hosts/{ip}",
		Summary:     "Add or update a /etc/hosts entry",
		Description: "Sets the hostnames for an IP. Replaces the existing line for that IP, " +
			"or appends a new one; all other lines (comments included) are left untouched.",
		Tags:     []string{tagNetworking},
		Metadata: op("write"),
		Errors:   writeErrors,
	}, func(ctx context.Context, in *HostUpsertInput) (*oscmd.StatusOutput, error) {
		if err := validateHost(in.IP, in.Body.Hostnames); err != nil {
			return nil, err
		}
		if err := upsertHost(in.IP, in.Body.Hostnames); err != nil {
			return nil, huma.Error500InternalServerError("write hosts failed", err)
		}
		return oscmd.OK(), nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "networking-delete-host",
		Method:      "DELETE",
		Path:        "/api/networking/hosts/{ip}",
		Summary:     "Remove a /etc/hosts entry",
		Description: "Removes the line(s) mapping the given IP. 404 if no entry exists for it.",
		Tags:        []string{tagNetworking},
		Metadata:    op("write"),
		Errors:      []int{400, 401, 403, 404, 500},
	}, func(ctx context.Context, in *HostDeleteInput) (*oscmd.StatusOutput, error) {
		if _, err := netip.ParseAddr(in.IP); err != nil {
			return nil, huma.Error400BadRequest("invalid IP: " + in.IP)
		}
		removed, err := deleteHost(in.IP)
		if err != nil {
			return nil, huma.Error500InternalServerError("write hosts failed", err)
		}
		if !removed {
			return nil, huma.Error404NotFound("no hosts entry for " + in.IP)
		}
		return oscmd.OK(), nil
	})
}

func validateHost(ip string, hostnames []string) error {
	if _, err := netip.ParseAddr(ip); err != nil {
		return huma.Error400BadRequest("invalid IP: " + ip)
	}
	if len(hostnames) == 0 {
		return huma.Error400BadRequest("at least one hostname is required")
	}
	for _, h := range hostnames {
		if !hostnameRe.MatchString(h) {
			return huma.Error400BadRequest("invalid hostname: " + h)
		}
	}
	return nil
}

// --- parse / render (pure, tested) -------------------------------------------

// hostFields returns the IP and hostnames on a hosts line, or ok=false for a
// blank or comment-only line. An inline "# comment" tail is stripped.
func hostFields(line string) (ip string, names []string, ok bool) {
	if i := strings.IndexByte(line, '#'); i >= 0 {
		line = line[:i]
	}
	f := strings.Fields(line)
	if len(f) < 2 {
		return "", nil, false
	}
	return f[0], f[1:], true
}

func parseHosts(text string) []HostEntry {
	entries := []HostEntry{}
	for line := range strings.SplitSeq(text, "\n") {
		if ip, names, ok := hostFields(line); ok {
			entries = append(entries, HostEntry{IP: ip, Hostnames: names})
		}
	}
	return entries
}

// renderHostLine formats one entry as a hosts line.
func renderHostLine(ip string, hostnames []string) string {
	return ip + "\t" + strings.Join(hostnames, " ")
}

// --- writes ------------------------------------------------------------------

// writeHostsAtomically writes content to /etc/hosts via a temp file + rename,
// so a crash mid-write leaves the original file intact.
func writeHostsAtomically(content string) error {
	tmp := hostsFile + ".nadir.tmp"
	if err := os.WriteFile(tmp, []byte(content), 0644); err != nil {
		return err
	}
	if err := os.Rename(tmp, hostsFile); err != nil {
		os.Remove(tmp)
		return err
	}
	return nil
}

// upsertHost replaces the line for ip (matched on the address field) or appends
// a new one, preserving every other line.
func upsertHost(ip string, hostnames []string) error {
	hostsMu.Lock()
	defer hostsMu.Unlock()

	data, err := os.ReadFile(hostsFile)
	if err != nil {
		return err
	}
	lines := strings.Split(string(data), "\n")
	newLine := renderHostLine(ip, hostnames)
	replaced := false
	for i, line := range lines {
		if got, _, ok := hostFields(line); ok && got == ip {
			lines[i] = newLine
			replaced = true
			break
		}
	}
	if !replaced {
		if n := len(lines); n > 0 && strings.TrimSpace(lines[n-1]) == "" {
			lines[n-1] = newLine
		} else {
			lines = append(lines, newLine)
		}
		lines = append(lines, "")
	}
	return writeHostsAtomically(strings.Join(lines, "\n"))
}

// deleteHost removes every line mapping ip and reports whether any were removed.
func deleteHost(ip string) (bool, error) {
	hostsMu.Lock()
	defer hostsMu.Unlock()

	data, err := os.ReadFile(hostsFile)
	if err != nil {
		return false, err
	}
	lines := strings.Split(string(data), "\n")
	kept := make([]string, 0, len(lines))
	removed := false
	for _, line := range lines {
		if got, _, ok := hostFields(line); ok && got == ip {
			removed = true
			continue
		}
		kept = append(kept, line)
	}
	if !removed {
		return false, nil
	}
	return true, writeHostsAtomically(strings.Join(kept, "\n"))
}
