package cmd

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"database/sql"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/atotto/clipboard"
	"github.com/salvacybersec/keyhunter/pkg/config"
	"github.com/salvacybersec/keyhunter/pkg/engine"
	"github.com/salvacybersec/keyhunter/pkg/output"
	"github.com/salvacybersec/keyhunter/pkg/providers"
	"github.com/salvacybersec/keyhunter/pkg/storage"
	"github.com/salvacybersec/keyhunter/pkg/verify"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// keys subcommand flags. Declared as package-level vars so tests can reset
// them between runs.
var (
	flagKeysUnmask      bool
	flagKeysProvider    string
	flagKeysVerified    bool
	flagKeysLimit       int
	flagKeysFormat      string
	flagKeysOutFile     string
	flagKeysYes         bool
)

// keysCmd is the root of the "keyhunter keys" command tree (Phase 6, KEYS-01..06).
var keysCmd = &cobra.Command{
	Use:   "keys",
	Short: "Manage stored API key findings (list, show, export, copy, delete, verify)",
	Long: `The keys command tree operates on findings already persisted in the local
KeyHunter database. All subcommands derive the encryption key from the stored
per-installation salt, so no passphrase prompt is required unless one has been
explicitly configured.`,
}

var keysListCmd = &cobra.Command{
	Use:   "list",
	Short: "List stored findings (masked by default)",
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		db, encKey, err := openDBWithKey()
		if err != nil {
			return err
		}
		defer db.Close()

		f := storage.Filters{
			Provider: flagKeysProvider,
			Limit:    flagKeysLimit,
		}
		if cmd.Flag("verified").Changed {
			v := flagKeysVerified
			f.Verified = &v
		}

		findings, err := db.ListFindingsFiltered(encKey, f)
		if err != nil {
			return fmt.Errorf("listing findings: %w", err)
		}

		w := cmd.OutOrStdout()
		for _, sf := range findings {
			key := sf.KeyMasked
			if flagKeysUnmask {
				key = sf.KeyValue
			}
			verified := "unverified"
			if sf.Verified {
				if sf.VerifyStatus != "" {
					verified = sf.VerifyStatus
				} else {
					verified = "verified"
				}
			}
			fmt.Fprintf(w, "[%d] %-16s %-6s %s  %s:%d  %s\n",
				sf.ID, sf.ProviderName, sf.Confidence, key,
				sf.SourcePath, sf.LineNumber, verified)
		}
		fmt.Fprintf(w, "%d key(s).\n", len(findings))
		return nil
	},
}

var keysShowCmd = &cobra.Command{
	Use:   "show <id>",
	Short: "Show full details for a single finding (unmasked)",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		id, err := parseID(args[0])
		if err != nil {
			return err
		}
		db, encKey, err := openDBWithKey()
		if err != nil {
			return err
		}
		defer db.Close()

		f, err := db.GetFinding(id, encKey)
		if err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return fmt.Errorf("no finding with id %d", id)
			}
			return fmt.Errorf("getting finding: %w", err)
		}
		renderFinding(cmd.OutOrStdout(), f)
		return nil
	},
}

var keysExportCmd = &cobra.Command{
	Use:   "export",
	Short: "Export all findings as JSON or CSV (unmasked) to stdout or --output file",
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		format := strings.ToLower(strings.TrimSpace(flagKeysFormat))
		if format == "" {
			format = "json"
		}
		if format != "json" && format != "csv" {
			return fmt.Errorf("keys export: unsupported format %q (supported: json, csv; SARIF is scan-only)", format)
		}

		formatter, err := output.Get(format)
		if err != nil {
			return fmt.Errorf("keys export: %w", err)
		}

		db, encKey, err := openDBWithKey()
		if err != nil {
			return err
		}
		defer db.Close()

		stored, err := db.ListFindingsFiltered(encKey, storage.Filters{})
		if err != nil {
			return fmt.Errorf("listing findings: %w", err)
		}
		findings := make([]engine.Finding, 0, len(stored))
		for _, sf := range stored {
			findings = append(findings, storageToEngine(sf))
		}

		opts := output.Options{
			Unmask:      true, // exports are full-fidelity (KEYS-03)
			ToolName:    "keyhunter",
			ToolVersion: "0.1.0",
		}

		var target io.Writer = cmd.OutOrStdout()
		if flagKeysOutFile != "" {
			tmp := flagKeysOutFile + ".tmp"
			f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
			if err != nil {
				return fmt.Errorf("creating export file: %w", err)
			}
			if err := formatter.Format(findings, f, opts); err != nil {
				f.Close()
				os.Remove(tmp)
				return fmt.Errorf("formatting export: %w", err)
			}
			if err := f.Close(); err != nil {
				os.Remove(tmp)
				return fmt.Errorf("closing export file: %w", err)
			}
			if err := os.Rename(tmp, flagKeysOutFile); err != nil {
				os.Remove(tmp)
				return fmt.Errorf("renaming export file: %w", err)
			}
			fmt.Fprintf(cmd.ErrOrStderr(), "Exported %d finding(s) to %s\n", len(findings), flagKeysOutFile)
			return nil
		}
		return formatter.Format(findings, target, opts)
	},
}

