"""Módulo de escaneo de seguridad para el supervisor.

Combina dos tipos de análisis:

1. **pip-audit** — escanea las dependencias Python del entorno virtual contra
   la base de datos OSV (Open Source Vulnerabilities) de Google.  No requiere
   Docker ni instalación extra más allá de `pip install pip-audit`.

2. **Trivy** (opcional) — si el binario `trivy` está en PATH, analiza el
   sistema de ficheros del proyecto buscando CVEs en dependencias de cualquier
   ecosistema (Python, npm, Go, Debian/Alpine packages).  Si no está instalado
   devuelve instrucciones de instalación sin fallar.

Ambos modos son *fail-safe*: si la herramienta falla, el sistema no se rompe
y el resultado incluye el error de diagnóstico.

Resultados persistidos en la tabla `security_scans` de SQLite para histórico
y consulta desde dashboard/Telegram.
"""

from __future__ import annotations

import asyncio
import json
import logging
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from .config import BASE_DIR

logger = logging.getLogger(__name__)

# ── Herramientas disponibles ──────────────────────────────────────────── #

PYTHON_EXECUTABLE = sys.executable
# Trivy binario — busca en PATH más rutas de instalación conocidas en Windows
_TRIVY_CANDIDATES = ["trivy", r"C:\tools\trivy\trivy.exe"]
TRIVY_BINARY: str = next(
    (c for c in _TRIVY_CANDIDATES if shutil.which(c) or (c.endswith(".exe") and Path(c).exists())),
    "trivy",
)


def _utc_now() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def trivy_available() -> bool:
    return shutil.which(TRIVY_BINARY) is not None


def pip_audit_available() -> bool:
    try:
        result = subprocess.run(
            [PYTHON_EXECUTABLE, "-m", "pip_audit", "--version"],
            capture_output=True, timeout=10,
        )
        return result.returncode == 0
    except Exception:
        return False


# ── Scanners ──────────────────────────────────────────────────────────── #

async def run_pip_audit(requirements_file: Path | None = None) -> dict[str, Any]:
    """
    Ejecuta pip-audit sobre el entorno actual o un requirements.txt.

    Devuelve:
        {
            "tool": "pip-audit",
            "status": "ok"|"error"|"not_available",
            "vulnerabilities": [...],   # lista de CVEs encontrados
            "packages_audited": int,
            "summary": str,
            "raw_output": str,          # salida JSON completa de pip-audit
            "error": str | None,
        }
    """
    if not pip_audit_available():
        return {
            "tool": "pip-audit",
            "status": "not_available",
            "vulnerabilities": [],
            "packages_audited": 0,
            "summary": "pip-audit no está instalado. Ejecuta: pip install pip-audit",
            "raw_output": "",
            "error": "pip_audit module not found",
        }

    cmd = [PYTHON_EXECUTABLE, "-m", "pip_audit", "--format", "json", "--progress-spinner", "off"]
    if requirements_file and requirements_file.is_file():
        cmd += ["-r", str(requirements_file)]

    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=str(BASE_DIR),
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
        raw = stdout.decode(errors="replace")
        err_txt = stderr.decode(errors="replace")

        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            data = {}

        vulns: list[dict[str, Any]] = []
        packages_audited = 0

        # pip-audit JSON format: {"dependencies": [...], "fixes": [...]}
        dependencies = data.get("dependencies", [])
        packages_audited = len(dependencies)

        for dep in dependencies:
            for vuln in dep.get("vulns", []):
                vulns.append({
                    "package": dep.get("name", ""),
                    "version": dep.get("version", ""),
                    "vuln_id": vuln.get("id", ""),
                    "aliases": vuln.get("aliases", []),
                    "description": vuln.get("description", "")[:200],
                    "fix_versions": vuln.get("fix_versions", []),
                    "severity": _guess_severity(vuln.get("id", ""), vuln.get("aliases", [])),
                })

        if vulns:
            summary = (
                f"{len(vulns)} vulnerabilidad(es) en {len({v['package'] for v in vulns})} paquete(s). "
                f"Auditados: {packages_audited} paquetes."
            )
            status = "vulnerabilities_found"
        elif proc.returncode == 0:
            summary = f"Sin vulnerabilidades conocidas. Auditados: {packages_audited} paquetes."
            status = "ok"
        else:
            summary = f"Error ejecutando pip-audit (exit {proc.returncode}). {err_txt[:120]}"
            status = "error"

        return {
            "tool": "pip-audit",
            "status": status,
            "vulnerabilities": vulns,
            "packages_audited": packages_audited,
            "summary": summary,
            "raw_output": raw[:4000],
            "error": err_txt[:500] if proc.returncode != 0 else None,
        }

    except asyncio.TimeoutError:
        return {
            "tool": "pip-audit",
            "status": "error",
            "vulnerabilities": [],
            "packages_audited": 0,
            "summary": "pip-audit tardó demasiado (>120 s) y fue cancelado.",
            "raw_output": "",
            "error": "timeout",
        }
    except Exception as exc:
        logger.exception("Error inesperado en run_pip_audit: %s", exc)
        return {
            "tool": "pip-audit",
            "status": "error",
            "vulnerabilities": [],
            "packages_audited": 0,
            "summary": f"Error inesperado: {exc}",
            "raw_output": "",
            "error": str(exc),
        }


