#!/usr/bin/env python3
"""Attach DBNames storage routes to structured metadata projection reports."""

from __future__ import annotations

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

PLUGIN_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PLUGIN_ROOT))

from parser.dbnames import DBNamesRecord  # noqa: E402
from parser.storage import storage_routes  # noqa: E402


METADATA_FIELDS = (
    "attributes",
    "tabular_sections",
    "dimensions",
    "resources",
    "forms",
    "templates",
    "commands",
    "addressing_attributes",
    "accounting_flags",
    "columns",
    "enum_values",
    "integration_service_channels",
    "operations",
    "url_templates",
    "tabular_section_attributes",
)


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


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 dbnames_records(report: dict[str, Any]) -> list[DBNamesRecord]:
    records = []
    for db_file in report.get("dbnames") or []:
        source = db_file.get("file_name") or db_file.get("source") or "DBNames"
        for row in db_file.get("records") or []:
            guid = (row.get("guid") or "").lower()
            role = row.get("storage_role") or ""
            number = row.get("sql_number")
            if not guid or not role or number is None:
                continue
            records.append(
                DBNamesRecord(
                    guid=guid,
                    storage_role=role,
                    sql_number=int(number),
                    index=int(row.get("index") or 0),
                    source=source,
                )
            )
    return records


def route_index(records: list[DBNamesRecord]) -> dict[str, list[dict[str, Any]]]:
    grouped: dict[str, list[DBNamesRecord]] = {}
    for record in records:
        grouped.setdefault(record.guid, []).append(record)
    return {
        guid: [route.to_dict() for route in storage_routes(rows)]
        for guid, rows in grouped.items()
    }


def enrich_report(report: dict[str, Any], routes_by_guid: dict[str, list[dict[str, Any]]]) -> dict[str, Any]:
    result = dict(report)
    object_guid = ((report.get("identity") or {}).get("guid") or "").lower()
    result["object_storage_routes"] = routes_by_guid.get(object_guid, [])
    object_tables = [
        route["physical_name_candidate"]
        for route in result["object_storage_routes"]
        if route.get("route_kind") == "table"
        and route.get("physical_name_candidate")
        and is_primary_object_table(route)
    ]
    matched = 0
    total = 0
    for field in METADATA_FIELDS:
        enriched_items = []
        for item in report.get(field) or []:
            total += 1
            copy = dict(item)
            guid = (copy.get("uuid") or "").lower()
            routes = routes_by_guid.get(guid, [])
            copy["storage_routes"] = routes
            copy["storage_route_count"] = len(routes)
            copy["physical_columns"] = predicted_columns(copy, routes)
            if field == "tabular_sections":
                copy["physical_tables"] = predicted_tabular_section_tables(copy, routes, object_tables)
            if field == "tabular_section_attributes":
                copy["parent_physical_tables"] = parent_tabular_section_tables(result, copy)
            if routes:
                matched += 1
            enriched_items.append(copy)
        result[field] = enriched_items
    result["storage_route_summary"] = {
        "metadata_item_count": total,
        "metadata_items_with_routes": matched,
        "object_route_count": len(result["object_storage_routes"]),
    }
    return result


def parent_tabular_section_tables(report: dict[str, Any], item: dict[str, Any]) -> list[dict[str, Any]]:
    parent_uuid = (item.get("tabular_section_uuid") or item.get("parent_uuid") or "").lower()
    if not parent_uuid:
        return []
    for tabular_section in report.get("tabular_sections") or []:
        if (tabular_section.get("uuid") or "").lower() == parent_uuid:
            return tabular_section.get("physical_tables") or []
    return []


def predicted_tabular_section_tables(
    item: dict[str, Any],
    routes: list[dict[str, Any]],
    object_tables: list[str],
) -> list[dict[str, Any]]:
    result = []
    vt_routes = [route for route in routes if route.get("storage_role") == "VT" and route.get("sql_number") is not None]
    line_routes = [route for route in routes if route.get("storage_role") == "LineNo"]
    for vt in vt_routes:
        for table in object_tables:
            result.append(
                {
                    "table": f"{table}_VT{vt['sql_number']}",
                    "reason": "tabular section VT route under object table",
                    "vt_sql_number": vt["sql_number"],
                    "line_no_sql_numbers": [route["sql_number"] for route in line_routes],
                }
            )
    return result


