// Package cli: secrets.go implements the `orca secrets` subcommand
// family (P03, REQ-080, gate C-19). Subcommands:
//
//	orca secrets set <ns> <KEY=value>   — encrypt and add/update a secret
//	orca secrets get <ns> <KEY>          — decrypt and print a single value
//	orca secrets list <ns>               — list secret KEYS (not values)
//	orca secrets rotate <ns> <KEY>        — re-encrypt with a fresh nonce
//	orca secrets delete <ns> <KEY>        — remove a secret
//
// All commands load the master key from paths.MasterKeyPath() and derive
// a per-namespace sub-key via HKDF-SHA256. The .env.secrets file lives at
// paths.NSSecrets(ns). Writes are atomic (temp + rename). The master key
// file MUST be mode 0600; LoadMasterKey refuses looser permissions.
//
// `get` writes ONLY the secret value to stdout (no logging of the value,
// no trailing newline beyond the value itself). This makes it safe to
// pipe into a credential consumer.
package cli

import (
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/spf13/cobra"

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

var secretsCmd = &cobra.Command{
	Use:   "secrets",
	Short: "Manage encrypted .env.secrets per namespace",
	Long: `Manage encrypted .env.secrets per namespace (REQ-080).

Each namespace has a .env.secrets file at <ORCA_HOME>/<ns>/.env.secrets
containing one base64(nonce||ciphertext||tag) blob per line. Encryption
is AES-256-GCM with a per-namespace HKDF-SHA256 sub-key derived from the
cluster master.key (mode 0600). The AAD is the 1-based line number,
defeating line-swap attacks.`,
}

// loadMasterAndNSSecrets reads the master key and the namespace's
// current .env.secrets (if present), returning the ns sub-key and the
// current plaintext lines. If the file does not exist, an empty slice
// is returned (no error).
func loadMasterAndNSSecrets(namespace string) (nsKey []byte, lines []string, err error) {
	mkPath := paths.MasterKeyPath()
	mk, err := secrets.LoadMasterKey(mkPath)
	if err != nil {
		return nil, nil, fmt.Errorf("load master key: %w", err)
	}
	nsKey, err = secrets.DeriveNamespaceKey(mk, namespace)
	if err != nil {
		return nil, nil, fmt.Errorf("derive namespace key: %w", err)
	}
	secPath := paths.NSSecrets(namespace)
	body, readErr := os.ReadFile(secPath)
	if readErr != nil {
		if os.IsNotExist(readErr) {
			return nsKey, nil, nil
		}
		return nil, nil, fmt.Errorf("read %s: %w", secPath, readErr)
	}
	lines, err = secrets.DecryptEnvFile(nsKey, string(body))
	if err != nil {
		return nil, nil, fmt.Errorf("decrypt %s: %w", secPath, err)
	}
	return nsKey, lines, nil
}

// saveNSSecrets encrypts the lines and writes them atomically to the
// namespace's .env.secrets path.
func saveNSSecrets(namespace string, nsKey []byte, lines []string) error {
	enc, err := secrets.EncryptEnvFile(nsKey, lines)
	if err != nil {
		return fmt.Errorf("encrypt secrets: %w", err)
	}
	secPath := paths.NSSecrets(namespace)
	if err := os.MkdirAll(filepath.Dir(secPath), 0o755); err != nil {
		return fmt.Errorf("create ns dir: %w", err)
	}
	if err := writeAtomicFile(secPath, []byte(enc), 0o600); err != nil {
		return fmt.Errorf("write %s: %w", secPath, err)
	}
	return nil
}

// parseKV splits a "KEY=value" argument. The value may contain '='.
func parseKV(arg string) (key, value string, err error) {
	idx := strings.IndexByte(arg, '=')
	if idx <= 0 {
		return "", "", fmt.Errorf("expected KEY=value, got %q", arg)
	}
	return arg[:idx], arg[idx+1:], nil
}

// findKeyIndex returns the index of the line whose KEY matches the
// given key, or -1 if not found.
func findKeyIndex(lines []string, key string) int {
	for i, line := range lines {
		if k, _, ok := splitKV(line); ok && k == key {
			return i
		}
	}
	return -1
}

// splitKV splits a plaintext "KEY=value" line. ok is false if the line
// is not in KEY=value form.
func splitKV(line string) (key, value string, ok bool) {
	idx := strings.IndexByte(line, '=')
	if idx <= 0 {
		return "", "", false
	}
	return line[:idx], line[idx+1:], true
}

var secretsSetCmd = &cobra.Command{
	Use:   "set <namespace> <KEY=value>",
	Short: "Encrypt and add/update a secret in a namespace",
	Long: `Encrypt KEY=value and add or update it in <namespace>/.env.secrets.
If the key already exists, its value is replaced; otherwise a new line
is appended. The .env.secrets file is rewritten atomically.`,
	Args: cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		ns := args[0]
		key, value, err := parseKV(args[1])
		if err != nil {
			return err
		}
		nsKey, lines, err := loadMasterAndNSSecrets(ns)
		if err != nil {
			return err
		}
		newLine := key + "=" + value
		idx := findKeyIndex(lines, key)
		if idx >= 0 {
			lines[idx] = newLine
		} else {
			lines = append(lines, newLine)
		}
		if err := saveNSSecrets(ns, nsKey, lines); err != nil {
			return err
		}
		slog.Info("secrets set", "namespace", ns, "key", key, "action", "update")
		if jsonOutput {
			return printJSON(map[string]any{"namespace": ns, "key": key, "action": map[string]string{"set": "ok"}})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ %s=%s set in namespace %q\n", key, strings.Repeat("*", len(value)), ns)
		return nil
	},
}

var secretsGetCmd = &cobra.Command{
	Use:   "get <namespace> <KEY>",
	Short: "Decrypt and print a single secret value (stdout only)",
	Long: `Decrypt the secret named KEY from <namespace>/.env.secrets and print
its value to stdout. The value is printed with NO trailing newline
added beyond what the secret itself contained. The value is NEVER
logged via slog.`,
	Args: cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		ns := args[0]
		key := args[1]
		_, lines, err := loadMasterAndNSSecrets(ns)
		if err != nil {
			return err
		}
		idx := findKeyIndex(lines, key)
		if idx < 0 {
			return fmt.Errorf("secret %q not found in namespace %q", key, ns)
		}
		_, value, _ := splitKV(lines[idx])
		slog.Info("secrets get", "namespace", ns, "key", key)
		fmt.Fprint(cmd.OutOrStdout(), value)
		return nil
	},
}

