# #region DashboardTopology [C:3] [TYPE Module] [SEMANTICS superset, topology, dashboard, layout, parsing]
# @BRIEF Dashboard topology extraction — parses Superset position_json into human-readable
#   text descriptions of Tab → Row → Chart hierarchy for LLM prompt context.
# @LAYER Plugin
# @RELATION DEPENDS_ON -> [json]
# @INVARIANT All functions are pure (no DB, no I/O beyond json parsing).
# @DATA_CONTRACT Input: Dashboard position_json + Charts → Output: Topology text.

import json
from typing import Any


# #region _chart_name [C:1] [TYPE Function]
# @BRIEF Return the display name for a chart.
def _chart_name(chart: dict) -> str:
    return chart.get("slice_name", chart.get("title", f"Chart {chart.get('id')}"))
# #endregion _chart_name


# #region _chart_params [C:1] [TYPE Function]
# @BRIEF Return parsed chart params dict.
def _chart_params(chart: dict) -> dict:
    params = chart.get("params", {})
    if isinstance(params, str):
        try:
            return json.loads(params)
        except (json.JSONDecodeError, TypeError):
            return {}
    return params
# #endregion _chart_params


# #region _describe_chart_dataset [C:2] [TYPE Function]
# @BRIEF Return dataset description line if available.
# @PRE  chart and params are chart dicts.
# @POST  Returns string with dataset line or empty string.
def _describe_chart_dataset(chart: dict, params: dict) -> str:
    ds_id = chart.get("datasource_id") or params.get("datasource_id")
    if ds_id:
        return f"\n    Dataset: {ds_id}"
    return ""
# #endregion _describe_chart_dataset


# #region _describe_chart_metrics [C:2] [TYPE Function]
# @BRIEF Return metrics description line if available.
# @PRE  params is a parsed chart params dict.
# @POST  Returns string with metrics line or empty string.
def _describe_chart_metrics(params: dict) -> str:
    metrics = params.get("metrics", [])
    if not metrics:
        return ""
    metric_names: list[str] = []
    for m in metrics:
        if isinstance(m, dict):
            metric_names.append(m.get("label", m.get("expression", str(m))))
        else:
            metric_names.append(str(m))
    if metric_names:
        return f"\n    Metrics: {', '.join(metric_names[:3])}"
    return ""
# #endregion _describe_chart_metrics


# #region _describe_chart_groupby [C:2] [TYPE Function]
# @BRIEF Return group-by description line if available.
# @PRE  params is a parsed chart params dict.
# @POST  Returns string with group-by line or empty string.
def _describe_chart_groupby(params: dict) -> str:
    groupby = params.get("groupby", [])
    if groupby:
        return f"\n    Group by: {', '.join(groupby[:3])}"
    return ""
# #endregion _describe_chart_groupby


# #region _describe_chart_filters [C:2] [TYPE Function]
# @BRIEF Return adhoc filters description line if available.
# @PRE  params is a parsed chart params dict.
# @POST  Returns string with filters line or empty string.
def _describe_chart_filters(params: dict) -> str:
    adhoc_filters = params.get("adhoc_filters", [])
    if not adhoc_filters:
        return ""
    filter_parts: list[str] = []
    for filt in adhoc_filters[:3]:
        if isinstance(filt, dict):
            subject = filt.get("subject", "")
            operator = filt.get("operator", "")
            comparator = filt.get("comparator", "")
            if subject and comparator:
                filter_parts.append(f"{subject} {operator} {comparator}")
            else:
                filter_parts.append(filt.get("clause", "where"))
    if filter_parts:
        return f"\n    Filters: {', '.join(filter_parts)}"
    return ""
# #endregion _describe_chart_filters


# #region _describe_chart [C:2] [TYPE Function]
# @BRIEF Return a multi-line description of a single chart.
# @PRE  chart is a chart detail dict with slice_name, viz_type, params.
# @POST  Returns formatted chart description string.
def _describe_chart(chart: dict) -> str:
    name = _chart_name(chart)
    viz_type = chart.get("viz_type", "unknown")
    params = _chart_params(chart)

    desc = f'  Chart "{name}" ({viz_type})'
    desc += _describe_chart_dataset(chart, params)
    desc += _describe_chart_metrics(params)
    desc += _describe_chart_groupby(params)
    desc += _describe_chart_filters(params)
    return desc
# #endregion _describe_chart


# #region _sort_chart_key [C:1] [TYPE Function]
# @BRIEF Sort key for chart lookup items: numeric IDs first, then string keys.
def _sort_chart_key(item):
    k, _ = item
    try:
        return (0, int(k))
    except (ValueError, TypeError):
        return (1, str(k))
# #endregion _sort_chart_key


# #region _resolve_child [C:2] [TYPE Function]
# @BRIEF Resolve a child reference which may be a string key or inline dict.
# @PRE  ref is a child reference from position_json, pos_json is the full layout.
# @POST  Returns the resolved child dict or None.
def _resolve_child(ref, pos_json: dict) -> dict | None:
    if isinstance(ref, dict):
        return ref
    if isinstance(ref, str) and ref in pos_json:
        return pos_json[ref]
    return None
# #endregion _resolve_child


