#!/usr/bin/env python3
"""Resolve a BSL expression inside an explicit 1C code context.

The resolver is intentionally conservative: a dotted BSL expression is not a
metadata path unless the expression itself is a full 1C path or the current
module context proves the first segment, for example an object-module
attribute. Local variables and parameters win over metadata-name guesses.
"""

from __future__ import annotations

import argparse
import base64
import json
import re
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))
from resolve_1c_fact import load_json, resolve_from_snapshot, split_fact_path  # noqa: E402
from resolve_1c_object import canonical_kind, normalize  # noqa: E402


KIND_ALIASES = {
    "Справочник": "Catalog",
    "Документ": "Document",
    "Перечисление": "Enum",
    "РегистрСведений": "InformationRegister",
    "РегистрНакопления": "AccumulationRegister",
    "РегистрБухгалтерии": "AccountingRegister",
    "Отчет": "Report",
    "Обработка": "DataProcessor",
    "ОбщийМодуль": "CommonModule",
}

STANDARD_OBJECT_MEMBERS = {
    "Catalog": {"Наименование", "Код", "ПометкаУдаления", "Ссылка"},
    "Document": {"Дата", "Номер", "ПометкаУдаления", "Ссылка", "Проведен"},
}


def decode_arg(value: str | None, encoded: str | None) -> str | None:
    if encoded:
        return base64.b64decode(encoded).decode("utf-8")
    return value


def path_join(*parts: Any) -> str:
    return ".".join(str(part).strip() for part in parts if str(part or "").strip())


def split_expression(expression: str) -> list[str]:
    return [part.strip() for part in str(expression or "").split(".") if part.strip()]


def same_name(left: Any, right: Any) -> bool:
    return normalize(str(left or "")) == normalize(str(right or ""))


def find_object(snapshot: dict[str, Any], *, kind: str | None, name: str | None) -> dict[str, Any] | None:
    if not name:
        return None
    wanted_kind = canonical_kind(kind) if kind else None
    for item in snapshot.get("objects") or []:
        actual_kind = canonical_kind(str(item.get("kind") or ""))
        if wanted_kind and actual_kind != wanted_kind:
            continue
        if same_name(item.get("name"), name) or same_name(item.get("full_name"), name) or same_name(item.get("synonym"), name):
            return item
    return None


def find_module(modules: dict[str, Any], *, module_id: str | None, object_name: str | None, object_kind: str | None) -> dict[str, Any] | None:
    items = modules.get("modules") or []
    if module_id:
        return next((item for item in items if str(item.get("module_id") or "") == module_id), None)
    if not object_name:
        return None
    wanted_kind = canonical_kind(object_kind) if object_kind else None
    for item in items:
        actual_kind = canonical_kind(str(item.get("object_kind") or ""))
        if wanted_kind and actual_kind != wanted_kind:
            continue
        if same_name(item.get("object_name"), object_name):
            return item
    return None


def routine_params(module: dict[str, Any], routine_name: str | None) -> set[str]:
    if not routine_name:
        routines = list(module.get("procedures") or []) + list(module.get("functions") or [])
    else:
        routines = [
            item
            for item in list(module.get("procedures") or []) + list(module.get("functions") or [])
            if same_name(item.get("name"), routine_name)
        ]
    result: set[str] = set()
    for routine in routines:
        for param in routine.get("params") or []:
            text = str(param or "").strip()
            if text:
                result.add(text)
    return result


def declared_symbols(text: str) -> set[str]:
    symbols: set[str] = set()
    for match in re.finditer(r"(?im)^\s*Перем\s+([^;\n]+)", text or ""):
        for part in re.split(r",", match.group(1)):
            name = re.sub(r"\s+Экспорт\b", "", part, flags=re.IGNORECASE).strip()
            if name:
                symbols.add(name)
    for match in re.finditer(r"(?im)^\s*(?:Для\s+Каждого|Для каждого)\s+([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s+Из\b", text or ""):
        symbols.add(match.group(1))
    for match in re.finditer(r"(?m)^\s*([A-Za-zА-Яа-я_][\wА-Яа-я]*)\s*=", text or ""):
        symbols.add(match.group(1))
    return symbols


def object_member_match(obj: dict[str, Any], name: str) -> tuple[str, dict[str, Any]] | None:
    for area, items in (
        ("attribute", obj.get("attributes") or []),
        ("form", obj.get("forms") or []),
        ("command", obj.get("commands") or []),
        ("module", obj.get("modules") or []),
    ):
        match = next((item for item in items if same_name(item.get("name"), name) or same_name(item.get("synonym"), name)), None)
        if match:
            return area, match
    canonical = canonical_kind(str(obj.get("kind") or ""))
    standard = next((item for item in STANDARD_OBJECT_MEMBERS.get(canonical or "", set()) if same_name(item, name)), None)
    if standard:
        return "standard_attribute", {"name": standard, "standard": True}
    return None


def full_metadata_path_resolution(snapshot: dict[str, Any], expression: str) -> dict[str, Any] | None:
    parts = split_expression(expression)
    if len(parts) < 2:
        return None
    if canonical_kind(parts[0]) is None and parts[0] not in KIND_ALIASES:
        return None
    kind, object_name, section, member = split_fact_path(expression)
    result = resolve_from_snapshot(
        snapshot,
        snapshot_path=Path("<memory>"),
        kind=kind,
        object_name=object_name,
        member=member,
        area="any",
        table_section=section,
        view="effective",
        extension=None,
    )
    if result.get("exists"):
        return result
    return None


