#!/usr/bin/env python3
"""Compare decoded SQL form semantics with exported 1C Form.xml semantics."""

from __future__ import annotations

import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "plugins" / "1c"))

from parser.form_payload import enrich_form_common_semantic, form_common_semantic  # noqa: E402
from parser.form_xml import decode_form_xml  # noqa: E402


PROPERTY_NAME_ALIASES = {
    "Action": "Действие",
    "MainAttribute": "ОсновнойРеквизит",
    "AutoEditMode": "АвтоРежимРедактирования",
    "HeightInTableRows": "ВысотаВСтрокахТаблицы",
    "RowSelectionMode": "РежимВыделенияСтроки",
    "HorizontalLinesBWA": "ГоризонтальныеЛинии",
    "VerticalLinesBWA": "ВертикальныеЛинии",
    "UseAlternationRowColorBWA": "ЧередованиеЦветовСтрок",
    "AutoInsertNewRow": "АвтоВставкаНовойСтроки",
    "EnableStartDrag": "РазрешитьНачалоПеретаскивания",
    "EnableDrag": "РазрешитьПеретаскивание",
    "FileDragMode": "РежимПеретаскиванияФайлов",
    "CommandBarLocation": "ПоложениеКоманднойПанели",
    "DefaultItem": "АктивизироватьПоУмолчанию",
    "Autofill": "Автозаполнение",
    "AutoMaxWidth": "АвтоМаксимальнаяШирина",
    "MaxWidth": "МаксимальнаяШирина",
    "MultiLine": "МногострочныйРежим",
    "AutoCommandBar": "АвтоКоманднаяПанель",
    "SearchStringAddition": "ДополнениеСтрокиПоиска",
    "ViewStatusAddition": "ДополнениеСостоянияПросмотра",
    "SearchControlAddition": "ДополнениеУправленияПоиском",
    "Width": "Ширина",
    "Height": "Высота",
    "HorizontalStretch": "РастягиватьПоГоризонтали",
    "VerticalStretch": "РастягиватьПоВертикали",
    "TextColor": "ЦветТекста",
    "BackColor": "ЦветФона",
    "HorizontalAlign": "ГоризонтальноеПоложениеВГруппе",
    "AutoMaxHeight": "АвтоМаксимальнаяВысота",
    "MaxHeight": "МаксимальнаяВысота",
    "AutoMarkIncomplete": "АвтоОтметкаНезаполненного",
    "ToolTipRepresentation": "ОтображениеПодсказки",
    "SpinButton": "КнопкаРегулирования",
    "Representation": "Отображение",
    "DefaultButton": "КнопкаПоУмолчанию",
    "OpenButton": "КнопкаОткрытия",
    "CreateButton": "КнопкаСоздания",
    "ChoiceHistoryOnInput": "ИсторияВыбораПриВводе",
    "BorderColor": "ЦветРамки",
    "ChangeRowSet": "ИзменятьСоставСтрок",
    "ShowInHeader": "ОтображатьВШапке",
    "AutoCellHeight": "АвтоВысотаЯчейки",
    "SearchStringLocation": "ПоложениеСтрокиПоиска",
    "ViewStatusLocation": "ПоложениеСостоянияПросмотра",
    "SearchControlLocation": "ПоложениеУправленияПоиском",
    "GroupHorizontalAlign": "ГоризонтальноеПоложениеВГруппе",
    "GroupVerticalAlign": "ВертикальноеПоложениеВГруппе",
    "ShapeRepresentation": "ОтображениеФигуры",
}


def read_json(path: Path) -> dict[str, Any]:
    data = json.loads(path.read_text(encoding="utf-8-sig"))
    if not isinstance(data, dict):
        raise ValueError(f"{path} must contain a JSON object")
    return data


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def scalar_text(value: Any) -> str:
    if value is True:
        return "true"
    if value is False:
        return "false"
    if value is None:
        return ""
    return str(value).strip()


