"""diagrams.net / Draw.io（.drawio）XML 生成与轻量解析。"""

from __future__ import annotations

import html
import re
import uuid
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from typing import Any

MAX_NODES = 800
MAX_EDGES = 2000
MAX_XML_CHARS = 4_000_000


def _utc_ts() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")


def _esc_attr(s: str) -> str:
    return (
        html.escape(s, quote=True)
        .replace("\n", "&#xa;")
        .replace("\r", "")
    )


def build_drawio_xml(
    nodes: list[dict[str, Any]],
    edges: list[dict[str, Any]],
    *,
    page_name: str = "Page-1",
    diagram_id: str | None = None,
    cols: int = 4,
    cell_width: int = 140,
    cell_height: int = 72,
    grid_x: int = 48,
    grid_y: int = 48,
    x_gap: int = 200,
    y_gap: int = 120,
) -> dict[str, Any]:
    """由简单 nodes/edges 生成未压缩的 .drawio（mxfile）XML 文本。"""
    if not isinstance(nodes, list) or not isinstance(edges, list):
        return {"ok": False, "error": "nodes and edges must be arrays"}
    if len(nodes) > MAX_NODES:
        return {"ok": False, "error": f"at most {MAX_NODES} nodes"}
    if len(edges) > MAX_EDGES:
        return {"ok": False, "error": f"at most {MAX_EDGES} edges"}
    cols = max(1, min(12, int(cols)))

    id_map: dict[str, str] = {}
    next_mx = 2

    norm_nodes: list[tuple[str, str, dict[str, Any]]] = []
    for i, raw in enumerate(nodes):
        if not isinstance(raw, dict):
            return {"ok": False, "error": f"nodes[{i}] must be object"}
        uid = str(raw.get("id") if raw.get("id") is not None else f"n{i}")
        if uid in id_map:
            return {"ok": False, "error": f"duplicate node id: {uid!r}"}
        mid = str(next_mx)
        next_mx += 1
        id_map[uid] = mid
        lbl = str(raw.get("label") if raw.get("label") is not None else uid)
        norm_nodes.append((uid, mid, {**raw, "label": lbl}))

    for i, e in enumerate(edges):
        if not isinstance(e, dict):
            return {"ok": False, "error": f"edges[{i}] must be object"}
        s = str(e.get("source", e.get("from", "")))
        t = str(e.get("target", e.get("to", "")))
        if s not in id_map or t not in id_map:
            return {
                "ok": False,
                "error": f"edges[{i}] unknown source/target: {s!r} -> {t!r}",
            }

    did = diagram_id or str(uuid.uuid4())
    page_name = (page_name or "Page-1").strip() or "Page-1"

    parts: list[str] = []
    parts.append(
        f'<mxfile host="app.diagrams.net" modified="{_utc_ts()}" '
        f'agent="Aicd" version="22.1.0" etag="{uuid.uuid4()}" type="device">'
    )
    parts.append(f'  <diagram id="{_esc_attr(did)}" name="{_esc_attr(page_name)}">')
    parts.append(
        '    <mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" '
        'tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" '
        'pageWidth="1169" pageHeight="827" math="0" shadow="0">'
    )
    parts.append("      <root>")
    parts.append('        <mxCell id="0"/>')
    parts.append('        <mxCell id="1" parent="0"/>')

    for idx, (_uid, mid, meta) in enumerate(norm_nodes):
        lbl = _esc_attr(str(meta["label"]))
        style = str(meta.get("style") or "rounded=1;whiteSpace=wrap;html=1;")
        if "fillColor" not in style and meta.get("fill_color"):
            style += f"fillColor={meta['fill_color']};"
        x = meta.get("x")
        y = meta.get("y")
        w = int(meta.get("width") or cell_width)
        h = int(meta.get("height") or cell_height)
        if x is None or y is None:
            c = idx % cols
            r = idx // cols
            x = grid_x + c * x_gap
            y = grid_y + r * y_gap
        else:
            x, y = int(x), int(y)
        parts.append(
            f'        <mxCell id="{mid}" value="{lbl}" style="{_esc_attr(style)}" '
            f'vertex="1" parent="1">'
            f'<mxGeometry x="{x}" y="{y}" width="{w}" height="{h}" as="geometry"/>'
            f"</mxCell>"
        )

    edge_style = (
        "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;"
        "jettySize=auto;html=1;"
    )
    for e in edges:
        s = str(e.get("source", e.get("from", "")))
        t = str(e.get("target", e.get("to", "")))
        sid = id_map[s]
        tid = id_map[t]
        eid = str(next_mx)
        next_mx += 1
        elbl = e.get("label")
        val = _esc_attr(str(elbl)) if elbl is not None and str(elbl) != "" else ""
        val_attr = f' value="{val}"' if val else ""
        parts.append(
            f'        <mxCell id="{eid}"{val_attr} style="{edge_style}" edge="1" '
            f'parent="1" source="{sid}" target="{tid}">'
            '<mxGeometry relative="1" as="geometry"/>'
            "</mxCell>"
        )

    parts.append("      </root>")
    parts.append("    </mxGraphModel>")
    parts.append("  </diagram>")
    parts.append("</mxfile>")
    xml = "\n".join(parts)
    return {
        "ok": True,
        "xml": xml,
        "node_count": len(norm_nodes),
        "edge_count": len(edges),
        "diagram_id": did,
    }


def _local_tag(tag: str) -> str:
    if "}" in tag:
        return tag.split("}", 1)[1]
    return tag


def summarize_drawio_xml(xml_text: str) -> dict[str, Any]:
    """从 .drawio 文本中提取顶点/边摘要（最佳努力，不校验全量 schema）。"""
    if len(xml_text) > MAX_XML_CHARS:
        return {"ok": False, "error": f"XML exceeds {MAX_XML_CHARS} chars"}
    try:
        root = ET.fromstring(xml_text)
    except ET.ParseError as e:
        return {"ok": False, "error": str(e)}

    nodes: list[dict[str, Any]] = []
    edges: list[dict[str, Any]] = []
    for el in root.iter():
        if _local_tag(el.tag) != "mxCell":
            continue
        cid = el.get("id")
        if el.get("vertex") == "1":
            geom = el.find("mxGeometry")
            x = y = w = h = None
            if geom is not None:
                x = geom.get("x")
                y = geom.get("y")
                w = geom.get("width")
                h = geom.get("height")
            nodes.append(
                {
                    "cell_id": cid,
                    "label": el.get("value") or "",
                    "style": el.get("style") or "",
                    "x": x,
                    "y": y,
                    "width": w,
                    "height": h,
                }
            )
        elif el.get("edge") == "1":
            edges.append(
                {
                    "cell_id": cid,
                    "source": el.get("source"),
                    "target": el.get("target"),
                    "label": el.get("value") or "",
                }
            )

    return {
        "ok": True,
        "vertices": nodes,
        "edges": edges,
        "vertex_count": len(nodes),
        "edge_count": len(edges),
    }


def strip_bom_and_xml_decl(s: str) -> str:
    s = s.lstrip("\ufeff")
    s = re.sub(r"^\s*<\?xml[^>]*\?>\s*", "", s, count=1, flags=re.I)
    return s.strip()
