package gocmd

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"github.com/host-uk/core/internal/cmd/qa"
	"github.com/host-uk/core/pkg/cli"
	"github.com/host-uk/core/pkg/i18n"
)

// QA command flags - comprehensive options for all agents
var (
	qaFix               bool
	qaChanged           bool
	qaAll               bool
	qaSkip              string
	qaOnly              string
	qaCoverage          bool
	qaThreshold         float64
	qaBranchThreshold   float64
	qaDocblockThreshold float64
	qaJSON              bool
	qaVerbose           bool
	qaQuiet             bool
	qaTimeout           time.Duration
	qaShort             bool
	qaRace              bool
	qaBench             bool
	qaFailFast          bool
	qaMod               bool
	qaCI                bool
)

func addGoQACommand(parent *cli.Command) {
	qaCmd := &cli.Command{
		Use:   "qa",
		Short: "Run QA checks",
		Long: `Run comprehensive code quality checks for Go projects.

Checks available: fmt, vet, lint, test, race, fuzz, vuln, sec, bench, docblock

Examples:
  core go qa                    # Default: fmt, lint, test
  core go qa --fix              # Auto-fix formatting and lint issues
  core go qa --only=test        # Only run tests
  core go qa --skip=vuln,sec    # Skip vulnerability and security scans
  core go qa --coverage --threshold=80  # Require 80% coverage
  core go qa --changed          # Only check changed files (git-aware)
  core go qa --ci               # CI mode: strict, coverage, fail-fast
  core go qa --race --short     # Quick tests with race detection
  core go qa --json             # Output results as JSON`,
		RunE: runGoQA,
	}

	// Fix and modification flags (persistent so subcommands inherit them)
	qaCmd.PersistentFlags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible")
	qaCmd.PersistentFlags().BoolVar(&qaMod, "mod", false, "Run go mod tidy before checks")

	// Scope flags
	qaCmd.PersistentFlags().BoolVar(&qaChanged, "changed", false, "Only check changed files (git-aware)")
	qaCmd.PersistentFlags().BoolVar(&qaAll, "all", false, "Check all files (override git-aware)")
	qaCmd.PersistentFlags().StringVar(&qaSkip, "skip", "", "Skip checks (comma-separated: fmt,vet,lint,test,race,fuzz,vuln,sec,bench)")
	qaCmd.PersistentFlags().StringVar(&qaOnly, "only", "", "Only run these checks (comma-separated)")

	// Coverage flags
	qaCmd.PersistentFlags().BoolVar(&qaCoverage, "coverage", false, "Include coverage reporting")
	qaCmd.PersistentFlags().BoolVarP(&qaCoverage, "cov", "c", false, "Include coverage reporting (shorthand)")
	qaCmd.PersistentFlags().Float64Var(&qaThreshold, "threshold", 0, "Minimum statement coverage threshold (0-100), fail if below")
	qaCmd.PersistentFlags().Float64Var(&qaBranchThreshold, "branch-threshold", 0, "Minimum branch coverage threshold (0-100), fail if below")
	qaCmd.PersistentFlags().Float64Var(&qaDocblockThreshold, "docblock-threshold", 80, "Minimum docblock coverage threshold (0-100)")

	// Test flags
	qaCmd.PersistentFlags().BoolVar(&qaShort, "short", false, "Run tests with -short flag")
	qaCmd.PersistentFlags().BoolVar(&qaRace, "race", false, "Include race detection in tests")
	qaCmd.PersistentFlags().BoolVar(&qaBench, "bench", false, "Include benchmarks")

	// Output flags
	qaCmd.PersistentFlags().BoolVar(&qaJSON, "json", false, "Output results as JSON")
	qaCmd.PersistentFlags().BoolVarP(&qaVerbose, "verbose", "v", false, "Show verbose output")
	qaCmd.PersistentFlags().BoolVarP(&qaQuiet, "quiet", "q", false, "Only show errors")

	// Control flags
	qaCmd.PersistentFlags().DurationVar(&qaTimeout, "timeout", 10*time.Minute, "Timeout for all checks")
	qaCmd.PersistentFlags().BoolVar(&qaFailFast, "fail-fast", false, "Stop on first failure")
	qaCmd.PersistentFlags().BoolVar(&qaCI, "ci", false, "CI mode: strict checks, coverage required, fail-fast")

	// Preset subcommands for convenience
	qaCmd.AddCommand(&cli.Command{
		Use:   "quick",
		Short: "Quick QA: fmt, vet, lint (no tests)",
		RunE:  func(cmd *cli.Command, args []string) error { qaOnly = "fmt,vet,lint"; return runGoQA(cmd, args) },
	})

	qaCmd.AddCommand(&cli.Command{
		Use:   "full",
		Short: "Full QA: all checks including race, vuln, sec",
		RunE: func(cmd *cli.Command, args []string) error {
			qaOnly = "fmt,vet,lint,test,race,vuln,sec"
			return runGoQA(cmd, args)
		},
	})

	qaCmd.AddCommand(&cli.Command{
		Use:   "pre-commit",
		Short: "Pre-commit checks: fmt --fix, lint --fix, test --short",
		RunE: func(cmd *cli.Command, args []string) error {
			qaFix = true
			qaShort = true
			qaOnly = "fmt,lint,test"
			return runGoQA(cmd, args)
		},
	})

	qaCmd.AddCommand(&cli.Command{
		Use:   "pr",
		Short: "PR checks: full QA with coverage threshold",
		RunE: func(cmd *cli.Command, args []string) error {
			qaCoverage = true
			if qaThreshold == 0 {
				qaThreshold = 50 // Default PR threshold
			}
			qaOnly = "fmt,vet,lint,test"
			return runGoQA(cmd, args)
		},
	})

	parent.AddCommand(qaCmd)
}