var keysCopyCmd = &cobra.Command{
	Use:   "copy <id>",
	Short: "Copy a stored key's plaintext value to the system clipboard",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		id, err := parseID(args[0])
		if err != nil {
			return err
		}
		db, encKey, err := openDBWithKey()
		if err != nil {
			return err
		}
		defer db.Close()

		f, err := db.GetFinding(id, encKey)
		if err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return fmt.Errorf("no finding with id %d", id)
			}
			return fmt.Errorf("getting finding: %w", err)
		}
		if err := clipboard.WriteAll(f.KeyValue); err != nil {
			return fmt.Errorf("writing to clipboard: %w", err)
		}
		fmt.Fprintf(cmd.OutOrStdout(), "Copied key for finding #%d (%s, %s) to clipboard.\n",
			f.ID, f.ProviderName, f.KeyMasked)
		return nil
	},
}

var keysDeleteCmd = &cobra.Command{
	Use:   "delete <id>",
	Short: "Delete a finding from the database (prompts unless --yes)",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		id, err := parseID(args[0])
		if err != nil {
			return err
		}
		db, encKey, err := openDBWithKey()
		if err != nil {
			return err
		}
		defer db.Close()

		f, err := db.GetFinding(id, encKey)
		if err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				fmt.Fprintf(cmd.OutOrStdout(), "No finding with id %d.\n", id)
				return nil
			}
			return fmt.Errorf("getting finding: %w", err)
		}

		if !flagKeysYes {
			fmt.Fprintf(cmd.OutOrStdout(), "Delete finding #%d (%s, %s)? [y/N]: ",
				f.ID, f.ProviderName, f.KeyMasked)
			reader := bufio.NewReader(cmd.InOrStdin())
			line, _ := reader.ReadString('\n')
			ans := strings.ToLower(strings.TrimSpace(line))
			if ans != "y" && ans != "yes" {
				fmt.Fprintln(cmd.OutOrStdout(), "Cancelled.")
				return nil
			}
		}

		n, err := db.DeleteFinding(id)
		if err != nil {
			return fmt.Errorf("deleting finding: %w", err)
		}
		if n == 0 {
			fmt.Fprintf(cmd.OutOrStdout(), "No finding with id %d.\n", id)
			return nil
		}
		fmt.Fprintf(cmd.OutOrStdout(), "Deleted finding #%d.\n", id)
		return nil
	},
}

var keysVerifyCmd = &cobra.Command{
	Use:   "verify <id>",
	Short: "Re-verify a stored finding against its provider endpoint",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		id, err := parseID(args[0])
		if err != nil {
			return err
		}
		db, encKey, err := openDBWithKey()
		if err != nil {
			return err
		}
		defer db.Close()

		f, err := db.GetFinding(id, encKey)
		if err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return fmt.Errorf("no finding with id %d", id)
			}
			return fmt.Errorf("getting finding: %w", err)
		}

		granted, consentErr := verify.EnsureConsent(db, cmd.InOrStdin(), cmd.ErrOrStderr())
		if consentErr != nil {
			return fmt.Errorf("consent check: %w", consentErr)
		}
		if !granted {
			fmt.Fprintln(cmd.ErrOrStderr(), "Verification skipped (consent not granted). Run `keyhunter legal` for details.")
			return nil
		}

		reg, err := providers.NewRegistry()
		if err != nil {
			return fmt.Errorf("loading providers: %w", err)
		}

		ef := storageToEngine(*f)
		verifier := verify.NewHTTPVerifier(10 * time.Second)
		results := verifier.VerifyAll(context.Background(), []engine.Finding{ef}, reg, 1)
		var r = <-results
		// Drain remainder (should already be closed).
		for range results {
		}

		// Persist updated verification state inline.
		var metaJSON interface{}
		if r.Metadata != nil {
			b, _ := json.Marshal(r.Metadata)
			metaJSON = string(b)
		} else {
			metaJSON = sql.NullString{}
		}
		if _, err := db.SQL().Exec(
			`UPDATE findings SET verified=1, verify_status=?, verify_http_code=?, verify_metadata_json=? WHERE id=?`,
			r.Status, r.HTTPCode, metaJSON, id,
		); err != nil {
			return fmt.Errorf("updating verify state: %w", err)
		}

		// Reload and render.
		updated, err := db.GetFinding(id, encKey)
		if err != nil {
			return fmt.Errorf("reloading finding: %w", err)
		}
		renderFinding(cmd.OutOrStdout(), updated)
		return nil
	},
}

// parseID parses a positional id argument into an int64.
func parseID(s string) (int64, error) {
	id, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid id %q: must be a positive integer", s)
	}
	if id <= 0 {
		return 0, fmt.Errorf("invalid id %d: must be a positive integer", id)
	}
	return id, nil
}

