#!/usr/bin/env python3
"""Validate editable 1C patch workspace files at the BSL/Form.xml level."""

from __future__ import annotations

import argparse
import json
import re
import xml.etree.ElementTree as ET
from collections import Counter
from pathlib import Path
from typing import Any

from check_1c_patch_workspace_integrity import check_workspace


WORD = r"А-Яа-яA-Za-z0-9_"
ROUTINE_RE = re.compile(rf"(?<![{WORD}])(Процедура|Функция)\s+([А-Яа-яA-Za-z_][{WORD}]*)", re.IGNORECASE)
END_ROUTINE_RE = re.compile(rf"(?<![{WORD}])(КонецПроцедуры|КонецФункции)(?![{WORD}])", re.IGNORECASE)
BLOCK_STARTS = {
    "Если": "КонецЕсли",
    "Для": "КонецЦикла",
    "Пока": "КонецЦикла",
    "Попытка": "КонецПопытки",
}
BLOCK_ENDS = {
    "КонецЕсли": {"Если"},
    "КонецЦикла": {"Для", "Пока"},
    "КонецПопытки": {"Попытка"},
}
TOKEN_RE = re.compile(
    rf"(?<![{WORD}])(Если|Для|Пока|Попытка|КонецЕсли|КонецЦикла|КонецПопытки)(?![{WORD}])",
    re.IGNORECASE,
)


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def issue(severity: str, code: str, message: str, *, path: Path | str | None = None, line: int | None = None, detail: dict[str, Any] | None = None) -> dict[str, Any]:
    result: dict[str, Any] = {"severity": severity, "code": code, "message": message}
    if path is not None:
        result["path"] = str(path)
    if line is not None:
        result["line"] = line
    if detail:
        result["detail"] = detail
    return result


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


def child_text(element: ET.Element, name: str) -> str | None:
    for child in element:
        if local_name(child.tag) == name:
            return child.text
    return None


def read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8-sig")
    except UnicodeDecodeError:
        return path.read_text(encoding="cp1251", errors="replace")


def code_segments(line: str, *, in_string: bool) -> tuple[str, bool]:
    result = []
    index = 0
    while index < len(line):
        if in_string:
            quote = line.find('"', index)
            if quote < 0:
                return "".join(result), True
            if quote + 1 < len(line) and line[quote + 1] == '"':
                index = quote + 2
                continue
            in_string = False
            index = quote + 1
            continue
        slash = line.find("//", index)
        quote = line.find('"', index)
        if slash >= 0 and (quote < 0 or slash < quote):
            result.append(line[index:slash])
            return "".join(result), False
        if quote < 0:
            result.append(line[index:])
            return "".join(result), False
        result.append(line[index:quote])
        in_string = True
        index = quote + 1
    return "".join(result), in_string


def routine_names(text: str) -> dict[str, list[int]]:
    names: dict[str, list[int]] = {}
    in_string = False
    for number, line in enumerate(text.splitlines(), start=1):
        code, in_string = code_segments(line, in_string=in_string)
        for match in ROUTINE_RE.finditer(code):
            names.setdefault(match.group(2).casefold(), []).append(number)
    return names