def comparable(value: Any) -> str:
    text = scalar_text(value)
    lowered = text.casefold()
    if lowered in {"истина", "true", "1"}:
        return "true"
    if lowered in {"ложь", "false", "0"}:
        return "false"
    if lowered in {"таблица формы", "таблица", "динамический список"}:
        return "table"
    if lowered in {"кнопка", "кнопка командной панели", "commandbarbutton"}:
        return "button"
    if lowered in {"декорация надписи", "labeldecoration"}:
        return "label_decoration"
    if lowered in {"декорация картинки", "picturedecoration"}:
        return "picture_decoration"
    if lowered in {"поле переключателя", "radiobuttonfield"}:
        return "radio_button_field"
    if lowered in {"в дополнительном подменю", "inadditionalsubmenu"}:
        return "in_additional_submenu"
    if lowered in {"в командной панели", "incommandbar"}:
        return "in_command_bar"
    if lowered in {"поле", "checkboxfield", "поле флажка"}:
        return "field"
    if lowered in {"колонка динамического списка", "column", "колонка реквизита"}:
        return "column"
    if lowered in {"attribute", "реквизит формы"}:
        return "attribute"
    if lowered in {"event", "событие"}:
        return "event"
    if lowered in {"command", "команда формы"}:
        return "command"
    if lowered in {"searchstringaddition", "дополнение строки поиска"}:
        return "search_string_addition"
    if lowered in {"viewstatusaddition", "дополнение состояния просмотра"}:
        return "view_status_addition"
    if lowered in {"searchcontroladdition", "дополнение управления поиском"}:
        return "search_control_addition"
    return lowered


def canonical_property_name(value: Any) -> str:
    text = str(value or "").strip()
    return PROPERTY_NAME_ALIASES.get(text, text)


def semantic_properties(row: dict[str, Any]) -> dict[str, dict[str, Any]]:
    result: dict[str, dict[str, Any]] = {}
    for direct_name, key in (
        ("Имя", "name"),
        ("Идентификатор", "id"),
        ("Заголовок", "title"),
        ("ПутьКДанным", "path_to_data"),
        ("Вид", "type_name"),
        ("Обработчик", "handler"),
    ):
        if key in row and row.get(key) not in {None, ""}:
            result[direct_name] = {"name": direct_name, "value": row.get(key), "source": "decoded_direct"}
    semantic = row.get("semantic") if isinstance(row.get("semantic"), dict) else {}
    for group, props in (semantic.get("groups") or {}).items():
        for prop in props or []:
            if not isinstance(prop, dict):
                continue
            name = canonical_property_name(prop.get("name"))
            if name and name not in result:
                result[name] = {**prop, "group": group}
    return result


def iter_sql_rows(form: dict[str, Any]) -> list[dict[str, Any]]:
    profile = form.get("profile") if isinstance(form.get("profile"), dict) else {}
    rows = []
    for section in ("items", "attributes", "parameters", "commands", "tables", "command_bars", "events"):
        for row in profile.get(section) or []:
            if isinstance(row, dict):
                rows.append({**row, "_profile_section": section})
    for item in profile.get("items") or []:
        if not isinstance(item, dict):
            continue
        for event in item.get("events") or []:
            if not isinstance(event, dict):
                continue
            event_name = event.get("event_name") or event.get("name")
            rows.append(
                {
                    **event,
                    "name": event_name,
                    "type_name": "Event",
                    "_profile_section": "events",
                    "_event_owner": item.get("name"),
                }
            )
    return rows


def row_key(row: dict[str, Any]) -> tuple[str, str]:
    if row.get("id") not in {None, ""}:
        return ("id", str(row.get("id")))
    return ("name", str(row.get("name") or ""))


