// Package cli: txn.go implements the `orca txn` subcommand family
// (P10a, v0.11; REQ-075, REQ-079; gates C-09, C-23). Subcommands:
//
//	orca txn apply <txn-id> --lead <peer> [--force --i-understand-the-risk | --namespace <ns>] [--timeout 5m]
//	orca txn list
//	orca txn show <txn-id>
//	orca txn rollback <txn-id> --lead <peer>
//
// `apply` runs an already-staged txn on the lead peer via the txn
// package. Cluster-wide txns (no --namespace) require --force +
// --i-understand-the-risk (or --yes); namespace-scoped txns only
// touch the given namespace (C-23).
package cli

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"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"
	"git.cloudinit.dev/coreci/orca/internal/txn"
)

var (
	txnApplyForce     bool
	txnApplyAckRisk   bool
	txnApplyYes       bool
	txnApplyNamespace string
	txnApplyTimeout   time.Duration
	txnApplyLead      string
	txnRollbackLead   string
)

// txnTransport is the SSH-push surface the txn CLI needs. *sshpush.Transport
// satisfies it; tests substitute a mock (same pattern as drain.go /
// logs.go).
type txnTransport interface {
	WriteFileIdempotent(ctx context.Context, peer string, path string, content []byte, mode os.FileMode) (bool, error)
	Exec(ctx context.Context, peer string, cmd string) ([]byte, error)
}

// txnTransportOverride is the package-level seam. When non-nil it
// replaces the production transport; tests set it and restore nil.
var txnTransportOverride txnTransport

func txnTransportFromCtx() (txnTransport, error) {
	if txnTransportOverride != nil {
		return txnTransportOverride, nil
	}
	keyPath := certpaths.SSHKeyPath()
	khPath := certpaths.KnownHostsPath()
	return sshpush.NewTransport(keyPath, khPath), nil
}

var txnCmd = &cobra.Command{
	Use:   "txn",
	Short: "Manage control-plane transactions (apply/list/show/rollback)",
	Long: `Manage orca's transactional control-plane updates (P10a).

A transaction (txn) is a content-addressed desired-state bundle
(apply.sh + verify.sh + rollback.sh + signed manifest) staged to the
lead peer and applied idempotently. Cluster-wide txns require explicit
operator acknowledgement (--force + --i-understand-the-risk, or --yes);
namespace-scoped txns only touch the given namespace (C-23).`,
}

var txnApplyCmd = &cobra.Command{
	Use:   "apply <txn-id>",
	Short: "Apply a staged txn on the lead peer",
	Long: `Apply a staged txn on the lead peer (idempotent; C-09 failure
contract). The txn must already be staged on the lead under
/run/orca/txns/<txn-id>/.

Cluster-wide txns (no --namespace) require --force +
--i-understand-the-risk (or --yes for non-interactive). Namespace-
scoped txns (--namespace <ns>) only touch that namespace.`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		// F4: validate txn ID before interpolation into a remote shell
		// command and filesystem path.
		if !validTxnID(args[0]) {
			return fmt.Errorf("txn apply: invalid txn id %q (expected T-[0-9a-f]{16})", args[0])
		}
		id := txn.TxnID(args[0])
		transport, err := txnTransportFromCtx()
		if err != nil {
			return fmt.Errorf("ssh transport: %w", err)
		}
		opts := txn.ApplyOptions{
			Force:           txnApplyForce,
			AcknowledgeRisk: txnApplyAckRisk,
			Yes:             txnApplyYes,
			Namespace:       txnApplyNamespace,
			Timeout:         txnApplyTimeout,
		}
		ctx := cmd.Context()
		if err := txn.Apply(ctx, id, txnApplyLead, transport, opts); err != nil {
			if errors.Is(err, txn.ErrAlreadyApplied) {
				printResult(fmt.Sprintf("✓ Txn %s already applied (no-op)", id), map[string]any{
					"txn_id": id, "status": "already-applied",
				})
				return nil
			}
			if errors.Is(err, txn.ErrClusterWideRequiresForce) {
				return fmt.Errorf("cluster-wide txn requires --force (C-23)")
			}
			if errors.Is(err, txn.ErrClusterWideRequiresAck) {
				return fmt.Errorf("cluster-wide --force requires --i-understand-the-risk (or --yes)")
			}
			return err
		}
		printResult(fmt.Sprintf("✓ Txn %s applied", id), map[string]any{
			"txn_id":    id,
			"status":    "applied",
			"namespace": txnApplyNamespace,
			"lead":      txnApplyLead,
		})
		return nil
	},
}

// txnListEntry is one row in `orca txn list` output.
type txnListEntry struct {
	ID     string `json:"txn_id"`
	Status string `json:"status"`
}

var txnListCmd = &cobra.Command{
	Use:   "list",
	Short: "List staged + applied transactions",
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		dir := paths.TxnDir()
		entries, err := os.ReadDir(dir)
		if err != nil {
			if os.IsNotExist(err) {
				printResult("No transactions.", []txnListEntry{})
				return nil
			}
			return fmt.Errorf("read txn dir: %w", err)
		}
		var rows []txnListEntry
		for _, e := range entries {
			if !e.IsDir() {
				continue
			}
			id := e.Name()
			status := "staged"
			if _, err := os.Stat(filepath.Join(dir, id, ".applied")); err == nil {
				status = "applied"
			}
			rows = append(rows, txnListEntry{ID: id, Status: status})
		}
		if jsonOutput {
			return printJSON(rows)
		}
		out := cmd.OutOrStdout()
		if len(rows) == 0 {
			fmt.Fprintln(out, "No transactions.")
			return nil
		}
		fmt.Fprintf(out, "%-20s  %s\n", "TXN-ID", "STATUS")
		for _, r := range rows {
			fmt.Fprintf(out, "%-20s  %s\n", r.ID, r.Status)
		}
		return nil
	},
}