// QAResult holds the result of a QA run for JSON output
type QAResult struct {
	Success         bool          `json:"success"`
	Duration        string        `json:"duration"`
	Checks          []CheckResult `json:"checks"`
	Coverage        *float64      `json:"coverage,omitempty"`
	BranchCoverage  *float64      `json:"branch_coverage,omitempty"`
	Threshold       *float64      `json:"threshold,omitempty"`
	BranchThreshold *float64      `json:"branch_threshold,omitempty"`
}

// CheckResult holds the result of a single check
type CheckResult struct {
	Name     string `json:"name"`
	Passed   bool   `json:"passed"`
	Duration string `json:"duration"`
	Error    string `json:"error,omitempty"`
	Output   string `json:"output,omitempty"`
	FixHint  string `json:"fix_hint,omitempty"`
}

func runGoQA(cmd *cli.Command, args []string) error {
	// Apply CI mode defaults
	if qaCI {
		qaCoverage = true
		qaFailFast = true
		if qaThreshold == 0 {
			qaThreshold = 50
		}
	}

	cwd, err := os.Getwd()
	if err != nil {
		return cli.Wrap(err, i18n.T("i18n.fail.get", "working directory"))
	}

	// Detect if this is a Go project
	if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
		return cli.Err("not a Go project (no go.mod found)")
	}

	// Determine which checks to run
	checkNames := determineChecks()

	if !qaJSON && !qaQuiet {
		cli.Print("%s %s\n\n", cli.DimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "Go QA"))
	}

	// Run go mod tidy if requested
	if qaMod {
		if !qaQuiet {
			cli.Print("%s %s\n", cli.DimStyle.Render("→"), "Running go mod tidy...")
		}
		modCmd := exec.Command("go", "mod", "tidy")
		modCmd.Dir = cwd
		if err := modCmd.Run(); err != nil {
			return cli.Wrap(err, "go mod tidy failed")
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), qaTimeout)
	defer cancel()

	startTime := time.Now()
	checks := buildChecks(checkNames)
	results := make([]CheckResult, 0, len(checks))
	passed := 0
	failed := 0

	for _, check := range checks {
		checkStart := time.Now()

		if !qaJSON && !qaQuiet {
			cli.Print("%s %s\n", cli.DimStyle.Render("→"), i18n.Progress(check.Name))
		}

		output, err := runCheckCapture(ctx, cwd, check)
		checkDuration := time.Since(checkStart)

		result := CheckResult{
			Name:     check.Name,
			Duration: checkDuration.Round(time.Millisecond).String(),
		}

		if err != nil {
			result.Passed = false
			result.Error = err.Error()
			if qaVerbose {
				result.Output = output
			}
			result.FixHint = fixHintFor(check.Name, output)
			failed++

			if !qaJSON && !qaQuiet {
				cli.Print("  %s %s\n", cli.ErrorStyle.Render(cli.Glyph(":cross:")), err.Error())
				if qaVerbose && output != "" {
					cli.Text(output)
				}
				if result.FixHint != "" {
					cli.Hint("fix", result.FixHint)
				}
			}

			if qaFailFast {
				results = append(results, result)
				break
			}
		} else {
			result.Passed = true
			if qaVerbose {
				result.Output = output
			}
			passed++

			if !qaJSON && !qaQuiet {
				cli.Print("  %s %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), i18n.T("i18n.done.pass"))
			}
		}

		results = append(results, result)
	}

	// Run coverage if requested
	var coverageVal *float64
	var branchVal *float64
	if qaCoverage && !qaFailFast || (qaCoverage && failed == 0) {
		cov, branch, err := runCoverage(ctx, cwd)
		if err == nil {
			coverageVal = &cov
			branchVal = &branch
			if !qaJSON && !qaQuiet {
				cli.Print("\n%s %.1f%%\n", cli.DimStyle.Render("Statement Coverage:"), cov)
				cli.Print("%s %.1f%%\n", cli.DimStyle.Render("Branch Coverage:"), branch)
			}
			if qaThreshold > 0 && cov < qaThreshold {
				failed++
				if !qaJSON && !qaQuiet {
					cli.Print("  %s Statement coverage %.1f%% below threshold %.1f%%\n",
						cli.ErrorStyle.Render(cli.Glyph(":cross:")), cov, qaThreshold)
				}
			}
			if qaBranchThreshold > 0 && branch < qaBranchThreshold {
				failed++
				if !qaJSON && !qaQuiet {
					cli.Print("  %s Branch coverage %.1f%% below threshold %.1f%%\n",
						cli.ErrorStyle.Render(cli.Glyph(":cross:")), branch, qaBranchThreshold)
				}
			}

			if failed > 0 && !qaJSON && !qaQuiet {
				cli.Hint("fix", "Run 'core go cov --open' to see uncovered lines, then add tests.")
			}
		}
	}

	duration := time.Since(startTime).Round(time.Millisecond)

	// JSON output
	if qaJSON {
		qaResult := QAResult{
			Success:        failed == 0,
			Duration:       duration.String(),
			Checks:         results,
			Coverage:       coverageVal,
			BranchCoverage: branchVal,
		}
		if qaThreshold > 0 {
			qaResult.Threshold = &qaThreshold
		}
		if qaBranchThreshold > 0 {
			qaResult.BranchThreshold = &qaBranchThreshold
		}
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(qaResult)
	}

	// Summary
	if !qaQuiet {
		cli.Blank()
		if failed > 0 {
			cli.Print("%s %s, %s (%s)\n",
				cli.ErrorStyle.Render(cli.Glyph(":cross:")),
				i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"),
				i18n.T("i18n.count.check", failed)+" "+i18n.T("i18n.done.fail"),
				duration)
		} else {
			cli.Print("%s %s (%s)\n",
				cli.SuccessStyle.Render(cli.Glyph(":check:")),
				i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"),
				duration)
		}
	}

	if failed > 0 {
		return cli.Err("QA checks failed: %d passed, %d failed", passed, failed)
	}
	return nil
}

