"""Замок на базу 1С: работаем только в доказанно тестовой.

Правила:
1. Объявление базы в chekan-1c.json в корне проекта. Нет файла -> отказ 'назовите базу в chekan-1c.json'.
2. Только файловые базы (/F). Серверные (/S) -> отказ 'серверная база считается боевой'.
3. Метка .chekan-testbase в каталоге базы или рядом с ним. Инструмент НИКОГДА не создаёт метку сам.
4. Чёрный список перевешивает метку (~/Services/onec-build-agent/ib, ib_stand, ib_u*).
5. Занятая база (файлы блокировок / активные сеансы) -> отказ.
6. Резервная копия базы перед прогоном. Не скопировалось -> не работаем.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path


class OnecLockError(Exception):
    """Отказ замка на 1С базу. Текст пишется пользователю дословно."""


DEFAULT_V8VERSION = "8.3.27.1859"

# Черный список каталогов баз, которые запрещено трогать даже с меткой .chekan-testbase
BLACKLISTED_PATTERNS = [
    Path.home() / "Services" / "onec-build-agent" / "ib",
    Path.home() / "Services" / "onec-build-agent" / "ib_stand",
]


def _is_blacklisted(db_path: Path, extra_blacklist: list[str] | None = None) -> bool:
    resolved_db = db_path.resolve()
    build_agent_dir = (Path.home() / "Services" / "onec-build-agent").resolve()

    # Проверка встроенного черного списка
    for b in BLACKLISTED_PATTERNS:
        b_resolved = b.resolve()
        if resolved_db == b_resolved or b_resolved in resolved_db.parents:
            return True

    # Проверка префиксов ib_u* и всех ib* в ~/Services/onec-build-agent
    if build_agent_dir in resolved_db.parents or resolved_db == build_agent_dir:
        name = resolved_db.name
        if name.startswith("ib"):
            return True

    # Дополнительные пути из конфигурации
    if extra_blacklist:
        for item in extra_blacklist:
            item_resolved = Path(item).expanduser().resolve()
            if resolved_db == item_resolved or item_resolved in resolved_db.parents:
                return True

    return False


def parse_ibconnection(conn_str: str) -> Path:
    """Извлекает путь к файловой базе из строки соединения или пути."""
    s = conn_str.strip()
    if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
        s = s[1:-1].strip()

    if s.upper().startswith("/S") or "SRVR=" in s.upper():
        raise OnecLockError("серверная база считается боевой. Разрешены только файловые базы (/F).")

    if s.upper().startswith("/F"):
        raw_path = s[2:].strip()
        if (raw_path.startswith('"') and raw_path.endswith('"')) or (raw_path.startswith("'") and raw_path.endswith("'")):
            raw_path = raw_path[1:-1].strip()
        return Path(raw_path).expanduser().resolve()

    return Path(s).expanduser().resolve()


def load_onec_config(root: Path) -> dict:
    """Загружает и валидирует chekan-1c.json из корня проекта."""
    cfg_file = root / "chekan-1c.json"
    if not cfg_file.is_file():
        raise OnecLockError("назовите базу в chekan-1c.json")

    try:
        content = cfg_file.read_text(encoding="utf-8").strip()
        if not content:
            raise OnecLockError("назовите базу в chekan-1c.json")
        data = json.loads(content)
    except json.JSONDecodeError as exc:
        raise OnecLockError(f"некорректный JSON в chekan-1c.json: {exc}") from exc

    if not isinstance(data, dict):
        raise OnecLockError("назовите базу в chekan-1c.json")

    conn = data.get("ibconnection") or data.get("db_path") or data.get("path")
    if not conn or not str(conn).strip():
        raise OnecLockError("назовите базу в chekan-1c.json")

    return data


def check_testbase_marker(db_dir: Path) -> bool:
    """Проверяет наличие метки .chekan-testbase в каталоге базы или рядом с ним.
    Инструмент НИКОГДА не создаёт этот файл сам."""
    marker_inside = db_dir / ".chekan-testbase"
    marker_beside = db_dir.parent / ".chekan-testbase"

    return marker_inside.is_file() or marker_beside.is_file()


def check_active_sessions(db_dir: Path) -> list[str]:
    """Проверяет признаки активных сеансов или блокировок 1С в файловой базе."""
    if not db_dir.is_dir():
        return []

    signs = []
    # Файлы блокировок платформы 1С:Предприятие
    lock_files = ["1Cv8.cfl", "1Cv8tmp.1CD", "1Cv8.pfl"]
    for lock in lock_files:
        if (db_dir / lock).exists():
            signs.append(f"файл блокировки {lock}")

    # Проверка открытых процессов через lsof если 1Cv8.1CD заблокирован
    cd_file = db_dir / "1Cv8.1CD"
    if cd_file.is_file():
        try:
            res = subprocess.run(
                ["lsof", "-t", str(cd_file)],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if res.returncode == 0 and res.stdout.strip():
                pids = res.stdout.strip().split()
                signs.append(f"файл 1Cv8.1CD открыт процессами (PID: {', '.join(pids)})")
        except (OSError, subprocess.SubprocessError):
            pass

    return signs


def create_testbase_backup(db_dir: Path) -> Path:
    """Создает полную резервную копию каталога базы перед прогоном тестов."""
    if not db_dir.is_dir():
        raise OnecLockError(f"каталог базы '{db_dir}' не существует")

    ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    backup_dir = db_dir.parent / f"{db_dir.name}_backup_{ts}"
    try:
        shutil.copytree(db_dir, backup_dir)
    except Exception as exc:
        raise OnecLockError(f"не удалось создать резервную копию базы: {exc}") from exc

    return backup_dir


def verify_and_lock_onec(root: Path, backup: bool = False) -> dict:
    """Полная проверка замка на тестовую базу 1С.

    Возвращает словарь с параметрами:
    - db_dir: Path
    - ibconnection: str (в формате /F<path>)
    - v8version: str
    - backup_path: Path | None
    """
    cfg = load_onec_config(root)

    conn_str = str(cfg.get("ibconnection") or cfg.get("db_path") or cfg.get("path"))
    db_dir = parse_ibconnection(conn_str)

    # 1. Чёрный список
    extra_bl = cfg.get("blacklist", [])
    if _is_blacklisted(db_dir, extra_bl):
        raise OnecLockError(f"база '{db_dir}' находится в чёрном списке боевых баз. Прогон запрещён.")

    # 2. Метка .chekan-testbase
    if not check_testbase_marker(db_dir):
        raise OnecLockError(
            f"отсутствует метка тестовой базы '.chekan-testbase' в каталоге '{db_dir}' или рядом с ним. "
            "Замок требует ручной установки метки владельцем."
        )

    # 3. Активные сеансы
    signs = check_active_sessions(db_dir)
    if signs:
        raise OnecLockError(
            f"база '{db_dir}' занята: {'; '.join(signs)}. Прогон запрещён."
        )

    backup_path = None
    if backup:
        backup_path = create_testbase_backup(db_dir)

    v8version = str(cfg.get("v8version") or DEFAULT_V8VERSION)
    ibconnection = f"/F{db_dir}"

    return {
        "db_dir": db_dir,
        "ibconnection": ibconnection,
        "v8version": v8version,
        "backup_path": backup_path,
    }
