#!/usr/bin/env python3
"""Return form structure context for a 1C object with base/effective/extension views."""

from __future__ import annotations

import argparse
import base64
import json
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))
from get_1c_object_code_context import extension_name_from_path  # noqa: E402
from resolve_1c_object import load_json, resolve_object  # noqa: E402


CONTAINER_TAGS = {"ChildItems", "Attributes", "Commands", "Events"}
STRUCTURE_TAGS = {
    "AutoCommandBar",
    "ButtonGroup",
    "Popup",
    "Button",
    "UsualGroup",
    "CommandBar",
    "InputField",
    "Table",
    "TableColumn",
    "Pages",
    "Page",
    "Decoration",
    "CheckBox",
    "LabelField",
}


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


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 and child.text is not None:
            return child.text
    return None


def ru_text(element: ET.Element, name: str) -> str | None:
    child = next((item for item in element if local_name(item.tag) == name), None)
    if child is None:
        return None
    for node in child.iter():
        if local_name(node.tag) == "content" and node.text:
            return node.text
    return None


def event_records(element: ET.Element) -> list[dict[str, Any]]:
    events = []
    for container in element:
        if local_name(container.tag) != "Events":
            continue
        for event in container:
            if local_name(event.tag) == "Event":
                events.append({"name": event.attrib.get("name"), "handler": event.text})
    return events


def direct_named_children(element: ET.Element, container_name: str, wanted_tags: set[str] | None = None) -> list[ET.Element]:
    result = []
    for container in element:
        if local_name(container.tag) != container_name:
            continue
        for child in container:
            if wanted_tags is None or local_name(child.tag) in wanted_tags:
                result.append(child)
    return result


def element_record(element: ET.Element, *, parent: str | None, depth: int) -> dict[str, Any] | None:
    tag = local_name(element.tag)
    if tag not in STRUCTURE_TAGS:
        return None
    name = element.attrib.get("name")
    record = {
        "name": name,
        "id": element.attrib.get("id"),
        "kind": tag,
        "parent": parent,
        "depth": depth,
        "title": ru_text(element, "Title"),
        "data_path": child_text(element, "DataPath"),
        "command_name": child_text(element, "CommandName"),
        "events": event_records(element),
    }
    return {key: value for key, value in record.items() if value not in (None, [], "")}


def walk_items(element: ET.Element, *, parent: str | None = None, depth: int = 0, limit: int = 500) -> list[dict[str, Any]]:
    records = []
    for child in element:
        tag = local_name(child.tag)
        if tag in CONTAINER_TAGS:
            records.extend(walk_items(child, parent=parent, depth=depth, limit=limit - len(records)))
            continue
        current_parent = parent
        record = element_record(child, parent=parent, depth=depth)
        if record:
            records.append(record)
            current_parent = record.get("name") or parent
        if len(records) >= limit:
            return records[:limit]
        records.extend(walk_items(child, parent=current_parent, depth=depth + 1, limit=limit - len(records)))
        if len(records) >= limit:
            return records[:limit]
    return records


def attribute_record(element: ET.Element) -> dict[str, Any]:
    columns = [attribute_column_record(item, parent_name=element.attrib.get("name")) for item in direct_named_children(element, "Columns", {"Column"})]
    record = {
        "name": element.attrib.get("name"),
        "id": element.attrib.get("id"),
        "title": ru_text(element, "Title"),
        "data_path": child_text(element, "DataPath"),
        "saved_data": child_text(element, "SavedData"),
        "view": child_text(element, "View"),
        "columns": columns,
    }
    value_type = child_text(element, "Type")
    if value_type:
        record["type"] = value_type
    return {key: value for key, value in record.items() if value not in (None, [], "")}


