"""
Dependency Vulnerability Scanner
==================================
Real implementation — runs `npm audit --json` on the generated project,
parses the structured JSON output, and uses Groq to suggest patched versions.

npm audit JSON schema (npm v7+):
  {
    "auditReportVersion": 2,
    "vulnerabilities": {
      "package-name": {
        "name": str,
        "severity": "info"|"low"|"moderate"|"high"|"critical",
        "isDirect": bool,
        "via": [...],
        "effects": [...],
        "range": str,
        "nodes": [...],
        "fixAvailable": bool | { name, version, isSemVerMajor }
      }
    },
    "metadata": {
      "vulnerabilities": { "info": N, "low": N, "moderate": N, "high": N, "critical": N, "total": N }
    }
  }
"""
from __future__ import annotations

import asyncio
import json
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from forge.core.session import GenerationSession
from forge.core.api import GroqClient

SEVERITY_ORDER = {"critical": 5, "high": 4, "moderate": 3, "low": 2, "info": 1}

FIX_ADVISOR_SYSTEM = """You are a security engineer reviewing npm dependency vulnerabilities.
Given a list of CVEs / vulnerabilities in a React Native / Expo project, suggest exact
package.json dependency version updates that resolve the issues.

Output JSON only:
{
  "updates": [
    {"package": "package-name", "current": "1.2.3", "recommended": "1.2.4", "reason": "patches CVE-..."}
  ],
  "manual_steps": ["Step if automatic fix unavailable"],
  "safe_to_ignore": ["package@version — reason it can be ignored in RN context"]
}"""


@dataclass
class Vulnerability:
    name: str
    severity: str
    is_direct: bool
    via: list[str]
    fix_available: bool
    fix_version: Optional[str]
    range: str
    cves: list[str] = field(default_factory=list)

    @property
    def severity_score(self) -> int:
        return SEVERITY_ORDER.get(self.severity, 0)


@dataclass
class AuditResult:
    ran: bool = False
    total: int = 0
    critical: int = 0
    high: int = 0
    moderate: int = 0
    low: int = 0
    info: int = 0
    vulnerabilities: list[Vulnerability] = field(default_factory=list)
    fix_suggestions: dict = field(default_factory=dict)
    applied_fixes: list[str] = field(default_factory=list)
    raw_output: str = ""
    error: Optional[str] = None

    @property
    def has_critical_or_high(self) -> bool:
        return self.critical > 0 or self.high > 0

    @property
    def summary(self) -> str:
        parts = []
        for sev in ("critical", "high", "moderate", "low", "info"):
            n = getattr(self, sev)
            if n:
                parts.append(f"{n} {sev}")
        return ", ".join(parts) if parts else "0 vulnerabilities"


