"""
TypeScript Verification Engine
================================
Real implementation — no mocks.

Flow:
  1. npm install (in generated project)
  2. npx tsc --noEmit --pretty false  (capture stdout+stderr)
  3. Parse structured TSC error output  (file, line, col, code, message)
  4. Group errors by file
  5. Feed each errored file + its errors back to Groq repair agent
  6. Rewrite file, repeat up to MAX_REPAIR_ROUNDS
  7. Final report: zero errors or list of remaining issues

All subprocess calls are real — no simulation, no patching.
"""
from __future__ import annotations

import asyncio
import re
import subprocess
import sys
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, strip_fences

# ── Constants ─────────────────────────────────────────────────────────────────

MAX_REPAIR_ROUNDS = 3
NPM_INSTALL_TIMEOUT = 180  # seconds
TSC_TIMEOUT = 120

# Regex for TSC error lines: "src/foo.tsx(12,5): error TS2304: ..."
TSC_ERROR_RE = re.compile(
    r"^(?P<file>[^(]+)\((?P<line>\d+),(?P<col>\d+)\):\s+(?P<severity>error|warning)\s+TS(?P<code>\d+):\s+(?P<message>.+)$"
)

REPAIR_SYSTEM = """You are a Principal TypeScript / React Native engineer doing targeted error repair.

You are given:
1. A TypeScript source file
2. A list of EXACT compiler errors (file, line, column, TS error code, message)

Fix ONLY the reported errors. Do not refactor or change working code.
Common fixes:
- TS2304 (Cannot find name): add import or declare variable
- TS2339 (Property does not exist): fix property name or add to interface
- TS7006 (Implicit any): add explicit type annotation
- TS2345 (Argument not assignable): fix type mismatch
- TS2322 (Type not assignable): add type assertion or fix type
- TS2554 (Wrong number of arguments): fix call signature
- TS2741 (Missing property): add required field
- TS1005 (Expected token): fix syntax
- TS2307 (Cannot find module): fix import path
- Navigation errors: use correct @react-navigation/native hook signatures

Output ONLY the complete corrected file. No markdown fences. No explanation."""


# ── Data structures ───────────────────────────────────────────────────────────

@dataclass
class TSError:
    file: str        # relative path
    line: int
    col: int
    code: int        # TS error code, e.g. 2304
    severity: str    # 'error' | 'warning'
    message: str

    def __str__(self) -> str:
        return f"{self.file}({self.line},{self.col}): TS{self.code}: {self.message}"


@dataclass
class VerificationResult:
    success: bool
    errors: list[TSError] = field(default_factory=list)
    warnings: list[TSError] = field(default_factory=list)
    npm_installed: bool = False
    rounds: int = 0
    repaired_files: list[str] = field(default_factory=list)
    raw_output: str = ""


# ── Subprocess helpers ────────────────────────────────────────────────────────

async def _run(
    cmd: list[str],
    cwd: Path,
    timeout: int,
    session: GenerationSession,
    agent: str,
) -> tuple[int, str, str]:
    """Run a subprocess asynchronously, streaming output to session log."""
    session.log(agent, f"$ {' '.join(cmd)}")
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        cwd=str(cwd),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    try:
        stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=timeout)
    except asyncio.TimeoutError:
        proc.kill()
        await proc.communicate()
        session.log(agent, f"  ✗ Timed out after {timeout}s", "error")
        return -1, "", f"Timeout after {timeout}s"

    return (
        proc.returncode,
        stdout_b.decode("utf-8", errors="replace"),
        stderr_b.decode("utf-8", errors="replace"),
    )


# ── npm install ───────────────────────────────────────────────────────────────

