"""Scan pipeline executor — reusable scan logic decoupled from CLI output.

This module provides the core scan pipeline (file collection, AST parsing,
knowledge graph construction, pattern detection, and compliance enrichment)
as a pure function that can be called from both the CLI and the API server.
"""

from __future__ import annotations

import ast
from dataclasses import dataclass, field
from fnmatch import fnmatch
from pathlib import Path

from sanicode.compliance.enrichment import EnrichedFinding, enrich_findings
from sanicode.compliance.mapper import ComplianceMapper
from sanicode.config import SanicodeConfig
from sanicode.graph.builder import KnowledgeGraph
from sanicode.report.persist import ScanResult, build_scan_result
from sanicode.scanner.ast_parser import parse_file
from sanicode.scanner.patterns import Finding, check_patterns


@dataclass
class ScanOutput:
    """Container for the complete output of a scan run."""

    result: ScanResult
    graph_data: dict
    enriched_findings: list[EnrichedFinding]
    file_count: int
    parse_errors: list[str] = field(default_factory=list)


def collect_files(target: Path, cfg: SanicodeConfig) -> list[Path]:
    """Collect files to scan based on config-driven filtering.

    Walks the target directory, matching files by extension and filtering
    by exclusion patterns and max file size from the scan configuration.

    Args:
        target: Root directory to scan.
        cfg: Sanicode configuration (uses scan.include_extensions,
            scan.exclude_patterns, and scan.max_file_size_kb).

    Returns:
        Sorted, deduplicated list of file paths to scan.
    """
    scan_cfg = cfg.scan
    max_bytes = scan_cfg.max_file_size_kb * 1024
    files: list[Path] = []
    for ext in scan_cfg.include_extensions:
        for f in target.rglob(f"*{ext}"):
            rel = str(f.relative_to(target))
            if any(fnmatch(rel, pat) for pat in scan_cfg.exclude_patterns):
                continue
            if f.stat().st_size > max_bytes:
                continue
            files.append(f)
    return sorted(set(files))


def run_scan(target: Path, cfg: SanicodeConfig) -> ScanOutput:
    """Execute the full scan pipeline on a target path.

    Steps: file collection -> AST parse -> knowledge graph build ->
    pattern detection -> compliance enrichment -> ScanResult assembly.

    Args:
        target: Path to a file or directory to scan.
        cfg: Sanicode configuration.

    Returns:
        A ScanOutput containing the scan result, graph data, enriched
        findings, file count, and any parse errors encountered.

    Raises:
        FileNotFoundError: If the target path does not exist.
    """
    target = target.resolve()
    if not target.exists():
        raise FileNotFoundError(f"Path does not exist: {target}")

    file_trees: list[tuple[Path, ast.Module]] = []
    parse_errors: list[str] = []

    if target.is_file():
        tree = parse_file(target)
        file_trees.append((target, tree))
    else:
        files = collect_files(target, cfg)
        for py_file in files:
            try:
                tree = parse_file(py_file)
                file_trees.append((py_file, tree))
            except SyntaxError as exc:
                parse_errors.append(f"{py_file}: {exc}")

    # Build knowledge graph.
    kg = KnowledgeGraph.from_parsed(file_trees)

    # Run static pattern detection.
    findings: list[Finding] = []
    for file_path, tree in file_trees:
        findings.extend(check_patterns(tree, file_path))

    # Enrich findings with compliance cross-references.
    mapper = ComplianceMapper()
    enriched = enrich_findings(findings, mapper)

    # Build scan result.
    result = build_scan_result(enriched, target, kg)

    return ScanOutput(
        result=result,
        graph_data=kg.to_dict(),
        enriched_findings=enriched,
        file_count=len(file_trees),
        parse_errors=parse_errors,
    )
