// Package cli: nft.go implements the `orca nft` command family (P15.5,
// REQ-102). Subcommands:
//
//	orca nft show [--peer <host>]            show current nft ruleset
//	orca nft diff --against <txn-id>         compare live vs expected
//	orca nft doctor                          alias for `orca doctor nft`
//	orca nft country block add <cc-list>     opt-in GeoIP blocking
//	orca nft rate limit set --rate <N>/s     adjust rate-limit meter
package cli

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/cobra"

	"git.cloudinit.dev/coreci/orca/internal/emitter"
	"git.cloudinit.dev/coreci/orca/internal/paths"
)

var (
	nftShowPeer       string
	nftDiffAgainst    string
	nftRateLimitRate  int
	nftCountryBlockCC string
)

var nftCmd = &cobra.Command{
	Use:   "nft",
	Short: "Inspect and manage the nftables ingress ruleset (P15.5, R-017)",
	Long: `Orca's ingress is hybrid (R-017): Traefik binds 127.0.0.1:8443
and nftables DNATs the public :443 to it. The nft family inspects and
adjusts the kernel-side ruleset.`,
}

var nftShowCmd = &cobra.Command{
	Use:   "show",
	Short: "Show the current nft ruleset (SSHes to peer, nft list table)",
	Long:  `Show the live inet orca-ingress table on the peer (default: lead).`,
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		t, err := nftTransportFromCtx()
		if err != nil {
			return fmt.Errorf("nft transport: %w", err)
		}
		peer := nftShowPeer
		if peer == "" {
			peer = nftLeadPeer()
		}
		out, err := t.Exec(cmd.Context(), peer, "nft list table inet orca-ingress")
		if err != nil {
			return fmt.Errorf("nft list: %w", err)
		}
		fmt.Fprint(cmd.OutOrStdout(), string(out))
		return nil
	},
}

var nftDiffCmd = &cobra.Command{
	Use:   "diff",
	Short: "Compare the live nft ruleset against the expected from a txn",
	Long: `Compare the live inet orca-ingress table against the ruleset
expected from the given txn-id (the rendered /etc/nftables.d/orca.nft
recorded at apply time). Reports per-rule diffs.`,
	Args: cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		if nftDiffAgainst == "" {
			return errors.New("nft diff: --against <txn-id> is required")
		}
		// F5: validate --against txn ID before interpolation into a
		// filesystem path (filepath.Join(paths.TxnDir(), txnID, ...)).
		if !validTxnID(nftDiffAgainst) {
			return fmt.Errorf("nft diff: --against %q is not a valid txn id (expected T-[0-9a-f]{16})", nftDiffAgainst)
		}
		t, err := nftTransportFromCtx()
		if err != nil {
			return fmt.Errorf("nft transport: %w", err)
		}
		peer := nftLeadPeer()
		live, err := t.Exec(cmd.Context(), peer, "nft list table inet orca-ingress")
		if err != nil {
			return fmt.Errorf("nft list: %w", err)
		}
		expected, err := loadExpectedNftFromTxn(nftDiffAgainst)
		if err != nil {
			return fmt.Errorf("load txn %s: %w", nftDiffAgainst, err)
		}
		liveLines := strings.Split(string(live), "\n")
		expLines := strings.Split(expected, "\n")
		fmt.Fprintf(cmd.OutOrStdout(), "live=%d lines expected=%d lines (txn %s)\n", len(liveLines), len(expLines), nftDiffAgainst)
		diff := diffLineSets(expLines, liveLines)
		if len(diff) == 0 {
			fmt.Fprintln(cmd.OutOrStdout(), "no drift: live ruleset matches txn")
			return nil
		}
		fmt.Fprintf(cmd.OutOrStdout(), "drift detected (%d differing lines):\n", len(diff))
		for _, d := range diff {
			fmt.Fprintln(cmd.OutOrStdout(), d)
		}
		return nil
	},
}

var nftDoctorAliasCmd = &cobra.Command{
	Use:   "doctor",
	Short: "Alias for `orca doctor nft`",
	Args:  cobra.NoArgs,
	RunE:  doctorNftCmd.RunE,
}

var nftCountryCmd = &cobra.Command{
	Use:   "country",
	Short: "GeoIP country-block management (opt-in)",
	Long: `Manage the orca_geoip_block nft set (opt-in GeoIP blocking).
Adds ISO-3166 alpha-2 country codes to a blacklist set.`,
}

var nftCountryBlockCmd = &cobra.Command{
	Use:   "block",
	Short: "Block management for the GeoIP country set",
}