def xml_match_keys(xml_item: dict[str, Any]) -> list[tuple[str, str]]:
    name = str(xml_item.get("name") or "")
    item_id = str(xml_item.get("id") or "")
    kind = str(xml_item.get("kind") or "")
    keys: list[tuple[str, str]] = []
    if kind in {"ExtendedTooltip", "Event", "Column"}:
        if name:
            keys.append(("name", name.casefold()))
        return keys
    if kind == "Button":
        if name:
            keys.append(("name", name.casefold()))
        if item_id:
            keys.append(("id", item_id))
        return keys
    if item_id:
        keys.append(("id", item_id))
    if name:
        keys.append(("name", name.casefold()))
    return keys


def index_rows(rows: list[dict[str, Any]], *, sections: set[str] | None = None) -> dict[tuple[str, str], dict[str, Any]]:
    by_key: dict[tuple[str, str], dict[str, Any]] = {}
    by_name: dict[str, dict[str, Any]] = {}
    for row in rows:
        if sections is not None and str(row.get("_profile_section") or "") not in sections:
            continue
        key = row_key(row)
        if key[1]:
            by_key.setdefault(key, row)
        name = str(row.get("name") or "")
        if name:
            by_name.setdefault(name.casefold(), row)
    for name, row in by_name.items():
        by_key.setdefault(("name", name), row)
    return by_key


def xml_sql_sections(xml_item: dict[str, Any]) -> set[str]:
    kind = str(xml_item.get("kind") or "")
    if kind == "Attribute":
        return {"attributes"}
    if kind == "Parameter":
        return {"parameters"}
    if kind == "Column":
        return {"items", "tables"}
    if kind == "Command":
        return {"commands"}
    if kind == "Button":
        return {"items", "commands"}
    if kind in {"CommandBar", "AutoCommandBar"}:
        return {"items", "command_bars"}
    if kind == "Event":
        return {"events"}
    return {"items", "commands", "command_bars"}


def xml_profile_from_context_form(form: dict[str, Any], *, max_items: int) -> dict[str, Any] | None:
    path_text = str(form.get("form_xml_path") or "")
    if not path_text:
        structure = form.get("structure") if isinstance(form.get("structure"), dict) else {}
        path_text = str(structure.get("form_xml_path") or "")
    if not path_text:
        return None
    path = Path(path_text)
    if not path.is_file():
        return {"status": "missing_xml_file", "source": {"path": path_text}, "items": []}
    profile = decode_form_xml(path, max_items=max_items)
    profile["form"]["name"] = form.get("name")
    profile["form"]["guid"] = form.get("uuid")
    return profile


def matching_sql_form(xml_form: dict[str, Any], sql_forms: list[dict[str, Any]]) -> dict[str, Any] | None:
    guid = str((xml_form.get("form") or {}).get("guid") or "").casefold()
    name = str((xml_form.get("form") or {}).get("name") or "").casefold()
    for form in sql_forms:
        if guid and str(form.get("guid") or "").casefold() == guid:
            return form
    for form in sql_forms:
        if name and str(form.get("name") or "").casefold() == name:
            return form
    return None


