"""Directory-based app discovery for n6k_server.

Wraps nextpy_server.discover to add data file loading and table registration.
"""

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

from fastapi import FastAPI

from n6k_server.app import DataSource, create_app
from n6k_server.registry import collect as collect_registry
from nextpy_server.discover import _slug_for_data_file
from nextpy_server.discover import discover_app as _base_discover_app

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


def discover_app(app_dir: str | Path) -> FastAPI:
    """Build a FastAPI app with data layer from a directory structure."""
    return _base_discover_app(
        app_dir,
        app_factory=create_app,
        on_data_file=_load_data_file,
    )


def _load_data_file(app: FastAPI, data_file: Path, app_dir: Path):
    """Import a single data.py and apply its registrations to the app."""
    # 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()
    _apply_to_app(app, registered, data_file, app_dir)

    logger.info(
        f"Loaded {data_file.relative_to(app_dir)}: "
        f"{len(registered['sources'])} source(s), "
        f"{len(registered['derivations'])} derivation(s), "
        f"{len(registered['bindings'])} binding(s), "
        f"{len(registered['actions'])} action(s), "
        f"{len(registered['views'])} view(s), "
        f"{len(registered['rpcs'])} rpc(s)"
    )


def _apply_to_app(app, registered: dict, source_file: Path, app_dir: Path):
    """Apply collected registry data to the app. Fails fast on conflicts."""
    registry = app.state.table_registry

    for name, fn in registered["sources"].items():
        if registry.is_known(name):
            raise ValueError(
                f"Duplicate data source '{name}' in {source_file}. " f"A table named '{name}' is already registered."
            )
        DataSource(app, name, fn)

    for parent_name, name, fn in registered["derivations"]:
        if registry.is_known(name):
            raise ValueError(
                f"Duplicate derived table '{name}' in {source_file}. " f"A table named '{name}' is already registered."
            )
        if not registry.is_known(parent_name):
            raise ValueError(
                f"Cannot derive '{name}' from '{parent_name}' in {source_file}: "
                f"parent table not found. Ensure the parent @data is defined "
                f"in this file or a parent data.py."
            )
        registry.register_derivation(parent_name, name, fn)

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

    for name, (sql, depends_on) in registered["views"].items():
        if name in app.state.views:
            raise ValueError(
                f"Duplicate view '{name}' in {source_file}. " f"A view named '{name}' is already registered."
            )
        if registry.is_known(name):
            raise ValueError(
                f"View name '{name}' in {source_file} conflicts with " f"an existing data table of the same name."
            )
        registry.register_view(name, sql, depends_on)

    for name, (fn, parent_table) in registered["rpcs"].items():
        if registry.get_rpc(name) is not None:
            raise ValueError(
                f"Duplicate RPC '{name}' in {source_file}. " f"An RPC named '{name}' is already registered."
            )
        if parent_table is not None and not registry.is_known(parent_table):
            raise ValueError(
                f"RPC '{name}' references parent table '{parent_table}' in {source_file}: " f"parent table not found."
            )
        registry.register_rpc(name, fn, parent_table)

    if registered["actions"]:
        slug = _slug_for_data_file(source_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 {source_file}.")
            page_actions[name] = fn
