package patchreport

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

type LineSet map[int]struct{}

type FileLineSet map[string]LineSet

type CoverageData struct {
	Executable FileLineSet
	Covered    FileLineSet
}

type ScopeCoverage struct {
	ChangedLines     int     `json:"changed_lines"`
	CoveredLines     int     `json:"covered_lines"`
	PatchCoveragePct float64 `json:"patch_coverage_pct"`
	Status           string  `json:"status"`
}

type FileCoverageDetail struct {
	Path                      string   `json:"path"`
	PatchCoveragePct          float64  `json:"patch_coverage_pct"`
	UncoveredChangedLines     int      `json:"uncovered_changed_lines"`
	UncoveredChangedLineRange []string `json:"uncovered_changed_line_ranges,omitempty"`
}

type ThresholdResolution struct {
	Value   float64
	Source  string
	Warning string
}

var hunkPattern = regexp.MustCompile(`^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@`)

const maxScannerTokenSize = 2 * 1024 * 1024

func newScannerWithLargeBuffer(input *strings.Reader) *bufio.Scanner {
	scanner := bufio.NewScanner(input)
	scanner.Buffer(make([]byte, 0, 64*1024), maxScannerTokenSize)
	return scanner
}

func newFileScannerWithLargeBuffer(file *os.File) *bufio.Scanner {
	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 0, 64*1024), maxScannerTokenSize)
	return scanner
}

func ResolveThreshold(envName string, defaultValue float64, lookup func(string) (string, bool)) ThresholdResolution {
	if lookup == nil {
		lookup = os.LookupEnv
	}

	raw, ok := lookup(envName)
	if !ok {
		return ThresholdResolution{Value: defaultValue, Source: "default"}
	}

	raw = strings.TrimSpace(raw)
	value, err := strconv.ParseFloat(raw, 64)
	if err != nil || value < 0 || value > 100 {
		return ThresholdResolution{
			Value:   defaultValue,
			Source:  "default",
			Warning: fmt.Sprintf("Ignoring invalid %s=%q; using default %.1f", envName, raw, defaultValue),
		}
	}

	return ThresholdResolution{Value: value, Source: "env"}
}

func ParseUnifiedDiffChangedLines(diffContent string) (FileLineSet, FileLineSet, error) {
	backendChanged := make(FileLineSet)
	frontendChanged := make(FileLineSet)

	var currentFile string
	currentScope := ""
	currentNewLine := 0
	inHunk := false

	scanner := newScannerWithLargeBuffer(strings.NewReader(diffContent))
	for scanner.Scan() {
		line := scanner.Text()

		if strings.HasPrefix(line, "+++") {
			currentFile = ""
			currentScope = ""
			inHunk = false

			newFile := strings.TrimSpace(strings.TrimPrefix(line, "+++"))
			if newFile == "/dev/null" {
				continue
			}
			newFile = strings.TrimPrefix(newFile, "b/")
			newFile = normalizeRepoPath(newFile)
			if strings.HasPrefix(newFile, "backend/") {
				currentFile = newFile
				currentScope = "backend"
			} else if strings.HasPrefix(newFile, "frontend/") {
				currentFile = newFile
				currentScope = "frontend"
			}
			continue
		}

		if matches := hunkPattern.FindStringSubmatch(line); matches != nil {
			startLine, err := strconv.Atoi(matches[1])
			if err != nil {
				return nil, nil, fmt.Errorf("parse hunk start line: %w", err)
			}
			currentNewLine = startLine
			inHunk = true
			continue
		}

		if !inHunk || currentFile == "" || currentScope == "" || line == "" {
			continue
		}

		switch line[0] {
		case '+':
			if strings.HasPrefix(line, "+++") {
				continue
			}
			switch currentScope {
			case "backend":
				addLine(backendChanged, currentFile, currentNewLine)
			case "frontend":
				addLine(frontendChanged, currentFile, currentNewLine)
			}
			currentNewLine++
		case '-':
		case ' ':
			currentNewLine++
		case '\\':
		default:
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, nil, fmt.Errorf("scan diff content: %w", err)
	}

	return backendChanged, frontendChanged, nil
}

func ParseGoCoverageProfile(profilePath string) (data CoverageData, err error) {
	validatedPath, err := validateReadablePath(profilePath)
	if err != nil {
		return CoverageData{}, fmt.Errorf("validate go coverage profile path: %w", err)
	}

	// #nosec G304 -- validatedPath is cleaned and resolved to an absolute path by validateReadablePath.
	file, err := os.Open(validatedPath)
	if err != nil {
		return CoverageData{}, fmt.Errorf("open go coverage profile: %w", err)
	}
	defer func() {
		if closeErr := file.Close(); closeErr != nil && err == nil {
			err = fmt.Errorf("close go coverage profile: %w", closeErr)
		}
	}()

	data = CoverageData{
		Executable: make(FileLineSet),
		Covered:    make(FileLineSet),
	}

	scanner := newFileScannerWithLargeBuffer(file)
	firstLine := true
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		if firstLine {
			firstLine = false
			if strings.HasPrefix(line, "mode:") {
				continue
			}
		}

		fields := strings.Fields(line)
		if len(fields) != 3 {
			continue
		}

		count, err := strconv.Atoi(fields[2])
		if err != nil {
			continue
		}

		filePart, startLine, endLine, err := parseCoverageRange(fields[0])
		if err != nil {
			continue
		}

		normalizedFile := normalizeGoCoveragePath(filePart)
		if normalizedFile == "" {
			continue
		}

		for lineNo := startLine; lineNo <= endLine; lineNo++ {
			addLine(data.Executable, normalizedFile, lineNo)
			if count > 0 {
				addLine(data.Covered, normalizedFile, lineNo)
			}
		}
	}

	if scanErr := scanner.Err(); scanErr != nil {
		return CoverageData{}, fmt.Errorf("scan go coverage profile: %w", scanErr)
	}

	return data, nil
}