def compare_form(sql_form: dict[str, Any] | None, xml_profile: dict[str, Any], *, sample_limit: int) -> dict[str, Any]:
    xml_items = [item for item in xml_profile.get("items") or [] if isinstance(item, dict)]
    sql_rows = iter_sql_rows(sql_form or {})
    sql_any_index = index_rows(sql_rows)
    sql_indexes_by_sections: dict[tuple[str, ...], dict[tuple[str, str], dict[str, Any]]] = {}
    counts = Counter()
    property_route_counts: Counter[tuple[str, str, str, str, str, str]] = Counter()
    samples: dict[str, list[dict[str, Any]]] = {
        "matched": [],
        "missing_sql_item": [],
        "xml_only_property": [],
        "mismatch": [],
        "command_name_match": [],
        "command_name_mismatch": [],
        "matched_form_property": [],
        "xml_only_form_property": [],
        "form_mismatch": [],
    }

    sql_profile = (sql_form or {}).get("profile") if isinstance((sql_form or {}).get("profile"), dict) else {}
    sql_form_semantic = sql_profile.get("form_semantic") if isinstance(sql_profile.get("form_semantic"), dict) else None
    if sql_form_semantic is None:
        sql_form_semantic = form_common_semantic(
            [item for item in sql_profile.get("form_parameters") or [] if isinstance(item, dict)],
            include_diagnostics=True,
        )
        enrich_form_common_semantic(
            sql_form_semantic,
            [item for item in sql_profile.get("items") or [] if isinstance(item, dict)],
        )
    xml_form = xml_profile.get("form") if isinstance(xml_profile.get("form"), dict) else {}
    xml_form_semantic = xml_form.get("semantic") if isinstance(xml_form.get("semantic"), dict) else {}
    sql_root_props = semantic_properties({"type_name": "Форма", "semantic": sql_form_semantic})
    xml_root_props = semantic_properties({"type_name": "Форма", "semantic": xml_form_semantic})
    form_property_routes = []
    for name, xml_prop in xml_root_props.items():
        sql_prop = sql_root_props.get(name)
        if sql_prop is None:
            counts["xml_only_form_properties"] += 1
            if len(samples["xml_only_form_property"]) < sample_limit:
                samples["xml_only_form_property"].append(
                    {"property": name, "xml_name": xml_prop.get("xml_name"), "xml_value": xml_prop.get("value")}
                )
            continue
        if comparable(sql_prop.get("value")) != comparable(xml_prop.get("value")):
            counts["form_mismatches"] += 1
            if len(samples["form_mismatch"]) < sample_limit:
                samples["form_mismatch"].append(
                    {
                        "property": name,
                        "xml_name": xml_prop.get("xml_name"),
                        "sql_value": sql_prop.get("value"),
                        "xml_value": xml_prop.get("value"),
                        "sql_source": sql_prop.get("source"),
                    }
                )
            continue
        counts["matched_form_properties"] += 1
        if len(samples["matched_form_property"]) < sample_limit:
            samples["matched_form_property"].append(
                {"property": name, "xml_name": xml_prop.get("xml_name"), "value": xml_prop.get("value")}
            )
        parameter_indices = sql_prop.get("parameter_indices")
        if isinstance(parameter_indices, list) and parameter_indices:
            form_property_routes.append(
                {
                    "xml_name": xml_prop.get("xml_name") or name,
                    "property": name,
                    "parameter_indices": parameter_indices,
                    "sql_source": sql_prop.get("source"),
                    "write_shape": sql_prop.get("write_shape"),
                }
            )

    for xml_item in xml_items:
        wanted_sections = xml_sql_sections(xml_item)
        section_key = tuple(sorted(wanted_sections))
        sql_item_index = sql_indexes_by_sections.setdefault(section_key, index_rows(sql_rows, sections=wanted_sections))
        keys = xml_match_keys(xml_item)
        sql_row = None
        if xml_item.get("kind") == "Column" and xml_item.get("additional_columns_table"):
            logical_path = f"{xml_item.get('additional_columns_table')}.{xml_item.get('name')}"
            sql_row = next((row for row in sql_rows if comparable(row.get("path_to_data")) == comparable(logical_path)), None)
            if sql_row is not None:
                counts["logical_column_sql_match"] += 1
                if len(samples.setdefault("logical_column_sql_match", [])) < sample_limit:
                    samples["logical_column_sql_match"].append(
                        {
                            "name": xml_item.get("name"),
                            "id": xml_item.get("id"),
                            "logical_path": logical_path,
                            "sql_element": sql_row.get("name"),
                            "sql_path": sql_row.get("path"),
                        }
                    )
        if xml_item.get("kind") == "Event" and xml_item.get("owner"):
            sql_row = next(
                (
                    row
                    for row in sql_rows
                    if str(row.get("_profile_section") or "") == "events"
                    and str(row.get("_event_owner") or "").casefold() == str(xml_item.get("owner") or "").casefold()
                    and str(row.get("name") or "").casefold() == str(xml_item.get("name") or "").casefold()
                ),
                None,
            )
        if sql_row is None and not (xml_item.get("kind") == "Event" and xml_item.get("owner")):
            sql_row = next((sql_item_index[key] for key in keys if key in sql_item_index), None)
        if sql_row is not None and xml_item.get("kind") == "Column" and xml_item.get("additional_columns_table"):
            counts["matched_items"] += 1
            if len(samples["matched"]) < sample_limit:
                samples["matched"].append(
                    {
                        "name": xml_item.get("name"),
                        "id": xml_item.get("id"),
                        "sql_path": sql_row.get("path"),
                        "sql_type": sql_row.get("type_name"),
                        "xml_kind": xml_item.get("kind_ru"),
                        "match_by": "additional_columns_data_path",
                    }
                )
            continue
        if sql_row is None:
            fallback = next((sql_any_index[key] for key in keys if key in sql_any_index), None)
            if fallback is not None:
                counts["non_item_sql_match"] += 1
                if len(samples.setdefault("non_item_sql_match", [])) < sample_limit:
                    samples["non_item_sql_match"].append({"name": xml_item.get("name"), "id": xml_item.get("id"), "xml_kind": xml_item.get("kind"), "sql_section": fallback.get("_profile_section"), "sql_type": fallback.get("type_name")})
        if sql_row is None:
            counts["missing_sql_item"] += 1
            if len(samples["missing_sql_item"]) < sample_limit:
                samples["missing_sql_item"].append({"name": xml_item.get("name"), "id": xml_item.get("id"), "kind": xml_item.get("kind")})
            continue
        counts["matched_items"] += 1
        if len(samples["matched"]) < sample_limit:
            samples["matched"].append({"name": xml_item.get("name"), "id": xml_item.get("id"), "sql_path": sql_row.get("path"), "sql_type": sql_row.get("type_name"), "xml_kind": xml_item.get("kind_ru")})
        sql_props = semantic_properties(sql_row)
        xml_props = semantic_properties(xml_item)
        sql_command = sql_props.get("ИмяКоманды")
        xml_command = xml_props.get("ИмяКоманды")
        if sql_command is not None and xml_command is not None:
            sql_command_value = sql_command.get("value")
            xml_command_value = xml_command.get("value")
            command_row = {
                "item": xml_item.get("name"),
                "id": xml_item.get("id"),
                "sql_value": sql_command_value,
                "xml_value": xml_command_value,
                "sql_path": sql_row.get("path"),
                "sql_source": sql_command.get("source"),
            }
            if comparable(sql_command_value) == comparable(xml_command_value):
                counts["command_name_matches"] += 1
                if len(samples["command_name_match"]) < sample_limit:
                    samples["command_name_match"].append(command_row)
            else:
                counts["command_name_mismatches"] += 1
                if len(samples["command_name_mismatch"]) < sample_limit:
                    samples["command_name_mismatch"].append(command_row)
        for name, xml_prop in xml_props.items():
            if name == "Идентификатор" and xml_item.get("kind") == "Button" and sql_row.get("_profile_section") == "commands":
                continue
            xml_value = xml_prop.get("value")
            sql_prop = sql_props.get(name)
            if sql_prop is None:
                counts["xml_only_properties"] += 1
                if len(samples["xml_only_property"]) < sample_limit:
                    samples["xml_only_property"].append({"item": xml_item.get("name"), "id": xml_item.get("id"), "property": name, "xml_value": xml_value, "xml_name": xml_prop.get("xml_name")})
                continue
            sql_value = sql_prop.get("value")
            if comparable(sql_value) != comparable(xml_value):
                counts["mismatches"] += 1
                if len(samples["mismatch"]) < sample_limit:
                    samples["mismatch"].append({"item": xml_item.get("name"), "id": xml_item.get("id"), "property": name, "sql_value": sql_value, "xml_value": xml_value, "sql_source": sql_prop.get("source"), "xml_name": xml_prop.get("xml_name")})
            else:
                counts["matched_properties"] += 1
                parameter_index = sql_prop.get("parameter_index")
                if parameter_index is not None:
                    property_route_counts[
                        (
                            str(xml_item.get("kind") or ""),
                            str(xml_prop.get("xml_name") or name),
                            name,
                            str(sql_row.get("marker") or ""),
                            str(parameter_index),
                            str(sql_prop.get("source") or ""),
                        )
                    ] += 1

    property_routes = [
        {
            "xml_kind": key[0],
            "xml_name": key[1],
            "property": key[2],
            "sql_marker": key[3] or None,
            "parameter_index": int(key[4]) if key[4].lstrip("-").isdigit() else key[4],
            "sql_source": key[5] or None,
            "matches": match_count,
        }
        for key, match_count in sorted(property_route_counts.items(), key=lambda item: (-item[1], item[0]))
    ]

    return {
        "sql_form": {key: (sql_form or {}).get(key) for key in ("name", "guid", "source") if (sql_form or {}).get(key) is not None},
        "xml_form": xml_profile.get("form"),
        "xml_source": xml_profile.get("source"),
        "counts": dict(counts),
        "form_property_routes": form_property_routes,
        "property_routes": property_routes,
        "samples": samples,
    }