async def _npm_install(project_dir: Path, session: GenerationSession) -> bool:
    """Run npm install in the project directory. Returns True on success."""
    session.log("VERIFIER", "Running npm install…")

    node_modules = project_dir / "node_modules"
    if node_modules.exists() and (node_modules / ".package-lock.json").exists():
        session.log("VERIFIER", "  node_modules already present, skipping install")
        return True

    # Detect npm / yarn / pnpm
    npm_cmd = "npm"
    for candidate in ("npm", "yarn", "pnpm"):
        try:
            r = subprocess.run(
                [candidate, "--version"],
                capture_output=True, timeout=5
            )
            if r.returncode == 0:
                npm_cmd = candidate
                break
        except (FileNotFoundError, subprocess.TimeoutExpired):
            continue

    install_args = {
        "npm": ["npm", "install", "--legacy-peer-deps", "--no-audit"],
        "yarn": ["yarn", "install", "--non-interactive"],
        "pnpm": ["pnpm", "install", "--frozen-lockfile=false"],
    }[npm_cmd]

    rc, out, err = await _run(install_args, project_dir, NPM_INSTALL_TIMEOUT, session, "VERIFIER")

    if rc != 0:
        session.log("VERIFIER", f"  ✗ npm install failed (rc={rc})", "error")
        session.log("VERIFIER", f"  {err[:300]}", "error")
        return False

    session.log("VERIFIER", "  ✓ npm install complete", "success")
    return True


# ── TSC error parser ──────────────────────────────────────────────────────────

def _parse_tsc_output(output: str, project_dir: Path) -> list[TSError]:
    """Parse `tsc --noEmit --pretty false` output into structured TSError list."""
    errors: list[TSError] = []
    for line in output.splitlines():
        m = TSC_ERROR_RE.match(line.strip())
        if not m:
            continue
        # Normalise file path to relative
        raw_file = m.group("file").strip()
        try:
            rel = str(Path(raw_file).relative_to(project_dir))
        except ValueError:
            rel = raw_file
        errors.append(TSError(
            file=rel,
            line=int(m.group("line")),
            col=int(m.group("col")),
            code=int(m.group("code")),
            severity=m.group("severity"),
            message=m.group("message").strip(),
        ))
    return errors


# ── TSC runner ────────────────────────────────────────────────────────────────

async def _run_tsc(project_dir: Path, session: GenerationSession) -> tuple[bool, list[TSError], str]:
    """Run `npx tsc --noEmit --pretty false`. Returns (success, errors, raw)."""
    session.log("VERIFIER", "Running tsc --noEmit…")

    # Use npx to ensure we use the project-local typescript
    rc, out, err = await _run(
        ["npx", "--yes", "tsc", "--noEmit", "--pretty", "false", "--skipLibCheck"],
        project_dir,
        TSC_TIMEOUT,
        session,
        "VERIFIER",
    )

    combined = out + "\n" + err
    errors = _parse_tsc_output(combined, project_dir)

    ts_errors = [e for e in errors if e.severity == "error"]
    ts_warnings = [e for e in errors if e.severity == "warning"]

    if rc == 0 and not ts_errors:
        session.log("VERIFIER", "  ✓ TypeScript: zero errors", "success")
        return True, [], combined
    else:
        session.log("VERIFIER", f"  ✗ TypeScript: {len(ts_errors)} error(s), {len(ts_warnings)} warning(s)", "error")
        for e in ts_errors[:10]:
            session.log("VERIFIER", f"    TS{e.code}: {e.file}:{e.line} — {e.message[:80]}")
        return False, ts_errors, combined


# ── Repair agent ──────────────────────────────────────────────────────────────

