#!/usr/bin/env python3
"""L0.5++ — сверка макетов: имя макета в `ПолучитьМакет("X")` и имя области
в `X.ПолучитьОбласть("Y")` против того, что реально есть в Templates/.

Платформа этого не проверяет вообще — как и имена таблиц в тексте запроса,
`ПолучитьМакет`/`ПолучитьОбласть` принимают обычную строку, разрешаемую
только в рантайме. `/CheckModules` компилирует вызов синтаксически валидным
независимо от того, существует ли макет или область.

Разбор эвристический, построчный, в пределах модуля (без разбора области
видимости процедур — переменная-макет считается «текущей» до следующего
переприсвоения тем же именем). Ловит опечатку в имени, не гарантирует
отсутствие ложных срабатываний на нетривиальных косвенных обращениях
(макет, полученный не напрямую по литералу, не проверяется — тихо
пропускается, не блокирует).

  mxl_check.py <каталог выгрузки>
"""
from __future__ import annotations

import re
import sys
import unicodedata
import xml.etree.ElementTree as ET
from pathlib import Path

SPREADSHEET_TAG = "document"
SPREADSHEET_NS = "http://v8.1c.ru/8.2/data/spreadsheet"

MAKET_ASSIGN_RE = re.compile(
    r'(\w+)\s*=\s*[\w.]+\.ПолучитьМакет\(\s*"([^"]+)"')
AREA_CALL_RE = re.compile(r'(\w+)\.ПолучитьОбласть\(\s*"([^"]+)"')


def _local(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]


def index_templates(root: Path) -> dict[str, set[str]]:
    """Имя макета (имя каталога-контейнера) → множество имён областей."""
    out: dict[str, set[str]] = {}
    for xml in root.rglob("Ext/Template.xml"):
        try:
            r = ET.parse(xml).getroot()
        except ET.ParseError:
            continue
        if _local(r.tag) != SPREADSHEET_TAG or SPREADSHEET_NS not in r.tag:
            continue
        # macOS отдаёт имена файлов в NFD (кириллица разложена на буква+
        # диакритика), а литералы в коде набраны в NFC — без нормализации
        # визуально одинаковые имена не совпадают побайтово.
        name = unicodedata.normalize(
            "NFC", xml.parent.parent.name)      # .../<Имя>/Ext/Template.xml
        areas = set()
        for item in r.iter():
            if _local(item.tag) == "namedItem":
                for child in item:
                    if _local(child.tag) == "name" and child.text:
                        areas.add(child.text)
        out[name] = areas
    return out


def check_file(text: str, templates: dict[str, set[str]]) -> list[dict]:
    # Имена макетов/областей в BSL регистронезависимы (реальный случай:
    # область "подвал" в XML макета, вызов ПолучитьОбласть("Подвал") в коде
    # — рабочий, платформа не различает регистр). Сверяем по lower().
    templates_lower = {k: {a.lower() for a in v} for k, v in templates.items()}
    findings = []
    current: str | None = None
    lines = text.split("\n")
    for line_no, line in enumerate(lines, 1):
        if line.lstrip().startswith("//"):
            continue                            # закомментированный код
        m = MAKET_ASSIGN_RE.search(line)
        if m:
            var, tpl = m.group(1), unicodedata.normalize("NFC", m.group(2))
            current = var
            if tpl not in templates:
                findings.append({
                    "severity": "block", "line": line_no, "var": var,
                    "message": f'ПолучитьМакет("{tpl}") — такого макета '
                               f'нет среди Templates',
                })
            continue
        m = AREA_CALL_RE.search(line)
        if m:
            var, area = m.group(1), m.group(2)
            if var != current:
                continue                        # макет неизвестен — не судим
            # последнее (некомментированное) присвоение var выше по тексту
            tpl_name = None
            for prev in lines[:line_no][::-1]:
                if prev.lstrip().startswith("//"):
                    continue
                pm = MAKET_ASSIGN_RE.search(prev)
                if pm and pm.group(1) == var:
                    tpl_name = unicodedata.normalize("NFC", pm.group(2))
                    break
            if tpl_name and tpl_name in templates_lower \
                    and area.lower() not in templates_lower[tpl_name]:
                findings.append({
                    "severity": "block", "line": line_no, "var": var,
                    "message": f'{var}.ПолучитьОбласть("{area}") — нет '
                               f'такой области в макете "{tpl_name}"',
                })
    return findings


def run(root: Path) -> dict:
    templates = index_templates(root)
    results: dict[str, list[dict]] = {}
    for bsl in sorted(root.rglob("*.bsl")):
        text = bsl.read_text(encoding="utf-8-sig", errors="replace")
        hits = check_file(text, templates)
        if hits:
            results[str(bsl.relative_to(root))] = hits
    blocking = sum(1 for v in results.values()
                   for f in v if f["severity"] == "block")
    return {"results": results, "blocking": blocking, "templates": {
        k: sorted(v) for k, v in templates.items()}}


def main() -> int:
    res = run(Path(sys.argv[1]).resolve())
    if not res["results"]:
        print(f"L0.5++ OK — макеты/области сходятся "
              f"({len(res['templates'])} макетов в выгрузке)")
        return 0
    print(f"L0.5++ FAIL — блокирующих: {res['blocking']}")
    for file, findings in res["results"].items():
        for f in findings:
            print(f"  [БЛОК] {file}:{f['line']} — {f['message']}")
    return 1 if res["blocking"] else 0


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