"""NavigationGraph — directed graph of pages and transitions.

Nodes represent pages (identified by a URL pattern).
Edges represent actions that navigate between pages.
The graph is serialised to / loaded from a JSON file so it persists between runs.
"""

from __future__ import annotations

import json
import logging
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any

log = logging.getLogger("antiqa.crawler")


@dataclass
class PageNode:
    """A discovered page in the application."""

    url: str
    url_pattern: str
    title: str = ""
    links: list[str] = field(default_factory=list)
    forms: list[dict[str, Any]] = field(default_factory=list)
    buttons: list[str] = field(default_factory=list)
    inputs: list[str] = field(default_factory=list)
    meta: dict[str, Any] = field(default_factory=dict)

    @property
    def id(self) -> str:
        return self.url_pattern


@dataclass
class Edge:
    """A transition from one page to another."""

    source: str  # url_pattern of the source page
    target: str  # url_pattern of the target page
    action: str  # "click_link", "submit_form", etc.
    selector: str = ""  # CSS selector that triggers the transition
    label: str = ""

    @property
    def key(self) -> str:
        return f"{self.source} -> {self.target} ({self.action})"


class NavigationGraph:
    """Directed graph of pages and the actions connecting them."""

    def __init__(self) -> None:
        self.nodes: dict[str, PageNode] = {}
        self.edges: list[Edge] = []

    # ── Mutation ───────────────────────────────────────

    def add_node(self, node: PageNode) -> None:
        """Add or update a page node."""
        self.nodes[node.id] = node

    def add_edge(self, edge: Edge) -> None:
        """Add an edge if an equivalent one doesn't already exist."""
        if not any(
            e.source == edge.source
            and e.target == edge.target
            and e.action == edge.action
            for e in self.edges
        ):
            self.edges.append(edge)

    def has_node(self, url_pattern: str) -> bool:
        return url_pattern in self.nodes

    # ── Queries ────────────────────────────────────────

    def get_node(self, url_pattern: str) -> PageNode | None:
        return self.nodes.get(url_pattern)

    def neighbours(self, url_pattern: str) -> list[str]:
        """Return url_patterns reachable from *url_pattern* in one step."""
        return [e.target for e in self.edges if e.source == url_pattern]

    def incoming(self, url_pattern: str) -> list[Edge]:
        """Edges leading into *url_pattern*."""
        return [e for e in self.edges if e.target == url_pattern]

    @property
    def page_count(self) -> int:
        return len(self.nodes)

    @property
    def edge_count(self) -> int:
        return len(self.edges)

    def all_selectors(self) -> dict[str, list[str]]:
        """Return a mapping of url_pattern -> list of notable selectors."""
        out: dict[str, list[str]] = {}
        for node in self.nodes.values():
            selectors = list(node.buttons) + list(node.inputs)
            for form in node.forms:
                selectors.append(form.get("selector", ""))
                selectors.extend(form.get("fields", []))
            out[node.id] = [s for s in selectors if s]
        return out

    def summary_for_ai(self) -> str:
        """Return a concise text summary for LLM consumption."""
        lines = [f"Navigation graph: {self.page_count} pages, {self.edge_count} transitions\n"]
        lines.append("Pages:")
        for node in self.nodes.values():
            lines.append(f"  - {node.url_pattern}  title=\"{node.title}\"")
            if node.forms:
                lines.append(f"    forms: {len(node.forms)}")
            if node.buttons:
                lines.append(f"    buttons: {node.buttons}")
            if node.inputs:
                lines.append(f"    inputs: {node.inputs}")

        lines.append("\nTransitions:")
        for edge in self.edges:
            lines.append(f"  {edge.source}  --[{edge.action}]--> {edge.target}")

        return "\n".join(lines)

    # ── Serialisation ──────────────────────────────────

    def to_dict(self) -> dict[str, Any]:
        return {
            "nodes": {k: asdict(v) for k, v in self.nodes.items()},
            "edges": [asdict(e) for e in self.edges],
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> NavigationGraph:
        g = cls()
        for _key, nd in data.get("nodes", {}).items():
            g.add_node(PageNode(**nd))
        for ed in data.get("edges", []):
            g.add_edge(Edge(**ed))
        return g

    def save(self, path: str | Path) -> Path:
        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(json.dumps(self.to_dict(), indent=2) + "\n")
        log.info("Graph saved to %s (%d pages, %d edges)", p, self.page_count, self.edge_count)
        return p

    @classmethod
    def load(cls, path: str | Path) -> NavigationGraph:
        p = Path(path)
        if not p.is_file():
            raise FileNotFoundError(f"No graph file at {p}")
        data = json.loads(p.read_text())
        g = cls.from_dict(data)
        log.info("Graph loaded from %s (%d pages, %d edges)", p, g.page_count, g.edge_count)
        return g

    def merge(self, other: "NavigationGraph") -> None:
        """Merge *other* graph into this one (for incremental re-crawls)."""
        for node in other.nodes.values():
            self.add_node(node)
        for edge in other.edges:
            self.add_edge(edge)
