"""
mermaid_graph.parser
=================
Parse Mermaid flowchart/graph text into the extended node_link_data dict.

Supports:
  - flowchart / graph directives with direction
  - All standard node shapes
  - Edge types: solid/dotted/thick × normal/none/circle/cross arrows, with labels
  - subgraph … end  (nested)
  - classDef, style, linkStyle, class assignments (:::)
"""

from __future__ import annotations

import re
import warnings
from typing import Any

from .exceptions import MermaidParseError
from .schema import MermaidGraph, MermaidLink, MermaidNode, Subgraph


# ════════════════════════════════════════════════════════════════════════
#  Node-ID pattern (allows word chars and hyphens, but must start with
#  a word char)
# ════════════════════════════════════════════════════════════════════════

_NODE_ID_RE = re.compile(r"[\w][\w-]*")


# ════════════════════════════════════════════════════════════════════════
#  Shape detection helpers
# ════════════════════════════════════════════════════════════════════════

# Ordered from longest/most-specific delimiters to shortest.
# Each entry: (open, close, shape_name)
_SHAPE_DELIMITERS: list[tuple[str, str, str]] = [
    ("(((", ")))",  "double_circle"),
    ("([",  "])",   "stadium"),
    ("((",  "))",   "circle"),
    ("[[",  "]]",   "subroutine"),
    ("[(", ")]",    "cylinder"),
    ("{{", "}}",    "hexagon"),
    ("[/", "\\]",   "trapezoid"),
    ("[\\", "/]",   "trapezoid_alt"),
    ("[/", "/]",    "parallelogram"),
    ("[\\", "\\]",  "parallelogram_alt"),
    ("{",   "}",    "diamond"),
    (">",   "]",    "asymmetric"),
    ("(",   ")",    "round_rect"),
    ("[",   "]",    "rect"),
]


def _parse_node_declaration(text: str) -> tuple[str, str, str, str | None] | None:
    """
    Try to parse *text* as a node declaration like ``A([Start]):::primary``.
    Returns (id, label, shape, css_class) or None.
    """
    text = text.strip()
    if not text:
        return None

    # Extract optional :::class suffix
    css_class: str | None = None
    m_class = re.search(r":::([\w-]+)\s*$", text)
    if m_class:
        css_class = m_class.group(1)
        text = text[: m_class.start()].rstrip()

    # Extract node id  (word-chars and hyphens, starting with a word char)
    m_id = _NODE_ID_RE.match(text)
    if not m_id:
        return None
    node_id = m_id.group(0)
    rest = text[m_id.end():]

    if not rest:
        # Bare id, no shape
        return node_id, node_id, "rect", css_class

    # Try each shape delimiter
    for open_d, close_d, shape_name in _SHAPE_DELIMITERS:
        if rest.startswith(open_d):
            inner_start = len(open_d)
            # Find the matching close delimiter
            close_idx = rest.find(close_d, inner_start)
            if close_idx != -1:
                label = rest[inner_start:close_idx].strip()
                # Strip surrounding quotes from label
                if len(label) >= 2 and label[0] == '"' and label[-1] == '"':
                    label = label[1:-1]
                return node_id, label if label else node_id, shape_name, css_class

    # Fallback: treat rest as label in square brackets (malformed but lenient)
    return node_id, node_id, "rect", css_class


# ════════════════════════════════════════════════════════════════════════
#  Edge / arrow detection
# ════════════════════════════════════════════════════════════════════════

# Unified arrow pattern that matches all Mermaid arrow variants.
# Order matters: longer/more specific patterns first.
_ARROW_PATTERN = re.compile(
    r"""(
      ={2,}>              # thick arrow   ==>
    | ={2,}               # thick no-arrow ===
    | -\.+-\.*>           # dotted arrow  -.-> or -..->
    | -\.+-\.*            # dotted no-arrow -.- or -..-
    | --+[ox]             # solid circle/cross --o --x
    | --+>                # solid arrow   --> or --->
    | --+                 # solid no-arrow --- or ----
    )""",
    re.VERBOSE,
)

# Pattern for "-- label text -->" style inline labels.
# Must be checked BEFORE splitting on bare arrows.
_INLINE_LABEL_RE = re.compile(
    r"""
    --\s+(.+?)\s+-->      # -- text -->
    | --\s+(.+?)\s+---    # -- text ---
    """,
    re.VERBOSE,
)