var txnShowCmd = &cobra.Command{
	Use:   "show <txn-id>",
	Short: "Show txn details (desired state, manifest, status)",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		// F4: validate txn ID before interpolation into a filesystem path.
		if !validTxnID(args[0]) {
			return fmt.Errorf("txn show: invalid txn id %q (expected T-[0-9a-f]{16})", args[0])
		}
		id := args[0]
		dir := filepath.Join(paths.TxnDir(), id)
		manifestPath := filepath.Join(dir, "manifest.json")
		desiredPath := filepath.Join(dir, "desired-state.json")
		manifest, err := os.ReadFile(manifestPath)
		if err != nil {
			return fmt.Errorf("read manifest for %s: %w", id, err)
		}
		desired, err := os.ReadFile(desiredPath)
		if err != nil {
			return fmt.Errorf("read desired-state for %s: %w", id, err)
		}
		status := "staged"
		if _, err := os.Stat(filepath.Join(dir, ".applied")); err == nil {
			status = "applied"
		}
		var m txn.Manifest
		_ = json.Unmarshal(manifest, &m)
		result := map[string]any{
			"txn_id":        id,
			"status":        status,
			"manifest":      json.RawMessage(manifest),
			"desired_state": json.RawMessage(desired),
		}
		if jsonOutput {
			return printJSON(result)
		}
		out := cmd.OutOrStdout()
		fmt.Fprintf(out, "Txn: %s\n", id)
		fmt.Fprintf(out, "Status: %s\n", status)
		if m.Timestamp != "" {
			fmt.Fprintf(out, "Timestamp: %s\n", m.Timestamp)
		}
		fmt.Fprintln(out, "Files:")
		for _, f := range m.Files {
			short := f.SHA256
			if len(short) > 16 {
				short = short[:16]
			}
			fmt.Fprintf(out, "  %s  %s\n", f.Name, short)
		}
		fmt.Fprintln(out, "Desired state:")
		fmt.Fprintln(out, string(desired))
		return nil
	},
}

var txnRollbackCmd = &cobra.Command{
	Use:   "rollback <txn-id>",
	Short: "Manually rollback a txn on the lead peer",
	Long: `Run rollback.sh for a staged txn on the lead peer. This is
the manual rollback path; orca-pull.sh runs rollback automatically on
verify failure.`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		// F4: validate txn ID before interpolation into a remote shell
		// command (bash <dir>/rollback.sh) and filesystem path.
		if !validTxnID(args[0]) {
			return fmt.Errorf("txn rollback: invalid txn id %q (expected T-[0-9a-f]{16})", args[0])
		}
		id := txn.TxnID(args[0])
		transport, err := txnTransportFromCtx()
		if err != nil {
			return fmt.Errorf("ssh transport: %w", err)
		}
		ctx := cmd.Context()
		dir := "/run/orca/txns/" + string(id)
		cmdStr := fmt.Sprintf("bash %s/rollback.sh", shellQuote(dir))
		out, err := transport.Exec(ctx, txnRollbackLead, cmdStr)
		if err != nil {
			return fmt.Errorf("rollback %s on %s: %w (output: %s)", id, txnRollbackLead, err, string(out))
		}
		printResult(fmt.Sprintf("✓ Txn %s rolled back", id), map[string]any{
			"txn_id": id,
			"status": "rolled-back",
			"lead":   txnRollbackLead,
			"output": string(out),
		})
		return nil
	},
}

func init() {
	txnApplyCmd.Flags().BoolVar(&txnApplyForce, "force", false, "override pre-flight checks (required for cluster-wide txns)")
	txnApplyCmd.Flags().BoolVar(&txnApplyAckRisk, "i-understand-the-risk", false, "acknowledge the risk of a cluster-wide --force txn")
	txnApplyCmd.Flags().BoolVar(&txnApplyYes, "yes", false, "non-interactive acknowledgement (equivalent to --i-understand-the-risk)")
	txnApplyCmd.Flags().StringVar(&txnApplyNamespace, "namespace", "", "namespace scope (empty = cluster-wide; requires --force + ack)")
	txnApplyCmd.Flags().DurationVar(&txnApplyTimeout, "timeout", 5*time.Minute, "apply+verify timeout")
	txnApplyCmd.Flags().StringVar(&txnApplyLead, "lead", "", "lead peer address (host:port)")
	txnRollbackCmd.Flags().StringVar(&txnRollbackLead, "lead", "", "lead peer address (host:port)")

	txnCmd.AddCommand(txnApplyCmd)
	txnCmd.AddCommand(txnListCmd)
	txnCmd.AddCommand(txnShowCmd)
	txnCmd.AddCommand(txnRollbackCmd)
	rootCmd.AddCommand(txnCmd)
}