def build_report(sql_details: dict[str, Any], xml_context: dict[str, Any], *, sample_limit: int, max_items: int) -> dict[str, Any]:
    sql_forms = [form for form in sql_details.get("forms") or [] if isinstance(form, dict)]
    comparisons = []
    for form in xml_context.get("forms") or []:
        if not isinstance(form, dict):
            continue
        profiles = []
        seen_xml_paths: set[str] = set()
        xml_profile = xml_profile_from_context_form(form, max_items=max_items)
        if xml_profile is not None:
            source_path = str((xml_profile.get("source") or {}).get("path") or "").casefold()
            if source_path:
                seen_xml_paths.add(source_path)
            profiles.append(xml_profile)
        for overlay in form.get("extension_overlays") or []:
            if isinstance(overlay, dict):
                overlay_profile = xml_profile_from_context_form(overlay, max_items=max_items)
                if overlay_profile is not None:
                    source_path = str((overlay_profile.get("source") or {}).get("path") or "").casefold()
                    if source_path and source_path in seen_xml_paths:
                        continue
                    if source_path:
                        seen_xml_paths.add(source_path)
                    profiles.append(overlay_profile)
        for profile in profiles:
            comparisons.append(compare_form(matching_sql_form(profile, sql_forms), profile, sample_limit=sample_limit))
    totals = Counter()
    route_counts: Counter[tuple[str, str, str, str, str, str]] = Counter()
    for comparison in comparisons:
        totals.update(comparison.get("counts") or {})
        for route in comparison.get("property_routes") or []:
            route_counts[
                (
                    str(route.get("xml_kind") or ""),
                    str(route.get("xml_name") or ""),
                    str(route.get("property") or ""),
                    str(route.get("sql_marker") or ""),
                    str(route.get("parameter_index") if route.get("parameter_index") is not None else ""),
                    str(route.get("sql_source") or ""),
                )
            ] += int(route.get("matches") or 0)
    grouped_routes: dict[tuple[str, str, str], list[tuple[tuple[str, str, str, str, str, str], int]]] = {}
    for key, match_count in route_counts.items():
        grouped_routes.setdefault(key[:3], []).append((key, match_count))
    stable_property_routes = []
    ambiguous_property_routes = []
    for identity, variants in sorted(grouped_routes.items()):
        rows = [
            {
                "xml_kind": key[0],
                "xml_name": key[1],
                "property": key[2],
                "sql_marker": key[3] or None,
                "parameter_index": int(key[4]) if key[4].lstrip("-").isdigit() else key[4],
                "sql_source": key[5] or None,
                "matches": match_count,
            }
            for key, match_count in sorted(variants, key=lambda item: (-item[1], item[0]))
        ]
        if len(rows) == 1 and rows[0]["matches"] >= 2:
            stable_property_routes.append(rows[0])
        elif len(rows) > 1:
            ambiguous_property_routes.append(
                {"xml_kind": identity[0], "xml_name": identity[1], "property": identity[2], "routes": rows}
            )
    totals["property_routes"] = len(route_counts)
    totals["stable_property_routes"] = len(stable_property_routes)
    totals["ambiguous_property_routes"] = len(ambiguous_property_routes)
    return {
        "schema": "onec_form_sql_xml_comparison.v1",
        "status": "ok",
        "object": sql_details.get("object") or xml_context.get("object"),
        "counts": {"forms_compared": len(comparisons), **dict(totals)},
        "stable_property_routes": stable_property_routes,
        "ambiguous_property_routes": ambiguous_property_routes,
        "comparisons": comparisons,
    }


