package analyzers

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

	"github.com/yourorg/devour/internal/quality"
)

type TestCoverageDetector struct {
	*quality.BaseDetector
	minCoverage float64
}

func NewTestCoverageDetector(finder quality.FileFinder) *TestCoverageDetector {
	return &TestCoverageDetector{
		BaseDetector: quality.NewBaseDetector("test_coverage", quality.SeverityT3, finder),
		minCoverage:  50.0,
	}
}

func (d *TestCoverageDetector) Name() string {
	return "test_coverage"
}

func (d *TestCoverageDetector) Severity() quality.Severity {
	return quality.SeverityT3
}

func (d *TestCoverageDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	coverFile := filepath.Join(path, "coverage.out")

	_, err := exec.LookPath("go")
	if err != nil {
		return nil, fmt.Errorf("go toolchain is not available: %w", err)
	}

	if _, err := os.Stat(coverFile); os.IsNotExist(err) {
		cmd := exec.CommandContext(ctx, "go", "test", "-coverprofile=coverage.out", "-covermode=atomic", "./...")
		cmd.Dir = path
		if err := cmd.Run(); err != nil {
			return nil, fmt.Errorf("failed to run test coverage: %w", err)
		}

		if _, err := os.Stat(coverFile); os.IsNotExist(err) {
			return nil, fmt.Errorf("coverage profile was not generated at %q", coverFile)
		}
	}

	coverage, err := d.parseCoverageFile(coverFile)
	if err != nil {
		return nil, fmt.Errorf("parse coverage profile %q: %w", coverFile, err)
	}

	var findings []quality.Finding

	for file, cov := range coverage {
		if cov.TotalLines == 0 {
			continue
		}

		coveragePercent := float64(cov.CoveredLines) / float64(cov.TotalLines) * 100

		if coveragePercent < d.minCoverage {
			finding := quality.Finding{
				ID:          fmt.Sprintf("test_coverage::%s", file),
				Type:        "test_coverage",
				Title:       fmt.Sprintf("Low test coverage: %s (%.1f%%)", filepath.Base(file), coveragePercent),
				Description: fmt.Sprintf("File '%s' has only %.1f%% test coverage (minimum: %.1f%%). Add more tests.", file, coveragePercent, d.minCoverage),
				File:        file,
				Line:        1,
				Severity:    quality.SeverityT3,
				Score:       int((d.minCoverage - coveragePercent) / 10),
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"coverage_percent": fmt.Sprintf("%.1f", coveragePercent),
					"covered_lines":    fmt.Sprintf("%d", cov.CoveredLines),
					"total_lines":      fmt.Sprintf("%d", cov.TotalLines),
					"min_coverage":     fmt.Sprintf("%.1f", d.minCoverage),
				},
			}
			findings = append(findings, finding)
		}
	}

	zeroCoverage := []string{}
	for file, cov := range coverage {
		if cov.CoveredLines == 0 && cov.TotalLines > 0 {
			zeroCoverage = append(zeroCoverage, file)
		}
	}

	if len(zeroCoverage) > 0 && len(zeroCoverage) <= 10 {
		for _, file := range zeroCoverage {
			finding := quality.Finding{
				ID:          fmt.Sprintf("no_test_coverage::%s", file),
				Type:        "test_coverage",
				Title:       fmt.Sprintf("No test coverage: %s", filepath.Base(file)),
				Description: fmt.Sprintf("File '%s' has 0%% test coverage. Consider adding tests.", file),
				File:        file,
				Line:        1,
				Severity:    quality.SeverityT2,
				Score:       5,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"coverage_percent": "0",
					"total_lines":      fmt.Sprintf("%d", coverage[file].TotalLines),
				},
			}
			findings = append(findings, finding)
		}
	}

	return findings, nil
}

func (d *TestCoverageDetector) parseCoverageFile(path string) (map[string]CoverageInfo, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	coverage := make(map[string]CoverageInfo)
	lines := strings.Split(string(data), "\n")

	for _, line := range lines {
		if line == "" || strings.HasPrefix(line, "mode:") {
			continue
		}

		parts := strings.Split(line, " ")
		if len(parts) < 3 {
			continue
		}

		fileRange := parts[0]
		colonIdx := strings.LastIndex(fileRange, ":")
		if colonIdx == -1 {
			continue
		}

		file := fileRange[:colonIdx]
		rangeStr := fileRange[colonIdx+1:]

		countStr := parts[2]
		var count int
		if _, err := fmt.Sscanf(countStr, "%d", &count); err != nil {
			continue
		}

		start, end := d.parseRange(rangeStr)
		lines := end - start + 1

		info := coverage[file]
		info.TotalLines += lines
		if count > 0 {
			info.CoveredLines += lines
		}
		coverage[file] = info
	}

	return coverage, nil
}

