# #region LayoutUtils [C:2] [TYPE Module] [SEMANTICS superset, dashboard, layout, position, json]
# @BRIEF Utility functions for manipulating Superset dashboard position_json layout.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Python:json]
# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]
# @RATIONALE Extracted from SupersetDashboardsWriteMixin to stay under INV_7 400 LOC.

import json
import re
from typing import Any


# #region parse_position_json [C:1] [TYPE Function]
# @BRIEF Parse position_json from a dashboard API response (may be string or dict).
# @PRE raw_position is a string, dict, or None.
# @POST Returns a dict.
def parse_position_json(raw_position: Any) -> dict:
    if isinstance(raw_position, str):
        return json.loads(raw_position) if raw_position.strip() else {}
    if isinstance(raw_position, dict):
        return raw_position
    return {}
# #endregion parse_position_json


# #region _estimate_markdown_height [C:1] [TYPE Function]
# @BRIEF Estimate grid height for a MARKDOWN element based on HTML content.
#   Counts block-level tags and newlines to estimate visual line count,
#   then converts to Superset grid units (1 unit ≈ 2 text lines + padding).
# @PRE content is a string of HTML/Markdown.
# @POST Returns int between 19 and 200 inclusive.
def _estimate_markdown_height(content: str) -> int:
    """Estimate MARKDOWN element height (position JSON units).

    Model: total = CSS min-height(40px) + content pixel height.
    Content height = text_lines × line-height(22px) + container padding.
    1 position JSON unit ≈ 8 rendered pixels.

    Reference: user's banner (5 text lines, padding:12) → height=21 → ~168px.
    """
    if not content:
        return 19

    # Detect container padding (e.g. padding:12 → 24px vertical)
    pad_match = re.search(r'padding\s*:\s*(\d+)', content)
    container_padding = int(pad_match.group(1)) * 2 if pad_match else 0

    # Strip HTML to get plain text
    plain_text = re.sub(r'<[^>]+>', '', content)
    plain_text = re.sub(r'\s+', ' ', plain_text).strip()

    if not plain_text:
        return 19

    # Text lines (wrapping at ~40 chars/line at 12col width)
    text_lines = len(plain_text) // 40 + 1

    # Content height (px): line-height(~22px) × lines + container padding
    content_px = text_lines * 22 + container_padding

    # Total: CSS min-height(40px) + content
    total_px = 40 + content_px

    # Convert to position JSON units (÷8)
    height = max(19, total_px // 8)
    return min(200, height)
# #endregion _estimate_markdown_height


# #region _generate_banner_id [C:1] [TYPE Function]
# @BRIEF Generate a deterministic row and markdown key for a banner chart.
def _generate_banner_id(chart_id: int) -> tuple[str, str]:
    row_key = f"ROW-banner-{chart_id}"
    md_key = f"MARKDOWN-banner-{chart_id}"
    return row_key, md_key
# #endregion _generate_banner_id


# #region insert_banner_markdown_at_top [C:2] [TYPE Function]
# @BRIEF Insert a ROW + native MARKDOWN pair at the top of the dashboard grid.
#   Creates a ROW entry connected to GRID_ID, and a MARKDOWN child inside it.
#   Shifts all existing grid ROWs down by 2 rows.
# @PRE position_json is a mutable dict. content is the HTML/markdown string.
#      chart_id is the database chart_id for key generation.
# @POST position_json is mutated; ROW + MARKDOWN inserted at top of GRID.
def insert_banner_markdown_at_top(
    position_json: dict, chart_id: int, content: str
) -> dict:
    row_key, md_key = _generate_banner_id(chart_id)

    # Shift existing ROW y-positions down
    for key, value in list(position_json.items()):
        if isinstance(value, dict):
            meta = value.get("meta", {})
            if isinstance(meta, dict):
                cur_y = meta.get("y", 0)
                if isinstance(cur_y, (int, float)):
                    meta["y"] = cur_y + 2

    # Add MARKDOWN entry with adaptive height
    height = _estimate_markdown_height(content)
    position_json[md_key] = {
        "type": "MARKDOWN",
        "id": md_key,
        "children": [],
        "parents": ["ROOT_ID", "GRID_ID", row_key],
        "meta": {
            "code": content,
            "width": 12,
            "height": height,
            "y": 0,
        },
    }

    # Add ROW entry connected to GRID_ID
    position_json[row_key] = {
        "type": "ROW",
        "id": row_key,
        "children": [md_key],
        "parents": ["ROOT_ID", "GRID_ID"],
        "meta": {
            "background": "BACKGROUND_TRANSPARENT",
            "y": 0,
        },
    }

    # Insert ROW at beginning of GRID_ID children
    grid = position_json.get("GRID_ID")
    if grid and isinstance(grid, dict):
        grid_children = grid.get("children", [])
        if isinstance(grid_children, list):
            if row_key in grid_children:
                grid_children.remove(row_key)
            grid_children.insert(0, row_key)

    return position_json
# #endregion insert_banner_markdown_at_top


# #region update_banner_markdown_content [C:1] [TYPE Function]
# @BRIEF Update the code content and adaptive height of an existing banner markdown element.
# @PRE position_json has markdown_key. content is the new HTML/markdown string.
# @POST position_json is mutated; markdown content and height updated.
def update_banner_markdown_content(
    position_json: dict, markdown_key: str, content: str
) -> dict:
    if markdown_key in position_json:
        meta = position_json[markdown_key].get("meta", {})
        if isinstance(meta, dict):
            meta["code"] = content
            meta["height"] = _estimate_markdown_height(content)
    return position_json
# #endregion update_banner_markdown_content


# #region remove_banner_from_position [C:2] [TYPE Function]
# @BRIEF Remove the banner ROW + MARKDOWN pair from the position dict and GRID children.
#   Shifts remaining items up by 2 if the banner was at y=0.
# @PRE position_json is a mutable dict. chart_id identifies the banner.
# @POST position_json is mutated; ROW and MARKDOWN entries removed.
def remove_banner_from_position(position_json: dict, chart_id: int) -> dict:
    row_key, md_key = _generate_banner_id(chart_id)

    removed_y = None
    # Remove MARKDOWN entry
    if md_key in position_json:
        markdown_data = position_json[md_key]
        if isinstance(markdown_data, dict):
            meta = markdown_data.get("meta", {})
            if isinstance(meta, dict):
                removed_y = meta.get("y", 0)
        del position_json[md_key]

    # Remove ROW entry
    position_json.pop(row_key, None)

    # Remove ROW from GRID_ID children
    grid = position_json.get("GRID_ID")
    if grid and isinstance(grid, dict):
        grid_children = grid.get("children", [])
        if isinstance(grid_children, list) and row_key in grid_children:
            grid_children.remove(row_key)

    # Shift remaining items up
    if removed_y is not None and removed_y == 0:
        for key, value in position_json.items():
            if isinstance(value, dict):
                meta = value.get("meta", {})
                if isinstance(meta, dict):
                    cur_y = meta.get("y", 0)
                    if isinstance(cur_y, (int, float)) and cur_y > 0:
                        meta["y"] = cur_y - 2
    return position_json
# #endregion remove_banner_from_position

# #endregion LayoutUtils
