"""Code analyzer that scans a project and scores it against AISA criteria.

Walks through all Python source files, matches patterns from patterns.py,
and produces per-criterion and per-layer coverage scores.
"""

from pathlib import Path
from dataclasses import dataclass, field

from ..layers import ALL_LAYERS, ALL_CONTRACTS, Layer, Contract
from .patterns import ALL_PATTERNS, Pattern


@dataclass
class CriterionResult:
    """Result of evaluating a single criterion."""
    criterion_id: str
    criterion_name: str
    covered: bool
    confidence: float  # 0.0 to 1.0
    evidence: list[str] = field(default_factory=list)  # matched signatures


@dataclass
class LayerResult:
    """Aggregated result for one AISA layer or contract."""
    id: str
    name: str
    criteria_results: list[CriterionResult] = field(default_factory=list)

    @property
    def coverage_score(self) -> float:
        """Percentage of criteria covered (0-100)."""
        if not self.criteria_results:
            return 0.0
        covered = sum(1 for cr in self.criteria_results if cr.covered)
        return (covered / len(self.criteria_results)) * 100

    @property
    def covered_count(self) -> int:
        return sum(1 for cr in self.criteria_results if cr.covered)

    @property
    def total_count(self) -> int:
        return len(self.criteria_results)


@dataclass
class ScanResult:
    """Complete scan result for a project."""
    project_path: str
    frameworks_detected: dict[str, float]
    layer_results: list[LayerResult] = field(default_factory=list)
    contract_results: list[LayerResult] = field(default_factory=list)
    files_scanned: int = 0
    total_lines: int = 0

    @property
    def overall_score(self) -> float:
        """Overall AISA compliance score (0-100)."""
        all_results = self.layer_results + self.contract_results
        if not all_results:
            return 0.0
        total_criteria = sum(r.total_count for r in all_results)
        total_covered = sum(r.covered_count for r in all_results)
        if total_criteria == 0:
            return 0.0
        return (total_covered / total_criteria) * 100

    @property
    def layer_score(self) -> float:
        """Score for layers only (0-100)."""
        if not self.layer_results:
            return 0.0
        total = sum(r.total_count for r in self.layer_results)
        covered = sum(r.covered_count for r in self.layer_results)
        return (covered / total) * 100 if total > 0 else 0.0

    @property
    def contract_score(self) -> float:
        """Score for contracts only (0-100)."""
        if not self.contract_results:
            return 0.0
        total = sum(r.total_count for r in self.contract_results)
        covered = sum(r.covered_count for r in self.contract_results)
        return (covered / total) * 100 if total > 0 else 0.0


def scan_project(project_path: Path, frameworks: dict[str, float] | None = None) -> ScanResult:
    """Scan a project directory and evaluate AISA compliance.

    Args:
        project_path: Root directory of the project to scan.
        frameworks: Pre-detected frameworks (optional, will auto-detect if not provided).

    Returns:
        ScanResult with per-layer and per-contract coverage.
    """
    from .detector import detect_frameworks

    if frameworks is None:
        frameworks = detect_frameworks(project_path)

    # Collect all source content
    source_files = _collect_files(project_path)
    all_content = ""
    total_lines = 0
    for file_path, content in source_files:
        all_content += content + "\n"
        total_lines += content.count("\n") + 1

    # Also check non-Python config files
    config_content = _collect_config_content(project_path)
    all_content += config_content

    # Evaluate layers
    layer_results = []
    for layer in ALL_LAYERS:
        patterns = ALL_PATTERNS.get(layer.id, [])
        lr = _evaluate_component(layer.id, layer.name, layer.criteria, patterns, all_content)
        layer_results.append(lr)

    # Evaluate contracts
    contract_results = []
    for contract in ALL_CONTRACTS:
        patterns = ALL_PATTERNS.get(contract.id, [])
        cr = _evaluate_component(contract.id, contract.name, contract.criteria, patterns, all_content)
        contract_results.append(cr)

    return ScanResult(
        project_path=str(project_path),
        frameworks_detected=frameworks,
        layer_results=layer_results,
        contract_results=contract_results,
        files_scanned=len(source_files),
        total_lines=total_lines,
    )


def _evaluate_component(
    component_id: str,
    component_name: str,
    criteria: list,
    patterns: list[Pattern],
    content: str,
) -> LayerResult:
    """Evaluate a single layer or contract against source content."""
    content_lower = content.lower()
    criteria_results = []

    # Build pattern lookup by criterion ID
    pattern_map: dict[str, Pattern] = {p.criterion_id: p for p in patterns}

    for criterion in criteria:
        pattern = pattern_map.get(criterion.id)
        if pattern is None:
            criteria_results.append(CriterionResult(
                criterion_id=criterion.id,
                criterion_name=criterion.name,
                covered=False,
                confidence=0.0,
            ))
            continue

        # Check each signature
        matched_signatures = []
        for sig in pattern.signatures:
            if sig.lower() in content_lower:
                matched_signatures.append(sig)

        # Determine coverage
        if matched_signatures:
            # More matches = higher confidence
            confidence = min(len(matched_signatures) / max(len(pattern.signatures) * 0.3, 1), 1.0)
            criteria_results.append(CriterionResult(
                criterion_id=criterion.id,
                criterion_name=criterion.name,
                covered=True,
                confidence=confidence,
                evidence=matched_signatures[:5],  # Cap evidence for readability
            ))
        else:
            criteria_results.append(CriterionResult(
                criterion_id=criterion.id,
                criterion_name=criterion.name,
                covered=False,
                confidence=0.0,
            ))

    return LayerResult(
        id=component_id,
        name=component_name,
        criteria_results=criteria_results,
    )


def _collect_files(project_path: Path) -> list[tuple[Path, str]]:
    """Collect all Python source files with their content."""
    files = []
    skip_dirs = {".git", ".venv", "venv", "env", "node_modules", "__pycache__",
                 ".tox", ".mypy_cache", ".pytest_cache", "dist", "build", ".eggs"}

    try:
        for py_file in project_path.rglob("*.py"):
            if any(part in skip_dirs or part.startswith(".") for part in py_file.parts):
                continue
            try:
                content = py_file.read_text(errors="ignore")
                if len(content) < 500_000:
                    files.append((py_file, content))
            except (PermissionError, OSError):
                continue
    except (PermissionError, OSError):
        pass

    return files


def _collect_config_content(project_path: Path) -> str:
    """Collect content from config/infra files (YAML, TOML, JSON, Dockerfiles, CI)."""
    config_patterns = [
        "*.yaml", "*.yml", "*.toml", "*.json", "*.cfg", "*.ini",
        "Dockerfile*", ".env*", "Makefile",
    ]
    ci_dirs = [".github/workflows", ".gitlab-ci", ".circleci"]

    parts: list[str] = []
    for pattern in config_patterns:
        for f in project_path.glob(pattern):
            if f.is_file():
                try:
                    text = f.read_text(errors="ignore")
                    if len(text) < 100_000:
                        parts.append(text)
                except (PermissionError, OSError):
                    continue

    for ci_dir in ci_dirs:
        ci_path = project_path / ci_dir
        if ci_path.exists():
            for f in ci_path.rglob("*"):
                if f.is_file():
                    try:
                        text = f.read_text(errors="ignore")
                        if len(text) < 100_000:
                            parts.append(text)
                    except (PermissionError, OSError):
                        continue

    return "\n".join(parts)
