"""Приёмка.

Разделение честное: механические команды CLI прогоняет сам и пишет в журнал, а живой
сценарий отдаёт профильному скиллу-проверяльщику — он умеет ходить в реальный сервис
с реальными секретами. Оригинальная конструкция здесь ломается: она требует «запустить
проект», а любой бот упирается в пустой .env, который сам же и создала.
"""

from __future__ import annotations

import json
import re
from pathlib import Path

from .junit import find_junit_reports, format_junit_summary, parse_junit_xml
from .proof import run_command
from .store import Run, git


def _map_path() -> Path:
    return Path(__file__).resolve().parent.parent / "accept-map.json"


def load_profiles() -> tuple[list[dict], dict]:
    data = json.loads(_map_path().read_text(encoding="utf-8"))
    return data["profiles"], data["fallback"]


def _haystack(root: Path) -> str:
    """Имя проекта и адрес репозитория — но не весь путь.

    Поиск подстрокой по абсолютному пути давал грубые ложные срабатывания: «erf»
    находится внутри «interface», «waterfall» и «perf-tests», и обычный проект
    получал приёмку для внешних обработок 1С.
    """
    remote = git(root, "remote", "get-url", "origin")
    tail = remote.rsplit("/", 1)[-1].removesuffix(".git") if remote else ""
    return f"{root.name} {tail}".lower()


def _hits(needle: str, haystack: str) -> bool:
    return re.search(rf"(?<![0-9a-zа-яё]){re.escape(needle)}(?![0-9a-zа-яё])", haystack) is not None


def detect(root: Path) -> dict:
    profiles, fallback = load_profiles()
    haystack = _haystack(root)

    for profile in profiles:
        needles = [n.lower() for n in profile.get("match", [])]
        if needles and any(_hits(n, haystack) for n in needles):
            return profile

    for profile in profiles:
        files = profile.get("files") or []
        if files and all(_exists(root, f) for f in files):
            return profile

    return fallback


def _exists(root: Path, pattern: str) -> bool:
    if any(ch in pattern for ch in "*?["):
        return any(root.glob(pattern))
    return (root / pattern).exists()


VENV_DIRS = ("venv", ".venv", "env")


def resolve_command(cmd: str, root: Path) -> str:
    """Подставляет инструмент из окружения проекта, если он там есть.

    Профиль в карте не знает, чем конкретный проект гоняет тесты. Системный
    `python3 -m pytest` в проекте с собственным venv падает на «No module named
    pytest» — и это выглядит как красная приёмка, хотя тесты никто не запускал.
    Для 1С проверяет наличие скрипта run_tests.sh, файла yaxunit.json или тестов.
    """
    for tool in ("pytest", "ruff", "mypy"):
        if f"-m {tool}" not in cmd and not cmd.startswith(tool):
            continue
        for vd in VENV_DIRS:
            py_bin = root / vd / "bin" / "python"
            if py_bin.is_file() or py_bin.is_symlink():
                tail = cmd.split(f"-m {tool}", 1)[-1] if f"-m {tool}" in cmd else (
                    cmd.split(tool, 1)[-1] if tool in cmd else ""
                )
                return f"{vd}/bin/python -m {tool}{tail}"
            binary = root / vd / "bin" / tool
            if binary.is_file():
                tail = cmd.split(f"-m {tool}", 1)[-1] if f"-m {tool}" in cmd else cmd[len(tool):]
                return f"{vd}/bin/{tool}{tail}"

    # Обработка 1С-команд
    if "vrunner" in cmd or "RunUnitTests" in cmd or "yaxunit" in cmd or "vanessa" in cmd:
        # Проверка замка на базу 1С
        try:
            from .onec_lock import OnecLockError, verify_and_lock_onec
            lock_info = verify_and_lock_onec(root, backup=True)
            v8ver = lock_info["v8version"]
            ibconn = lock_info["ibconnection"]
        except OnecLockError as exc:
            return f'echo "ОШИБКА: {exc}" >&2 && exit 1'

        # 1. Поиск скрипта запуска тестов в проекте
        for script_name in ("run_tests.sh", "tests.sh", "test.sh", "tools/run_tests.sh"):
            script_path = root / script_name
            if script_path.is_file():
                return f"./{script_name}" if not script_name.startswith("./") else script_name

        # 2. Модульные тесты YAxUnit — единственный доказанный путь.
        yaxunit_configs = ["yaxunit.json", "yaxunit-all.json", "yaxunit-run.json", "tests/yaxunit.json", ".yaxunit.json"]
        for cfg in yaxunit_configs:
            if (root / cfg).is_file():
                return (
                    f'vrunner run --command "RunUnitTests={cfg}" '
                    f'--v8version "{v8ver}" '
                    f'--ibconnection "{ibconn}"'
                )

        # 3. UI/BDD Vanessa-Automation — отказ, а не молчаливое зависание.
        #
        # `vrunner vanessa` открывает интерактивный клиент `1cv8c ENTERPRISE` и ждёт
        # человека: на macOS прогон висит без отчёта и без кода возврата (проверено
        # 21.08.2026 на полигоне). Приёмка, которая висит, хуже упавшей — она не
        # краснеет и не зеленеет, а просто съедает время. Пока запуск не доказан
        # прогоном, Чекан его не составляет.
        va_configs = ["VAParams.json", "vanessa-automation.json", "va.json", "vanessasettings.json", "tools/VAParams.json", "tests/VAParams.json"]
        found_va = next((cfg for cfg in va_configs if (root / cfg).is_file()), None)
        if not found_va:
            for fdir in ("features", "tests/features", "bdd/features"):
                p_dir = root / fdir
                if p_dir.is_dir() and any(p_dir.rglob("*.feature")):
                    found_va = fdir
                    break

        if found_va:
            return (
                f'echo "ОШИБКА: найдены только UI/BDD-тесты Vanessa-Automation ({found_va}). '
                'Чекан их не запускает: прогон открывает интерактивный клиент 1С и виснет без результата. '
                'Добавьте модульные тесты YAxUnit (yaxunit.json) или свой run_tests.sh." >&2 && exit 1'
            )

        # 4. Проверка наличия исходников тестов
        has_tests = False
        for p in (root / "src_tests_ext", root / "tests", root / "tests_ext"):
            if p.is_dir() and (any(p.rglob("*.bsl")) or any(p.rglob("*.os")) or any(p.rglob("*.feature"))):
                has_tests = True
                break

        if not has_tests:
            return 'echo "ОШИБКА: в 1С-проекте не найдены тесты (нет yaxunit.json или run_tests.sh). Приёмка без тестов запрещена." >&2 && exit 1'

    return cmd


