package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/Wikid82/charon/backend/internal/patchreport"
)

type thresholdJSON struct {
	Overall  float64 `json:"overall_patch_coverage_min"`
	Backend  float64 `json:"backend_patch_coverage_min"`
	Frontend float64 `json:"frontend_patch_coverage_min"`
}

type thresholdSourcesJSON struct {
	Overall  string `json:"overall"`
	Backend  string `json:"backend"`
	Frontend string `json:"frontend"`
}

type artifactsJSON struct {
	Markdown string `json:"markdown"`
	JSON     string `json:"json"`
}

type reportJSON struct {
	Baseline             string                           `json:"baseline"`
	GeneratedAt          string                           `json:"generated_at"`
	Mode                 string                           `json:"mode"`
	Thresholds           thresholdJSON                    `json:"thresholds"`
	ThresholdSources     thresholdSourcesJSON             `json:"threshold_sources"`
	Overall              patchreport.ScopeCoverage        `json:"overall"`
	Backend              patchreport.ScopeCoverage        `json:"backend"`
	Frontend             patchreport.ScopeCoverage        `json:"frontend"`
	FilesNeedingCoverage []patchreport.FileCoverageDetail `json:"files_needing_coverage,omitempty"`
	Warnings             []string                         `json:"warnings,omitempty"`
	Artifacts            artifactsJSON                    `json:"artifacts"`
}

