"""Directory-based app discovery.

Walks a directory to find page.astro/page.tsx files (routes) and optionally
delegates data file loading via callbacks.
"""

import importlib.util
import logging
import sys
from pathlib import Path
from typing import Callable

from fastapi import FastAPI

from nextpy_server.app import create_app
from nextpy_server.pages import page as register_page
from nextpy_server.registry import collect as collect_registry

logger = logging.getLogger("n6k.discover")

PAGE_EXTENSIONS = (".astro", ".tsx")


def discover_app(
    app_dir: str | Path,
    *,
    app_factory: Callable[[], FastAPI] | None = None,
    on_data_file: Callable[[FastAPI, Path, Path], None] | None = None,
) -> FastAPI:
    """Build a FastAPI app from a directory structure.

    Directory layout:
        app_dir/
            n6k.py          # optional — must export `app`
            data.py          # root data pipeline
            page.astro         # root page → route /
            summary/
                data.py      # child data (can derive from parent)
                page.astro     # route /summary

    app_factory: override the default create_app function.
    on_data_file: callback(app, data_file, app_dir) for each data.py found.
    """
    app_dir = Path(app_dir).resolve()

    if not app_dir.is_dir():
        raise ValueError(f"App directory not found: {app_dir}")

    # Clean stale _n6k_ modules from previous loads (uvicorn reload)
    for key in [k for k in sys.modules if k.startswith("_n6k_")]:
        del sys.modules[key]

    factory = app_factory or create_app

    # 1. Load app from n6k.py or create default
    n6k_py = app_dir / "n6k.py"
    if n6k_py.exists():
        app = _load_app_from_file(n6k_py)
        logger.info(f"Loaded app from {n6k_py}")
    else:
        app = factory()
        logger.info("Created default app (no n6k.py found)")

    # 2. Discover and load data.py files
    data_files = _find_data_files(app_dir)
    for data_file in data_files:
        if on_data_file is not None:
            on_data_file(app, data_file, app_dir)
        else:
            _load_bindings_only(app, data_file, app_dir)

    # 3. Discover page files and register routes
    pages = _find_pages(app_dir)
    if not pages:
        logger.warning(f"No page files found in {app_dir}")
    _register_pages(app, pages, app_dir)

    # Store source dir and entry points for build pipeline
    app.state._n6k_source_dir = str(app_dir)
    app.state._n6k_entry_points = {slug: str(page_file) for slug, page_file in pages}

    return app


def _load_app_from_file(n6k_py: Path) -> FastAPI:
    spec = importlib.util.spec_from_file_location("_n6k_app_config", str(n6k_py))
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    app: FastAPI | None = getattr(module, "app", None)
    if app is None:
        raise ValueError(f"{n6k_py} must export an 'app' object")
    return app


def _find_data_files(app_dir: Path) -> list[Path]:
    """Find all data.py files, sorted parent-first (shortest path first)."""
    return sorted(app_dir.rglob("data.py"), key=lambda p: len(p.parts))


def _slug_for_data_file(data_file: Path, app_dir: Path) -> str:
    """Derive a page slug from a data.py's parent directory."""
    rel = data_file.parent.relative_to(app_dir)
    slug = str(rel).replace("\\", "/")
    return "index" if slug == "." else slug


def _load_bindings_only(app: FastAPI, data_file: Path, app_dir: Path):
    """Import a data.py and apply only bindings and actions (no data sources)."""
    # Clear registry before import
    collect_registry()

    safe_name = str(data_file.relative_to(app_dir)).replace("/", "_").replace(".", "_")
    module_name = f"_n6k_{safe_name}"

    spec = importlib.util.spec_from_file_location(module_name, str(data_file))
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)

    registered = collect_registry()

    for name, default in registered["bindings"].items():
        if name in app.state.bindings:
            raise ValueError(
                f"Duplicate binding '{name}' in {data_file}. " f"A binding named '{name}' is already registered."
            )
        app.state.bindings[name] = default

    if registered["actions"]:
        slug = _slug_for_data_file(data_file, app_dir)
        page_actions = app.state.actions.setdefault(slug, {})
        for name, fn in registered["actions"].items():
            if name in page_actions:
                raise ValueError(f"Duplicate action '{name}' in {data_file}.")
            page_actions[name] = fn

    logger.info(
        f"Loaded {data_file.relative_to(app_dir)}: "
        f"{len(registered['bindings'])} binding(s), "
        f"{len(registered['actions'])} action(s)"
    )


def _find_pages(app_dir: Path) -> list[tuple[str, Path]]:
    """Find all page.astro/page.tsx files and return (slug, path) pairs."""
    pages = []
    seen_slugs = set()

    page_files: list[Path] = []
    for ext in PAGE_EXTENSIONS:
        page_files.extend(app_dir.rglob(f"page{ext}"))
    page_files.sort(key=lambda p: len(p.parts))

    for page_file in page_files:
        rel = page_file.parent.relative_to(app_dir)
        slug = str(rel).replace("\\", "/")
        if slug == ".":
            slug = "index"

        if slug in seen_slugs:
            raise ValueError(
                f"Duplicate page slug '{slug}': found multiple page files " f"that resolve to the same route."
            )
        seen_slugs.add(slug)
        pages.append((slug, page_file))

    return pages


def _register_pages(app, pages: list[tuple[str, Path]], app_dir: Path):
    """Register HTTP routes for discovered pages."""
    for slug, page_file in pages:
        route_path = "/" if slug == "index" else f"/{slug}"

        if page_file.suffix == ".astro":
            snippet = page_file.read_text()
        else:
            snippet = ""
        register_page(app, route_path, snippet)

        logger.info(f"Registered page: {route_path} ← {page_file.relative_to(app_dir)}")