def _split_edge_line(line: str) -> list[tuple[str, str, str, str, str]]:
    """
    Split a line that may contain chained edges like ``A --> B --> C``.
    Returns list of (source, target, line_type, arrow_type, label).
    """
    results: list[tuple[str, str, str, str, str]] = []

    # Tokenise: alternate between node-tokens and arrow-tokens.
    tokens: list[tuple[str, ...]] = []
    remaining = line.strip()

    while remaining:
        remaining = remaining.strip()

        # Search for both inline-label arrows and regular arrows
        inline_m = _INLINE_LABEL_RE.search(remaining)
        arrow_m = _ARROW_PATTERN.search(remaining)

        # Decide which match to use: prefer inline label when it starts
        # at or before the regular arrow (inline is more specific).
        use_inline = False
        if inline_m and arrow_m:
            use_inline = inline_m.start() <= arrow_m.start()
        elif inline_m:
            use_inline = True

        if use_inline:
            # Extract node before the inline-label arrow
            if inline_m.start() > 0:
                tokens.append(("NODE", remaining[: inline_m.start()].strip()))
            label = (inline_m.group(1) or inline_m.group(2) or "").strip()
            full_match = inline_m.group(0)
            if full_match.rstrip().endswith("-->"):
                arrow_text = "-->"
            else:
                arrow_text = "---"
            tokens.append(("ARROW", arrow_text, label))
            remaining = remaining[inline_m.end():]
        elif arrow_m:
            # Extract node before the arrow
            if arrow_m.start() > 0:
                tokens.append(("NODE", remaining[: arrow_m.start()].strip()))
            # Check for pipe label right after arrow
            after_arrow = remaining[arrow_m.end():]
            label = ""
            arrow_text = arrow_m.group(1)
            pipe_m = re.match(r"\|([^|]*)\|\s*", after_arrow)
            if pipe_m:
                label = pipe_m.group(1)
                remaining = after_arrow[pipe_m.end():]
            else:
                remaining = after_arrow
            tokens.append(("ARROW", arrow_text, label))
        else:
            tokens.append(("NODE", remaining.strip()))
            break

    # Now build edge tuples from token sequence
    nodes = [t for t in tokens if t[0] == "NODE"]
    arrows = [t for t in tokens if t[0] == "ARROW"]

    for i, arr in enumerate(arrows):
        if i >= len(nodes) - 1:
            break
        src = _clean_node_token(nodes[i][1])
        tgt = _clean_node_token(nodes[i + 1][1])
        if not src or not tgt:
            continue
        arrow_text = arr[1]
        label = arr[2] if len(arr) > 2 else ""
        line_type, arrow_type = _classify_arrow(arrow_text)
        results.append((src, tgt, line_type, arrow_type, label))

    return results


def _clean_node_token(token: str) -> str:
    """Return just the node id from a token that might include shape syntax."""
    token = token.strip().rstrip(";")
    m = _NODE_ID_RE.match(token)
    return m.group(0) if m else ""


def _classify_arrow(arrow: str) -> tuple[str, str]:
    """Return (line_type, arrow_type) for an arrow string."""
    arrow = arrow.strip()
    if arrow.startswith("="):
        line_type = "thick"
    elif "-." in arrow or ".-" in arrow:
        line_type = "dotted"
    else:
        line_type = "solid"

    if arrow.endswith(">"):
        arrow_type = "normal"
    elif arrow.endswith("o"):
        arrow_type = "circle"
    elif arrow.endswith("x"):
        arrow_type = "cross"
    else:
        arrow_type = "none"

    return line_type, arrow_type


# ════════════════════════════════════════════════════════════════════════
#  Shape regex for _ensure_node (pre-compiled, token-independent part)
# ════════════════════════════════════════════════════════════════════════

_SHAPE_SUFFIX_RE = re.compile(
    r"""(
        \(\(\(.*?\)\)\)   |  # double_circle
        \(\[.*?\]\)       |  # stadium
        \(\(.*?\)\)       |  # circle
        \[\[.*?\]\]       |  # subroutine
        \[\(.*?\)\]       |  # cylinder
        \{\{.*?\}\}       |  # hexagon
        \[/.*?\\\]        |  # trapezoid
        \[\\.*?/\]        |  # trapezoid_alt
        \[/.*?/\]         |  # parallelogram
        \[\\.*?\\\]       |  # parallelogram_alt
        \{.*?\}           |  # diamond
        >.*?\]            |  # asymmetric
        \(.*?\)           |  # round_rect
        \[.*?\]              # rect
    )?(:::([\w-]+))?""",
    re.VERBOSE,
)


# ════════════════════════════════════════════════════════════════════════
#  Main parser
# ════════════════════════════════════════════════════════════════════════