func determineChecks() []string {
	// If --only is specified, use those
	if qaOnly != "" {
		return strings.Split(qaOnly, ",")
	}

	// Default checks
	checks := []string{"fmt", "lint", "test", "fuzz", "docblock"}

	// Add race if requested
	if qaRace {
		// Replace test with race (which includes test)
		for i, c := range checks {
			if c == "test" {
				checks[i] = "race"
				break
			}
		}
	}

	// Add bench if requested
	if qaBench {
		checks = append(checks, "bench")
	}

	// Remove skipped checks
	if qaSkip != "" {
		skipMap := make(map[string]bool)
		for _, s := range strings.Split(qaSkip, ",") {
			skipMap[strings.TrimSpace(s)] = true
		}
		filtered := make([]string, 0, len(checks))
		for _, c := range checks {
			if !skipMap[c] {
				filtered = append(filtered, c)
			}
		}
		checks = filtered
	}

	return checks
}

// QACheck represents a single QA check.
type QACheck struct {
	Name    string
	Command string
	Args    []string
}

func buildChecks(names []string) []QACheck {
	var checks []QACheck
	for _, name := range names {
		name = strings.TrimSpace(name)
		check := buildCheck(name)
		if check.Command != "" {
			checks = append(checks, check)
		}
	}
	return checks
}