# #region _process_tab [C:2] [TYPE Function]
# @BRIEF Process a TAB type position_json node.
# @POST  Returns indented lines for the tab and its children.
def _process_tab(
    child: dict, depth: int, pos_json: dict, chart_lookup: dict[str, dict]
) -> list[str]:
    meta = child.get("meta", {}) or {}
    tab_name = meta.get("text", meta.get("label", "Tab"))
    sub_lines = _process_children(child, depth + 1, pos_json, chart_lookup)
    chart_count = sum(1 for line in sub_lines if line.strip().startswith("Chart"))
    indent = "  " * depth
    return [f'{indent}─── Tab: "{tab_name}" ({chart_count} charts) ───', *sub_lines]
# #endregion _process_tab


# #region _process_chart [C:2] [TYPE Function]
# @BRIEF Process a CHART type position_json node.
# @POST  Returns indented line for the chart description or empty list.
def _process_chart(
    child: dict, depth: int, chart_lookup: dict[str, dict]
) -> list[str]:
    meta = child.get("meta", {}) or {}
    chart_id = meta.get("chartId") or meta.get("slice_id")
    if chart_id is not None and str(chart_id) in chart_lookup:
        indent = "  " * depth
        return [f"{indent}{_describe_chart(chart_lookup[str(chart_id)])}"]
    return []
# #endregion _process_chart


# #region _process_children [C:2] [TYPE Function]
# @BRIEF Recursively process position_json children building indented lines.
# @PRE  parent is a position_json node dict, pos_json is the full layout, chart_lookup maps IDs.
# @POST  Returns list of formatted topology lines.
def _process_children(
    parent: dict, depth: int, pos_json: dict, chart_lookup: dict[str, dict]
) -> list[str]:
    result: list[str] = []
    children = parent.get("children", []) if isinstance(parent, dict) else []

    for child_ref in children:
        child = _resolve_child(child_ref, pos_json)
        if not child or not isinstance(child, dict):
            continue

        child_type = child.get("type", "")

        if child_type == "TAB":
            result.extend(_process_tab(child, depth, pos_json, chart_lookup))
        elif child_type == "ROW":
            result.extend(_process_children(child, depth, pos_json, chart_lookup))
        elif child_type == "CHART":
            result.extend(_process_chart(child, depth, chart_lookup))

    return result
# #endregion _process_children


# #region _extract_topology_children [C:2] [TYPE Function]
# @BRIEF Extract formatted topology lines from position_json.
# @PRE  position_json is a parsed layout dict, chart_lookup maps chart IDs to chart dicts.
# @POST  Returns list of formatted topology description lines.
def _extract_topology_children(
    position_json: dict, chart_lookup: dict[str, dict]
) -> list[str]:
    lines: list[str] = []
    grid_nodes = [
        v
        for v in position_json.values()
        if isinstance(v, dict) and v.get("type") in ("GRID", "ROOT", "CONTAINER")
    ]
    if grid_nodes:
        for grid in grid_nodes:
            lines.extend(_process_children(grid, depth=1, pos_json=position_json, chart_lookup=chart_lookup))
    else:
        for key, value in position_json.items():
            if (
                isinstance(value, dict)
                and value.get("type") in ("TAB", "ROW", "CHART")
                and key != "DASHBOARD_VERSION_KEY"
            ):
                lines.extend(_process_children({"children": [key]}, depth=0, pos_json=position_json, chart_lookup=chart_lookup))
    return lines
# #endregion _extract_topology_children


# #region build_dashboard_topology [C:2] [TYPE Function]
# @BRIEF Build a structured text description of dashboard layout from position_json.
# @PARAM  dashboard (dict) - Dashboard metadata dict with position_json, dashboard_title.
# @PARAM  charts (list[dict]) - List of fetched chart detail dicts for name/viz lookup.
# @POST  Returns a human-readable text description of the dashboard topology.
# @RELATION DEPENDS_ON -> [_extract_topology_children]
# @RELATION DEPENDS_ON -> [_sort_chart_key]
def build_dashboard_topology(dashboard: dict, charts: list[dict]) -> str:
    """
    Build a structured text description of dashboard layout from position_json.

    Returns a human-readable text like:
      Dashboard: "Sales Overview"
        --- Tab: "Revenue" (3 charts) ---
          Chart "Total Revenue YTD" (big_number_total)
            Dataset: 5
            Metrics: SUM(revenue)
          ...
        --- Tab: "Pipeline" (2 charts) ---
          ...
    """
    lines: list[str] = []
    title = dashboard.get(
        "dashboard_title",
        dashboard.get("title", f"Dashboard {dashboard.get('id', '')}"),
    )
    lines.append(f'Dashboard: "{title}"')

    position_json = dashboard.get("position_json", {})
    if isinstance(position_json, str):
        try:
            position_json = json.loads(position_json)
        except (json.JSONDecodeError, TypeError):
            position_json = {}
    if not isinstance(position_json, dict):
        position_json = {}

    chart_lookup: dict[str, dict] = {}
    for c in charts:
        cid = c.get("id") or c.get("slice_id")
        if cid is not None:
            chart_lookup[str(cid)] = c

    try:
        children = _extract_topology_children(position_json, chart_lookup)
        lines.extend(children)
    except Exception as e:
        lines.append(f"\n[Topology parse warning: {e}]")

    if chart_lookup:
        lines.append(f"\nCharts: {len(chart_lookup)} total")
        for chart_id, c in sorted(chart_lookup.items(), key=_sort_chart_key):
            lines.append(
                f"  - {c.get('slice_name', f'chart_{chart_id}')} "
                f"({c.get('viz_type', 'unknown')})"
            )

    return "\n".join(lines)
# #endregion build_dashboard_topology


# #endregion DashboardTopology