async def run_trivy_fs(scan_path: Path | None = None) -> dict[str, Any]:
    """
    Ejecuta `trivy fs` sobre el directorio del proyecto.

    Devuelve:
        {
            "tool": "trivy",
            "status": "ok"|"error"|"not_installed",
            "vulnerabilities": [...],
            "summary": str,
            "raw_output": str,
            "install_instructions": str | None,
        }
    """
    if not trivy_available():
        instructions = (
            "Trivy no está instalado. Opciones de instalación:\n"
            "• Windows (winget): winget install --id AquaSecurity.Trivy\n"
            "• Docker: docker run --rm aquasec/trivy fs /code\n"
            "• Binario directo: https://github.com/aquasecurity/trivy/releases\n"
            "Una vez instalado, el escaneo de filesystem del proyecto detectará CVEs "
            "en dependencias Python, npm, binarios del SO y archivos de configuración."
        )
        return {
            "tool": "trivy",
            "status": "not_installed",
            "vulnerabilities": [],
            "summary": "Trivy no está en PATH. Ver install_instructions para instalar.",
            "raw_output": "",
            "install_instructions": instructions,
        }

    target = str(scan_path or BASE_DIR)
    cmd = [
        TRIVY_BINARY, "fs",
        "--format", "json",
        "--exit-code", "0",     # no fallar aunque haya vulns
        "--no-progress",
        "--quiet",
        target,
    ]

    try:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
        raw = stdout.decode(errors="replace")
        err_txt = stderr.decode(errors="replace")

        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            data = {}

        vulns: list[dict[str, Any]] = []
        for result in data.get("Results", []):
            target_name = result.get("Target", "")
            for v in result.get("Vulnerabilities") or []:
                vulns.append({
                    "package": v.get("PkgName", ""),
                    "version": v.get("InstalledVersion", ""),
                    "vuln_id": v.get("VulnerabilityID", ""),
                    "aliases": [],
                    "description": str(v.get("Description", ""))[:200],
                    "fix_versions": [v.get("FixedVersion", "")] if v.get("FixedVersion") else [],
                    "severity": v.get("Severity", "UNKNOWN").lower(),
                    "target": target_name,
                })

        if vulns:
            by_sev = _count_by_severity(vulns)
            summary = f"Trivy: {len(vulns)} vulnerabilidad(es). {_severity_str(by_sev)}"
            status = "vulnerabilities_found"
        else:
            summary = f"Trivy: sin vulnerabilidades en {target}."
            status = "ok"

        return {
            "tool": "trivy",
            "status": status,
            "vulnerabilities": vulns,
            "summary": summary,
            "raw_output": raw[:4000],
            "install_instructions": None,
        }

    except asyncio.TimeoutError:
        return {
            "tool": "trivy",
            "status": "error",
            "vulnerabilities": [],
            "summary": "Trivy tardó demasiado (>300 s) y fue cancelado.",
            "raw_output": "",
            "install_instructions": None,
        }
    except Exception as exc:
        logger.exception("Error inesperado en run_trivy_fs: %s", exc)
        return {
            "tool": "trivy",
            "status": "error",
            "vulnerabilities": [],
            "summary": f"Error inesperado en Trivy: {exc}",
            "raw_output": "",
            "install_instructions": None,
        }