def run_acceptance(run: Run, explicit: str | None = None) -> int:
    root = run.root
    profile = detect(root)
    skill = explicit or profile.get("skill")

    print(f"Профиль приёмки: {profile['id']}")
    if profile.get("note"):
        print(f"  {profile['note']}")
    print()

    failures = []
    for raw in profile.get("checks", []):
        cmd = resolve_command(raw, root)
        if cmd != raw:
            print(f"  (команда взята из окружения проекта: {raw} → {cmd})")
        row = run_command(run, cmd, kind="accept")
        mark = "зелено" if row["exit"] == 0 else f"красно ({row['exit']})"
        print(f"$ {cmd}\n→ {mark}")

        # Разбор отчетов JUnit, если они сформированы
        reports = find_junit_reports(root)
        if reports:
            latest_report = reports[0]
            try:
                junit_res = parse_junit_xml(latest_report)
                if not junit_res.is_empty:
                    print(format_junit_summary(junit_res))
                    if junit_res.unique_failures_count > 0 or junit_res.unique_errors_count > 0:
                        if cmd not in failures:
                            failures.append(cmd)
                elif profile.get("id") == "onec":
                    print("  ✗ Отчёт YAxUnit пуст (0 тестов). Приёмка без тестов запрещена.")
                    if cmd not in failures:
                        failures.append(cmd)
            except Exception:
                pass

        if row["exit"] != 0:
            if cmd not in failures:
                failures.append(cmd)
            if row["out_tail"]:
                print(row["out_tail"])
        print()

    st = run.state
    entry = st.setdefault("gates", {}).setdefault("G4", {})
    checks = [
        {"cmd": c, "exit": next(
            (e["exit"] for e in reversed(run.evidence()) if e["cmd"] == c), None
        )}
        for c in [resolve_command(x, root) for x in profile.get("checks", [])]
    ]
    entry["acceptance"] = {
        "profile": profile["id"],
        "skill": skill,
        "cmd": " && ".join(resolve_command(c, root) for c in profile.get("checks", [])) or None,
        "exit": 0 if not failures else 1,
        "checks": checks,
    }
    if not profile.get("checks"):
        entry["acceptance"]["exit"] = None
    run.save(st)

    if failures:
        print("Приёмка красная. Чините и прогоняйте заново — гейт G4 не пустит.")
        return 1

    if profile.get("checks"):
        print(f"Механическая часть приёмки зелёная ({len(profile['checks'])} проверок).")
    else:
        print(
            "Механических проверок для этого профиля НЕТ — значит ничего и не доказано.\n"
            "Назовите реальную команду сами и прогоните её:\n"
            "  chekan run \"<сборка или тесты>\" --kind accept\n"
            "Гейт G4 не пустит прогон, пока приёмка не подтверждена исполнением."
        )
    print()
    if skill:
        print(f"Живой сценарий: вызовите скилл `{skill}`.")
        print("Он ходит в реальный сервис — это то, чего не может доказать ни один тест.")
    else:
        print("Профильного скилла нет: живой сценарий проходите сами (браузер / curl / запуск).")
    print()
    print("Затем — слепой приёмщик: субагент со свежим контекстом, которому дают")
    print(f"ТОЛЬКО {(run.dir / 'brief.md').relative_to(root)} и репозиторий. Спеку и таски не давать.")
    print("Его вердикт положите в JSON и подайте:")
    print("  chekan gate G4 --blind verdict.json")
    print()
    print('Формат: {"done": ["что работает"], "missing": ["что заказано, но не работает"]}')
    return 0
