"""Optional cross-cutting contributions a module ships alongside its manifest.

Phase 6 introduced :class:`~simorgh.apps.modules.registry.ModuleManifest`
for service contracts, features and permissions. Phase 10 (workspaces
+ unified search) needs modules to also declare:

* **navigation** — items to install in workspaces.
* **searchables** — adapters for global search.
* **entities** — semantic metadata for AI / form rendering.
* **actions** — declarative operations.

Rather than expand ``ModuleManifest`` (and risk Phase 6 invariants),
contributions live in a parallel registry. A module that wants any of
these calls :func:`register_contributions` from its ``AppConfig.ready``,
and the workspace + search + semantic + actions systems consume them.

Contributions are pure declarations — no DB writes happen here. The
:mod:`simorgh.apps.workspaces.services` layer materialises navigation
contributions into ``NavigationItem`` rows when a module is enabled for
a tenant.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from threading import Lock
from typing import TYPE_CHECKING, ClassVar

from simorgh.core import actions as core_actions
from simorgh.core import search as core_search
from simorgh.core import semantic as core_semantic

if TYPE_CHECKING:
    pass


class ContributionError(RuntimeError):
    """Raised on duplicate / inconsistent contribution registrations."""


# ---------------------------------------------------------------------------
# Navigation
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class NavContribution:
    """A navigation entry a module wants installed in a workspace.

    Materialised into a ``NavigationItem`` row when the module is
    enabled for a tenant. ``key`` is a stable identifier the module
    chooses; reusing the same key (e.g. on module upgrade) updates the
    existing row instead of creating a duplicate.

    Phase B: ``feature`` removed — navigation is permission-gated only.
    Feature gating happens at the entitlement layer.
    """

    key: str
    label_key: str
    route: str
    icon: str = ""
    permission: str = ""
    order: int = 100
    parent_key: str = ""

    def __post_init__(self) -> None:
        if not self.key:
            raise ContributionError("NavContribution.key is required")
        if not self.label_key:
            raise ContributionError(f"nav {self.key!r}: label_key is required")
        if not self.route:
            raise ContributionError(f"nav {self.key!r}: route is required")


# ---------------------------------------------------------------------------
# Registry container
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class ModuleContributions:
    module: str
    navigation: tuple[NavContribution, ...] = ()
    searchables: tuple[core_search.SearchableSpec, ...] = ()
    entities: tuple[core_semantic.EntitySpec, ...] = ()
    actions: tuple[core_actions.ActionSpec, ...] = ()


@dataclass
class _ContribRegistry:
    by_module: dict[str, ModuleContributions] = field(default_factory=dict)
    lock: ClassVar[Lock] = Lock()


_REGISTRY = _ContribRegistry()


def register_contributions(
    module: str,
    *,
    navigation: tuple[NavContribution, ...] = (),
    searchables: tuple[core_search.SearchableSpec, ...] = (),
    entities: tuple[core_semantic.EntitySpec, ...] = (),
    actions: tuple[core_actions.ActionSpec, ...] = (),
) -> ModuleContributions:
    """Register contributions for ``module`` and propagate to platform registries.

    Calling this twice with identical contributions is a no-op. Calling
    it with a different set raises :class:`ContributionError`.

    Side effects:
      * ``entities`` are registered with :mod:`simorgh.core.semantic`.
      * ``actions`` are registered with :mod:`simorgh.core.actions`.
      * ``searchables`` are registered with :mod:`simorgh.core.search`.
      * ``navigation`` is held until a workspace asks for it.
    """
    bundle = ModuleContributions(
        module=module,
        navigation=tuple(navigation),
        searchables=tuple(searchables),
        entities=tuple(entities),
        actions=tuple(actions),
    )
    with _REGISTRY.lock:
        existing = _REGISTRY.by_module.get(module)
        if existing is not None:
            if existing == bundle:
                return existing
            raise ContributionError(
                f"module {module!r} already registered different contributions",
            )
        for ent in bundle.entities:
            core_semantic.register_entity(ent)
        for act in bundle.actions:
            core_actions.register_action(act)
        for s in bundle.searchables:
            core_search.register_searchable(s)
        _REGISTRY.by_module[module] = bundle
    return bundle


def get_contributions(module: str) -> ModuleContributions:
    return _REGISTRY.by_module.get(module, ModuleContributions(module=module))


def list_contributions() -> list[ModuleContributions]:
    return sorted(_REGISTRY.by_module.values(), key=lambda c: c.module)


def reset_for_tests() -> None:
    with _REGISTRY.lock:
        _REGISTRY.by_module.clear()


__all__ = (
    "ContributionError",
    "ModuleContributions",
    "NavContribution",
    "get_contributions",
    "list_contributions",
    "register_contributions",
    "reset_for_tests",
)