def _parse_audit_json(raw: str, report: "AuditResult") -> None:
    """Parse npm audit --json output into AuditResult."""
    data = json.loads(raw)
    report.raw_output = raw

    # npm v7+ format
    if "auditReportVersion" in data and data.get("auditReportVersion") == 2:
        meta = data.get("metadata", {}).get("vulnerabilities", {})
        report.total    = meta.get("total", 0)
        report.critical = meta.get("critical", 0)
        report.high     = meta.get("high", 0)
        report.moderate = meta.get("moderate", 0)
        report.low      = meta.get("low", 0)
        report.info     = meta.get("info", 0)

        for pkg_name, vuln_data in data.get("vulnerabilities", {}).items():
            fix = vuln_data.get("fixAvailable")
            fix_ver = None
            if isinstance(fix, dict):
                fix_ver = fix.get("version")
                fix = True

            # Extract CVE IDs from via field
            cves = []
            for v in vuln_data.get("via", []):
                if isinstance(v, dict):
                    url = v.get("url", "")
                    if "CVE-" in url:
                        cve = url.split("CVE-")[-1].split("/")[0]
                        cves.append(f"CVE-{cve}")

            report.vulnerabilities.append(Vulnerability(
                name=pkg_name,
                severity=vuln_data.get("severity", "unknown"),
                is_direct=vuln_data.get("isDirect", False),
                via=[v if isinstance(v, str) else v.get("name", "") for v in vuln_data.get("via", [])],
                fix_available=bool(fix),
                fix_version=fix_ver,
                range=vuln_data.get("range", ""),
                cves=cves,
            ))

    else:
        # npm v6 format
        advisories = data.get("advisories", {})
        meta = data.get("metadata", {}).get("vulnerabilities", {})
        report.total    = sum(meta.values())
        report.critical = meta.get("critical", 0)
        report.high     = meta.get("high", 0)
        report.moderate = meta.get("moderate", 0)
        report.low      = meta.get("low", 0)
        report.info     = meta.get("info", 0)

        seen = set()
        for adv_id, adv in advisories.items():
            pkg = adv.get("module_name", "")
            if pkg in seen:
                continue
            seen.add(pkg)
            report.vulnerabilities.append(Vulnerability(
                name=pkg,
                severity=adv.get("severity", "unknown"),
                is_direct=False,
                via=[adv.get("module_name", "")],
                fix_available=bool(adv.get("patched_versions")),
                fix_version=adv.get("patched_versions"),
                range=adv.get("vulnerable_versions", ""),
                cves=adv.get("cves", []),
            ))

    # Sort by severity descending
    report.vulnerabilities.sort(key=lambda v: -v.severity_score)