# ── Orquestador principal ─────────────────────────────────────────────── #

async def run_full_scan(actor: str = "sistema") -> dict[str, Any]:
    """
    Ejecuta pip-audit + Trivy en paralelo y devuelve un informe unificado.

    Retorna:
        {
            "scan_id": str,
            "actor": str,
            "started_at": str (ISO8601),
            "finished_at": str (ISO8601),
            "tools_used": [...],
            "total_vulnerabilities": int,
            "critical": int, "high": int, "medium": int, "low": int,
            "status": "clean"|"vulnerabilities_found"|"error",
            "summary": str,
            "results": {"pip_audit": {...}, "trivy": {...}},
        }
    """
    import uuid
    scan_id = uuid.uuid4().hex
    started_at = _utc_now()
    logger.info("Iniciando scan de seguridad completo", extra={"scan_id": scan_id, "actor": actor})

    req_file = BASE_DIR / "requirements.txt"
    pip_result, trivy_result = await asyncio.gather(
        run_pip_audit(req_file if req_file.is_file() else None),
        run_trivy_fs(BASE_DIR),
    )

    finished_at = _utc_now()

    all_vulns = pip_result.get("vulnerabilities", []) + trivy_result.get("vulnerabilities", [])
    by_sev = _count_by_severity(all_vulns)

    tools_used = []
    if pip_result["status"] != "not_available":
        tools_used.append("pip-audit")
    if trivy_result["status"] != "not_installed":
        tools_used.append("trivy")

    if any(r["status"] == "error" for r in [pip_result, trivy_result]):
        overall = "error"
    elif all_vulns:
        overall = "vulnerabilities_found"
    else:
        overall = "clean"

    summary_parts = []
    if pip_result["status"] != "not_available":
        summary_parts.append(pip_result["summary"])
    else:
        summary_parts.append("pip-audit: no disponible.")
    if trivy_result["status"] != "not_installed":
        summary_parts.append(trivy_result["summary"])
    else:
        summary_parts.append("Trivy: no instalado (opcional).")

    return {
        "scan_id": scan_id,
        "actor": actor,
        "started_at": started_at,
        "finished_at": finished_at,
        "tools_used": tools_used,
        "total_vulnerabilities": len(all_vulns),
        "critical": by_sev.get("critical", 0),
        "high": by_sev.get("high", 0),
        "medium": by_sev.get("medium", 0),
        "low": by_sev.get("low", 0),
        "status": overall,
        "summary": " | ".join(summary_parts),
        "results": {
            "pip_audit": pip_result,
            "trivy": trivy_result,
        },
    }


# ── Helpers ───────────────────────────────────────────────────────────── #

def _guess_severity(vuln_id: str, aliases: list[str]) -> str:
    """Intenta inferir severidad desde el ID si pip-audit no la proporciona."""
    # GHSA-...-critical estilo gh
    combined = " ".join([vuln_id] + aliases).upper()
    # CVE no tiene severidad en el ID; GHSA tampoco siempre
    # Usamos "unknown" y dejamos al usuario valorar
    return "unknown"


def _count_by_severity(vulns: list[dict[str, Any]]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for v in vulns:
        sev = v.get("severity", "unknown").lower()
        counts[sev] = counts.get(sev, 0) + 1
    return counts


def _severity_str(by_sev: dict[str, int]) -> str:
    parts = []
    for sev in ("critical", "high", "medium", "low", "unknown"):
        if by_sev.get(sev, 0) > 0:
            parts.append(f"{sev.upper()}: {by_sev[sev]}")
    return ", ".join(parts) if parts else "sin severidades clasificadas"
