"""Deterministic page/route graph discovery.

Scans a frontend source tree and extracts:
- All navigable pages / route paths
- Navigation links between pages (selector + target)

Zero AI — results are fully reproducible.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path

from maeris_mcp.tidy import SUPPORTED_EXTENSIONS, SKIP_DIRS

# ---------------------------------------------------------------------------
# Result dataclass
# ---------------------------------------------------------------------------


@dataclass
class DiscoveryResult:
    pages: list[str]        # unique route paths, e.g. ["/", "/login", "/checkout"]
    page_graph: list[dict]  # [{from, to, selector}] — compatible with _bfs_nav_path
    nav_links: list[dict]   # raw [{from_page, to_page, selector, file}]
    scan_root: str
    files_scanned: int


# ---------------------------------------------------------------------------
# Route extraction patterns
# ---------------------------------------------------------------------------

# React Router: <Route path="/foo" ...> or path="/foo"
_REACT_ROUTE_RE = re.compile(r'[Rr]oute\s+[^>]*path=["\'`]([/\w\-:*?]+)["\'`]')
# React Router v6: createBrowserRouter([{path: "/foo", ...}])
_ROUTER_OBJ_RE = re.compile(r'path:\s*["\'`]([/\w\-:*?]+)["\'`]')
# Next.js pages directory: pages/foo/bar.tsx → /foo/bar
# (handled via filesystem walk)
# Vue Router: { path: '/foo', ... }
_VUE_ROUTE_RE = re.compile(r'path:\s*["\'`]([/\w\-:*?]+)["\'`]')

# Navigation link patterns
_LINK_TO_RE = re.compile(r'<Link\s[^>]*to=["\'`]([/\w\-?#]+)["\'`]', re.IGNORECASE)
_HREF_RE = re.compile(r'href=["\'`]([/\w\-?#][^"\'`]*)["\'`]')
_ROUTER_PUSH_RE = re.compile(r'(?:router\.push|navigate|push)\s*\(\s*["\'`]([/\w\-?#]+)["\'`]')
_LINK_SELECTOR_RE = re.compile(
    r'(?:to|href)=["\'`]([/\w\-?#][^"\'`]*)["\'`][^>]*>([^<]{0,60})'
)

# Selector heuristics for nav links
_DATA_TESTID_RE = re.compile(r'data-testid=["\'`]([^"\'`]+)["\'`]')
_DATA_AUTOMATION_RE = re.compile(r'data-automation-id=["\'`]([^"\'`]+)["\'`]')
_ARIA_LABEL_RE = re.compile(r'aria-label=["\'`]([^"\'`]+)["\'`]')


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _is_external(path: str) -> bool:
    return path.startswith(("http://", "https://", "//", "mailto:", "tel:"))


def _normalize_route(raw: str) -> str:
    """Strip dynamic segments and query/hash; return normalized path."""
    path = raw.split("?")[0].split("#")[0]
    # Replace dynamic segments like :id, [id], <id>
    path = re.sub(r":[^/]+", ":param", path)
    path = re.sub(r"\[([^\]]+)\]", ":param", path)
    path = re.sub(r"<([^>]+)>", ":param", path)
    # Strip wildcard
    path = path.rstrip("*").rstrip("/") or "/"
    if not path.startswith("/"):
        path = "/" + path
    return path


def _extract_routes_from_content(content: str) -> list[str]:
    routes: list[str] = []
    for pattern in (_REACT_ROUTE_RE, _ROUTER_OBJ_RE, _VUE_ROUTE_RE):
        for m in pattern.finditer(content):
            r = _normalize_route(m.group(1))
            if r not in routes:
                routes.append(r)
    return routes


def _infer_link_selector(surrounding: str) -> str:
    """Try to infer a meaningful CSS selector for a nav link element."""
    for pat in (_DATA_TESTID_RE, _DATA_AUTOMATION_RE):
        m = pat.search(surrounding)
        if m:
            return f'[data-testid="{m.group(1)}"]'
    m = _ARIA_LABEL_RE.search(surrounding)
    if m:
        return f'[aria-label="{m.group(1)}"]'
    # fallback: try to get link text
    text_m = re.search(r">([^<]{2,40})<", surrounding)
    if text_m:
        text = text_m.group(1).strip()
        if text:
            return f"text={text}"
    return "a"


def _extract_nav_links_from_content(
    content: str,
    file_path: str,
    current_route: str,
) -> list[dict]:
    """Extract navigation links from a source file."""
    links: list[dict] = []
    seen_targets: set[str] = set()

    # React <Link to=...>
    for m in _LINK_TO_RE.finditer(content):
        target = m.group(1)
        if _is_external(target):
            continue
        target_norm = _normalize_route(target)
        if target_norm in seen_targets:
            continue
        seen_targets.add(target_norm)
        # Get surrounding context (100 chars before)
        start = max(0, m.start() - 100)
        surrounding = content[start: m.end() + 60]
        selector = _infer_link_selector(surrounding)
        links.append({
            "from_page": current_route,
            "to_page": target_norm,
            "selector": selector,
            "file": file_path,
        })

    # <a href=...>
    for m in _HREF_RE.finditer(content):
        target = m.group(1)
        if _is_external(target):
            continue
        target_norm = _normalize_route(target)
        if target_norm in seen_targets or target_norm == current_route:
            continue
        seen_targets.add(target_norm)
        start = max(0, m.start() - 100)
        surrounding = content[start: m.end() + 60]
        selector = _infer_link_selector(surrounding)
        links.append({
            "from_page": current_route,
            "to_page": target_norm,
            "selector": selector,
            "file": file_path,
        })

    # router.push(...) / navigate(...)
    for m in _ROUTER_PUSH_RE.finditer(content):
        target = m.group(1)
        if _is_external(target):
            continue
        target_norm = _normalize_route(target)
        if target_norm in seen_targets:
            continue
        seen_targets.add(target_norm)
        links.append({
            "from_page": current_route,
            "to_page": target_norm,
            "selector": "",  # programmatic nav — no DOM selector
            "file": file_path,
        })

    return links


def _guess_route_for_file(file_path: Path, scan_root: Path) -> str:
    """Heuristic: guess the route from a file's path (Next.js pages/ / app/)."""
    try:
        rel = file_path.relative_to(scan_root)
    except ValueError:
        return "/"
    parts = list(rel.parts)
    # Strip leading pages/ or app/
    if parts and parts[0] in ("pages", "app", "views", "routes"):
        parts = parts[1:]
    if not parts:
        return "/"
    # Remove file extension
    stem = Path(parts[-1]).stem
    if stem in ("index", "page", "route"):
        parts[-1] = ""
    else:
        parts[-1] = stem
    route = "/" + "/".join(p for p in parts if p)
    return _normalize_route(route)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def discover_journeys(scan_root: Path) -> DiscoveryResult:
    """Scan frontend source tree for pages and navigation links.

    Deterministic: no AI, no network calls.

    Args:
        scan_root: Root directory of the frontend project.

    Returns:
        DiscoveryResult with pages, page_graph, nav_links.
    """
    all_routes: set[str] = {"/"}
    nav_links: list[dict] = []
    files_scanned = 0

    for path in sorted(scan_root.rglob("*")):
        if not path.is_file():
            continue
        if path.suffix not in SUPPORTED_EXTENSIONS:
            continue
        if any(part in SKIP_DIRS for part in path.parts):
            continue
        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
        except OSError:
            continue
        files_scanned += 1

        # Detect current route from file path (for link attribution)
        file_route = _guess_route_for_file(path, scan_root)

        # Extract declared routes
        for r in _extract_routes_from_content(content):
            all_routes.add(r)

        # Extract navigation links
        links = _extract_nav_links_from_content(content, str(path), file_route)
        for lnk in links:
            all_routes.add(lnk["to_page"])
            nav_links.append(lnk)

    # Build page_graph compatible with _bfs_nav_path from tools/test_generation.py
    # Shape: [{from: str, to: str, selector: str}]
    page_graph: list[dict] = []
    seen_edges: set[tuple[str, str]] = set()
    for lnk in nav_links:
        if not lnk["selector"]:
            continue  # skip programmatic navs without a DOM target
        edge = (lnk["from_page"], lnk["to_page"])
        if edge in seen_edges:
            continue
        seen_edges.add(edge)
        page_graph.append({
            "from": lnk["from_page"],
            "to": lnk["to_page"],
            "selector": lnk["selector"],
        })

    pages = sorted(all_routes)
    return DiscoveryResult(
        pages=pages,
        page_graph=page_graph,
        nav_links=nav_links,
        scan_root=str(scan_root),
        files_scanned=files_scanned,
    )