func (d *TestCoverageDetector) parseRange(s string) (start, end int) {
	parts := strings.Split(s, ",")
	if len(parts) != 2 {
		return 0, 0
	}

	if _, err := fmt.Sscanf(parts[0], "%d", &start); err != nil {
		return 0, 0
	}
	if _, err := fmt.Sscanf(parts[1], "%d", &end); err != nil {
		return 0, 0
	}

	return start, end
}

type CoverageInfo struct {
	TotalLines   int
	CoveredLines int
}

type UntestedFuncDetector struct {
	*quality.BaseDetector
}

func NewUntestedFuncDetector(finder quality.FileFinder) *UntestedFuncDetector {
	return &UntestedFuncDetector{
		BaseDetector: quality.NewBaseDetector("untested_func", quality.SeverityT2, finder),
	}
}

func (d *UntestedFuncDetector) Name() string {
	return "untested_func"
}

func (d *UntestedFuncDetector) Severity() quality.Severity {
	return quality.SeverityT2
}

func (d *UntestedFuncDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	coverFile := filepath.Join(path, "coverage.out")
	data, err := os.ReadFile(coverFile)
	if err != nil {
		return nil, fmt.Errorf("read coverage profile %q: %w", coverFile, err)
	}

	uncoveredFuncs := make(map[string][]UncoveredFunc)

	lines := strings.Split(string(data), "\n")
	for _, line := range lines {
		if line == "" || strings.HasPrefix(line, "mode:") {
			continue
		}

		parts := strings.Fields(line)
		if len(parts) < 3 {
			continue
		}

		countStr := parts[len(parts)-1]
		var count int
		if _, err := fmt.Sscanf(countStr, "%d", &count); err != nil {
			continue
		}

		if count == 0 {
			fileRange := parts[0]
			colonIdx := strings.LastIndex(fileRange, ":")
			if colonIdx == -1 {
				continue
			}

			file := fileRange[:colonIdx]
			rangeStr := fileRange[colonIdx+1:]

			start, _ := d.parseRange(rangeStr)

			funcName := d.findFuncAtLine(file, start)
			if funcName != "" {
				uncoveredFuncs[file] = append(uncoveredFuncs[file], UncoveredFunc{
					Name: funcName,
					Line: start,
				})
			}
		}
	}

	var findings []quality.Finding
	for file, funcs := range uncoveredFuncs {
		seen := make(map[string]bool)
		for _, fn := range funcs {
			if seen[fn.Name] {
				continue
			}
			seen[fn.Name] = true

			if strings.HasPrefix(fn.Name, "Test") || fn.Name == "main" || fn.Name == "init" {
				continue
			}

			finding := quality.Finding{
				ID:          fmt.Sprintf("untested_func::%s::%s", file, fn.Name),
				Type:        "test_coverage",
				Title:       fmt.Sprintf("Untested function: %s", fn.Name),
				Description: fmt.Sprintf("Function '%s' in %s has no test coverage.", fn.Name, filepath.Base(file)),
				File:        file,
				Line:        fn.Line,
				Severity:    quality.SeverityT2,
				Score:       3,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"function": fn.Name,
				},
			}
			findings = append(findings, finding)
		}
	}

	return findings, nil
}

func (d *UntestedFuncDetector) parseRange(s string) (start, end int) {
	parts := strings.Split(s, ",")
	if len(parts) != 2 {
		return 0, 0
	}
	if _, err := fmt.Sscanf(parts[0], "%d", &start); err != nil {
		return 0, 0
	}
	if _, err := fmt.Sscanf(parts[1], "%d", &end); err != nil {
		return 0, 0
	}
	return start, end
}

func (d *UntestedFuncDetector) findFuncAtLine(file string, line int) string {
	data, err := os.ReadFile(file)
	if err != nil {
		return ""
	}

	lines := strings.Split(string(data), "\n")
	if line > len(lines) {
		return ""
	}

	for i := line - 1; i >= 0 && i >= line-20; i-- {
		l := lines[i]
		if strings.HasPrefix(strings.TrimSpace(l), "func ") {
			parts := strings.Fields(strings.TrimSpace(l))
			if len(parts) >= 2 {
				name := parts[1]
				if idx := strings.Index(name, "("); idx > 0 {
					name = name[:idx]
				}
				return name
			}
		}
	}

	return ""
}

type UncoveredFunc struct {
	Name string
	Line int
}

type OrphanedFileDetector struct {
	*quality.BaseDetector
}

func NewOrphanedFileDetector(finder quality.FileFinder) *OrphanedFileDetector {
	return &OrphanedFileDetector{
		BaseDetector: quality.NewBaseDetector("orphaned_file", quality.SeverityT3, finder),
	}
}

func (d *OrphanedFileDetector) Name() string {
	return "orphaned_file"
}

func (d *OrphanedFileDetector) Severity() quality.Severity {
	return quality.SeverityT3
}