func buildCheck(name string) QACheck {
	switch name {
	case "fmt", "format":
		args := []string{"-l", "."}
		if qaFix {
			args = []string{"-w", "."}
		}
		return QACheck{Name: "format", Command: "gofmt", Args: args}

	case "vet":
		return QACheck{Name: "vet", Command: "go", Args: []string{"vet", "./..."}}

	case "lint":
		args := []string{"run"}
		if qaFix {
			args = append(args, "--fix")
		}
		if qaChanged && !qaAll {
			args = append(args, "--new-from-rev=HEAD")
		}
		args = append(args, "./...")
		return QACheck{Name: "lint", Command: "golangci-lint", Args: args}

	case "test":
		args := []string{"test"}
		if qaShort {
			args = append(args, "-short")
		}
		if qaVerbose {
			args = append(args, "-v")
		}
		args = append(args, "./...")
		return QACheck{Name: "test", Command: "go", Args: args}

	case "race":
		args := []string{"test", "-race"}
		if qaShort {
			args = append(args, "-short")
		}
		if qaVerbose {
			args = append(args, "-v")
		}
		args = append(args, "./...")
		return QACheck{Name: "race", Command: "go", Args: args}

	case "bench":
		args := []string{"test", "-bench=.", "-benchmem", "-run=^$"}
		args = append(args, "./...")
		return QACheck{Name: "bench", Command: "go", Args: args}

	case "vuln":
		return QACheck{Name: "vuln", Command: "govulncheck", Args: []string{"./..."}}

	case "sec":
		return QACheck{Name: "sec", Command: "gosec", Args: []string{"-quiet", "./..."}}

	case "fuzz":
		return QACheck{Name: "fuzz", Command: "_internal_"}

	case "docblock":
		// Special internal check - handled separately
		return QACheck{Name: "docblock", Command: "_internal_"}

	default:
		return QACheck{}
	}
}

// fixHintFor returns an actionable fix instruction for a given check failure.
func fixHintFor(checkName, output string) string {
	switch checkName {
	case "format", "fmt":
		return "Run 'core go qa fmt --fix' to auto-format."
	case "vet":
		return "Fix the issues reported by go vet — typically genuine bugs."
	case "lint":
		return "Run 'core go qa lint --fix' for auto-fixable issues."
	case "test":
		if name := extractFailingTest(output); name != "" {
			return fmt.Sprintf("Run 'go test -run %s -v ./...' to debug.", name)
		}
		return "Run 'go test -run <TestName> -v ./path/' to debug."
	case "race":
		return "Data race detected. Add mutex, channel, or atomic to synchronise shared state."
	case "bench":
		return "Benchmark regression. Run 'go test -bench=. -benchmem' to reproduce."
	case "vuln":
		return "Run 'govulncheck ./...' for details. Update affected deps with 'go get -u'."
	case "sec":
		return "Review gosec findings. Common fixes: validate inputs, parameterised queries."
	case "fuzz":
		return "Add a regression test for the crashing input in testdata/fuzz/<Target>/."
	case "docblock":
		return "Add doc comments to exported symbols: '// Name does X.' before each declaration."
	default:
		return ""
	}
}

var failTestRe = regexp.MustCompile(`--- FAIL: (\w+)`)

// extractFailingTest parses the first failing test name from go test output.
func extractFailingTest(output string) string {
	if m := failTestRe.FindStringSubmatch(output); len(m) > 1 {
		return m[1]
	}
	return ""
}

