package nat

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const tableName = "alpine-router-nat"

// Config holds NAT masquerade settings per interface.
type Config struct {
	// Interfaces is the list of LAN interface names for which masquerade is enabled.
	// Traffic arriving on these interfaces will be NATted to the outgoing WAN interface.
	Interfaces []string `json:"interfaces"`
}

// configPath returns the path to nat.json next to the running binary.
func configPath() string {
	exe, err := os.Executable()
	if err != nil {
		return filepath.Join("configs", "nat.json")
	}
	return filepath.Join(filepath.Dir(exe), "configs", "nat.json")
}

// IsInstalled reports whether the nft (nftables) binary is available.
func IsInstalled() bool {
	_, err := exec.LookPath("nft")
	return err == nil
}

// Load reads the NAT config from disk.
// Returns an empty config if the file does not exist yet.
func Load() (*Config, error) {
	data, err := os.ReadFile(configPath())
	if err != nil {
		if os.IsNotExist(err) {
			return &Config{Interfaces: []string{}}, nil
		}
		return nil, fmt.Errorf("read nat config: %w", err)
	}
	var cfg Config
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse nat config: %w", err)
	}
	if cfg.Interfaces == nil {
		cfg.Interfaces = []string{}
	}
	return &cfg, nil
}

// Save writes the NAT config to the configs/ directory next to the binary.
func Save(cfg *Config) error {
	p := configPath()
	if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
		return fmt.Errorf("mkdir configs: %w", err)
	}
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(p, data, 0644)
}

// ApplyRules flushes the existing alpine-router NAT table and recreates it
// from the provided config. Called on every daemon startup and on config save.
//
// nftables is used instead of iptables because it applies all rules atomically
// in a single kernel call, which is faster and avoids partial-state issues.
func ApplyRules(cfg *Config) error {
	return ApplyRulesWithBlocked(cfg, nil)
}

// ApplyRulesWithBlocked is like ApplyRules but also installs drop rules for
// the given list of blocked client IPs.
func ApplyRulesWithBlocked(cfg *Config, blockedIPs []string) error {
	if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644); err != nil {
		return fmt.Errorf("enable ip_forward: %w", err)
	}

	exec.Command("nft", "delete", "table", "ip", tableName).Run()

	if len(cfg.Interfaces) == 0 && len(blockedIPs) == 0 {
		return nil
	}

	var sb strings.Builder
	fmt.Fprintf(&sb, "table ip %s {\n", tableName)

	sb.WriteString("    chain forward {\n")
	sb.WriteString("        type filter hook forward priority filter; policy drop;\n")
	sb.WriteString("        ct state established,related accept\n")

	for _, ip := range blockedIPs {
		fmt.Fprintf(&sb, "        ip saddr %s drop\n", ip)
		fmt.Fprintf(&sb, "        ip daddr %s drop\n", ip)
	}

	for _, iface := range cfg.Interfaces {
		fmt.Fprintf(&sb, "        iifname \"%s\" accept\n", iface)
	}
	sb.WriteString("    }\n")

	sb.WriteString("    chain postrouting {\n")
	sb.WriteString("        type nat hook postrouting priority srcnat; policy accept;\n")
	for _, iface := range cfg.Interfaces {
		fmt.Fprintf(&sb, "        iifname \"%s\" masquerade\n", iface)
	}
	sb.WriteString("    }\n")
	sb.WriteString("}\n")

	cmd := exec.Command("nft", "-f", "-")
	cmd.Stdin = strings.NewReader(sb.String())
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("nft apply: %s: %w", strings.TrimSpace(string(out)), err)
	}
	return nil
}