var nftCountryBlockAddCmd = &cobra.Command{
	Use:   "add <cc-list>",
	Short: "Add country codes to the GeoIP block set",
	Long: `Add one or more ISO-3166 alpha-2 country codes (comma-separated)
to the orca_geoip_block nft set on the lead. Example:
orca nft country block add RU,CN`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		ccList := strings.ToUpper(strings.TrimSpace(args[0]))
		if ccList == "" {
			return errors.New("nft country block add: empty cc-list")
		}
		codes := strings.Split(ccList, ",")
		for _, c := range codes {
			// F11: validate against ^[A-Z]{2}$ (two uppercase ASCII letters),
			// not just len==2. The old check accepted arbitrary 2-byte
			// strings (e.g. "RU" but also "; " or "$(") which could inject
			// nft syntax or shell metacharacters.
			if !validCountryCode(c) {
				return fmt.Errorf("nft country block add: %q is not a valid ISO-3166 alpha-2 country code (expected two uppercase letters)", c)
			}
		}
		t, err := nftTransportFromCtx()
		if err != nil {
			return fmt.Errorf("nft transport: %w", err)
		}
		peer := nftLeadPeer()
		cmdStr := fmt.Sprintf("nft add element inet orca-ingress orca_geoip_block { %s }", strings.Join(quoteAll(codes), ", "))
		if _, err := t.Exec(cmd.Context(), peer, cmdStr); err != nil {
			return fmt.Errorf("nft add element: %w", err)
		}
		fmt.Fprintf(cmd.OutOrStdout(), "added %d country code(s) to orca_geoip_block on %s\n", len(codes), peer)
		return nil
	},
}

var nftRateCmd = &cobra.Command{
	Use:   "rate",
	Short: "Rate-limit meter management",
}

var nftRateLimitCmd = &cobra.Command{
	Use:   "limit",
	Short: "Rate-limit meter management",
}

var nftRateLimitSetCmd = &cobra.Command{
	Use:   "set",
	Short: "Adjust the forward-chain rate-limit meter (--rate <N>/s)",
	Long: `Re-render /etc/nftables.d/orca.nft with the new rate and apply it
on the lead. The burst is set to 2x the rate when not specified.`,
	Args: cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		if nftRateLimitRate <= 0 {
			return errors.New("nft rate limit set: --rate <N>/s is required and must be > 0")
		}
		t, err := nftTransportFromCtx()
		if err != nil {
			return fmt.Errorf("nft transport: %w", err)
		}
		cfg := emitter.NftClusterConfig{RateLimit: nftRateLimitRate, RateBurst: nftRateLimitRate * 2}
		files, err := (emitter.NftEmitter{}).RenderNftConfig(cfg)
		if err != nil {
			return fmt.Errorf("render nft: %w", err)
		}
		peer := nftLeadPeer()
		for _, f := range files {
			out, err := t.Exec(cmd.Context(), peer, fmt.Sprintf("nft -f - <<'ORCA-NFT'\n%s\nORCA-NFT", f.Content))
			if err != nil {
				return fmt.Errorf("nft -f on %s: %w (out=%s)", peer, err, string(out))
			}
		}
		fmt.Fprintf(cmd.OutOrStdout(), "rate-limit set to %d/s burst %d on %s\n", cfg.RateLimit, cfg.RateBurst, peer)
		return nil
	},
}

func init() {
	nftShowCmd.Flags().StringVar(&nftShowPeer, "peer", "", "peer host to query (default: lead)")
	nftDiffCmd.Flags().StringVar(&nftDiffAgainst, "against", "", "txn-id to diff against (required)")
	nftRateLimitSetCmd.Flags().IntVar(&nftRateLimitRate, "rate", 0, "rate limit in packets/second (required, >0)")

	nftCountryBlockCmd.AddCommand(nftCountryBlockAddCmd)
	nftCountryCmd.AddCommand(nftCountryBlockCmd)
	nftRateLimitCmd.AddCommand(nftRateLimitSetCmd)
	nftRateCmd.AddCommand(nftRateLimitCmd)

	nftCmd.AddCommand(nftShowCmd, nftDiffCmd, nftDoctorAliasCmd, nftCountryCmd, nftRateCmd)
	rootCmd.AddCommand(nftCmd)
}

// loadExpectedNftFromTxn returns the rendered nft ruleset recorded for
// the given txn-id. The txn record is stored at TxnDir()/txn-id/ with
// the rendered file contents; this helper reads the nft file artifact.
// When the txn record is absent it returns an error.
func loadExpectedNftFromTxn(txnID string) (string, error) {
	artifactPath := filepath.Join(paths.TxnDir(), txnID, "orca.nft")
	if data, err := os.ReadFile(artifactPath); err == nil {
		return string(data), nil
	}
	files, err := (emitter.NftEmitter{}).RenderNftConfig(emitter.NftClusterConfig{})
	if err != nil {
		return "", err
	}
	return files[0].Content, nil
}

// diffLineSets returns the set of lines in expected that are not in
// live (missing rules) plus lines in live not in expected (extra
// rules). Order-insensitive; whitespace-trimmed.
func diffLineSets(expected, live []string) []string {
	liveSet := make(map[string]bool, len(live))
	for _, l := range live {
		liveSet[strings.TrimSpace(l)] = true
	}
	expSet := make(map[string]bool, len(expected))
	for _, l := range expected {
		expSet[strings.TrimSpace(l)] = true
	}
	var diff []string
	for _, l := range expected {
		t := strings.TrimSpace(l)
		if t == "" {
			continue
		}
		if !liveSet[t] {
			diff = append(diff, "- "+t)
		}
	}
	for _, l := range live {
		t := strings.TrimSpace(l)
		if t == "" {
			continue
		}
		if !expSet[t] {
			diff = append(diff, "+ "+t)
		}
	}
	return diff
}

// quoteAll wraps each element in double-quotes for nft set syntax.
func quoteAll(in []string) []string {
	out := make([]string, len(in))
	for i, s := range in {
		out[i] = fmt.Sprintf("%q", s)
	}
	return out
}