func main() {
	repoRootFlag := flag.String("repo-root", ".", "Repository root path")
	baselineFlag := flag.String("baseline", "origin/development...HEAD", "Git diff baseline")
	backendCoverageFlag := flag.String("backend-coverage", "backend/coverage.txt", "Backend Go coverage profile")
	frontendCoverageFlag := flag.String("frontend-coverage", "frontend/coverage/lcov.info", "Frontend LCOV coverage report")
	jsonOutFlag := flag.String("json-out", "test-results/local-patch-report.json", "Path to JSON output report")
	mdOutFlag := flag.String("md-out", "test-results/local-patch-report.md", "Path to markdown output report")
	flag.Parse()

	repoRoot, err := filepath.Abs(*repoRootFlag)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error resolving repo root: %v\n", err)
		os.Exit(1)
	}

	backendCoveragePath := resolvePath(repoRoot, *backendCoverageFlag)
	frontendCoveragePath := resolvePath(repoRoot, *frontendCoverageFlag)
	jsonOutPath := resolvePath(repoRoot, *jsonOutFlag)
	mdOutPath := resolvePath(repoRoot, *mdOutFlag)

	err = assertFileExists(backendCoveragePath, "backend coverage file")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	err = assertFileExists(frontendCoveragePath, "frontend coverage file")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	diffContent, err := gitDiff(repoRoot, *baselineFlag)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error generating git diff: %v\n", err)
		os.Exit(1)
	}

	backendChanged, frontendChanged, err := patchreport.ParseUnifiedDiffChangedLines(diffContent)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error parsing changed lines from diff: %v\n", err)
		os.Exit(1)
	}

	backendCoverage, err := patchreport.ParseGoCoverageProfile(backendCoveragePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error parsing backend coverage: %v\n", err)
		os.Exit(1)
	}
	frontendCoverage, err := patchreport.ParseLCOVProfile(frontendCoveragePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error parsing frontend coverage: %v\n", err)
		os.Exit(1)
	}

	overallThreshold := patchreport.ResolveThreshold("CHARON_OVERALL_PATCH_COVERAGE_MIN", 90, nil)
	backendThreshold := patchreport.ResolveThreshold("CHARON_BACKEND_PATCH_COVERAGE_MIN", 85, nil)
	frontendThreshold := patchreport.ResolveThreshold("CHARON_FRONTEND_PATCH_COVERAGE_MIN", 85, nil)

	backendScope := patchreport.ComputeScopeCoverage(backendChanged, backendCoverage)
	frontendScope := patchreport.ComputeScopeCoverage(frontendChanged, frontendCoverage)
	overallScope := patchreport.MergeScopeCoverage(backendScope, frontendScope)
	backendFilesNeedingCoverage := patchreport.ComputeFilesNeedingCoverage(backendChanged, backendCoverage, backendThreshold.Value)
	frontendFilesNeedingCoverage := patchreport.ComputeFilesNeedingCoverage(frontendChanged, frontendCoverage, frontendThreshold.Value)
	filesNeedingCoverage := patchreport.MergeFileCoverageDetails(backendFilesNeedingCoverage, frontendFilesNeedingCoverage)

	backendScope = patchreport.ApplyStatus(backendScope, backendThreshold.Value)
	frontendScope = patchreport.ApplyStatus(frontendScope, frontendThreshold.Value)
	overallScope = patchreport.ApplyStatus(overallScope, overallThreshold.Value)

	warnings := patchreport.SortedWarnings([]string{
		overallThreshold.Warning,
		backendThreshold.Warning,
		frontendThreshold.Warning,
	})
	if overallScope.Status == "warn" {
		warnings = append(warnings, fmt.Sprintf("Overall patch coverage %.1f%% is below threshold %.1f%%", overallScope.PatchCoveragePct, overallThreshold.Value))
	}
	if backendScope.Status == "warn" {
		warnings = append(warnings, fmt.Sprintf("Backend patch coverage %.1f%% is below threshold %.1f%%", backendScope.PatchCoveragePct, backendThreshold.Value))
	}
	if frontendScope.Status == "warn" {
		warnings = append(warnings, fmt.Sprintf("Frontend patch coverage %.1f%% is below threshold %.1f%%", frontendScope.PatchCoveragePct, frontendThreshold.Value))
	}

	report := reportJSON{
		Baseline:    *baselineFlag,
		GeneratedAt: time.Now().UTC().Format(time.RFC3339),
		Mode:        "warn",
		Thresholds: thresholdJSON{
			Overall:  overallThreshold.Value,
			Backend:  backendThreshold.Value,
			Frontend: frontendThreshold.Value,
		},
		ThresholdSources: thresholdSourcesJSON{
			Overall:  overallThreshold.Source,
			Backend:  backendThreshold.Source,
			Frontend: frontendThreshold.Source,
		},
		Overall:              overallScope,
		Backend:              backendScope,
		Frontend:             frontendScope,
		FilesNeedingCoverage: filesNeedingCoverage,
		Warnings:             warnings,
		Artifacts: artifactsJSON{
			Markdown: relOrAbs(repoRoot, mdOutPath),
			JSON:     relOrAbs(repoRoot, jsonOutPath),
		},
	}

	if err := os.MkdirAll(filepath.Dir(jsonOutPath), 0o750); err != nil {
		fmt.Fprintf(os.Stderr, "error creating json output directory: %v\n", err)
		os.Exit(1)
	}
	if err := os.MkdirAll(filepath.Dir(mdOutPath), 0o750); err != nil {
		fmt.Fprintf(os.Stderr, "error creating markdown output directory: %v\n", err)
		os.Exit(1)
	}

	if err := writeJSON(jsonOutPath, report); err != nil {
		fmt.Fprintf(os.Stderr, "error writing json report: %v\n", err)
		os.Exit(1)
	}
	if err := writeMarkdown(mdOutPath, report, relOrAbs(repoRoot, backendCoveragePath), relOrAbs(repoRoot, frontendCoveragePath)); err != nil {
		fmt.Fprintf(os.Stderr, "error writing markdown report: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Local patch report generated (mode=%s)\n", report.Mode)
	fmt.Printf("JSON: %s\n", relOrAbs(repoRoot, jsonOutPath))
	fmt.Printf("Markdown: %s\n", relOrAbs(repoRoot, mdOutPath))
	for _, warning := range warnings {
		fmt.Printf("WARN: %s\n", warning)
	}
}

func resolvePath(repoRoot, configured string) string {
	if filepath.IsAbs(configured) {
		return configured
	}
	return filepath.Join(repoRoot, configured)
}

func relOrAbs(repoRoot, path string) string {
	rel, err := filepath.Rel(repoRoot, path)
	if err != nil {
		return filepath.ToSlash(path)
	}
	return filepath.ToSlash(rel)
}

func assertFileExists(path, label string) error {
	info, err := os.Stat(path)
	if err != nil {
		return fmt.Errorf("missing %s at %s: %w", label, path, err)
	}
	if info.IsDir() {
		return fmt.Errorf("expected %s to be a file but found directory: %s", label, path)
	}
	return nil
}