def validate_bsl(path: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    findings: list[dict[str, Any]] = []
    text = read_text(path)
    routines = routine_names(text)
    for name, lines in routines.items():
        if len(lines) > 1:
            findings.append(issue("error", "duplicate_bsl_routine", f"Duplicate BSL routine: {name}", path=path, line=lines[1], detail={"lines": lines}))

    routine_stack: list[tuple[str, int]] = []
    block_stack: list[tuple[str, int]] = []
    in_string = False
    for number, line in enumerate(text.splitlines(), start=1):
        code, in_string = code_segments(line, in_string=in_string)
        if ROUTINE_RE.search(code):
            routine_stack.append(("routine", number))
        for match in END_ROUTINE_RE.finditer(code):
            if not routine_stack:
                findings.append(issue("error", "unexpected_routine_end", f"Unexpected {match.group(1)}.", path=path, line=number))
            else:
                routine_stack.pop()
        for match in TOKEN_RE.finditer(code):
            token = match.group(1)
            normalized = next((key for key in (*BLOCK_STARTS.keys(), *BLOCK_ENDS.keys()) if key.casefold() == token.casefold()), token)
            if normalized in BLOCK_STARTS:
                block_stack.append((normalized, number))
            elif normalized in BLOCK_ENDS:
                allowed = BLOCK_ENDS[normalized]
                if not block_stack:
                    findings.append(issue("error", "unexpected_block_end", f"Unexpected {normalized}.", path=path, line=number))
                else:
                    start, _start_line = block_stack[-1]
                    if start not in allowed:
                        findings.append(issue("error", "mismatched_bsl_block", f"{normalized} closes {start}.", path=path, line=number))
                    block_stack.pop()

    for _kind, line in routine_stack:
        findings.append(issue("error", "unclosed_bsl_routine", "Unclosed BSL procedure/function.", path=path, line=line))
    for kind, line in block_stack:
        findings.append(issue("error", "unclosed_bsl_block", f"Unclosed BSL block: {kind}.", path=path, line=line))

    return findings, {
        "path": str(path),
        "routine_count": sum(len(lines) for lines in routines.values()),
        "unique_routine_count": len(routines),
        "routines": sorted(routines),
    }


def iter_direct(container: ET.Element, container_name: str, item_name: str) -> list[ET.Element]:
    result = []
    for child in container:
        if local_name(child.tag) != container_name:
            continue
        for item in child:
            if local_name(item.tag) == item_name:
                result.append(item)
    return result


def parse_form(path: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    findings: list[dict[str, Any]] = []
    try:
        root = ET.parse(path).getroot()
    except ET.ParseError as exc:
        return [issue("error", "invalid_form_xml", f"Invalid Form.xml: {exc}", path=path)], {"path": str(path), "parse_error": str(exc)}

    commands = iter_direct(root, "Commands", "Command")
    command_names = [cmd.attrib.get("name") for cmd in commands if cmd.attrib.get("name")]
    command_ids = [cmd.attrib.get("id") for cmd in commands if cmd.attrib.get("id")]
    command_by_name = {name: cmd for name, cmd in zip(command_names, commands) if name}
    command_by_id = {cmd.attrib.get("id"): cmd for cmd in commands if cmd.attrib.get("id")}

    for name, count in Counter(command_names).items():
        if count > 1:
            findings.append(issue("error", "duplicate_form_command", f"Duplicate form command: {name}", path=path))
    for command_id, count in Counter(command_ids).items():
        if count > 1:
            findings.append(issue("error", "duplicate_form_command_id", f"Duplicate form command id: {command_id}", path=path))

    actions: list[str] = []
    for command in commands:
        name = command.attrib.get("name")
        action = child_text(command, "Action")
        if action:
            actions.append(action.strip())
        else:
            findings.append(issue("warning", "form_command_without_action", f"Form command has no action: {name}", path=path, detail={"command": name}))

    item_count = 0
    unresolved_items = []
    for element in root.iter():
        tag = local_name(element.tag)
        if tag in {"Button", "ButtonGroup", "Popup", "UsualGroup", "CommandBar", "Table", "TableColumn", "InputField", "CheckBoxField", "LabelDecoration", "PictureDecoration"}:
            item_count += 1
        command_name = child_text(element, "CommandName")
        if not command_name or command_name == "0":
            continue
        if command_name.startswith("Form.StandardCommand.") or command_name.startswith("StandardCommand."):
            continue
        command_key = command_name.removeprefix("Form.Command.")
        if command_key not in command_by_name and command_key not in command_by_id:
            unresolved_items.append({"item": element.attrib.get("name"), "command_name": command_name})
    for unresolved in unresolved_items[:20]:
        findings.append(issue("error", "unresolved_form_item_command", "Form item references an unknown command.", path=path, detail=unresolved))

    return findings, {
        "path": str(path),
        "command_count": len(commands),
        "item_count": item_count,
        "commands": [{"name": command.attrib.get("name"), "id": command.attrib.get("id"), "action": child_text(command, "Action")} for command in commands],
        "actions": sorted(set(actions)),
        "unresolved_item_command_count": len(unresolved_items),
    }


def record_path(workspace: Path, record: dict[str, Any]) -> Path:
    return workspace / "working" / Path(str(record.get("relative_path") or ""))


def expected_form_module_rel(form_rel: str) -> str:
    rel = form_rel.replace("\\", "/")
    if rel.endswith("/Ext/Form.xml"):
        return rel[: -len("/Ext/Form.xml")] + "/Ext/Form/Module.bsl"
    return ""


def validate_workspace(workspace: Path) -> dict[str, Any]:
    integrity = check_workspace(workspace)
    findings: list[dict[str, Any]] = []
    files: list[dict[str, Any]] = []
    if not integrity.get("passed"):
        findings.append(issue("error", "workspace_integrity_failed", "Workspace integrity failed; semantic validation skipped.", path=workspace))
        return build_result(workspace, findings, files, integrity=integrity)

    manifest = load_json(workspace / "manifest.json")
    records = manifest.get("files") or []
    bsl_by_rel: dict[str, dict[str, Any]] = {}
    form_files: list[tuple[dict[str, Any], dict[str, Any]]] = []

    for record in records:
        path = record_path(workspace, record)
        if record.get("kind") == "bsl_module":
            bsl_findings, summary = validate_bsl(path)
            findings.extend(bsl_findings)
            files.append({"relative_path": record.get("relative_path"), "kind": "bsl_module", "summary": summary})
            bsl_by_rel[str(record.get("relative_path") or "").replace("\\", "/")] = summary
        elif record.get("kind") == "form_xml":
            form_findings, summary = parse_form(path)
            findings.extend(form_findings)
            files.append({"relative_path": record.get("relative_path"), "kind": "form_xml", "summary": summary})
            form_files.append((record, summary))

    for record, form_summary in form_files:
        module_rel = expected_form_module_rel(str(record.get("relative_path") or ""))
        if not module_rel:
            continue
        module_summary = bsl_by_rel.get(module_rel)
        if not module_summary:
            findings.append(issue("warning", "missing_form_module_in_workspace", "Form module is not present in the patch workspace; command handlers were not checked.", detail={"form": record.get("relative_path"), "expected_module": module_rel}))
            continue
        routines = {name.casefold() for name in module_summary.get("routines") or []}
        for action in form_summary.get("actions") or []:
            if action and action.casefold() not in routines:
                findings.append(issue("error", "missing_form_command_handler", f"Form command action has no matching module routine: {action}", path=record_path(workspace, record), detail={"module": module_rel, "action": action}))

    return build_result(workspace, findings, files, integrity=integrity)


def build_result(workspace: Path, findings: list[dict[str, Any]], files: list[dict[str, Any]], *, integrity: dict[str, Any]) -> dict[str, Any]:
    errors = [row for row in findings if row.get("severity") == "error"]
    warnings = [row for row in findings if row.get("severity") == "warning"]
    return {
        "schema": "onec_patch_workspace_semantic_validation.v1",
        "workspace": str(workspace),
        "passed": not errors,
        "integrity": {
            "schema": integrity.get("schema"),
            "passed": integrity.get("passed"),
            "counts": integrity.get("counts"),
        },
        "findings": findings,
        "files": files,
        "counts": {
            "files": len(files),
            "errors": len(errors),
            "warnings": len(warnings),
        },
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate BSL/Form.xml semantics in a 1C patch workspace.")
    parser.add_argument("--workspace", type=Path, required=True)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    result = validate_workspace(args.workspace)
    output = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
    if args.output:
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(output, encoding="utf-8")
    print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "counts": result["counts"]}, ensure_ascii=False))
    return 0 if result["passed"] else 2


if __name__ == "__main__":
    raise SystemExit(main())