async def _repair_file(
    project_dir: Path,
    rel_path: str,
    errors: list[TSError],
    session: GenerationSession,
    groq: GroqClient,
) -> bool:
    """
    Feed a file + its errors to Groq for targeted repair.
    Writes the fixed code back to disk. Returns True if file was repaired.
    """
    full_path = project_dir / rel_path
    if not full_path.exists():
        session.log("VERIFIER", f"  ⚠ File not found for repair: {rel_path}", "warning")
        return False

    original = full_path.read_text(encoding="utf-8")
    error_block = "\n".join(str(e) for e in errors)

    session.log("VERIFIER", f"  Repairing {rel_path} ({len(errors)} error(s))…")

    prompt = (
        f"File: {rel_path}\n\n"
        f"TypeScript errors to fix:\n{error_block}\n\n"
        f"Current file content:\n{original}"
    )

    try:
        fixed_raw = await groq.complete(
            messages=[
                {"role": "system", "content": REPAIR_SYSTEM},
                {"role": "user", "content": prompt},
            ],
            model=session.settings.plan_model,
            temperature=0.05,
            max_tokens=6000,
        )
        fixed = strip_fences(fixed_raw)

        if len(fixed.strip()) < 20:
            session.log("VERIFIER", f"  ⚠ Repair response too short, skipping", "warning")
            return False

        full_path.write_text(fixed, encoding="utf-8")

        # Mirror back into session.files if present
        if rel_path in session.files:
            session.files[rel_path].content = fixed

        session.log("VERIFIER", f"  ✓ {rel_path} repaired", "success")
        return True

    except Exception as e:
        session.log("VERIFIER", f"  ✗ Repair failed for {rel_path}: {e}", "error")
        return False


# ── Main verification agent ───────────────────────────────────────────────────

async def run_verifier(
    session: GenerationSession,
    groq: GroqClient,
) -> VerificationResult:
    """
    Full TypeScript verification pipeline:
    1. npm install
    2. tsc --noEmit
    3. If errors: repair errored files, re-run tsc
    4. Repeat up to MAX_REPAIR_ROUNDS
    """
    project_dir = session.project_dir
    if not project_dir or not project_dir.exists():
        session.log("VERIFIER", "✗ Project directory not found — skip verification", "error")
        return VerificationResult(success=False)

    agent_run = session.start_agent("VERIFIER", f"tsc + Groq repair [{session.settings.plan_model}]")
    result = VerificationResult(success=False)

    # Step 1: npm install
    result.npm_installed = await _npm_install(project_dir, session)
    if not result.npm_installed:
        session.log("VERIFIER", "⚠ Skipping tsc (npm install failed)", "warning")
        session.finish_agent(agent_run, npm=False, errors=0)
        return result

    # Step 2+: tsc → repair loop
    for round_num in range(1, MAX_REPAIR_ROUNDS + 1):
        session.log("VERIFIER", f"Round {round_num}/{MAX_REPAIR_ROUNDS}: tsc check…")
        result.rounds = round_num
        success, errors, raw = await _run_tsc(project_dir, session)
        result.raw_output = raw

        if success:
            result.success = True
            result.errors = []
            break

        result.errors = errors

        if round_num == MAX_REPAIR_ROUNDS:
            session.log("VERIFIER", f"  Max repair rounds reached. {len(errors)} error(s) remain.", "warning")
            break

        # Group errors by file
        by_file: dict[str, list[TSError]] = {}
        for e in errors:
            by_file.setdefault(e.file, []).append(e)

        # Repair each file (prioritise files with most errors first)
        for rel_path, file_errors in sorted(by_file.items(), key=lambda x: -len(x[1])):
            repaired = await _repair_file(project_dir, rel_path, file_errors, session, groq)
            if repaired:
                result.repaired_files.append(rel_path)

    # Final summary
    if result.success:
        session.log_success("VERIFIER", f"✓ TypeScript verification passed in {result.rounds} round(s)")
    else:
        remaining = len(result.errors)
        session.log_warn("VERIFIER", f"⚠ {remaining} TypeScript error(s) remain after {result.rounds} repair round(s)")
        session.log_warn("VERIFIER", "  App may still run — some errors are import-path only")

    session.finish_agent(
        agent_run,
        npm=result.npm_installed,
        rounds=result.rounds,
        repaired=len(result.repaired_files),
        remaining_errors=len(result.errors),
    )

    return result
