"""Main orchestrator: resolves targets, runs all layers, aggregates results."""
from __future__ import annotations

import asyncio
import time
import uuid
from pathlib import Path

from pyguard.downloader import download_and_extract
from pyguard.layers.ai import AIAnalysisLayer
from pyguard.layers.base import BaseLayer
from pyguard.layers.sandbox import SandboxLayer
from pyguard.layers.static import StaticAnalysisLayer
from pyguard.models import Finding, PackageInfo, ScanResult, Severity
from pyguard.resolver import discover_requirements, parse_requirements_txt
from pyguard.utils.config import ScanConfig
from pyguard.utils.package_loader import load_local_package

SEVERITY_ORDER: dict[Severity, int] = {
    Severity.CRITICAL: 5,
    Severity.HIGH: 4,
    Severity.MEDIUM: 3,
    Severity.LOW: 2,
    Severity.INFO: 1,
}


class Scanner:
    def __init__(self, config: ScanConfig) -> None:
        self.config = config
        self.layers = self._build_layers()

    def _build_layers(self) -> list[BaseLayer]:
        layers: list[BaseLayer] = [StaticAnalysisLayer()]
        if self.config.enable_ai:
            layers.append(AIAnalysisLayer())
        if self.config.enable_sandbox:
            layers.append(SandboxLayer())
        return layers

    # ------------------------------------------------------------------
    # Single-package scan (original API, kept for backward compat)
    # ------------------------------------------------------------------
    async def scan(self) -> ScanResult:
        package = await self._resolve_single(self.config.target)
        return await self._scan_package(package)

    # ------------------------------------------------------------------
    # Multi-package scan (requirements.txt / project directory)
    # ------------------------------------------------------------------
    async def scan_many(self) -> list[ScanResult]:
        target = self.config.target
        packages = await self._resolve_many(target)
        tasks = [self._scan_package(pkg) for pkg in packages]
        return list(await asyncio.gather(*tasks))

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------
    async def _resolve_single(self, target: str) -> PackageInfo:
        """Resolve a single target string to a PackageInfo."""
        if "==" in target:
            name, version = target.split("==", maxsplit=1)
            try:
                return await download_and_extract(name, version)
            except Exception:  # noqa: BLE001
                # Fall back to bare stub (no source files)
                from pyguard.models import PackageMetadata  # noqa: PLC0415

                return PackageInfo(
                    name=name,
                    version=version,
                    source_path=Path.cwd(),
                    metadata=PackageMetadata(),
                )

        path = Path(target)
        if path.exists():
            return load_local_package(target)

        # Bare name without version → stub (metadata only checks)
        from pyguard.models import PackageMetadata  # noqa: PLC0415

        return PackageInfo(
            name=target, version="latest", source_path=Path.cwd(), metadata=PackageMetadata()
        )

    async def _resolve_many(self, target: str) -> list[PackageInfo]:
        """Resolve a project dir or requirements file to a list of PackageInfos."""
        path = Path(target)

        # requirements.txt file
        if path.is_file() and path.suffix == ".txt":
            specs = parse_requirements_txt(path)
            tasks = [self._resolve_single(f"{n}=={v}" if v != "latest" else n) for n, v in specs]
            return list(await asyncio.gather(*tasks))

        # Project directory
        if path.is_dir():
            specs = discover_requirements(path)
            if specs:
                tasks = [
                    self._resolve_single(f"{n}=={v}" if v != "latest" else n) for n, v in specs
                ]
                return list(await asyncio.gather(*tasks))
            # No requirements found → scan the directory itself
            return [load_local_package(target)]

        # Fallback: treat as single target
        return [await self._resolve_single(target)]

    async def _scan_package(self, package: PackageInfo) -> ScanResult:
        started = time.perf_counter()
        findings: list[Finding] = []
        layers_run: list[str] = []

        for layer in self.layers:
            layer_findings = await layer.analyze(package)
            findings.extend(layer_findings)
            layers_run.append(layer.name)

        risk_label = max(
            (f.severity for f in findings),
            default=Severity.INFO,
            key=lambda s: SEVERITY_ORDER[s],
        )
        duration_ms = int((time.perf_counter() - started) * 1000)

        # Extract AI verdict from synthetic INFO finding if present
        ai_verdict: str | None = None
        ai_confidence: float | None = None
        for f in findings:
            if f.layer == "ai" and f.category == "ai_verdict":
                ai_verdict = f.evidence.get("verdict")
                ai_confidence = f.confidence
                break

        return ScanResult(
            package=package,
            findings=findings,
            risk_score=self._risk_score(findings),
            risk_label=risk_label,
            layers_run=layers_run,
            scan_duration_ms=duration_ms,
            ai_verdict=ai_verdict,
            ai_confidence=ai_confidence,
            scan_id=str(uuid.uuid4()),
        )

    @staticmethod
    def _risk_score(findings: list[Finding]) -> float:
        if not findings:
            return 0.0
        max_rank = max(SEVERITY_ORDER[f.severity] for f in findings)
        return round((max_rank / 5) * 10, 1)