func gitDiff(repoRoot, baseline string) (string, error) {
	cmd := exec.Command("git", "-C", repoRoot, "diff", "--unified=0", baseline)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("git diff %s failed: %w (%s)", baseline, err, strings.TrimSpace(string(output)))
	}
	return string(output), nil
}

func writeJSON(path string, report reportJSON) error {
	encoded, err := json.MarshalIndent(report, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal report json: %w", err)
	}
	encoded = append(encoded, '\n')
	if err := os.WriteFile(path, encoded, 0o600); err != nil {
		return fmt.Errorf("write report json file: %w", err)
	}
	return nil
}

func writeMarkdown(path string, report reportJSON, backendCoveragePath, frontendCoveragePath string) error {
	var builder strings.Builder
	builder.WriteString("# Local Patch Coverage Report\n\n")
	builder.WriteString("## Metadata\n\n")
	builder.WriteString(fmt.Sprintf("- Generated: %s\n", report.GeneratedAt))
	builder.WriteString(fmt.Sprintf("- Baseline: `%s`\n", report.Baseline))
	builder.WriteString(fmt.Sprintf("- Mode: `%s`\n\n", report.Mode))

	builder.WriteString("## Inputs\n\n")
	builder.WriteString(fmt.Sprintf("- Backend coverage: `%s`\n", backendCoveragePath))
	builder.WriteString(fmt.Sprintf("- Frontend coverage: `%s`\n\n", frontendCoveragePath))

	builder.WriteString("## Resolved Thresholds\n\n")
	builder.WriteString("| Scope | Minimum (%) | Source |\n")
	builder.WriteString("|---|---:|---|\n")
	builder.WriteString(fmt.Sprintf("| Overall | %.1f | %s |\n", report.Thresholds.Overall, report.ThresholdSources.Overall))
	builder.WriteString(fmt.Sprintf("| Backend | %.1f | %s |\n", report.Thresholds.Backend, report.ThresholdSources.Backend))
	builder.WriteString(fmt.Sprintf("| Frontend | %.1f | %s |\n\n", report.Thresholds.Frontend, report.ThresholdSources.Frontend))

	builder.WriteString("## Coverage Summary\n\n")
	builder.WriteString("| Scope | Changed Lines | Covered Lines | Patch Coverage (%) | Status |\n")
	builder.WriteString("|---|---:|---:|---:|---|\n")
	builder.WriteString(scopeRow("Overall", report.Overall))
	builder.WriteString(scopeRow("Backend", report.Backend))
	builder.WriteString(scopeRow("Frontend", report.Frontend))
	builder.WriteString("\n")

	if len(report.FilesNeedingCoverage) > 0 {
		builder.WriteString("## Files Needing Coverage\n\n")
		builder.WriteString("| Path | Patch Coverage (%) | Uncovered Changed Lines | Uncovered Changed Line Ranges |\n")
		builder.WriteString("|---|---:|---:|---|\n")
		for _, fileCoverage := range report.FilesNeedingCoverage {
			ranges := "-"
			if len(fileCoverage.UncoveredChangedLineRange) > 0 {
				ranges = strings.Join(fileCoverage.UncoveredChangedLineRange, ", ")
			}
			builder.WriteString(fmt.Sprintf("| `%s` | %.1f | %d | %s |\n", fileCoverage.Path, fileCoverage.PatchCoveragePct, fileCoverage.UncoveredChangedLines, ranges))
		}
		builder.WriteString("\n")
	}

	if len(report.Warnings) > 0 {
		builder.WriteString("## Warnings\n\n")
		for _, warning := range report.Warnings {
			builder.WriteString(fmt.Sprintf("- %s\n", warning))
		}
		builder.WriteString("\n")
	}

	builder.WriteString("## Artifacts\n\n")
	builder.WriteString(fmt.Sprintf("- Markdown: `%s`\n", report.Artifacts.Markdown))
	builder.WriteString(fmt.Sprintf("- JSON: `%s`\n", report.Artifacts.JSON))

	if err := os.WriteFile(path, []byte(builder.String()), 0o600); err != nil {
		return fmt.Errorf("write markdown file: %w", err)
	}
	return nil
}

func scopeRow(name string, scope patchreport.ScopeCoverage) string {
	return fmt.Sprintf("| %s | %d | %d | %.1f | %s |\n", name, scope.ChangedLines, scope.CoveredLines, scope.PatchCoveragePct, scope.Status)
}