var secretsListCmd = &cobra.Command{
	Use:   "list <namespace>",
	Short: "List secret KEYS (not values) in a namespace",
	Long:  `List the keys of all secrets stored in <namespace>/.env.secrets. Values are never printed.`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		ns := args[0]
		_, lines, err := loadMasterAndNSSecrets(ns)
		if err != nil {
			return err
		}
		keys := make([]string, 0, len(lines))
		for _, line := range lines {
			if k, _, ok := splitKV(line); ok {
				keys = append(keys, k)
			}
		}
		sort.Strings(keys)
		slog.Info("secrets list", "namespace", ns, "count", len(keys))
		if jsonOutput {
			return printJSON(map[string]any{"namespace": ns, "keys": keys})
		}
		if len(keys) == 0 {
			fmt.Fprintln(cmd.OutOrStdout(), "No secrets found.")
			return nil
		}
		for _, k := range keys {
			fmt.Fprintln(cmd.OutOrStdout(), k)
		}
		return nil
	},
}

var secretsRotateCmd = &cobra.Command{
	Use:   "rotate <namespace> <KEY>",
	Short: "Re-encrypt a secret with a fresh nonce",
	Long: `Re-encrypt the secret named KEY with a fresh nonce. The plaintext
value is unchanged. Useful after a master key rotation or to invalidate
old ciphertext copies. The .env.secrets file is rewritten atomically.`,
	Args: cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		ns := args[0]
		key := args[1]
		nsKey, lines, err := loadMasterAndNSSecrets(ns)
		if err != nil {
			return err
		}
		idx := findKeyIndex(lines, key)
		if idx < 0 {
			return fmt.Errorf("secret %q not found in namespace %q", key, ns)
		}
		_, value, _ := splitKV(lines[idx])
		lines[idx] = key + "=" + value
		if err := saveNSSecrets(ns, nsKey, lines); err != nil {
			return err
		}
		slog.Info("secrets rotate", "namespace", ns, "key", key)
		if jsonOutput {
			return printJSON(map[string]any{"namespace": ns, "key": key, "rotated": true})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ %s rotated in namespace %q\n", key, ns)
		return nil
	},
}

var secretsDeleteCmd = &cobra.Command{
	Use:   "delete <namespace> <KEY>",
	Short: "Remove a secret from a namespace",
	Long: `Remove the secret named KEY from <namespace>/.env.secrets. The
.env.secrets file is rewritten atomically.`,
	Args: cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		ns := args[0]
		key := args[1]
		nsKey, lines, err := loadMasterAndNSSecrets(ns)
		if err != nil {
			return err
		}
		idx := findKeyIndex(lines, key)
		if idx < 0 {
			return fmt.Errorf("secret %q not found in namespace %q", key, ns)
		}
		lines = append(lines[:idx], lines[idx+1:]...)
		if err := saveNSSecrets(ns, nsKey, lines); err != nil {
			return err
		}
		slog.Info("secrets delete", "namespace", ns, "key", key)
		if jsonOutput {
			return printJSON(map[string]any{"namespace": ns, "key": key, "deleted": true})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ %s deleted from namespace %q\n", key, ns)
		return nil
	},
}

