// Package cli: doctor_nft.go implements `orca doctor nft` (P15.5,
// REQ-101). The check verifies the R-017 nftables ingress ruleset is
// present, parses, and matches the latest applied txn's hash.
//
// Checks (each emits a PASS/WARN/FAIL line):
//
//  1. table inet orca-ingress exists (nft list table)
//  2. DNAT :443 -> 127.0.0.1:8443 present
//  3. DNAT :80  -> 127.0.0.1:8080 present
//  4. rate-limit meter ora_rl present
//  5. /etc/nftables.d/orca.nft parses (nft -c -f)
//  6. file hash matches the latest applied txn (drift, P10b)
package cli

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/cobra"

	"git.cloudinit.dev/coreci/orca/internal/certpaths"
	"git.cloudinit.dev/coreci/orca/internal/paths"
	"git.cloudinit.dev/coreci/orca/internal/sshpush"
)

// nftTransport is the SSH-push surface doctor nft needs. Mirrors the
// drift CLI seam; tests substitute a mock.
type nftTransport interface {
	Exec(ctx context.Context, peer string, cmd string) ([]byte, error)
	ReadFile(ctx context.Context, peer string, path string) ([]byte, error)
}

// nftTransportOverride is the package-level test seam.
var nftTransportOverride nftTransport

// nftCheckResult is one line of `orca doctor nft` output.
type nftCheckResult struct {
	Name    string `json:"name"`
	Result  string `json:"result"`
	Message string `json:"message"`
}

func nftTransportFromCtx() (nftTransport, error) {
	if nftTransportOverride != nil {
		return nftTransportOverride, nil
	}
	keyPath := certpaths.SSHKeyPath()
	khPath := certpaths.KnownHostsPath()
	return sshpush.NewTransport(keyPath, khPath), nil
}

// nftLeadPeerOverride is the test seam for the peer to probe.
var nftLeadPeerOverride string

func nftLeadPeer() string {
	if nftLeadPeerOverride != "" {
		return nftLeadPeerOverride
	}
	return "lead"
}

var doctorNftCmd = &cobra.Command{
	Use:   "nft",
	Short: "Run the nftables ingress self-check (P15.5, REQ-101)",
	Long: `Verify the R-017 nftables ingress ruleset: table exists, DNAT
:443->127.0.0.1:8443 and :80->127.0.0.1:8080 present, rate-limit meter
present, /etc/nftables.d/orca.nft parses, and the on-disk file hash
matches the latest applied txn (drift, P10b).`,
	Args: cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		results := runNftChecks(cmd.Context())
		if jsonOutput {
			return printJSON(results)
		}
		for _, r := range results {
			fmt.Fprintf(cmd.OutOrStdout(), "%-28s %-5s  %s\n", r.Name, r.Result, r.Message)
		}
		return nil
	},
}

// runNftChecks executes the nft doctor checks against nftLeadPeer().
func runNftChecks(ctx context.Context) []nftCheckResult {
	t, err := nftTransportFromCtx()
	if err != nil {
		return []nftCheckResult{{Name: "nft:transport", Result: "FAIL", Message: err.Error()}}
	}
	peer := nftLeadPeer()
	var results []nftCheckResult

	tableOut, tableErr := t.Exec(ctx, peer, "nft list table inet orca-ingress")
	if tableErr != nil {
		results = append(results, nftCheckResult{Name: "nft:table", Result: "FAIL", Message: tableErr.Error()})
	} else {
		results = append(results, nftCheckResult{Name: "nft:table", Result: "PASS", Message: "table inet orca-ingress present"})
	}
	tableStr := string(tableOut)

	if strings.Contains(tableStr, "dnat to 127.0.0.1:8443") {
		results = append(results, nftCheckResult{Name: "nft:dnat-443", Result: "PASS", Message: "DNAT :443->127.0.0.1:8443 present"})
	} else {
		results = append(results, nftCheckResult{Name: "nft:dnat-443", Result: "FAIL", Message: "DNAT :443->127.0.0.1:8443 missing"})
	}

	if strings.Contains(tableStr, "dnat to 127.0.0.1:8080") {
		results = append(results, nftCheckResult{Name: "nft:dnat-80", Result: "PASS", Message: "DNAT :80->127.0.0.1:8080 present"})
	} else {
		results = append(results, nftCheckResult{Name: "nft:dnat-80", Result: "FAIL", Message: "DNAT :80->127.0.0.1:8080 missing"})
	}

	if strings.Contains(tableStr, "ora_rl") {
		results = append(results, nftCheckResult{Name: "nft:rate-limit", Result: "PASS", Message: "rate-limit meter ora_rl present"})
	} else {
		results = append(results, nftCheckResult{Name: "nft:rate-limit", Result: "FAIL", Message: "rate-limit meter ora_rl missing"})
	}

	if _, err := t.Exec(ctx, peer, "nft -c -f /etc/nftables.d/orca.nft"); err != nil {
		results = append(results, nftCheckResult{Name: "nft:parse", Result: "FAIL", Message: fmt.Sprintf("nft -c -f failed: %v", err)})
	} else {
		results = append(results, nftCheckResult{Name: "nft:parse", Result: "PASS", Message: "/etc/nftables.d/orca.nft parses cleanly"})
	}

	results = append(results, checkNftHashDrift(ctx, t, peer))
	return results
}

// checkNftHashDrift compares the on-peer file hash against the
// locally-recorded hash from the latest applied txn. The locally
// recorded hash is stored at ClusterDir()/nft.applied.sha256 (written by
// the apply path; the drift check reads it). When the local record is
// absent the check WARNs (no baseline to compare against).
func checkNftHashDrift(ctx context.Context, t nftTransport, peer string) nftCheckResult {
	liveHashOut, err := t.Exec(ctx, peer, "sha256sum /etc/nftables.d/orca.nft 2>/dev/null")
	if err != nil {
		return nftCheckResult{Name: "nft:hash-drift", Result: "FAIL", Message: fmt.Sprintf("remote sha256sum: %v", err)}
	}
	fields := strings.Fields(strings.TrimSpace(string(liveHashOut)))
	if len(fields) == 0 {
		return nftCheckResult{Name: "nft:hash-drift", Result: "FAIL", Message: "remote sha256sum returned no output"}
	}
	liveHash := fields[0]

	recordPath := filepath.Join(paths.ClusterDir(), "nft.applied.sha256")
	recorded, rerr := os.ReadFile(recordPath)
	if rerr != nil {
		return nftCheckResult{Name: "nft:hash-drift", Result: "WARN", Message: "no applied-txn hash baseline (first apply or record missing)"}
	}
	want := strings.TrimSpace(string(recorded))
	if liveHash == want {
		return nftCheckResult{Name: "nft:hash-drift", Result: "PASS", Message: "on-disk hash matches latest applied txn"}
	}
	return nftCheckResult{Name: "nft:hash-drift", Result: "FAIL", Message: fmt.Sprintf("DRIFT: live=%s recorded=%s", liveHash, want)}
}

// localSha256OfFile is a small helper for tests that compute the
// expected recorded hash from rendered content.
func localSha256OfFile(content []byte) string {
	sum := sha256.Sum256(content)
	return hex.EncodeToString(sum[:])
}
