package output

import (
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"github.com/charmbracelet/lipgloss"
	"github.com/salvacybersec/keyhunter/pkg/engine"
)

func init() {
	Register("table", TableFormatter{})
}

// TableFormatter renders findings as a colored terminal table.
//
// Colors are emitted only when the destination writer is a TTY and the
// NO_COLOR environment variable is not set. When writing to a non-TTY
// (pipes, files, bytes.Buffer in tests), all styles collapse to plain
// text so no ANSI escape sequences appear in the output.
//
// When any finding has Verified=true, an extra VERIFY column is appended
// and per-finding VerifyMetadata (if present) is rendered on an indented
// secondary line. Backward compatible: if no finding is verified, the
// layout matches the pre-Phase-5 output exactly.
type TableFormatter struct{}

// Format implements the Formatter interface.
func (TableFormatter) Format(findings []engine.Finding, w io.Writer, opts Options) error {
	if len(findings) == 0 {
		_, err := fmt.Fprintln(w, "No API keys found.")
		return err
	}

	styles := newTableStyles(ColorsEnabled(w))

	anyVerified := false
	for _, f := range findings {
		if f.Verified {
			anyVerified = true
			break
		}
	}

	if anyVerified {
		if _, err := fmt.Fprintf(w, "%-20s  %-40s  %-10s  %-30s  %-5s  %s\n",
			styles.header.Render("PROVIDER"),
			styles.header.Render("KEY"),
			styles.header.Render("CONFIDENCE"),
			styles.header.Render("SOURCE"),
			styles.header.Render("LINE"),
			styles.header.Render("VERIFY"),
		); err != nil {
			return err
		}
	} else {
		if _, err := fmt.Fprintf(w, "%-20s  %-40s  %-10s  %-30s  %s\n",
			styles.header.Render("PROVIDER"),
			styles.header.Render("KEY"),
			styles.header.Render("CONFIDENCE"),
			styles.header.Render("SOURCE"),
			styles.header.Render("LINE"),
		); err != nil {
			return err
		}
	}
	if _, err := fmt.Fprintln(w, styles.divider.Render(strings.Repeat("─", 106))); err != nil {
		return err
	}

	for _, f := range findings {
		keyDisplay := f.KeyMasked
		if opts.Unmask {
			keyDisplay = f.KeyValue
		}

		confStyle := styles.low
		switch f.Confidence {
		case "high":
			confStyle = styles.high
		case "medium":
			confStyle = styles.medium
		}

		if anyVerified {
			if _, err := fmt.Fprintf(w, "%-20s  %-40s  %-10s  %-30s  %-5d  %s\n",
				f.ProviderName,
				keyDisplay,
				confStyle.Render(f.Confidence),
				truncate(f.Source, 28),
				f.LineNumber,
				verifySymbolStyled(f, styles),
			); err != nil {
				return err
			}
		} else {
			if _, err := fmt.Fprintf(w, "%-20s  %-40s  %-10s  %-30s  %d\n",
				f.ProviderName,
				keyDisplay,
				confStyle.Render(f.Confidence),
				truncate(f.Source, 28),
				f.LineNumber,
			); err != nil {
				return err
			}
		}

		// Indented metadata line with deterministic key order.
		if len(f.VerifyMetadata) > 0 {
			parts := make([]string, 0, len(f.VerifyMetadata))
			for k, v := range f.VerifyMetadata {
				parts = append(parts, fmt.Sprintf("%s: %s", k, v))
			}
			sort.Strings(parts)
			if _, err := fmt.Fprintf(w, "    ↳ %s\n", strings.Join(parts, ", ")); err != nil {
				return err
			}
		}
	}
	if _, err := fmt.Fprintf(w, "\n%d key(s) found.\n", len(findings)); err != nil {
		return err
	}
	return nil
}

// tableStyles bundles every lipgloss.Style used by TableFormatter so that
// callers can construct either a colored or a plain (no-op) style set via
// newTableStyles.
type tableStyles struct {
	header     lipgloss.Style
	divider    lipgloss.Style
	high       lipgloss.Style
	medium     lipgloss.Style
	low        lipgloss.Style
	verifyLive lipgloss.Style
	verifyDead lipgloss.Style
	verifyRate lipgloss.Style
	verifyErr  lipgloss.Style
	verifyUnk  lipgloss.Style
}

// newTableStyles returns a styled set when colored is true, or a fully plain
// set (every field == zero lipgloss.Style) when false. A zero lipgloss.Style
// passes text through verbatim, which guarantees no ANSI escape sequences
// are emitted on non-TTY writers.
func newTableStyles(colored bool) tableStyles {
	if !colored {
		plain := lipgloss.NewStyle()
		return tableStyles{
			header: plain, divider: plain,
			high: plain, medium: plain, low: plain,
			verifyLive: plain, verifyDead: plain, verifyRate: plain,
			verifyErr: plain, verifyUnk: plain,
		}
	}
	return tableStyles{
		header:     lipgloss.NewStyle().Bold(true).Underline(true),
		divider:    lipgloss.NewStyle().Foreground(lipgloss.Color("8")),
		high:       lipgloss.NewStyle().Foreground(lipgloss.Color("2")), // green
		medium:     lipgloss.NewStyle().Foreground(lipgloss.Color("3")), // yellow
		low:        lipgloss.NewStyle().Foreground(lipgloss.Color("1")), // red
		verifyLive: lipgloss.NewStyle().Foreground(lipgloss.Color("2")),
		verifyDead: lipgloss.NewStyle().Foreground(lipgloss.Color("1")),
		verifyRate: lipgloss.NewStyle().Foreground(lipgloss.Color("3")),
		verifyErr:  lipgloss.NewStyle().Foreground(lipgloss.Color("1")),
		verifyUnk:  lipgloss.NewStyle().Foreground(lipgloss.Color("8")),
	}
}

// verifySymbolStyled maps a finding's verification status to a glyph, styled
// via the provided tableStyles. Returns an empty string for unverified
// findings so the VERIFY column is simply blank in mixed-result tables.
func verifySymbolStyled(f engine.Finding, s tableStyles) string {
	if !f.Verified {
		return ""
	}
	switch f.VerifyStatus {
	case "live":
		return s.verifyLive.Render("✓ live")
	case "dead":
		return s.verifyDead.Render("✗ dead")
	case "rate_limited":
		return s.verifyRate.Render("⚠ rate")
	case "error":
		return s.verifyErr.Render("! err")
	default:
		return s.verifyUnk.Render("? unk")
	}
}

// PrintFindings writes findings as a colored terminal table to stdout.
// Deprecated: kept for backward compatibility with scan.go. New callers should
// use TableFormatter{}.Format directly or look the formatter up via Get("table").
func PrintFindings(findings []engine.Finding, unmask bool) {
	_ = TableFormatter{}.Format(findings, os.Stdout, Options{Unmask: unmask})
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return "..." + s[len(s)-max+3:]
}