def is_primary_object_table(route: dict[str, Any]) -> bool:
    role = route.get("storage_role") or ""
    if role.endswith("ChngR") or role.endswith("SInf"):
        return False
    if role in {"BPrPoints", "AccumRgT", "AccumRgOpt", "AccRgAT0", "AccRgCT", "AccRgOpt"}:
        return False
    return True


def predicted_columns(item: dict[str, Any], routes: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Predict physical SQL column names for proven simple type cases."""

    value_type = item.get("value_type") or {}
    types = value_type.get("types") or []
    if len(types) > 1:
        return predicted_composite_columns(types, routes)
    if len(types) == 0:
        return [{"status": "no_value_type"}] if routes else []

    value = types[0]
    result = []
    for route in routes:
        if route.get("storage_role") != "Fld" or not route.get("physical_name_candidate"):
            continue
        base = route["physical_name_candidate"]
        if value.startswith("cfg:") and "Ref." in value:
            result.append({"column": f"{base}RRef", "reason": "single 1C reference type", "value_type": value})
        elif value in {"xs:string", "xs:decimal", "xs:boolean", "xs:dateTime"}:
            result.append({"column": base, "reason": "single primitive XML type", "value_type": value})
        else:
            result.append({"status": "unmapped_single_type", "value_type": value, "base": base})
    return result


def predicted_composite_columns(types: list[str], routes: list[dict[str, Any]]) -> list[dict[str, Any]]:
    result = []
    has_reference = any(value.startswith("cfg:") and "Ref." in value for value in types)
    reference_types = [value for value in types if value.startswith("cfg:") and "Ref." in value]
    has_string = "xs:string" in types
    has_decimal = "xs:decimal" in types
    has_boolean = "xs:boolean" in types
    has_datetime = "xs:dateTime" in types
    for route in routes:
        if route.get("storage_role") != "Fld" or not route.get("physical_name_candidate"):
            continue
        base = route["physical_name_candidate"]
        result.append({"column": f"{base}_TYPE", "reason": "composite value discriminator", "value_types": types})
        if has_reference:
            if len(reference_types) > 1:
                result.append({"column": f"{base}_RTRef", "reason": "composite reference type id", "value_types": types})
            result.append({"column": f"{base}_RRRef", "reason": "composite reference value", "value_types": types})
        if has_string:
            result.append({"column": f"{base}_S", "reason": "composite string value", "value_types": types})
        if has_decimal:
            result.append({"column": f"{base}_N", "reason": "composite numeric value", "value_types": types})
        if has_boolean:
            result.append({"column": f"{base}_B", "reason": "composite boolean value", "value_types": types})
        if has_datetime:
            result.append({"column": f"{base}_T", "reason": "composite datetime value", "value_types": types})
    return result or [{"status": "unmapped_composite_type", "value_types": types}]


def enrich_batch(args: argparse.Namespace) -> dict[str, Any]:
    db_report = load_json(args.dbnames)
    routes_by_guid = route_index(dbnames_records(db_report))
    batch = load_json(args.summary)
    outputs = []
    totals = {"metadata_item_count": 0, "metadata_items_with_routes": 0, "object_route_count": 0}
    for row in batch.get("outputs") or []:
        source = Path(row["output"])
        report = load_json(source)
        enriched = enrich_report(report, routes_by_guid)
        output = args.output_dir / source.name
        write_json(output, enriched)
        summary = enriched["storage_route_summary"]
        for key in totals:
            totals[key] += summary[key]
        outputs.append({**row, "output": str(output), "storage_route_summary": summary})
    return {
        "schema": "onec_structured_metadata_dbnames_enrichment_batch.v1",
        "summary": str(args.summary),
        "dbnames": str(args.dbnames),
        "outputs": outputs,
        "totals": totals,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Attach DBNames storage routes to structured metadata reports.")
    parser.add_argument("--dbnames", type=Path, required=True)
    parser.add_argument("--summary", type=Path, required=True)
    parser.add_argument("--output-dir", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()

    result = enrich_batch(args)
    write_json(args.output, result)
    print(json.dumps({"output": str(args.output), "totals": result["totals"]}, ensure_ascii=False))
    return 0


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