func runCheckCapture(ctx context.Context, dir string, check QACheck) (string, error) {
	// Handle internal checks
	if check.Command == "_internal_" {
		return runInternalCheck(check)
	}

	// Check if command exists
	if _, err := exec.LookPath(check.Command); err != nil {
		return "", cli.Err("%s: not installed", check.Command)
	}

	cmd := exec.CommandContext(ctx, check.Command, check.Args...)
	cmd.Dir = dir

	// For gofmt -l, capture output to check if files need formatting
	if check.Name == "format" && len(check.Args) > 0 && check.Args[0] == "-l" {
		output, err := cmd.Output()
		if err != nil {
			return string(output), err
		}
		if len(output) > 0 {
			// Show files that need formatting
			if !qaQuiet && !qaJSON {
				cli.Text(string(output))
			}
			return string(output), cli.Err("files need formatting (use --fix)")
		}
		return "", nil
	}

	// For other commands, stream or capture based on quiet mode
	if qaQuiet || qaJSON {
		output, err := cmd.CombinedOutput()
		return string(output), err
	}

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return "", cmd.Run()
}

func runCoverage(ctx context.Context, dir string) (float64, float64, error) {
	// Create temp file for coverage data
	covFile, err := os.CreateTemp("", "coverage-*.out")
	if err != nil {
		return 0, 0, err
	}
	covPath := covFile.Name()
	_ = covFile.Close()
	defer os.Remove(covPath)

	args := []string{"test", "-cover", "-covermode=atomic", "-coverprofile=" + covPath}
	if qaShort {
		args = append(args, "-short")
	}
	args = append(args, "./...")

	cmd := exec.CommandContext(ctx, "go", args...)
	cmd.Dir = dir
	if !qaQuiet && !qaJSON {
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}

	if err := cmd.Run(); err != nil {
		return 0, 0, err
	}

	// Parse statement coverage
	coverCmd := exec.CommandContext(ctx, "go", "tool", "cover", "-func="+covPath)
	output, err := coverCmd.Output()
	if err != nil {
		return 0, 0, err
	}

	// Parse last line for total coverage
	lines := strings.Split(strings.TrimSpace(string(output)), "\n")
	var statementPct float64
	if len(lines) > 0 {
		lastLine := lines[len(lines)-1]
		fields := strings.Fields(lastLine)
		if len(fields) >= 3 {
			// Parse percentage (e.g., "45.6%")
			pctStr := strings.TrimSuffix(fields[len(fields)-1], "%")
			_, _ = fmt.Sscanf(pctStr, "%f", &statementPct)
		}
	}

	// Parse branch coverage
	branchPct, err := calculateBlockCoverage(covPath)
	if err != nil {
		return statementPct, 0, err
	}

	return statementPct, branchPct, nil
}

// runInternalCheck runs internal Go-based checks (not external commands).
func runInternalCheck(check QACheck) (string, error) {
	switch check.Name {
	case "fuzz":
		// Short burst fuzz in QA (3s per target)
		duration := 3 * time.Second
		if qaTimeout > 0 && qaTimeout < 30*time.Second {
			duration = 2 * time.Second
		}
		return "", runGoFuzz(duration, "", "", qaVerbose)

	case "docblock":
		result, err := qa.CheckDocblockCoverage([]string{"./..."})
		if err != nil {
			return "", err
		}
		result.Threshold = qaDocblockThreshold
		result.Passed = result.Coverage >= qaDocblockThreshold

		if !result.Passed {
			var output strings.Builder
			output.WriteString(fmt.Sprintf("Docblock coverage: %.1f%% (threshold: %.1f%%)\n",
				result.Coverage, qaDocblockThreshold))
			for _, m := range result.Missing {
				output.WriteString(fmt.Sprintf("%s:%d\n", m.File, m.Line))
			}
			return output.String(), cli.Err("docblock coverage %.1f%% below threshold %.1f%%",
				result.Coverage, qaDocblockThreshold)
		}
		return fmt.Sprintf("Docblock coverage: %.1f%%", result.Coverage), nil

	default:
		return "", cli.Err("unknown internal check: %s", check.Name)
	}
}