def resolve_symbol(
    metadata: dict[str, Any],
    modules: dict[str, Any],
    *,
    expression: str,
    module_id: str | None = None,
    object_kind: str | None = None,
    object_name: str | None = None,
    routine_name: str | None = None,
) -> dict[str, Any]:
    parts = split_expression(expression)
    query = {
        "expression": expression,
        "module_id": module_id,
        "object_kind": canonical_kind(object_kind) if object_kind else None,
        "object_name": object_name,
        "routine_name": routine_name,
    }
    result: dict[str, Any] = {
        "schema": "onec_bsl_symbol_resolution.v1",
        "query": {key: value for key, value in query.items() if value not in (None, "")},
        "status": "unresolved",
        "path_kind": "code_symbol",
        "segments": parts,
    }
    if not parts:
        result.update({"status": "error", "reason": "empty_expression"})
        return result

    full_path = full_metadata_path_resolution(metadata, expression)
    if full_path:
        result.update(
            {
                "status": "resolved",
                "resolution_kind": "metadata_path",
                "path_kind": (full_path.get("match") or {}).get("path_kind") or "metadata_path",
                "canonical_path": (full_path.get("match") or {}).get("canonical_path"),
                "area": full_path.get("area"),
                "match": full_path.get("match"),
                "safe_as_metadata_path": True,
            }
        )
        return result

    module = find_module(modules, module_id=module_id, object_name=object_name, object_kind=object_kind)
    if module:
        result["module"] = {
            key: module.get(key)
            for key in ("module_id", "object_kind", "object_name", "module_type")
            if module.get(key) not in (None, "")
        }
        params = routine_params(module, routine_name)
        first = parts[0]
        param = next((item for item in params if same_name(item, first)), None)
        if param:
            result.update(
                {
                    "status": "resolved",
                    "resolution_kind": "parameter",
                    "symbol": param,
                    "context_path": path_join(param, *parts[1:]) if len(parts) > 1 else param,
                    "safe_as_metadata_path": False,
                }
            )
            return result
        local = next((item for item in declared_symbols(str(module.get("content") or "")) if same_name(item, first)), None)
        if local:
            result.update(
                {
                    "status": "resolved",
                    "resolution_kind": "local_variable",
                    "symbol": local,
                    "context_path": path_join(local, *parts[1:]) if len(parts) > 1 else local,
                    "safe_as_metadata_path": False,
                }
            )
            return result

    obj = find_object(metadata, kind=object_kind, name=object_name)
    if obj:
        result["object"] = {
            "kind": obj.get("kind"),
            "name": obj.get("name"),
            "canonical_path": obj.get("full_name") or path_join(object_kind, object_name),
        }
        matched = object_member_match(obj, parts[0])
        if matched:
            area, member = matched
            canonical_path = path_join(result["object"]["canonical_path"], member.get("name"), *parts[1:])
            result.update(
                {
                    "status": "resolved",
                    "resolution_kind": "context_metadata_member",
                    "path_kind": "metadata_member",
                    "area": area,
                    "canonical_path": canonical_path,
                    "context_path": path_join(member.get("name"), *parts[1:]),
                    "match": {"area": area, "name": member.get("name"), "synonym": member.get("synonym")},
                }
            )
            return result

    candidates = []
    for item in metadata.get("objects") or []:
        if same_name(item.get("name"), parts[0]) or same_name(item.get("synonym"), parts[0]):
            candidates.append(
                {
                    "canonical_path": item.get("full_name"),
                    "kind": item.get("kind"),
                    "name": item.get("name"),
                    "reason": "short_object_name_requires_kind",
                }
            )
    result.update(
        {
            "status": "unresolved",
            "reason": "not_a_confirmed_metadata_path_or_local_symbol",
            "safe_as_metadata_path": False,
            "candidates": candidates[:10],
        }
    )
    return result


def main() -> int:
    parser = argparse.ArgumentParser(description="Resolve a BSL expression inside a concrete 1C module/form context.")
    parser.add_argument("--metadata", type=Path, required=True, help="Metadata snapshot for current configuration.")
    parser.add_argument("--modules", type=Path, required=True, help="BSL module snapshot for current configuration.")
    parser.add_argument("--expression")
    parser.add_argument("--expression-b64")
    parser.add_argument("--module-id")
    parser.add_argument("--object-kind")
    parser.add_argument("--object-name")
    parser.add_argument("--routine-name")
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    expression = decode_arg(args.expression, args.expression_b64)
    if not expression:
        raise SystemExit("Use --expression or --expression-b64.")
    result = resolve_symbol(
        load_json(args.metadata),
        load_json(args.modules),
        expression=expression,
        module_id=args.module_id,
        object_kind=args.object_kind,
        object_name=args.object_name,
        routine_name=args.routine_name,
    )
    text = 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(text, encoding="utf-8")
        print(json.dumps({"output": str(args.output), "status": result.get("status")}, ensure_ascii=False))
    else:
        print(text)
    return 0


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