func ParseLCOVProfile(lcovPath string) (data CoverageData, err error) {
	validatedPath, err := validateReadablePath(lcovPath)
	if err != nil {
		return CoverageData{}, fmt.Errorf("validate lcov profile path: %w", err)
	}

	// #nosec G304 -- validatedPath is cleaned and resolved to an absolute path by validateReadablePath.
	file, err := os.Open(validatedPath)
	if err != nil {
		return CoverageData{}, fmt.Errorf("open lcov profile: %w", err)
	}
	defer func() {
		if closeErr := file.Close(); closeErr != nil && err == nil {
			err = fmt.Errorf("close lcov profile: %w", closeErr)
		}
	}()

	data = CoverageData{
		Executable: make(FileLineSet),
		Covered:    make(FileLineSet),
	}

	currentFiles := make([]string, 0, 2)
	scanner := newFileScannerWithLargeBuffer(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		switch {
		case strings.HasPrefix(line, "SF:"):
			sourceFile := strings.TrimSpace(strings.TrimPrefix(line, "SF:"))
			currentFiles = normalizeFrontendCoveragePaths(sourceFile)
		case strings.HasPrefix(line, "DA:"):
			if len(currentFiles) == 0 {
				continue
			}
			parts := strings.Split(strings.TrimPrefix(line, "DA:"), ",")
			if len(parts) < 2 {
				continue
			}
			lineNo, err := strconv.Atoi(strings.TrimSpace(parts[0]))
			if err != nil {
				continue
			}
			hits, err := strconv.Atoi(strings.TrimSpace(parts[1]))
			if err != nil {
				continue
			}
			for _, filePath := range currentFiles {
				addLine(data.Executable, filePath, lineNo)
				if hits > 0 {
					addLine(data.Covered, filePath, lineNo)
				}
			}
		case line == "end_of_record":
			currentFiles = currentFiles[:0]
		}
	}

	if scanErr := scanner.Err(); scanErr != nil {
		return CoverageData{}, fmt.Errorf("scan lcov profile: %w", scanErr)
	}

	return data, nil
}