def parse(text: str) -> dict[str, Any]:
    """
    Parse a Mermaid flowchart/graph string and return an extended
    node_link_data dict.

    Raises
    ------
    MermaidParseError
        If the text contains an unsupported diagram type.
    """
    lines = text.strip().splitlines()
    if not lines:
        return MermaidGraph().to_dict()

    graph = MermaidGraph()
    known_nodes: dict[str, MermaidNode] = {}
    all_links: list[MermaidLink] = []
    link_counter = 0

    # Subgraph stack: list of Subgraph being built (innermost last)
    sg_stack: list[Subgraph] = []
    # Top-level subgraphs go here
    top_subgraphs: list[Subgraph] = []

    # node inline styles set via ``style A fill:#f00``
    node_inline_styles: dict[str, dict[str, str]] = {}

    # Deferred class assignments (class A,B className)
    deferred_class_assignments: list[tuple[list[str], str]] = []

    # Check for multigraph: track edge pairs to detect parallel edges
    edge_pairs: set[tuple[str, str]] = set()
    has_parallel_edges = False

    i = 0
    while i < len(lines):
        raw_line = lines[i]
        line = raw_line.strip()
        i += 1

        if not line or line.startswith("%%"):
            continue

        # ── Header: flowchart / graph ───────────────────────────────
        m_header = re.match(
            r"^(flowchart|graph)\s+(TD|TB|BT|LR|RL)\s*$", line, re.IGNORECASE
        )
        if m_header:
            graph.diagram_type = m_header.group(1).lower()
            # normalise: "graph" -> "flowchart" for consistency
            if graph.diagram_type == "graph":
                graph.diagram_type = "flowchart"
            graph.direction = m_header.group(2).upper()
            continue

        # ── Reject unsupported diagram types ─────────────────────────
        m_unsupported = re.match(
            r"^(sequenceDiagram|classDiagram|stateDiagram|erDiagram"
            r"|gantt|pie|gitGraph|journey|requirementDiagram"
            r"|C4Context|mindmap|timeline|sankey|xychart)\b",
            line,
        )
        if m_unsupported:
            raise MermaidParseError(
                f"Unsupported diagram type: {m_unsupported.group(1)!r}. "
                f"Only flowchart/graph diagrams are supported."
            )

        # ── classDef ────────────────────────────────────────────────
        m_cd = re.match(r"^classDef\s+([\w-]+)\s+(.+)$", line)
        if m_cd:
            cls_name = m_cd.group(1)
            styles = _parse_style_string(m_cd.group(2))
            graph.class_defs[cls_name] = styles
            continue

        # ── linkStyle ───────────────────────────────────────────────
        m_ls = re.match(r"^linkStyle\s+(\d+)\s+(.+)$", line)
        if m_ls:
            idx = m_ls.group(1)
            styles = _parse_style_string(m_ls.group(2))
            graph.link_styles[idx] = styles
            continue

        # ── style <nodeId> ──────────────────────────────────────────
        m_sty = re.match(r"^style\s+([\w-]+)\s+(.+)$", line)
        if m_sty:
            node_id = m_sty.group(1)
            styles = _parse_style_string(m_sty.group(2))
            node_inline_styles[node_id] = styles
            continue

        # ── class assignment: class A,B className ───────────────────
        m_cls = re.match(r"^class\s+([\w,\s-]+)\s+([\w-]+)\s*$", line)
        if m_cls:
            ids = [x.strip() for x in m_cls.group(1).split(",")]
            cls_name = m_cls.group(2)
            deferred_class_assignments.append((ids, cls_name))
            continue

        # ── subgraph ────────────────────────────────────────────────
        m_sg = re.match(
            r'^subgraph\s+([\w-]+)\s*\["?([^"]*)"?\]\s*$', line
        )
        if not m_sg:
            m_sg = re.match(r"^subgraph\s+([\w-]+)\s*$", line)
        if m_sg:
            sg_id = m_sg.group(1)
            sg_label = m_sg.group(2).strip('"') if m_sg.lastindex and m_sg.lastindex >= 2 else sg_id
            sg = Subgraph(id=sg_id, label=sg_label)
            sg_stack.append(sg)
            continue

        # ── end (close subgraph) ────────────────────────────────────
        if re.match(r"^end\s*$", line, re.IGNORECASE):
            if sg_stack:
                closed = sg_stack.pop()
                if sg_stack:
                    sg_stack[-1].subgraphs.append(closed)
                else:
                    top_subgraphs.append(closed)
            continue

        # ── direction inside subgraph ───────────────────────────────
        m_dir = re.match(r"^direction\s+(TD|TB|BT|LR|RL)\s*$", line, re.IGNORECASE)
        if m_dir and sg_stack:
            sg_stack[-1].direction = m_dir.group(1).upper()
            continue

        # ── Edge / node lines ───────────────────────────────────────
        # May contain chained edges: A --> B --> C
        # or standalone node declarations: A([Start]):::primary

        edges = _split_edge_line(line)
        if edges:
            for src_tok, tgt_tok, lt, at, lbl in edges:
                # Ensure nodes exist
                _ensure_node(src_tok, line, known_nodes, sg_stack)
                _ensure_node(tgt_tok, line, known_nodes, sg_stack)

                # Track parallel edges for multigraph detection
                pair = (src_tok, tgt_tok)
                if pair in edge_pairs:
                    has_parallel_edges = True
                edge_pairs.add(pair)

                link = MermaidLink(
                    source=src_tok,
                    target=tgt_tok,
                    label=lbl if lbl else None,
                    line_type=lt,
                    arrow=at,
                    link_index=link_counter,
                )
                all_links.append(link)
                link_counter += 1
        else:
            # Might be a standalone node declaration
            parsed = _parse_node_declaration(line)
            if parsed:
                nid, label, shape, css_class = parsed
                if nid not in known_nodes:
                    node = MermaidNode(id=nid, label=label, shape=shape, css_class=css_class)
                    known_nodes[nid] = node
                    if sg_stack:
                        sg_stack[-1].nodes.append(nid)
                else:
                    # Update label/shape if richer than before
                    existing = known_nodes[nid]
                    if label != nid:
                        existing.label = label
                    if shape != "rect":
                        existing.shape = shape
                    if css_class:
                        existing.css_class = css_class

    # ── Warn about unclosed subgraphs ────────────────────────────────
    if sg_stack:
        unclosed_ids = [sg.id for sg in sg_stack]
        warnings.warn(
            f"Unclosed subgraph(s): {unclosed_ids}. "
            f"Missing 'end' statement(s).",
            stacklevel=2,
        )
        # Flush remaining subgraphs to top-level
        while sg_stack:
            closed = sg_stack.pop()
            if sg_stack:
                sg_stack[-1].subgraphs.append(closed)
            else:
                top_subgraphs.append(closed)

    # ── Apply deferred class assignments ─────────────────────────────
    for ids, cls_name in deferred_class_assignments:
        for nid in ids:
            if nid in known_nodes:
                known_nodes[nid].css_class = cls_name

    # ── Apply inline styles ─────────────────────────────────────────
    for nid, sty in node_inline_styles.items():
        if nid in known_nodes:
            known_nodes[nid].style = sty

    # ── Assemble ────────────────────────────────────────────────────
    graph.subgraphs = top_subgraphs
    graph.nodes = list(known_nodes.values())
    graph.links = all_links
    graph.multigraph = has_parallel_edges

    return graph.to_dict()