def attribute_column_record(element: ET.Element, *, parent_name: str | None) -> dict[str, Any]:
    name = element.attrib.get("name")
    record = {
        "name": name,
        "id": element.attrib.get("id"),
        "title": ru_text(element, "Title"),
        "data_path": ".".join(item for item in (parent_name, name) if item),
    }
    value_type = child_text(element, "Type")
    if value_type:
        record["type"] = value_type
    return {key: value for key, value in record.items() if value not in (None, [], "")}


def command_record(element: ET.Element) -> dict[str, Any]:
    record = {
        "name": element.attrib.get("name"),
        "id": element.attrib.get("id"),
        "title": ru_text(element, "Title"),
        "action": child_text(element, "Action"),
        "group": child_text(element, "Group"),
        "representation": child_text(element, "Representation"),
    }
    return {key: value for key, value in record.items() if value not in (None, [], "")}


def form_attribute_title_index(attributes: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
    result: dict[str, dict[str, Any]] = {}
    for attribute in attributes:
        name = attribute.get("name")
        if name:
            result[str(name)] = {"source": "form_attribute", "record": attribute}
        for column in attribute.get("columns") or []:
            data_path = column.get("data_path")
            if data_path:
                result[str(data_path)] = {"source": "form_attribute_column", "record": column, "parent": attribute}
    return result


def title_edit_targets(source: str, source_name: str | None) -> dict[str, Any]:
    default_target = {
        "form_item_title": "form_item",
        "form_command_title": "form_command",
        "form_attribute_title": "form_attribute",
        "form_attribute_column_title": "form_attribute_column",
        "fallback_name": "form_item",
        "unresolved": "form_item",
        "platform_standard_command": "form_item",
    }.get(source, "form_item")
    return {
        "default_change_target": default_target,
        "one_off_change_target": "form_item",
        "source_name": source_name,
    }


def resolve_item_title(
    item: dict[str, Any],
    *,
    commands_by_name: dict[str, dict[str, Any]],
    attributes_by_path: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    explicit_title = item.get("title")
    if explicit_title:
        return {
            "effective_title": explicit_title,
            "source": "form_item_title",
            "source_name": item.get("name"),
            "edit_targets": title_edit_targets("form_item_title", item.get("name")),
        }

    command_name = str(item.get("command_name") or "")
    if command_name.startswith("Form.Command."):
        name = command_name.removeprefix("Form.Command.")
        command = commands_by_name.get(name) or {}
        title = command.get("title") or name
        source = "form_command_title" if command.get("title") else "fallback_name"
        return {
            "effective_title": title,
            "source": source,
            "source_name": name,
            "edit_targets": title_edit_targets(source, name),
        }
    if command_name.startswith("Form.StandardCommand.") or command_name.startswith("StandardCommand."):
        return {
            "effective_title": command_name.rsplit(".", 1)[-1],
            "source": "platform_standard_command",
            "source_name": command_name,
            "edit_targets": title_edit_targets("platform_standard_command", item.get("name")),
        }

    data_path = item.get("data_path")
    if data_path:
        attribute_hit = attributes_by_path.get(str(data_path))
        if attribute_hit:
            record = attribute_hit["record"]
            title = record.get("title") or record.get("name") or str(data_path).split(".")[-1]
            source = f"{attribute_hit['source']}_title" if record.get("title") else "fallback_name"
            return {
                "effective_title": title,
                "source": source,
                "source_name": record.get("data_path") or record.get("name"),
                "edit_targets": title_edit_targets(source, record.get("data_path") or record.get("name")),
            }
        tail = str(data_path).split(".")[-1]
        return {
            "effective_title": tail,
            "source": "fallback_name",
            "source_name": data_path,
            "edit_targets": title_edit_targets("fallback_name", data_path),
        }

    name = item.get("name")
    return {
        "effective_title": name,
        "source": "fallback_name" if name else "unresolved",
        "source_name": name,
        "edit_targets": title_edit_targets("fallback_name" if name else "unresolved", name),
    }


def attach_item_title_resolution(items: list[dict[str, Any]], attributes: list[dict[str, Any]], commands: list[dict[str, Any]]) -> list[dict[str, Any]]:
    commands_by_name = {str(item.get("name")): item for item in commands if item.get("name")}
    attributes_by_path = form_attribute_title_index(attributes)
    resolved = []
    for item in items:
        copy = dict(item)
        resolution = resolve_item_title(copy, commands_by_name=commands_by_name, attributes_by_path=attributes_by_path)
        copy["effective_title"] = resolution["effective_title"]
        copy["title_resolution"] = {key: value for key, value in resolution.items() if key != "effective_title"}
        resolved.append(copy)
    return resolved


def parse_form_xml(path: Path, *, source: str, extension_name: str | None, max_items: int) -> dict[str, Any]:
    root = ET.parse(path).getroot()
    attributes = [attribute_record(item) for item in direct_named_children(root, "Attributes", {"Attribute"})]
    commands = [command_record(item) for item in direct_named_children(root, "Commands", {"Command"})]
    items = attach_item_title_resolution(walk_items(root, limit=max_items), attributes, commands)
    return {
        "origin": {"layer": source, **({"extension": extension_name} if extension_name else {})},
        "form_xml_path": str(path),
        "size": path.stat().st_size,
        "events": event_records(root),
        "items": items,
        "attributes": attributes,
        "commands": commands,
        "counts": {
            "items": len(items),
            "attributes": len(attributes),
            "commands": len(commands),
            "events": len(event_records(root)),
            "items_truncated": len(items) >= max_items,
        },
    }


def form_meta_record(path: Path) -> dict[str, Any]:
    root = ET.parse(path).getroot()
    form = next((item for item in root.iter() if local_name(item.tag) == "Form"), root)
    props = next((item for item in form if local_name(item.tag) == "Properties"), form)
    name = child_text(props, "Name") or path.stem
    return {
        "name": name,
        "synonym": ru_text(props, "Synonym"),
        "uuid": form.attrib.get("uuid"),
        "meta_xml_path": str(path),
        "form_xml_path": str(path.with_suffix("") / "Ext" / "Form.xml"),
        "module_path": str(path.with_suffix("") / "Ext" / "Form" / "Module.bsl"),
    }


def collect_forms(owner_path: str | None, *, source: str, extension_name: str | None = None) -> list[dict[str, Any]]:
    if not owner_path:
        return []
    owner_dir = Path(owner_path).with_suffix("")
    forms_dir = owner_dir / "Forms"
    if not forms_dir.is_dir():
        return []
    result = []
    for path in sorted(forms_dir.glob("*.xml")):
        record = form_meta_record(path)
        record["origin"] = {"layer": source, **({"extension": extension_name} if extension_name else {})}
        result.append(record)
    return result


def norm(value: str | None) -> str:
    return "".join(ch for ch in str(value or "").casefold() if not ch.isspace() and ch not in "._-")


def select_form(forms: list[dict[str, Any]], wanted: str | None) -> list[dict[str, Any]]:
    if not wanted:
        return forms
    wanted_norm = norm(wanted)
    return [
        form
        for form in forms
        if norm(form.get("name")) == wanted_norm
        or norm(form.get("synonym")) == wanted_norm
        or wanted_norm in norm(form.get("name"))
    ]


def merge_effective(base_forms: list[dict[str, Any]], extension_forms: list[dict[str, Any]]) -> list[dict[str, Any]]:
    result = []
    by_name: dict[str, dict[str, Any]] = {}
    for form in base_forms:
        copy = dict(form)
        copy["effective_action"] = "base"
        by_name[norm(copy.get("name"))] = copy
        result.append(copy)
    for form in extension_forms:
        copy = dict(form)
        key = norm(copy.get("name"))
        if key in by_name:
            copy["effective_action"] = "extended"
            by_name[key].setdefault("extension_overlays", []).append(copy)
        else:
            copy["effective_action"] = "added"
            result.append(copy)
    return result


def attach_structure(forms: list[dict[str, Any]], *, max_items: int) -> list[dict[str, Any]]:
    result = []
    for form in forms:
        copy = dict(form)
        form_xml = Path(str(copy.get("form_xml_path") or ""))
        if form_xml.is_file():
            origin = copy.get("origin") or {}
            copy["structure"] = parse_form_xml(
                form_xml,
                source=origin.get("layer") or "base",
                extension_name=origin.get("extension"),
                max_items=max_items,
            )
        overlays = []
        for overlay in copy.get("extension_overlays") or []:
            overlay_copy = dict(overlay)
            overlay_xml = Path(str(overlay_copy.get("form_xml_path") or ""))
            if overlay_xml.is_file():
                origin = overlay_copy.get("origin") or {}
                overlay_copy["structure"] = parse_form_xml(
                    overlay_xml,
                    source=origin.get("layer") or "extension",
                    extension_name=origin.get("extension"),
                    max_items=max_items,
                )
            overlays.append(overlay_copy)
        if overlays:
            copy["extension_overlays"] = overlays
        result.append(copy)
    return result


def build_context(
    index: dict[str, Any],
    *,
    kind: str,
    name: str,
    form: str | None,
    view: str,
    extension: str | None,
    max_items: int,
) -> dict[str, Any]:
    resolution = resolve_object(index, kind=kind, name=name, limit=100)
    canonical = resolution.get("canonical")
    if not canonical:
        raise SystemExit(f"Object not found: {kind}.{name}")

    base_forms = collect_forms(canonical.get("path"), source="base")
    extension_forms = []
    for overlay in resolution.get("extension_overlays") or []:
        overlay_extension = extension_name_from_path(overlay.get("path")) or overlay.get("extension_name")
        extension_forms.extend(collect_forms(overlay.get("path"), source="extension", extension_name=overlay_extension))

    if view == "base":
        forms = [{**item, "effective_action": "base"} for item in base_forms]
    elif view == "effective":
        forms = merge_effective(base_forms, extension_forms)
    elif view == "extension":
        if not extension:
            raise SystemExit("Use --extension with --view extension.")
        forms = [{**item, "effective_action": "extended_or_added"} for item in extension_forms if (item.get("origin") or {}).get("extension") == extension]
    else:
        raise SystemExit(f"Unsupported view: {view}")

    selected = attach_structure(select_form(forms, form), max_items=max_items)
    return {
        "schema": "onec_form_context.v1",
        "view": view,
        "extension": extension if view == "extension" else None,
        "object": {
            "kind": canonical.get("kind"),
            "name": canonical.get("name"),
            "synonym": canonical.get("synonym"),
            "uuid": canonical.get("guid"),
        },
        "query": {"form": form, "max_items": max_items},
        "forms": selected,
        "counts": {
            "forms": len(selected),
            "base_forms": len(base_forms),
            "extension_forms": len(extension_forms),
        },
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Get 1C form context.")
    parser.add_argument("--index", type=Path, required=True)
    parser.add_argument("--kind")
    parser.add_argument("--name")
    parser.add_argument("--kind-b64")
    parser.add_argument("--name-b64")
    parser.add_argument("--form")
    parser.add_argument("--form-b64")
    parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective")
    parser.add_argument("--extension")
    parser.add_argument("--max-items", type=int, default=500)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    kind = decode_arg(args.kind, args.kind_b64)
    name = decode_arg(args.name, args.name_b64)
    form = decode_arg(args.form, args.form_b64)
    if not kind or not name:
        raise SystemExit("Use --kind/--name or --kind-b64/--name-b64.")
    result = build_context(
        load_json(args.index),
        kind=kind,
        name=name,
        form=form,
        view=args.view,
        extension=args.extension,
        max_items=args.max_items,
    )
    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), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False))
    else:
        print(text)
    return 0


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