import os
import re
import shlex
from pathlib import Path

from fastapi import APIRouter
from pydantic import BaseModel

router = APIRouter()

SOURCES_DIR = Path(os.environ.get("SOURCES_DIR", "/data/sources"))
PAGES_DIR = Path(os.environ.get("PAGES_DIR", "/data/pages"))

# Match µFrame link nodes: link "display" "/page/slug.mu" or link "display" "slug"
_UF_LINK = re.compile(r'^\s*link\s+', re.IGNORECASE)
# Fallback: Micron links [label`slug] or [label`slug.mu]
_MICRON_LINK = re.compile(r'\[([^`\]]+)`([a-zA-Z0-9_-]+)(?:\.mu)?\]')


class GraphNode(BaseModel):
    id: str
    published: bool
    title: str | None = None


class GraphEdge(BaseModel):
    source: str
    target: str


class GraphData(BaseModel):
    nodes: list[GraphNode]
    edges: list[GraphEdge]


def _all_page_names() -> set[str]:
    names: set[str] = set()
    if PAGES_DIR.is_dir():
        for f in PAGES_DIR.iterdir():
            if f.suffix == ".mu" and f.is_file():
                names.add(f.stem)
    if SOURCES_DIR.is_dir():
        for f in SOURCES_DIR.iterdir():
            if f.suffix in (".uf", ".mu") and f.is_file():
                names.add(f.stem)
    return names


def _extract_title(source: str) -> str | None:
    """Extract title from µFrame page or heading, or legacy Micron >Title."""
    for line in source.splitlines():
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        if stripped.lower().startswith("page "):
            try:
                parts = shlex.split(stripped)
                if len(parts) >= 2:
                    return parts[1]
            except ValueError:
                pass
            break
        if stripped.lower().startswith("heading "):
            try:
                parts = shlex.split(stripped)
                if len(parts) >= 3:
                    return parts[2]
            except ValueError:
                pass
            break
        if stripped.startswith(">") and not stripped.startswith(">>"):
            return stripped[1:].strip()
        break
    return None


def _extract_links(source: str, all_names: set[str]) -> list[str]:
    """Extract internal link targets from µFrame or Micron source."""
    targets: list[str] = []

    for line in source.splitlines():
        stripped = line.strip()

        # µFrame: link "display" "/page/slug.mu" or link "display" "slug"
        if _UF_LINK.match(stripped):
            try:
                parts = shlex.split(stripped)
                if len(parts) >= 3:
                    dest = parts[2]
                    # Normalize: /page/slug.mu → slug
                    slug = dest.rsplit("/", 1)[-1].removesuffix(".mu")
                    if slug in all_names:
                        targets.append(slug)
            except ValueError:
                pass
            continue

        # Fallback: Micron link syntax [label`slug]
        for m in _MICRON_LINK.finditer(stripped):
            slug = m.group(2)
            if slug in all_names:
                targets.append(slug)

    return targets


def _source_path(name: str) -> Path | None:
    """Get source file path, preferring .uf over .mu."""
    uf = SOURCES_DIR / f"{name}.uf"
    if uf.is_file():
        return uf
    mu = SOURCES_DIR / f"{name}.mu"
    return mu if mu.is_file() else None


@router.get("/graph", response_model=GraphData)
async def get_graph():
    all_names = _all_page_names()
    nodes: list[GraphNode] = []
    edges: list[GraphEdge] = []

    for name in sorted(all_names):
        src_path = _source_path(name)
        mu_path = PAGES_DIR / f"{name}.mu"

        title = None
        if src_path:
            content = src_path.read_text(encoding="utf-8")
            title = _extract_title(content)
            for target in _extract_links(content, all_names):
                edges.append(GraphEdge(source=name, target=target))

        nodes.append(GraphNode(
            id=name,
            published=mu_path.is_file(),
            title=title,
        ))

    return GraphData(nodes=nodes, edges=edges)