def markdown_table_row(values: list[Any]) -> str:
    return "| " + " | ".join(str(value).replace("\n", " ") for value in values) + " |"


def render_markdown(report: dict[str, Any]) -> str:
    lines = ["# 1C Form SQL/XML Comparison", ""]
    obj = report.get("object") or {}
    lines.append(f"- Object: `{obj.get('kind')}.{obj.get('name')}`")
    counts = report.get("counts") or {}
    lines.append(f"- Forms compared: `{counts.get('forms_compared')}`")
    lines.append(f"- Matched items: `{counts.get('matched_items', 0)}`")
    lines.append(f"- Matched properties: `{counts.get('matched_properties', 0)}`")
    lines.append(f"- XML-only properties: `{counts.get('xml_only_properties', 0)}`")
    lines.append(f"- Mismatches: `{counts.get('mismatches', 0)}`")
    lines.append(f"- CommandName matches: `{counts.get('command_name_matches', 0)}`")
    lines.append(f"- CommandName mismatches: `{counts.get('command_name_mismatches', 0)}`")
    lines.append("")
    for comparison in report.get("comparisons") or []:
        xml_form = comparison.get("xml_form") or {}
        sql_form = comparison.get("sql_form") or {}
        lines.append(f"## {xml_form.get('name') or sql_form.get('name')}")
        lines.append("")
        lines.append(markdown_table_row(["Metric", "Count"]))
        lines.append(markdown_table_row(["---", "---:"]))
        for key, value in sorted((comparison.get("counts") or {}).items()):
            lines.append(markdown_table_row([key, value]))
        for title, key in (("CommandName Matches", "command_name_match"), ("CommandName Mismatches", "command_name_mismatch"), ("XML-Only Properties", "xml_only_property"), ("Mismatches", "mismatch"), ("Missing SQL Items", "missing_sql_item")):
            sample = (comparison.get("samples") or {}).get(key) or []
            if not sample:
                continue
            lines.append("")
            lines.append(f"### {title}")
            lines.append("")
            lines.append("```json")
            lines.append(json.dumps(sample[:10], ensure_ascii=False, indent=2))
            lines.append("```")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def main() -> int:
    parser = argparse.ArgumentParser(description="Compare decoded SQL form semantics with Form.xml semantics.")
    parser.add_argument("--sql-details", type=Path, required=True)
    parser.add_argument("--xml-context", type=Path, required=True)
    parser.add_argument("--output-json", type=Path, required=True)
    parser.add_argument("--output-markdown", type=Path)
    parser.add_argument("--sample-limit", type=int, default=20)
    parser.add_argument("--max-items", type=int, default=5000)
    args = parser.parse_args()

    report = build_report(read_json(args.sql_details), read_json(args.xml_context), sample_limit=args.sample_limit, max_items=args.max_items)
    write_json(args.output_json, report)
    if args.output_markdown:
        args.output_markdown.parent.mkdir(parents=True, exist_ok=True)
        args.output_markdown.write_text(render_markdown(report), encoding="utf-8")
    print(json.dumps({"schema": "onec_form_sql_xml_comparison_cli_summary.v1", "status": report["status"], "counts": report["counts"], "output_json": str(args.output_json), "output_markdown": str(args.output_markdown) if args.output_markdown else None}, ensure_ascii=False, indent=2))
    return 0


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