func (d *OrphanedFileDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	files, err := d.FindFiles(path, "go")
	if err != nil {
		return nil, err
	}

	testFiles := make(map[string]bool)
	for _, file := range files {
		if strings.HasSuffix(file, "_test.go") {
			base := strings.TrimSuffix(filepath.Base(file), "_test.go")
			dir := filepath.Dir(file)
			testFiles[filepath.Join(dir, base+".go")] = true
		}
	}

	var findings []quality.Finding
	for _, file := range files {
		if strings.HasSuffix(file, "_test.go") {
			continue
		}

		if strings.Contains(file, "/cmd/") || strings.Contains(file, "\\cmd\\") {
			continue
		}

		base := filepath.Base(file)
		if strings.HasPrefix(base, "main.go") || strings.HasPrefix(base, "doc.go") {
			continue
		}

		if !testFiles[file] {
			dir := filepath.Dir(file)
			files, _ := os.ReadDir(dir)
			goCount := 0
			testCount := 0
			for _, f := range files {
				if strings.HasSuffix(f.Name(), ".go") && !strings.HasSuffix(f.Name(), "_test.go") {
					goCount++
				}
				if strings.HasSuffix(f.Name(), "_test.go") {
					testCount++
				}
			}

			if goCount > 1 && testCount > 0 {
				finding := quality.Finding{
					ID:          fmt.Sprintf("orphaned_file::%s", file),
					Type:        "orphaned_file",
					Title:       fmt.Sprintf("File without dedicated tests: %s", filepath.Base(file)),
					Description: fmt.Sprintf("File '%s' has no corresponding _test.go file, but sibling files do. Consider adding tests.", file),
					File:        file,
					Line:        1,
					Severity:    quality.SeverityT3,
					Score:       2,
					Status:      quality.StatusOpen,
					Metadata: map[string]string{
						"sibling_tests": fmt.Sprintf("%d", testCount),
						"sibling_go":    fmt.Sprintf("%d", goCount),
					},
				}
				findings = append(findings, finding)
			}
		}
	}

	return findings, nil
}

type DeprecatedUsageDetector struct {
	*quality.BaseDetector
}

func NewDeprecatedUsageDetector(finder quality.FileFinder) *DeprecatedUsageDetector {
	return &DeprecatedUsageDetector{
		BaseDetector: quality.NewBaseDetector("deprecated", quality.SeverityT2, finder),
	}
}

func (d *DeprecatedUsageDetector) Name() string {
	return "deprecated"
}

func (d *DeprecatedUsageDetector) Severity() quality.Severity {
	return quality.SeverityT2
}

func (d *DeprecatedUsageDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	files, err := d.FindFiles(path, "go")
	if err != nil {
		return nil, err
	}

	var findings []quality.Finding

	for _, file := range files {
		if strings.HasSuffix(file, "_test.go") {
			continue
		}

		data, err := os.ReadFile(file)
		if err != nil {
			continue
		}

		content := string(data)

		deprecatedPatterns := []struct {
			pattern string
			alt     string
		}{
			{"io/ioutil", "io and os packages"},
			{"context.WithDeadline", "context.WithTimeout for relative times"},
			{"interface{}", "any"},
		}

		for _, p := range deprecatedPatterns {
			if strings.Contains(content, p.pattern) {
				finding := quality.Finding{
					ID:          fmt.Sprintf("deprecated::%s::%s", file, p.pattern),
					Type:        "deprecated",
					Title:       fmt.Sprintf("Deprecated usage: %s", p.pattern),
					Description: fmt.Sprintf("Found deprecated '%s'. Use %s instead.", p.pattern, p.alt),
					File:        file,
					Line:        1,
					Severity:    quality.SeverityT2,
					Score:       3,
					Status:      quality.StatusOpen,
					Metadata: map[string]string{
						"deprecated":  p.pattern,
						"alternative": p.alt,
					},
				}
				findings = append(findings, finding)
			}
		}
	}

	return findings, nil
}

func ParseGoTestJSON(output []byte) ([]TestResult, error) {
	var results []TestResult
	lines := strings.Split(string(output), "\n")

	for _, line := range lines {
		if line == "" {
			continue
		}

		var event TestEvent
		if err := json.Unmarshal([]byte(line), &event); err != nil {
			continue
		}

		if event.Action == "pass" || event.Action == "fail" {
			results = append(results, TestResult{
				Package: event.Package,
				Test:    event.Test,
				Elapsed: event.Elapsed,
				Action:  event.Action,
			})
		}
	}

	return results, nil
}

type TestEvent struct {
	Time    string  `json:"Time"`
	Action  string  `json:"Action"`
	Package string  `json:"Package"`
	Test    string  `json:"Test"`
	Elapsed float64 `json:"Elapsed"`
	Output  string  `json:"Output"`
}

type TestResult struct {
	Package string
	Test    string
	Elapsed float64
	Action  string
}
