"""MCP Security Audit Scanner — Core scanning engine.

Coordinates rule execution, file scanning, and result aggregation.
Supports both Python (AST-based) and TypeScript/JavaScript (regex-based)
scanning with automatic language detection.
"""

from __future__ import annotations

import ast
import time
from pathlib import Path
from typing import Sequence

from .deobfuscate import DeobfuscationFinding, deobfuscate
from .models import Finding, ScanResult, ScanSummary, Severity
from .rules import Rule, get_all_rules
from .ts_scanner import TSSecurityScanner, is_ts_js_file, TS_JS_EXTENSIONS

# Python file extensions
PY_EXTENSIONS = {".py", ".pyw"}

# All supported extensions
ALL_EXTENSIONS = PY_EXTENSIONS | TS_JS_EXTENSIONS


def detect_language(filepath: Path) -> str:
    """Detect the programming language from a file extension.

    Args:
        filepath: Path to the source file.

    Returns:
        Language string: 'python', 'typescript', or 'unknown'.
    """
    ext = filepath.suffix.lower()
    if ext in PY_EXTENSIONS:
        return "python"
    if ext in TS_JS_EXTENSIONS:
        return "typescript"
    return "unknown"


class MCPSecurityScanner:
    """Static security scanner for MCP servers.

    Scans Python source files using AST analysis and regex pattern matching,
    and TypeScript/JavaScript files using regex pattern matching.
    Auto-detects language from file extensions. Zero external dependencies.
    """

    def __init__(
        self,
        rules: list[Rule] | None = None,
        deobfuscate_enabled: bool = True,
    ) -> None:
        """Initialize scanner with rules.

        Args:
            rules: Optional list of Python rules. If None, all built-in rules.
            deobfuscate_enabled: Run deobfuscation layer before scanning.
                Strips zero-width chars, Unicode tags, BiDi controls, etc.
                Default: True.
        """
        self.rules: list[Rule] = rules if rules is not None else get_all_rules()
        self.deobfuscate_enabled: bool = deobfuscate_enabled
        self._ts_scanner = TSSecurityScanner(
            deobfuscate_enabled=deobfuscate_enabled,
        )

    def scan_file(self, filepath: Path) -> ScanResult:
        """Scan a single source file and return findings.

        Auto-detects language from extension. Delegates to the
        appropriate scanner (Python AST or TS/JS regex).

        Args:
            filepath: Path to the source file to scan.

        Returns:
            ScanResult with all findings for this file.
        """
        lang = detect_language(filepath)

        if lang == "typescript":
            return self._ts_scanner.scan_file(filepath)

        return self._scan_python_file(filepath)

    def _scan_python_file(self, filepath: Path) -> ScanResult:
        """Scan a single Python file using AST analysis.

        If deobfuscation is enabled, the source is pre-processed to strip
        obfuscated payloads before AST parsing. Deobfuscation findings
        are included with rule_id prefix ``DEOB-``.

        Args:
            filepath: Path to the Python file to scan.

        Returns:
            ScanResult with all findings for this file.
        """
        start: float = time.monotonic()
        source: str = filepath.read_text(encoding="utf-8", errors="replace")
        lines: list[str] = source.splitlines()
        result = ScanResult(file=str(filepath), lines_scanned=len(lines))

        # Deobfuscation layer — runs BEFORE AST parsing
        if self.deobfuscate_enabled:
            source, deob_findings = deobfuscate(source)
            result.deobfuscation_findings = deob_findings
            for df in deob_findings:
                result.findings.append(Finding(
                    rule_id=f"DEOB-{df.category.upper()[:4]}",
                    severity=Severity.HIGH if df.category in (
                        "unicode_tag", "bidi"
                    ) else Severity.MEDIUM,
                    title=f"Obfuscation detected: {df.category}",
                    description=df.description,
                    file=str(filepath),
                    line=df.line,
                ))

        try:
            tree: ast.Module = ast.parse(source, filename=str(filepath))
        except SyntaxError as e:
            result.parse_error = str(e)
            result.findings.append(Finding(
                rule_id="PARSE-001",
                severity=Severity.INFO,
                title="Error de parseo",
                description=f"No se pudo parsear: {e}",
                file=str(filepath),
                line=e.lineno or 0,
            ))
            result.scan_time_ms = (time.monotonic() - start) * 1000
            return result

        for rule in self.rules:
            findings: list[Finding] = rule.check(tree, source)
            for f in findings:
                f.file = str(filepath)
            result.findings.extend(findings)

        result.scan_time_ms = (time.monotonic() - start) * 1000
        return result

    def scan_directory(self, dirpath: Path) -> list[ScanResult]:
        """Scan all supported files in a directory recursively.

        Scans both .py and .ts/.js files, auto-detecting language.

        Args:
            dirpath: Path to the directory to scan.

        Returns:
            List of ScanResult, one per file.
        """
        results: list[ScanResult] = []

        # Scan Python files
        for py_file in sorted(dirpath.rglob("*.py")):
            results.append(self._scan_python_file(py_file))

        # Scan TypeScript/JavaScript files
        ts_results = self._ts_scanner.scan_directory(dirpath)
        results.extend(ts_results)

        return results

    def scan(self, target: Path) -> ScanSummary:
        """Scan a file or directory and return an aggregate summary.

        Args:
            target: Path to a file or directory.

        Returns:
            ScanSummary with all results aggregated.
        """
        start: float = time.monotonic()

        if target.is_file():
            results: list[ScanResult] = [self.scan_file(target)]
        elif target.is_dir():
            results = self.scan_directory(target)
        else:
            raise FileNotFoundError(f"Target not found: {target}")

        summary = ScanSummary(results=results)
        summary.total_scan_time_ms = (time.monotonic() - start) * 1000
        return summary
