"""脚本语法检查：Python AST、Node、Bash、PowerShell；BAT/CMD 启发式。"""

from __future__ import annotations

import ast
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any

MAX_SOURCE_CHARS = 1_500_000
CHECK_TIMEOUT_SEC = 45


def _write_temp(suffix: str, source: str) -> Path:
    fd, name = tempfile.mkstemp(suffix=suffix, text=True)
    os.close(fd)
    p = Path(name)
    p.write_text(source, encoding="utf-8", newline="\n")
    return p


def check_python(source: str) -> dict[str, Any]:
    try:
        ast.parse(source, filename="<agent>")
    except SyntaxError as e:
        return {
            "ok": False,
            "language": "python",
            "error": str(e),
            "lineno": e.lineno,
            "offset": e.offset,
        }
    return {"ok": True, "language": "python"}


def check_node(source: str, *, ext: str = ".js") -> dict[str, Any]:
    node = shutil.which("node")
    if not node:
        return {
            "ok": False,
            "language": "javascript",
            "error": "node not on PATH",
            "hint": "Install Node.js to enable js/mjs --check",
        }
    path = _write_temp(ext, source)
    try:
        r = subprocess.run(
            [node, "--check", str(path)],
            capture_output=True,
            text=True,
            timeout=CHECK_TIMEOUT_SEC,
            encoding="utf-8",
            errors="replace",
        )
    except subprocess.TimeoutExpired:
        return {"ok": False, "language": "javascript", "error": "node --check timeout"}
    finally:
        path.unlink(missing_ok=True)
    if r.returncode != 0:
        err = (r.stderr or r.stdout or "").strip() or f"exit {r.returncode}"
        return {"ok": False, "language": "javascript", "error": err[:4000]}
    return {"ok": True, "language": "javascript"}


def check_bash(source: str) -> dict[str, Any]:
    bash = shutil.which("bash")
    if not bash:
        return {
            "ok": False,
            "language": "bash",
            "error": "bash not on PATH",
            "hint": "Git Bash or WSL provides bash -n",
        }
    path = _write_temp(".sh", source)
    try:
        r = subprocess.run(
            [bash, "-n", str(path)],
            capture_output=True,
            text=True,
            timeout=CHECK_TIMEOUT_SEC,
            encoding="utf-8",
            errors="replace",
        )
    except subprocess.TimeoutExpired:
        return {"ok": False, "language": "bash", "error": "bash -n timeout"}
    finally:
        path.unlink(missing_ok=True)
    if r.returncode != 0:
        err = (r.stderr or r.stdout or "").strip() or f"exit {r.returncode}"
        return {"ok": False, "language": "bash", "error": err[:4000]}
    return {"ok": True, "language": "bash"}


def _ps_quote_path(p: str) -> str:
    return str(p).replace("'", "''")


def check_powershell_file(path: Path) -> dict[str, Any]:
    pwsh = shutil.which("pwsh") or shutil.which("powershell")
    if not pwsh:
        return {
            "ok": False,
            "language": "powershell",
            "error": "pwsh/powershell not on PATH",
        }
    ps_path = _ps_quote_path(str(path.resolve()))
    cmd = (
        f"$e=$null;$t=$null;"
        f"[void][System.Management.Automation.Language.Parser]::ParseFile("
        f"'{ps_path}',[ref]$t,[ref]$e);"
        f"if($e){{$e|ForEach-Object{{$_.Message}};exit 1}};exit 0"
    )
    try:
        r = subprocess.run(
            [pwsh, "-NoProfile", "-NonInteractive", "-Command", cmd],
            capture_output=True,
            text=True,
            timeout=CHECK_TIMEOUT_SEC,
            encoding="utf-8",
            errors="replace",
        )
    except subprocess.TimeoutExpired:
        return {"ok": False, "language": "powershell", "error": "parser timeout"}
    if r.returncode != 0:
        err = (r.stdout or r.stderr or "").strip() or f"exit {r.returncode}"
        return {"ok": False, "language": "powershell", "error": err[:4000]}
    return {"ok": True, "language": "powershell"}


def check_powershell(source: str) -> dict[str, Any]:
    path = _write_temp(".ps1", source)
    try:
        return check_powershell_file(path)
    finally:
        path.unlink(missing_ok=True)


def check_bat_heuristic(source: str) -> dict[str, Any]:
    """CMD/BAT 无官方语法检查，仅做启发式警告。"""
    warnings: list[str] = []
    lines = source.splitlines()
    if len(lines) > 5000:
        warnings.append("very many lines (>5000)")
    for i, line in enumerate(lines[:2000], 1):
        if len(line) > 8190:
            warnings.append(f"line {i} exceeds CMD ~8191 char limit")
    o = source.count("(")
    c = source.count(")")
    if o != c:
        warnings.append(f"unbalanced parentheses count ( vs ): {o} vs {c}")
    pct = source.count("%")
    if pct % 2 != 0:
        warnings.append("odd number of % — possible unclosed %var%")
    rem = re.findall(r"rem\s+(.+)", source, re.I)
    for rline in rem[:5]:
        if "<" in rline and ">" in rline and ">>" not in rline:
            warnings.append("REM line may contain redirection tokens; verify intent")
            break
    return {
        "ok": True,
        "language": "bat",
        "heuristic_only": True,
        "warnings": warnings,
        "note": "no formal BAT parser; fix issues flagged above if any",
    }


def run_check(
    kind: str,
    *,
    source: str | None = None,
    file_path: Path | None = None,
) -> dict[str, Any]:
    k = kind.strip().lower().lstrip(".")
    if file_path is not None:
        if not file_path.is_file():
            return {"ok": False, "error": f"not a file: {file_path}"}
        raw = file_path.read_text(encoding="utf-8", errors="replace")
        if len(raw) > MAX_SOURCE_CHARS:
            return {"ok": False, "error": f"file exceeds {MAX_SOURCE_CHARS} chars"}
        source = raw
    if source is None:
        return {"ok": False, "error": "provide source string or path"}
    if len(source) > MAX_SOURCE_CHARS:
        return {"ok": False, "error": f"source exceeds {MAX_SOURCE_CHARS} chars"}

    if k in ("py", "python"):
        return check_python(source)
    if k in ("js", "javascript", "mjs"):
        ext = ".mjs" if k == "mjs" else ".js"
        return check_node(source, ext=ext)
    if k in ("bash", "sh", "shell"):
        return check_bash(source)
    if k in ("ps1", "powershell", "pwsh"):
        return check_powershell(source)
    if k in ("bat", "cmd"):
        return check_bat_heuristic(source)
    return {
        "ok": False,
        "error": f"unknown kind: {kind!r}",
        "hint": "py|js|mjs|bash|sh|ps1|bat|cmd",
    }