func ComputeScopeCoverage(changedLines FileLineSet, coverage CoverageData) ScopeCoverage {
	changedCount := 0
	coveredCount := 0

	for filePath, lines := range changedLines {
		executable, ok := coverage.Executable[filePath]
		if !ok {
			continue
		}
		coveredLines := coverage.Covered[filePath]

		for lineNo := range lines {
			if _, executableLine := executable[lineNo]; !executableLine {
				continue
			}
			changedCount++
			if _, isCovered := coveredLines[lineNo]; isCovered {
				coveredCount++
			}
		}
	}

	pct := 100.0
	if changedCount > 0 {
		pct = roundToOneDecimal(float64(coveredCount) * 100 / float64(changedCount))
	}

	return ScopeCoverage{
		ChangedLines:     changedCount,
		CoveredLines:     coveredCount,
		PatchCoveragePct: pct,
	}
}

func MergeScopeCoverage(scopes ...ScopeCoverage) ScopeCoverage {
	changed := 0
	covered := 0
	for _, scope := range scopes {
		changed += scope.ChangedLines
		covered += scope.CoveredLines
	}

	pct := 100.0
	if changed > 0 {
		pct = roundToOneDecimal(float64(covered) * 100 / float64(changed))
	}

	return ScopeCoverage{
		ChangedLines:     changed,
		CoveredLines:     covered,
		PatchCoveragePct: pct,
	}
}

func ApplyStatus(scope ScopeCoverage, minThreshold float64) ScopeCoverage {
	scope.Status = "pass"
	if scope.PatchCoveragePct < minThreshold {
		scope.Status = "warn"
	}
	return scope
}

func ComputeFilesNeedingCoverage(changedLines FileLineSet, coverage CoverageData, minThreshold float64) []FileCoverageDetail {
	details := make([]FileCoverageDetail, 0, len(changedLines))

	for filePath, lines := range changedLines {
		executable, ok := coverage.Executable[filePath]
		if !ok {
			continue
		}

		coveredLines := coverage.Covered[filePath]
		executableChanged := 0
		coveredChanged := 0
		uncoveredLines := make([]int, 0, len(lines))

		for lineNo := range lines {
			if _, executableLine := executable[lineNo]; !executableLine {
				continue
			}
			executableChanged++
			if _, isCovered := coveredLines[lineNo]; isCovered {
				coveredChanged++
			} else {
				uncoveredLines = append(uncoveredLines, lineNo)
			}
		}

		if executableChanged == 0 {
			continue
		}

		patchCoveragePct := roundToOneDecimal(float64(coveredChanged) * 100 / float64(executableChanged))
		uncoveredCount := executableChanged - coveredChanged
		if uncoveredCount == 0 && patchCoveragePct >= minThreshold {
			continue
		}

		sort.Ints(uncoveredLines)
		details = append(details, FileCoverageDetail{
			Path:                      filePath,
			PatchCoveragePct:          patchCoveragePct,
			UncoveredChangedLines:     uncoveredCount,
			UncoveredChangedLineRange: formatLineRanges(uncoveredLines),
		})
	}

	sortFileCoverageDetails(details)
	return details
}

func MergeFileCoverageDetails(groups ...[]FileCoverageDetail) []FileCoverageDetail {
	count := 0
	for _, group := range groups {
		count += len(group)
	}

	merged := make([]FileCoverageDetail, 0, count)
	for _, group := range groups {
		merged = append(merged, group...)
	}

	sortFileCoverageDetails(merged)
	return merged
}

func SortedWarnings(warnings []string) []string {
	filtered := make([]string, 0, len(warnings))
	for _, warning := range warnings {
		if strings.TrimSpace(warning) != "" {
			filtered = append(filtered, warning)
		}
	}
	sort.Strings(filtered)
	return filtered
}

func parseCoverageRange(rangePart string) (string, int, int, error) {
	pathAndRange := strings.SplitN(rangePart, ":", 2)
	if len(pathAndRange) != 2 {
		return "", 0, 0, fmt.Errorf("invalid range format")
	}

	filePart := strings.TrimSpace(pathAndRange[0])
	rangeSpec := strings.TrimSpace(pathAndRange[1])
	coords := strings.SplitN(rangeSpec, ",", 2)
	if len(coords) != 2 {
		return "", 0, 0, fmt.Errorf("invalid coordinate format")
	}

	startParts := strings.SplitN(coords[0], ".", 2)
	endParts := strings.SplitN(coords[1], ".", 2)
	if len(startParts) == 0 || len(endParts) == 0 {
		return "", 0, 0, fmt.Errorf("invalid line coordinate")
	}

	startLine, err := strconv.Atoi(startParts[0])
	if err != nil {
		return "", 0, 0, fmt.Errorf("parse start line: %w", err)
	}
	endLine, err := strconv.Atoi(endParts[0])
	if err != nil {
		return "", 0, 0, fmt.Errorf("parse end line: %w", err)
	}
	if startLine <= 0 || endLine <= 0 || endLine < startLine {
		return "", 0, 0, fmt.Errorf("invalid line range")
	}

	return filePart, startLine, endLine, nil
}