async def _apply_auto_fixes(project_dir: Path, report: AuditResult, session: GenerationSession) -> None:
    """
    Run `npm audit fix` for auto-fixable vulns, then `npm audit fix --force`
    for breaking-change fixes (only if critical/high remain after first pass).
    """
    session.log("AUDITOR", "  Running npm audit fix…")
    proc = await asyncio.create_subprocess_exec(
        "npm", "audit", "fix", "--legacy-peer-deps",
        cwd=str(project_dir),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    out_b, err_b = await asyncio.wait_for(proc.communicate(), timeout=120)
    out = out_b.decode("utf-8", errors="replace")

    if "fixed" in out.lower() or "changed" in out.lower():
        session.log("AUDITOR", "  ✓ npm audit fix applied auto-fixes", "success")
        report.applied_fixes.append("npm audit fix")

    # If critical/high remain, try --force (major version bumps)
    if report.has_critical_or_high:
        session.log("AUDITOR", "  Running npm audit fix --force (major bumps)…", "warning")
        proc2 = await asyncio.create_subprocess_exec(
            "npm", "audit", "fix", "--force", "--legacy-peer-deps",
            cwd=str(project_dir),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        out2_b, _ = await asyncio.wait_for(proc2.communicate(), timeout=120)
        out2 = out2_b.decode("utf-8", errors="replace")
        if proc2.returncode == 0:
            session.log("AUDITOR", "  ✓ Major version fixes applied (verify app still works)", "success")
            report.applied_fixes.append("npm audit fix --force")


async def _get_fix_suggestions(
    report: AuditResult,
    session: GenerationSession,
    groq: GroqClient,
) -> None:
    """Ask Groq for targeted fix suggestions for remaining vulnerabilities."""
    critical_high = [v for v in report.vulnerabilities if v.severity_score >= 4]
    if not critical_high:
        return

    vuln_list = "\n".join(
        f"- {v.name} ({v.severity}): range={v.range}, cves={v.cves}, fix_available={v.fix_available}"
        for v in critical_high[:20]
    )

    try:
        suggestions = await groq.complete_json(
            messages=[
                {"role": "system", "content": FIX_ADVISOR_SYSTEM},
                {"role": "user", "content": f"React Native Expo project vulnerabilities:\n{vuln_list}"},
            ],
            model=session.settings.plan_model,
            temperature=0.1,
        )
        report.fix_suggestions = suggestions
    except Exception as e:
        session.log("AUDITOR", f"  ⚠ Could not get fix suggestions: {e}", "warning")


async def run_audit(
    session: GenerationSession,
    groq: GroqClient,
    auto_fix: bool = True,
) -> AuditResult:
    """
    Full vulnerability audit:
    1. npm audit --json
    2. Parse results
    3. Auto-fix if requested
    4. Get AI fix suggestions for remaining critical/high
    """
    project_dir = session.project_dir
    result = AuditResult()

    if not project_dir or not project_dir.exists():
        result.error = "Project directory not found"
        return result

    pkg_json = project_dir / "package.json"
    if not pkg_json.exists():
        result.error = "No package.json found"
        return result

    node_modules = project_dir / "node_modules"
    if not node_modules.exists():
        result.error = "node_modules not present — run npm install first"
        return result

    agent_run = session.start_agent("AUDITOR", "npm audit")
    session.log("AUDITOR", "Running npm audit --json…")

    proc = await asyncio.create_subprocess_exec(
        "npm", "audit", "--json",
        cwd=str(project_dir),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    try:
        out_b, err_b = await asyncio.wait_for(proc.communicate(), timeout=60)
    except asyncio.TimeoutError:
        result.error = "npm audit timed out"
        session.fail_agent(agent_run, result.error)
        return result

    raw = out_b.decode("utf-8", errors="replace")

    try:
        _parse_audit_json(raw, result)
        result.ran = True
    except (json.JSONDecodeError, KeyError) as e:
        result.error = f"Failed to parse npm audit output: {e}"
        session.fail_agent(agent_run, result.error)
        return result

    session.log(
        "AUDITOR",
        f"Found: {result.summary}",
        "error" if result.has_critical_or_high else "warning" if result.total > 0 else "success",
    )

    # Log top vulnerabilities
    for v in result.vulnerabilities[:8]:
        prefix = {"critical": "🔴", "high": "🟠", "moderate": "🟡", "low": "🟢"}.get(v.severity, "⚪")
        session.log(
            "AUDITOR",
            f"  {prefix} {v.name} ({v.severity}) via {', '.join(v.via[:2])} "
            f"{'[fix available]' if v.fix_available else '[manual fix needed]'}",
        )

    # Auto-fix
    if auto_fix and result.total > 0:
        await _apply_auto_fixes(project_dir, result, session)

    # AI fix suggestions for remaining critical/high
    if result.has_critical_or_high:
        await _get_fix_suggestions(result, session, groq)
        if result.fix_suggestions.get("updates"):
            session.log("AUDITOR", "  AI fix recommendations:")
            for upd in result.fix_suggestions["updates"][:5]:
                session.log("AUDITOR", f"    {upd['package']}: {upd['current']} → {upd['recommended']} ({upd['reason']})")

    # Save audit report alongside project
    report_path = project_dir / ".forge" / "audit.json"
    report_path.parent.mkdir(exist_ok=True)
    report_path.write_text(json.dumps({
        "summary": result.summary,
        "total": result.total,
        "critical": result.critical,
        "high": result.high,
        "moderate": result.moderate,
        "low": result.low,
        "applied_fixes": result.applied_fixes,
        "suggestions": result.fix_suggestions,
        "vulnerabilities": [
            {"name": v.name, "severity": v.severity, "cves": v.cves, "fix": v.fix_version}
            for v in result.vulnerabilities
        ],
    }, indent=2))

    if result.total == 0:
        session.log_success("AUDITOR", "✓ Zero vulnerabilities — clean project")
    elif not result.has_critical_or_high:
        session.log("AUDITOR", f"⚠ {result.total} low/moderate vuln(s) — review .forge/audit.json", "warning")
    else:
        session.log("AUDITOR", f"✗ {result.critical + result.high} critical/high vuln(s) remain — review immediately", "error")

    session.finish_agent(
        agent_run,
        total=result.total,
        critical=result.critical,
        high=result.high,
        auto_fixed=len(result.applied_fixes),
    )

    return result
