#!/usr/bin/env python3
"""1c-testbench — единая проверка доработки 1С перед сдачей.

  tb.py check <путь>   путь = .epf/.erf или каталог XML-выгрузки
  tb.py check <путь> --fast          только статика (без платформы)
  tb.py check <путь> --json <файл>   машинный отчёт

Слои (по возрастанию цены):
  L0    gates      — грабли, на которых уже обжигались (мгновенно)
  L0    lint       — BSL Language Server, ошибки разбора (~10 с)
  L0.5  meta       — имена метаданных в текстах запросов/схем СКД vs конфигурация (~1 с)
  L0.5  mxl        — имена макетов/областей в ПолучитьМакет/ПолучитьОбласть (~1 с)
  L1    platform   — компиляция настоящей платформой через расширение (~40 с)

Код возврата: 0 — можно отдавать, 1 — есть блокирующие находки.
"""
from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
import zipfile
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))

import ext_wrap          # noqa: E402
import gates             # noqa: E402
import lint_bsl          # noqa: E402
import meta_check        # noqa: E402
import mxl_check         # noqa: E402
import render_form       # noqa: E402
import v8run             # noqa: E402

from urllib.parse import quote  # noqa: E402

# Карты форм складываются в каталог, зарегистрированный как dev-сервер
# (.claude/launch.json → «1c-form-maps»): только так превью-панель отдаёт
# полноценный DOM и скриншот, file:// показывает статичный снимок.
PREVIEW = Path(os.environ.get(
    "ONEC_TB_PREVIEW", str(Path.home() / "Разработки/.tb_preview")))
PREVIEW_URL = os.environ.get("ONEC_TB_PREVIEW_URL", "http://localhost:8911")

AGENT = os.environ.get("ONEC_AGENT_URL", "http://127.0.0.1:8788")
AGENT_HOME = Path(os.environ.get(
    "ONEC_AGENT_HOME", str(Path.home() / "Services/onec-build-agent")))
STAND_IB = Path(os.environ.get("ONEC_TB_IB", str(AGENT_HOME / "ib_stand")))
UID = os.environ.get("ONEC_TB_UID", "1005954706")
EXT_NAME = os.environ.get("ONEC_TB_EXT", "аСтенд")


def dump_epf(epf: Path, work: Path) -> Path:
    """Готовый .epf → XML-выгрузка настоящим Конфигуратором через агента."""
    token = (AGENT_HOME / ".token").read_text(encoding="utf-8").strip()
    zip_path = work / "dump.zip"
    ext = epf.suffix.lstrip(".").lower()
    r = subprocess.run(
        ["curl", "-s", "-m", "900", "-X", "POST",
         "-H", f"Authorization: Bearer {token}",
         "--data-binary", f"@{epf}", "-o", str(zip_path),
         "-w", "%{http_code}",
         f"{AGENT}/dump-epf?ext={ext}&uid={UID}"],
        capture_output=True, text=True, check=False)
    if r.stdout.strip() != "200":
        body = (zip_path.read_text(errors="replace")[:500]
                if zip_path.exists() else "")
        raise SystemExit(f"агент не выгрузил {epf.name}: HTTP {r.stdout}\n{body}")
    src = work / "src_dump"
    src.mkdir()
    with zipfile.ZipFile(zip_path) as zf:
        zf.extractall(src)
    return src


def platform_check(src: Path, work: Path) -> dict:
    """L1: обработка → объект расширения → загрузка в стенд-базу → CheckModules."""
    if not (STAND_IB / "1Cv8.1CD").exists():
        return {"status": "skip",
                "message": f"нет стенд-базы {STAND_IB} (см. tools/setup.sh)"}
    ext_dir = work / "ext"
    info = ext_wrap.wrap(src, ext_dir, EXT_NAME)
    ok, log, secs = v8run.run(
        ["DESIGNER", "/F", str(STAND_IB), "/DisableStartupDialogs",
         "/LoadConfigFromFiles", str(ext_dir), "-Extension", EXT_NAME], 900)
    if not ok or log:
        return {"status": "fail", "stage": "load", "seconds": round(secs),
                "object": f"{info['kind']}.{info['name']}",
                "log": log or "таймаут загрузки расширения"}
    ok, log, secs2 = v8run.run(
        ["DESIGNER", "/F", str(STAND_IB), "/DisableStartupDialogs",
         "/CheckModules", "-Extension", EXT_NAME,
         "-ThinClient", "-Server", "-ThickClientManagedApplication",
         "-ExtendedModulesCheck"], 900)
    clean = "Синтаксических ошибок не обнаружено" in log
    return {"status": "ok" if (ok and clean) else "fail",
            "stage": "checkmodules", "seconds": round(secs + secs2),
            "object": f"{info['kind']}.{info['name']}", "log": log}