// storageToEngine converts a persisted storage.Finding into the
// engine.Finding shape used by verifiers and formatters.
func storageToEngine(f storage.Finding) engine.Finding {
	return engine.Finding{
		ProviderName:   f.ProviderName,
		KeyValue:       f.KeyValue,
		KeyMasked:      f.KeyMasked,
		Confidence:     f.Confidence,
		Source:         f.SourcePath,
		SourceType:     f.SourceType,
		LineNumber:     f.LineNumber,
		DetectedAt:     f.CreatedAt,
		Verified:       f.Verified,
		VerifyStatus:   f.VerifyStatus,
		VerifyHTTPCode: f.VerifyHTTPCode,
		VerifyMetadata: f.VerifyMetadata,
	}
}

// renderFinding writes a full, unmasked, human-readable representation of a
// finding. Used by `keys show` and `keys verify`.
func renderFinding(w io.Writer, f *storage.Finding) {
	fmt.Fprintf(w, "ID:            %d\n", f.ID)
	fmt.Fprintf(w, "Provider:      %s\n", f.ProviderName)
	fmt.Fprintf(w, "Confidence:    %s\n", f.Confidence)
	fmt.Fprintf(w, "Key:           %s\n", f.KeyValue)
	fmt.Fprintf(w, "KeyMasked:     %s\n", f.KeyMasked)
	fmt.Fprintf(w, "Source:        %s\n", f.SourcePath)
	fmt.Fprintf(w, "SourceType:    %s\n", f.SourceType)
	fmt.Fprintf(w, "Line:          %d\n", f.LineNumber)
	if !f.CreatedAt.IsZero() {
		fmt.Fprintf(w, "CreatedAt:     %s\n", f.CreatedAt.Format(time.RFC3339))
	}
	fmt.Fprintf(w, "Verified:      %t\n", f.Verified)
	if f.VerifyStatus != "" {
		fmt.Fprintf(w, "VerifyStatus:  %s\n", f.VerifyStatus)
	}
	if f.VerifyHTTPCode != 0 {
		fmt.Fprintf(w, "VerifyHTTP:    %d\n", f.VerifyHTTPCode)
	}
	if len(f.VerifyMetadata) > 0 {
		fmt.Fprintln(w, "VerifyMetadata:")
		keys := make([]string, 0, len(f.VerifyMetadata))
		for k := range f.VerifyMetadata {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		for _, k := range keys {
			fmt.Fprintf(w, "  %s: %s\n", k, f.VerifyMetadata[k])
		}
	}
}

// openDBWithKey opens the KeyHunter SQLite database using the same precedence
// rules as the scan command (viper database.path, then config.Load default)
// and derives the per-installation encryption key.
func openDBWithKey() (*storage.DB, []byte, error) {
	cfg := config.Load()
	dbPath := viper.GetString("database.path")
	if dbPath == "" {
		dbPath = cfg.DBPath
	}
	if err := os.MkdirAll(filepath.Dir(dbPath), 0700); err != nil {
		return nil, nil, fmt.Errorf("creating database directory: %w", err)
	}
	db, err := storage.Open(dbPath)
	if err != nil {
		return nil, nil, fmt.Errorf("opening database: %w", err)
	}
	passphrase := viper.GetString("encryption.passphrase")
	if passphrase == "" {
		passphrase = os.Getenv("KEYHUNTER_PASSPHRASE")
	}
	if passphrase == "" {
		passphrase = cfg.Passphrase
	}
	encKey, err := loadOrCreateEncKey(db, passphrase)
	if err != nil {
		db.Close()
		return nil, nil, fmt.Errorf("preparing encryption key: %w", err)
	}
	return db, encKey, nil
}

func init() {
	// list flags
	keysListCmd.Flags().BoolVar(&flagKeysUnmask, "unmask", false, "show full key values")
	keysListCmd.Flags().StringVar(&flagKeysProvider, "provider", "", "filter by provider name")
	keysListCmd.Flags().BoolVar(&flagKeysVerified, "verified", false, "filter: verified only (use --verified=false for unverified only)")
	keysListCmd.Flags().IntVar(&flagKeysLimit, "limit", 0, "max rows (0 = unlimited)")

	// export flags
	keysExportCmd.Flags().StringVar(&flagKeysFormat, "format", "json", "export format: json, csv")
	keysExportCmd.Flags().StringVar(&flagKeysOutFile, "output", "", "write to file instead of stdout")

	// delete flags
	keysDeleteCmd.Flags().BoolVar(&flagKeysYes, "yes", false, "skip confirmation prompt")

	// subcommand wiring
	keysCmd.AddCommand(keysListCmd, keysShowCmd, keysExportCmd, keysCopyCmd, keysDeleteCmd, keysVerifyCmd)

	_ = viper.BindPFlag("keys.unmask", keysListCmd.Flags().Lookup("unmask"))
}
