"""Main codebase scanner that orchestrates all analyzers.

This is the primary entry point for scanning a codebase. It runs
all analyzers and produces a unified scan result.
"""

from __future__ import annotations

import subprocess
import time
import uuid
from pathlib import Path
from typing import Any

from ..analyzers.complexity import ComplexityAnalyzer
from ..analyzers.coupling import CouplingAnalyzer
from ..analyzers.duplication import DuplicationAnalyzer
from ..analyzers.dead_code import DeadCodeAnalyzer
from ..analyzers.test_coverage import TestCoverageAnalyzer
from ..analyzers.dependency_age import DependencyAgeAnalyzer
from ..analyzers.documentation import DocumentationAnalyzer
from ..store import DebtStore, ScanRecord


class Scanner:
    """Orchestrate all analyzers to produce a comprehensive scan."""

    def __init__(self, store: DebtStore | None = None) -> None:
        self.store = store or DebtStore()
        self.complexity = ComplexityAnalyzer()
        self.coupling = CouplingAnalyzer()
        self.duplication = DuplicationAnalyzer()
        self.dead_code = DeadCodeAnalyzer()
        self.test_coverage = TestCoverageAnalyzer()
        self.dependency_age = DependencyAgeAnalyzer()
        self.documentation = DocumentationAnalyzer()

    def scan(
        self,
        directory: str | Path,
        exclude_patterns: list[str] | None = None,
        skip_dependency_check: bool = False,
    ) -> dict[str, Any]:
        """Run a full scan of the codebase.

        Args:
            directory: Root directory to scan.
            exclude_patterns: Directory names to exclude.
            skip_dependency_check: Skip PyPI version checks (faster).

        Returns:
            Complete scan results dictionary.
        """
        directory = Path(directory).resolve()
        scan_id = str(uuid.uuid4())[:12]
        start_time = time.time()

        # Get git info
        branch = self._get_git_branch(directory)
        commit_sha = self._get_git_commit(directory)

        # Run all analyzers
        complexity_results = self.complexity.analyze_directory(directory, exclude_patterns)
        coupling_results = self.coupling.analyze_directory(directory, exclude_patterns)
        duplication_results = self.duplication.analyze_directory(directory, exclude_patterns)
        dead_code_results = self.dead_code.analyze_directory(directory, exclude_patterns)
        test_coverage_results = self.test_coverage.analyze_directory(directory)
        documentation_results = self.documentation.analyze_directory(directory, exclude_patterns)

        dependency_results = []
        if not skip_dependency_check:
            dependency_results = self.dependency_age.analyze_directory(directory)

        # Build summaries
        complexity_summary = self.complexity.summary(complexity_results)
        coupling_summary = self.coupling.summary(coupling_results)
        duplication_summary = self.duplication.summary(duplication_results)
        dead_code_summary = self.dead_code.summary(dead_code_results)
        test_coverage_summary = self.test_coverage.summary(test_coverage_results)
        doc_summary = self.documentation.summary(documentation_results)
        dep_summary = self.dependency_age.summary(dependency_results)

        # Calculate overall debt score (0-100, higher = more debt)
        total_score = self._calculate_overall_score(
            complexity_summary,
            coupling_summary,
            duplication_summary,
            dead_code_summary,
            test_coverage_summary,
            doc_summary,
            dep_summary,
        )

        # Count total files scanned
        file_count = len(complexity_results)
        total_issues = (
            complexity_summary.get("complex_count", 0)
            + complexity_summary.get("very_complex_count", 0)
            + complexity_summary.get("unmaintainable_count", 0)
            + coupling_summary.get("highly_coupled_count", 0)
            + duplication_summary.get("duplicate_block_count", 0)
            + dead_code_summary.get("total_dead_code_items", 0)
            + test_coverage_summary.get("total_uncovered_items", 0)
            + doc_summary.get("total_missing_docs", 0)
            + dep_summary.get("outdated_count", 0)
        )

        elapsed = time.time() - start_time

        summary = {
            "complexity": complexity_summary,
            "coupling": coupling_summary,
            "duplication": duplication_summary,
            "dead_code": dead_code_summary,
            "test_coverage": test_coverage_summary,
            "documentation": doc_summary,
            "dependencies": dep_summary,
        }

        details = {
            "complexity_hotspots": [
                f.to_dict()
                for f in self.complexity.get_hotspots(complexity_results, threshold=10, limit=20)
            ],
            "most_coupled": [
                m.to_dict()
                for m in self.coupling.get_most_coupled(coupling_results, limit=20)
            ],
            "circular_dependencies": self.coupling.find_circular_dependencies(),
            "duplicate_blocks": [d.to_dict() for d in duplication_results[:20]],
            "dead_code_items": [d.to_dict() for d in dead_code_results[:30]],
            "uncovered_items": [u.to_dict() for u in test_coverage_results[:30]],
            "missing_docs": [d.to_dict() for d in documentation_results[:30]],
            "outdated_deps": [d.to_dict() for d in dependency_results if d.is_outdated],
        }

        # Save to store
        record = ScanRecord(
            scan_id=scan_id,
            repo_path=str(directory),
            branch=branch,
            commit_sha=commit_sha,
            timestamp=time.time(),
            total_score=total_score,
            file_count=file_count,
            total_issues=total_issues,
            summary=summary,
            details=details,
        )
        self.store.save_scan(record)

        return {
            "scan_id": scan_id,
            "repo_path": str(directory),
            "branch": branch,
            "commit_sha": commit_sha,
            "total_score": round(total_score, 1),
            "file_count": file_count,
            "total_issues": total_issues,
            "elapsed_seconds": round(elapsed, 2),
            "summary": summary,
            "details": details,
        }

    def _calculate_overall_score(
        self,
        complexity: dict,
        coupling: dict,
        duplication: dict,
        dead_code: dict,
        test_coverage: dict,
        documentation: dict,
        dependencies: dict,
    ) -> float:
        """Calculate a 0-100 overall technical debt score.

        Higher score = more debt.
        """
        scores: list[tuple[float, float]] = []  # (score, weight)

        # Complexity: ratio of complex+ functions
        total_funcs = complexity.get("total_functions", 0) or 1
        complex_ratio = (
            complexity.get("complex_count", 0)
            + complexity.get("very_complex_count", 0) * 2
            + complexity.get("unmaintainable_count", 0) * 4
        ) / total_funcs
        scores.append((min(100, complex_ratio * 100), 0.25))

        # Coupling
        avg_coupling = coupling.get("avg_coupling", 0)
        coupling_score = min(100, avg_coupling * 10)
        scores.append((coupling_score, 0.15))

        # Duplication
        dup_count = duplication.get("duplicate_block_count", 0)
        dup_score = min(100, dup_count * 2)
        scores.append((dup_score, 0.15))

        # Dead code
        dead_count = dead_code.get("total_dead_code_items", 0)
        dead_score = min(100, dead_count * 1.5)
        scores.append((dead_score, 0.10))

        # Test coverage gaps
        uncovered = test_coverage.get("total_uncovered_items", 0)
        test_score = min(100, uncovered * 2)
        scores.append((test_score, 0.15))

        # Documentation
        missing_docs = documentation.get("total_missing_docs", 0)
        doc_score = min(100, missing_docs * 1)
        scores.append((doc_score, 0.10))

        # Dependencies
        outdated = dependencies.get("outdated_count", 0)
        critical = dependencies.get("critical_outdated", 0)
        dep_score = min(100, outdated * 3 + critical * 15)
        scores.append((dep_score, 0.10))

        # Weighted average
        total_weight = sum(w for _, w in scores)
        if total_weight == 0:
            return 0
        return sum(s * w for s, w in scores) / total_weight

    def _get_git_branch(self, directory: Path) -> str:
        """Get current git branch."""
        try:
            result = subprocess.run(
                ["git", "rev-parse", "--abbrev-ref", "HEAD"],
                capture_output=True,
                text=True,
                cwd=directory,
                timeout=5,
            )
            return result.stdout.strip() if result.returncode == 0 else ""
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return ""

    def _get_git_commit(self, directory: Path) -> str:
        """Get current git commit SHA."""
        try:
            result = subprocess.run(
                ["git", "rev-parse", "--short", "HEAD"],
                capture_output=True,
                text=True,
                cwd=directory,
                timeout=5,
            )
            return result.stdout.strip() if result.returncode == 0 else ""
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return ""