def main() -> int:
    if len(sys.argv) < 3 or sys.argv[1] != "check":
        raise SystemExit(__doc__)
    target = Path(sys.argv[2]).resolve()
    fast = "--fast" in sys.argv
    work = Path(tempfile.mkdtemp(prefix="tb_", dir="/tmp"))
    report: dict = {"target": str(target), "layers": {}}
    t0 = time.time()
    try:
        src = dump_epf(target, work) if target.is_file() else target
        report["source"] = str(src)

        g = gates.run(src)
        report["layers"]["gates"] = g
        print(f"L0   гейты      : блокирующих {g['blocking']}")
        for file, findings in g["results"].items():
            for f in findings:
                tag = "БЛОК" if f["severity"] == "block" else "пред"
                where = f":{f['line']}" if f["line"] else ""
                print(f"     [{tag}] {file}{where} — {f['message']}")

        try:
            m = meta_check.run(src)
            report["layers"]["meta"] = m
            print(f"L0.5 метаданные : блокирующих {m['blocking']}")
            for file, findings in m["results"].items():
                for f in findings:
                    tag = "БЛОК" if f["severity"] == "block" else "пред"
                    where = (f"набор данных «{f['dataset']}»" if "dataset" in f
                             else f"запрос со строки {f['line']}")
                    print(f"     [{tag}] {file} ({where}) — {f['message']}")
        except SystemExit as exc:
            report["layers"]["meta"] = {"status": "skip", "message": str(exc)}
            print(f"L0.5 метаданные : пропуск — {exc}")

        mxl = mxl_check.run(src)
        report["layers"]["mxl"] = mxl
        print(f"L0.5 макеты      : блокирующих {mxl['blocking']}"
              f"   ({len(mxl['templates'])} макетов в выгрузке)")
        for file, findings in mxl["results"].items():
            for f in findings:
                print(f"     [БЛОК] {file}:{f['line']} — {f['message']}")

        lint = lint_bsl.analyze(src)
        report["layers"]["lint"] = lint
        print(f"L0   BSL LS      : ошибок разбора {len(lint['blocking'])}"
              f"   (справочно: {lint['counts']})")
        for b in lint["blocking"]:
            print(f"     [БЛОК] {Path(b['file']).name}:{b['line']}:{b['column']}"
                  f" — {b['message'][:160]}")

        if fast:
            report["layers"]["platform"] = {"status": "skip",
                                            "message": "--fast"}
            print("L1   платформа   : пропуск (--fast)")
        else:
            p = platform_check(src, work)
            report["layers"]["platform"] = p
            if p["status"] == "skip":
                print(f"L1   платформа   : пропуск — {p['message']}")
            else:
                print(f"L1   платформа   : {p['status']} "
                      f"({p['seconds']} с, {p['object']})")
                if p["status"] == "fail":
                    for line in p["log"].split("\n"):
                        print(f"     {line}")

        if "--no-render" not in sys.argv:
            try:
                forms = render_form.find_forms(src)
                for f in forms:
                    render_form.SYNONYMS.clear()
                    render_form.SYNONYMS.update(render_form.object_synonyms(f))
                    form = render_form.parse_form(f)
                    PREVIEW.mkdir(parents=True, exist_ok=True)
                    (PREVIEW / f"{form['name']}.html").write_text(
                        render_form.to_html(form, str(f)), encoding="utf-8")
                if forms:
                    names = [render_form.parse_form(f)["name"] for f in forms]
                    links = "".join(
                        f'<li><a href="{quote(n)}.html">{n}</a></li>'
                        for n in names)
                    (PREVIEW / "index.html").write_text(
                        f'<!doctype html><meta charset="utf-8">'
                        f'<title>Формы: {target.name}</title>'
                        f'<h3>{target.name}</h3><ul>{links}</ul>',
                        encoding="utf-8")
                    print(f"L3a  формы       : {len(forms)} "
                          f"({', '.join(names)}) → {PREVIEW_URL}/")
                    report["layers"]["forms"] = {"count": len(forms),
                                                 "names": names,
                                                 "dir": str(PREVIEW)}
            except Exception as exc:                     # noqa: BLE001
                print(f"L3a  формы       : не отрисовал — {exc}")

        blocking = (g["blocking"]
                    + report["layers"]["meta"].get("blocking", 0)
                    + mxl["blocking"]
                    + len(lint["blocking"])
                    + (1 if report["layers"]["platform"].get("status")
                       == "fail" else 0))
        report["blocking"] = blocking
        report["seconds"] = round(time.time() - t0)
        print(f"\n{'ОТДАВАТЬ НЕЛЬЗЯ' if blocking else 'ЧИСТО — можно отдавать'}"
              f"  ({report['seconds']} с)")
        if "--json" in sys.argv:
            Path(sys.argv[sys.argv.index("--json") + 1]).write_text(
                json.dumps(report, ensure_ascii=False, indent=2),
                encoding="utf-8")
        return 1 if blocking else 0
    finally:
        shutil.rmtree(work, ignore_errors=True)


if __name__ == "__main__":
    sys.exit(main())