func normalizeRepoPath(input string) string {
	cleaned := filepath.ToSlash(filepath.Clean(strings.TrimSpace(input)))
	cleaned = strings.TrimPrefix(cleaned, "./")
	return cleaned
}

func normalizeGoCoveragePath(input string) string {
	cleaned := normalizeRepoPath(input)
	if cleaned == "" {
		return ""
	}

	if strings.HasPrefix(cleaned, "backend/") {
		return cleaned
	}
	if idx := strings.Index(cleaned, "/backend/"); idx >= 0 {
		return cleaned[idx+1:]
	}

	repoRelativePrefixes := []string{"cmd/", "internal/", "pkg/", "api/", "integration/", "tools/"}
	for _, prefix := range repoRelativePrefixes {
		if strings.HasPrefix(cleaned, prefix) {
			return "backend/" + cleaned
		}
	}

	return cleaned
}

func normalizeFrontendCoveragePaths(input string) []string {
	cleaned := normalizeRepoPath(input)
	if cleaned == "" {
		return nil
	}

	seen := map[string]struct{}{}
	result := make([]string, 0, 3)
	add := func(value string) {
		value = normalizeRepoPath(value)
		if value == "" {
			return
		}
		if _, ok := seen[value]; ok {
			return
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}

	add(cleaned)
	if idx := strings.Index(cleaned, "/frontend/"); idx >= 0 {
		frontendPath := cleaned[idx+1:]
		add(frontendPath)
		add(strings.TrimPrefix(frontendPath, "frontend/"))
	} else if strings.HasPrefix(cleaned, "frontend/") {
		add(strings.TrimPrefix(cleaned, "frontend/"))
	} else {
		add("frontend/" + cleaned)
	}

	return result
}

func addLine(set FileLineSet, filePath string, lineNo int) {
	if lineNo <= 0 || filePath == "" {
		return
	}
	if _, ok := set[filePath]; !ok {
		set[filePath] = make(LineSet)
	}
	set[filePath][lineNo] = struct{}{}
}

func roundToOneDecimal(value float64) float64 {
	return float64(int(value*10+0.5)) / 10
}

func formatLineRanges(lines []int) []string {
	if len(lines) == 0 {
		return nil
	}

	ranges := make([]string, 0, len(lines))
	start := lines[0]
	end := lines[0]

	for index := 1; index < len(lines); index++ {
		lineNo := lines[index]
		if lineNo == end+1 {
			end = lineNo
			continue
		}

		ranges = append(ranges, formatLineRange(start, end))
		start = lineNo
		end = lineNo
	}

	ranges = append(ranges, formatLineRange(start, end))
	return ranges
}

func formatLineRange(start, end int) string {
	if start == end {
		return strconv.Itoa(start)
	}
	return fmt.Sprintf("%d-%d", start, end)
}

func sortFileCoverageDetails(details []FileCoverageDetail) {
	sort.Slice(details, func(left, right int) bool {
		if details[left].PatchCoveragePct != details[right].PatchCoveragePct {
			return details[left].PatchCoveragePct < details[right].PatchCoveragePct
		}
		return details[left].Path < details[right].Path
	})
}

func validateReadablePath(rawPath string) (string, error) {
	trimmedPath := strings.TrimSpace(rawPath)
	if trimmedPath == "" {
		return "", fmt.Errorf("path is empty")
	}

	cleanedPath := filepath.Clean(trimmedPath)
	absolutePath, err := filepath.Abs(cleanedPath)
	if err != nil {
		return "", fmt.Errorf("resolve absolute path: %w", err)
	}

	return absolutePath, nil
}