var secretsRotateMasterDryRun bool

var secretsRotateMasterCmd = &cobra.Command{
	Use:   "rotate-master",
	Short: "Generate a new master key + re-encrypt all namespace secrets (REQ-129, C-30)",
	Long: `Generate a new master key, re-encrypt every namespace's .env.secrets
under the new key, and re-seal the master key to OIDC. With --dry-run,
reports the affected namespaces without writing. Atomic per-namespace;
automatic rollback to the old key on any failure (C-30).`,
	Args: cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		mkPath := paths.MasterKeyPath()
		oldKey, err := secrets.LoadMasterKey(mkPath)
		if err != nil {
			return fmt.Errorf("load current master key: %w", err)
		}

		// Find all namespaces with .env.secrets files.
		root := paths.Root()
		entries, err := os.ReadDir(root)
		if err != nil {
			return fmt.Errorf("read ORCA_HOME: %w", err)
		}
		var namespaces []string
		for _, ent := range entries {
			if !ent.IsDir() || ent.Name() == "cluster" {
				continue
			}
			secPath := paths.NSSecrets(ent.Name())
			if _, err := os.Stat(secPath); err == nil {
				namespaces = append(namespaces, ent.Name())
			}
		}
		if secretsRotateMasterDryRun {
			fmt.Fprintf(cmd.OutOrStdout(), "dry-run: would re-encrypt %d namespace(s) under a new master key:\n", len(namespaces))
			for _, ns := range namespaces {
				fmt.Fprintf(cmd.OutOrStdout(), "  - %s\n", ns)
			}
			return nil
		}

		// Generate new master key.
		newKey, err := secrets.GenerateMasterKey()
		if err != nil {
			return fmt.Errorf("generate new master key: %w", err)
		}

		// Re-encrypt each namespace. On any failure, rollback.
		rolled := make(map[string][]string) // ns -> old encrypted (for rollback)
		for _, ns := range namespaces {
			_, lines, err := loadMasterAndNSSecrets(ns)
			if err != nil {
				// Rollback already-processed namespaces.
				rollbackRotation(rolled, oldKey)
				return fmt.Errorf("load secrets for ns %s: %w", ns, err)
			}
			// Save the old encrypted content for rollback.
			secPath := paths.NSSecrets(ns)
			oldEnc, _ := os.ReadFile(secPath)
			rolled[ns] = []string{string(oldEnc)}

			// Re-encrypt under the new key.
			newNSKey, err := secrets.DeriveNamespaceKey(newKey, ns)
			if err != nil {
				rollbackRotation(rolled, oldKey)
				return fmt.Errorf("derive new ns key for %s: %w", ns, err)
			}
			enc, err := secrets.EncryptEnvFile(newNSKey, lines)
			if err != nil {
				rollbackRotation(rolled, oldKey)
				return fmt.Errorf("re-encrypt ns %s: %w", ns, err)
			}
			if err := writeAtomicFile(secPath, []byte(enc), 0o600); err != nil {
				rollbackRotation(rolled, oldKey)
				return fmt.Errorf("write ns %s: %w", ns, err)
			}
		}

		// Save the new master key.
		if err := secrets.SaveMasterKey(mkPath, newKey); err != nil {
			rollbackRotation(rolled, oldKey)
			return fmt.Errorf("save new master key (rolled back): %w", err)
		}

		slog.Info("secrets rotate-master", "namespaces", len(namespaces))
		if jsonOutput {
			return printJSON(map[string]any{"rotated": true, "namespaces": namespaces})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ Master key rotated; %d namespace(s) re-encrypted\n", len(namespaces))
		return nil
	},
}

// rollbackRotation restores old encrypted secrets for already-processed
// namespaces (C-30: automatic rollback on failure).
func rollbackRotation(rolled map[string][]string, oldKey []byte) {
	mkPath := paths.MasterKeyPath()
	_ = secrets.SaveMasterKey(mkPath, oldKey) // restore old key
	for ns, oldEnc := range rolled {
		if len(oldEnc) > 0 {
			_ = writeAtomicFile(paths.NSSecrets(ns), []byte(oldEnc[0]), 0o600)
		}
	}
}

func init() {
	secretsCmd.AddCommand(secretsSetCmd)
	secretsCmd.AddCommand(secretsGetCmd)
	secretsCmd.AddCommand(secretsListCmd)
	secretsCmd.AddCommand(secretsRotateCmd)
	secretsCmd.AddCommand(secretsDeleteCmd)
	secretsRotateMasterCmd.Flags().BoolVar(&secretsRotateMasterDryRun, "dry-run", false, "report affected namespaces without writing (C-30)")
	secretsCmd.AddCommand(secretsRotateMasterCmd)
	rootCmd.AddCommand(secretsCmd)
}