# ── Internal helpers ────────────────────────────────────────────────────

def _ensure_node(
    token: str,
    full_line: str,
    known: dict[str, MermaidNode],
    sg_stack: list[Subgraph],
) -> None:
    """Make sure a node id extracted from an edge exists in *known*."""
    if token in known:
        return

    # Try to parse richer declaration from the full line
    # e.g.  A([Start]):::primary --> B{Decision}
    # We search for the token in the original line and try to parse shape.
    pattern = re.compile(re.escape(token) + _SHAPE_SUFFIX_RE.pattern, re.VERBOSE)
    m = pattern.search(full_line)
    if m:
        node_text = m.group(0)
        parsed = _parse_node_declaration(node_text)
        if parsed:
            nid, label, shape, css_class = parsed
            known[nid] = MermaidNode(id=nid, label=label, shape=shape, css_class=css_class)
            if sg_stack:
                if nid not in sg_stack[-1].nodes:
                    sg_stack[-1].nodes.append(nid)
            return

    # Fallback: bare node
    known[token] = MermaidNode(id=token)
    if sg_stack:
        if token not in sg_stack[-1].nodes:
            sg_stack[-1].nodes.append(token)


def _parse_style_string(s: str) -> dict[str, str]:
    """Parse ``fill:#f9f,stroke:#333`` into a dict."""
    result: dict[str, str] = {}
    for part in s.split(","):
        part = part.strip()
        if ":" in part:
            k, v = part.split(":", 1)
            result[k.strip()] = v.strip()
    return result
