from __future__ import annotations

import re
from collections import defaultdict, deque
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, replace

from govoplan_core.core.modules import (
    CapabilityFactory,
    DeleteVetoIssue,
    DeleteVetoProvider,
    DeleteVetoProviderRegistration,
    ModuleInterfaceProvider,
    ModuleInterfaceRequirement,
    ModuleContext,
    ModuleManifest,
    NavItem,
    PermissionDefinition,
    PublicFrontendRoute,
    ResourceAclProvider,
    RoleTemplate,
    SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION,
    SUPPORTED_MANIFEST_CONTRACT_VERSION,
    TenantSummaryBatchProvider,
    TenantSummaryProvider,
    user_workflow_scope_condition_issues,
)
from govoplan_core.core.ownership import (
    OwnershipProviderRegistration,
    ResourceOwnershipProvider,
)
from govoplan_core.core.provider_governance import (
    ExternalProviderDeclaration,
    ExternalProviderStateProviderRegistration,
    ModuleArchitectureDeclaration,
    module_architecture_issues,
)
from govoplan_core.core.versioning import format_version_range, version_range_is_valid, version_satisfies_range
from govoplan_core.core.search import (
    RegisteredSearchProvider,
    RegisteredSearchSourceProvider,
    SearchProvider,
    SearchSourceProvider,
)
from govoplan_core.core.views import (
    ViewSurface,
    module_view_surface_id,
    navigation_view_surface_id,
    route_view_surface_id,
    validate_view_surface_id,
)
from govoplan_core.core.workflows import (
    workflow_definition_contribution_hash,
)

_MODULE_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
_NPM_PACKAGE_RE = re.compile(r"^(?:@[a-z0-9][a-z0-9_.-]*/)?[a-z0-9][a-z0-9_.-]*$")
_SCOPE_RE = re.compile(r"^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$")
_WILDCARD_RE = re.compile(r"^([a-z][a-z0-9_]*|\*):\*$|^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*:\*$")
_INTERFACE_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")


class RegistryError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class RegistrySnapshot:
    manifests: tuple[ModuleManifest, ...]
    permissions: tuple[PermissionDefinition, ...]
    role_templates: tuple[RoleTemplate, ...]
    nav_items: tuple[NavItem, ...]


class PlatformRegistry:
    def __init__(self) -> None:
        self._manifests: dict[str, ModuleManifest] = {}
        self._tenant_summary_providers: dict[str, TenantSummaryProvider] = {}
        self._tenant_summary_batch_providers: dict[str, TenantSummaryBatchProvider] = {}
        self._delete_veto_providers: dict[str, list[DeleteVetoProviderRegistration]] = defaultdict(list)
        self._ownership_providers: dict[str, OwnershipProviderRegistration] = {}
        self._capability_factories: dict[str, CapabilityFactory] = {}
        self._capabilities: dict[str, object] = {}
        self._capability_context: ModuleContext | None = None
        self._search_provider_registrations: list[RegisteredSearchProvider] = []
        self._search_providers: dict[str, SearchProvider] = {}
        self._search_source_registrations: list[
            RegisteredSearchSourceProvider
        ] = []
        self._search_sources: dict[str, SearchSourceProvider] = {}

    def register(self, manifest: ModuleManifest) -> ModuleManifest:
        if manifest.id in self._manifests:
            raise RegistryError(f"Duplicate module id: {manifest.id}")
        self._manifests[manifest.id] = manifest
        for provider in manifest.tenant_summary_providers:
            self.register_tenant_summary_provider(manifest.id, provider)
        for provider in manifest.tenant_summary_batch_providers:
            self.register_tenant_summary_batch_provider(manifest.id, provider)
        for resource_type, providers in manifest.delete_veto_providers.items():
            for provider in providers:
                self.register_delete_veto(manifest.id, resource_type, provider)
        for registration in manifest.ownership_providers:
            self.register_ownership_provider(manifest.id, registration)
        for name, factory in manifest.capability_factories.items():
            self.register_capability_factory(manifest.id, name, factory)
        for registration in manifest.search_providers:
            self._search_provider_registrations.append(
                RegisteredSearchProvider(
                    module_id=manifest.id,
                    registration=registration,
                )
            )
        for registration in manifest.search_sources:
            self._search_source_registrations.append(
                RegisteredSearchSourceProvider(
                    module_id=manifest.id,
                    registration=registration,
                )
            )
        return manifest

    def replace(self, manifests: Iterable[ModuleManifest]) -> RegistrySnapshot:
        """Replace the active manifest set without replacing this registry object."""

        replacement = PlatformRegistry()
        for manifest in manifests:
            replacement.register(manifest)
        snapshot = replacement.validate()

        self._manifests = dict(replacement._manifests)
        self._tenant_summary_providers = dict(replacement._tenant_summary_providers)
        self._tenant_summary_batch_providers = dict(
            replacement._tenant_summary_batch_providers
        )
        self._delete_veto_providers = defaultdict(list, {
            resource_type: list(providers)
            for resource_type, providers in replacement._delete_veto_providers.items()
        })
        self._ownership_providers = dict(replacement._ownership_providers)
        self._capability_factories = dict(replacement._capability_factories)
        self._search_provider_registrations = list(
            replacement._search_provider_registrations
        )
        self._search_source_registrations = list(
            replacement._search_source_registrations
        )
        self._capabilities.clear()
        self._search_providers.clear()
        self._search_sources.clear()
        return snapshot

    def get(self, module_id: str) -> ModuleManifest | None:
        return self._manifests.get(module_id)

    def has_module(self, module_id: str) -> bool:
        return module_id in self._manifests

    def require_module(self, module_id: str) -> ModuleManifest:
        manifest = self.get(module_id)
        if manifest is None:
            raise RegistryError(f"Required module is not installed: {module_id}")
        return manifest

    def integration_enabled(self, module_id: str, dependency_id: str) -> bool:
        manifest = self.require_module(module_id)
        return dependency_id in manifest.dependencies or (dependency_id in manifest.optional_dependencies and self.has_module(dependency_id))

    def manifests(self) -> tuple[ModuleManifest, ...]:
        return tuple(self._topologically_sorted())

    def module_architectures(
        self,
    ) -> tuple[tuple[str, ModuleArchitectureDeclaration], ...]:
        return tuple(
            (manifest.id, manifest.architecture)
            for manifest in self.manifests()
            if manifest.architecture is not None
        )

    def external_provider_declarations(
        self,
    ) -> tuple[ExternalProviderDeclaration, ...]:
        return tuple(
            declaration
            for manifest in self.manifests()
            for declaration in manifest.external_providers
        )

    def external_provider_state_providers(
        self,
    ) -> tuple[ExternalProviderStateProviderRegistration, ...]:
        return tuple(
            registration
            for manifest in self.manifests()
            for registration in manifest.external_provider_state_providers
        )

    def permissions(self) -> tuple[PermissionDefinition, ...]:
        return tuple(permission for manifest in self.manifests() for permission in manifest.permissions)

    def role_templates(self) -> tuple[RoleTemplate, ...]:
        return tuple(template for manifest in self.manifests() for template in manifest.role_templates)

    def nav_items(self) -> tuple[NavItem, ...]:
        return tuple(sorted((item for manifest in self.manifests() for item in manifest.nav_items), key=lambda item: item.order))

    def view_surfaces(self) -> tuple[ViewSurface, ...]:
        return tuple(
            surface
            for manifest in self.manifests()
            for surface in manifest_view_surfaces(manifest)
        )

    def resource_acl_providers(self) -> tuple[ResourceAclProvider, ...]:
        return tuple(provider for manifest in self.manifests() for provider in manifest.resource_acl_providers)

    def register_ownership_provider(
        self,
        module_id: str,
        registration: OwnershipProviderRegistration,
    ) -> None:
        resource_type = registration.resource_type.strip().lower()
        if not resource_type:
            raise RegistryError(
                f"Ownership provider in {module_id} has no resource type"
            )
        if resource_type in self._ownership_providers:
            raise RegistryError(
                f"Duplicate ownership provider for resource type: {resource_type}"
            )
        if not isinstance(registration.provider, ResourceOwnershipProvider):
            raise RegistryError(
                f"Ownership provider for {resource_type} does not implement "
                "ResourceOwnershipProvider"
            )
        self._ownership_providers[resource_type] = OwnershipProviderRegistration(
            resource_type=resource_type,
            provider=registration.provider,
            module_id=module_id,
        )

    def ownership_provider(
        self,
        resource_type: str,
    ) -> ResourceOwnershipProvider | None:
        registration = self.ownership_provider_registration(resource_type)
        return registration.provider if registration else None

    def ownership_provider_registration(
        self,
        resource_type: str,
    ) -> OwnershipProviderRegistration | None:
        return self._ownership_providers.get(resource_type.strip().lower())

    def configure_capability_context(self, context: ModuleContext) -> None:
        self._capability_context = context
        self._capabilities.clear()
        self._search_providers.clear()
        self._search_sources.clear()

    def register_capability_factory(self, module_id: str, name: str, factory: CapabilityFactory) -> None:
        if name in self._capability_factories:
            raise RegistryError(f"Duplicate capability: {name}")
        self._capability_factories[name] = factory

    def has_capability(self, name: str) -> bool:
        return name in self._capability_factories

    def capability_names(self) -> tuple[str, ...]:
        return tuple(sorted(self._capability_factories))

    def capability(self, name: str) -> object | None:
        if name in self._capabilities:
            return self._capabilities[name]
        factory = self._capability_factories.get(name)
        if factory is None:
            return None
        if self._capability_context is None:
            raise RegistryError(f"Capability context is not configured: {name}")
        capability = factory(self._capability_context)
        self._capabilities[name] = capability
        return capability

    def require_capability(self, name: str) -> object:
        capability = self.capability(name)
        if capability is None:
            raise RegistryError(f"Required capability is not available: {name}")
        return capability

    def search_provider_registrations(
        self,
    ) -> tuple[RegisteredSearchProvider, ...]:
        return tuple(
            sorted(
                self._search_provider_registrations,
                key=lambda item: (
                    item.registration.order,
                    item.module_id,
                    item.registration.id,
                ),
            )
        )

    def search_providers(
        self,
    ) -> tuple[tuple[RegisteredSearchProvider, SearchProvider], ...]:
        if self._capability_context is None:
            if self._search_provider_registrations:
                raise RegistryError("Search provider context is not configured.")
            return ()
        providers: list[tuple[RegisteredSearchProvider, SearchProvider]] = []
        for registered in self.search_provider_registrations():
            key = f"{registered.module_id}:{registered.registration.id}"
            provider = self._search_providers.get(key)
            if provider is None:
                provider = registered.registration.create(self._capability_context)
                self._search_providers[key] = provider
            providers.append((registered, provider))
        return tuple(providers)

    def search_source_registrations(
        self,
    ) -> tuple[RegisteredSearchSourceProvider, ...]:
        return tuple(
            sorted(
                self._search_source_registrations,
                key=lambda item: (
                    item.registration.order,
                    item.module_id,
                    item.registration.id,
                ),
            )
        )

    def search_sources(
        self,
    ) -> tuple[
        tuple[RegisteredSearchSourceProvider, SearchSourceProvider],
        ...,
    ]:
        if self._capability_context is None:
            if self._search_source_registrations:
                raise RegistryError(
                    "Search source context is not configured."
                )
            return ()
        providers: list[
            tuple[RegisteredSearchSourceProvider, SearchSourceProvider]
        ] = []
        for registered in self.search_source_registrations():
            key = f"{registered.module_id}:{registered.registration.id}"
            provider = self._search_sources.get(key)
            if provider is None:
                provider = registered.registration.create(
                    self._capability_context
                )
                self._search_sources[key] = provider
            providers.append((registered, provider))
        return tuple(providers)

    def register_tenant_summary_provider(self, module_id: str, provider: TenantSummaryProvider) -> None:
        self._tenant_summary_providers[module_id] = provider

    def tenant_summary_providers(self) -> Mapping[str, TenantSummaryProvider]:
        return dict(self._tenant_summary_providers)

    def register_tenant_summary_batch_provider(
        self,
        module_id: str,
        provider: TenantSummaryBatchProvider,
    ) -> None:
        self._tenant_summary_batch_providers[module_id] = provider

    def tenant_summary_batch_providers(
        self,
    ) -> Mapping[str, TenantSummaryBatchProvider]:
        return dict(self._tenant_summary_batch_providers)

    def register_delete_veto(self, module_id: str, resource_type: str, provider: DeleteVetoProvider) -> None:
        self._delete_veto_providers[resource_type].append(DeleteVetoProviderRegistration(
            module_id=module_id,
            resource_type=resource_type,
            provider=provider,
        ))

    def delete_veto_providers(self, resource_type: str) -> tuple[DeleteVetoProvider, ...]:
        return tuple(registration.provider for registration in self._delete_veto_providers.get(resource_type, ()))

    def delete_veto_provider_registrations(self, resource_type: str) -> tuple[DeleteVetoProviderRegistration, ...]:
        return tuple(self._delete_veto_providers.get(resource_type, ()))

    def collect_delete_veto_issues(self, resource_type: str, session: object, tenant_id: str, resource_id: str) -> tuple[DeleteVetoIssue, ...]:
        issues: list[DeleteVetoIssue] = []
        for registration in self.delete_veto_provider_registrations(resource_type):
            try:
                result = registration.provider(session, tenant_id, resource_id)
            except Exception as exc:
                issues.append(DeleteVetoIssue(
                    severity="blocker",
                    code="module_veto",
                    message=str(exc),
                    module_id=registration.module_id,
                    details={
                        "resource_type": resource_type,
                        "resource_id": resource_id,
                    },
                ))
                continue
            issues.extend(_normalize_delete_veto_result(result, registration=registration, resource_id=resource_id))
        return tuple(issues)

    def validate(self) -> RegistrySnapshot:
        ordered = tuple(self._topologically_sorted())
        available_capabilities = set(self._capability_factories)
        for manifest in ordered:
            _validate_manifest_shape(manifest)
            _validate_manifest_relationships(
                manifest,
                known_modules=self._manifests,
                available_capabilities=available_capabilities,
            )
        permissions = _collect_manifest_permissions(ordered)
        _validate_public_frontend_route_uniqueness(ordered)
        _validate_interface_closure(ordered)
        _validate_role_template_scopes(ordered, known_scopes=set(permissions))

        return RegistrySnapshot(
            manifests=ordered,
            permissions=tuple(permissions.values()),
            role_templates=tuple(template for manifest in ordered for template in manifest.role_templates),
            nav_items=self.nav_items(),
        )

    def _topologically_sorted(self) -> Iterable[ModuleManifest]:
        incoming: dict[str, set[str]] = {module_id: set(manifest.dependencies) for module_id, manifest in self._manifests.items()}
        dependents: dict[str, set[str]] = defaultdict(set)
        for module_id, dependencies in incoming.items():
            for dependency in dependencies:
                dependents[dependency].add(module_id)

        ready = deque(sorted(module_id for module_id, dependencies in incoming.items() if not dependencies))
        ordered: list[str] = []
        while ready:
            module_id = ready.popleft()
            ordered.append(module_id)
            for dependent in sorted(dependents.get(module_id, ())):
                incoming[dependent].discard(module_id)
                if not incoming[dependent]:
                    ready.append(dependent)

        if len(ordered) != len(self._manifests):
            unresolved = ", ".join(sorted(module_id for module_id, dependencies in incoming.items() if dependencies))
            raise RegistryError(f"Module dependency cycle or unresolved dependency: {unresolved}")
        return (self._manifests[module_id] for module_id in ordered)


def manifest_view_surfaces(manifest: ModuleManifest) -> tuple[ViewSurface, ...]:
    frontend = manifest.frontend
    if frontend is None:
        return ()
    root_id = module_view_surface_id(manifest.id)
    surfaces = [
        ViewSurface(
            id=root_id,
            module_id=manifest.id,
            kind="module",
            label=manifest.name,
            order=min((item.order for item in frontend.nav_items), default=100),
        )
    ]
    surfaces.extend(
        ViewSurface(
            id=item.surface_id or navigation_view_surface_id(manifest.id, item.path),
            module_id=manifest.id,
            kind="navigation",
            label=item.label,
            parent_id=root_id,
            order=item.order,
        )
        for item in frontend.nav_items
    )
    surfaces.extend(
        ViewSurface(
            id=route.surface_id or route_view_surface_id(manifest.id, route.path),
            module_id=manifest.id,
            kind="route",
            label=route.component,
            parent_id=root_id,
            description=route.path,
            order=route.order,
        )
        for route in (*frontend.routes, *frontend.settings_routes)
    )
    surfaces.extend(
        replace(
            surface,
            parent_id=surface.parent_id or root_id,
        )
        for surface in frontend.view_surfaces
    )
    return tuple(surfaces)


def _normalize_delete_veto_result(
    result: object,
    *,
    registration: DeleteVetoProviderRegistration,
    resource_id: str,
) -> tuple[DeleteVetoIssue, ...]:
    if result is None:
        return ()
    if isinstance(result, DeleteVetoIssue):
        return (_attribute_delete_veto_issue(result, registration=registration, resource_id=resource_id),)
    if isinstance(result, str):
        return (DeleteVetoIssue(
            severity="blocker",
            code="module_veto",
            message=result,
            module_id=registration.module_id,
            details={
                "resource_type": registration.resource_type,
                "resource_id": resource_id,
            },
        ),)
    if isinstance(result, Iterable):
        return tuple(
            _attribute_delete_veto_issue(issue, registration=registration, resource_id=resource_id)
            for issue in result
            if isinstance(issue, DeleteVetoIssue)
        )
    return ()


def _attribute_delete_veto_issue(
    issue: DeleteVetoIssue,
    *,
    registration: DeleteVetoProviderRegistration,
    resource_id: str,
) -> DeleteVetoIssue:
    details = {
        "resource_type": registration.resource_type,
        "resource_id": resource_id,
        **dict(issue.details),
    }
    if issue.module_id == registration.module_id and issue.details == details:
        return issue
    return replace(issue, module_id=issue.module_id or registration.module_id, details=details)


def _validate_manifest_relationships(
    manifest: ModuleManifest,
    *,
    known_modules: Mapping[str, ModuleManifest],
    available_capabilities: set[str],
) -> None:
    for dependency in manifest.dependencies:
        if dependency not in known_modules:
            raise RegistryError(f"Module {manifest.id!r} depends on unknown module {dependency!r}")
    for capability in manifest.required_capabilities:
        if capability not in available_capabilities:
            raise RegistryError(f"Module {manifest.id!r} requires unavailable capability {capability!r}")
    for dependency in manifest.optional_dependencies:
        if dependency == manifest.id:
            raise RegistryError(f"Module {manifest.id!r} cannot list itself as an optional dependency")


def _collect_manifest_permissions(manifests: tuple[ModuleManifest, ...]) -> dict[str, PermissionDefinition]:
    permissions: dict[str, PermissionDefinition] = {}
    for manifest in manifests:
        for permission in manifest.permissions:
            _validate_manifest_permission(manifest, permission, permissions)
            permissions[permission.scope] = permission
    return permissions


def _validate_manifest_permission(
    manifest: ModuleManifest,
    permission: PermissionDefinition,
    seen_permissions: Mapping[str, PermissionDefinition],
) -> None:
    permission_namespace = manifest.permission_namespace or manifest.id
    if permission.module_id != permission_namespace:
        raise RegistryError(
            f"Permission {permission.scope!r} has mismatched module id "
            f"{permission.module_id!r}; expected {permission_namespace!r}"
        )
    if not _SCOPE_RE.match(permission.scope):
        raise RegistryError(f"Permission scope must be <module>:<resource>:<action>: {permission.scope!r}")
    expected_prefix = f"{permission.module_id}:{permission.resource}:"
    if not permission.scope.startswith(expected_prefix) or permission.scope.rsplit(":", 1)[-1] != permission.action:
        raise RegistryError(f"Permission fields do not match scope {permission.scope!r}")
    if permission.scope in seen_permissions:
        raise RegistryError(f"Duplicate permission scope: {permission.scope}")


def _validate_role_template_scopes(
    manifests: tuple[ModuleManifest, ...],
    *,
    known_scopes: set[str],
) -> None:
    seen_templates: dict[tuple[str, str], str] = {}
    for manifest in manifests:
        for template in manifest.role_templates:
            template_key = (template.level, template.slug)
            previous_module = seen_templates.get(template_key)
            if previous_module is not None:
                raise RegistryError(
                    f"Duplicate {template.level} role template slug {template.slug!r} "
                    f"in modules {previous_module!r} and {manifest.id!r}"
                )
            seen_templates[template_key] = manifest.id
            if template.default_authenticated and template.level != "tenant":
                raise RegistryError(
                    f"Default authenticated role template {template.slug!r} must be tenant-level"
                )
            if template.default_authenticated and not template.managed:
                raise RegistryError(
                    f"Default authenticated role template {template.slug!r} must be managed"
                )
            for scope in template.permissions:
                if template.default_authenticated and (
                    scope in {"*", "tenant:*", "system:*"}
                    or _WILDCARD_RE.match(scope)
                ):
                    raise RegistryError(
                        f"Default authenticated role template {template.slug!r} "
                        "must use explicit permissions, not wildcard scopes"
                    )
                if _role_template_scope_known(scope, known_scopes):
                    continue
                raise RegistryError(f"Role template {template.slug!r} references unknown permission {scope!r}")


def _role_template_scope_known(scope: str, known_scopes: set[str]) -> bool:
    if scope in {"*", "tenant:*", "system:*"}:
        return True
    if _WILDCARD_RE.match(scope):
        return True
    return scope in known_scopes


def _validate_manifest_shape(manifest: ModuleManifest) -> None:
    _validate_manifest_identity(manifest)
    _validate_manifest_contract_lists(manifest)
    _validate_manifest_overlaps(manifest)
    _validate_manifest_migration_spec(manifest)
    _validate_manifest_frontend(manifest)
    for item in manifest.nav_items:
        _validate_nav_item(manifest.id, item)
    for topic in manifest.documentation:
        if not version_range_is_valid(
            version_min=topic.version_min,
            version_max_exclusive=topic.version_max_exclusive,
        ):
            raise RegistryError(
                f"Module {manifest.id!r} documentation topic {topic.id!r} "
                "declares an empty version range"
            )
        for issue in user_workflow_scope_condition_issues(topic):
            raise RegistryError(
                f"Module {manifest.id!r} documentation topic {topic.id!r}: {issue}"
            )
    _validate_documentation_extensions(manifest)
    _validate_architecture_declarations(manifest)
    _validate_workflow_definition_contributions(manifest)


def _validate_architecture_declarations(manifest: ModuleManifest) -> None:
    architecture = manifest.architecture
    if architecture is not None:
        for issue in module_architecture_issues(
            architecture,
            has_migrations=manifest.migration_spec is not None,
        ):
            raise RegistryError(
                f"Module {manifest.id!r} architecture declaration: {issue}"
            )

    provider_ids: set[str] = set()
    declared_capabilities = {
        *manifest.required_capabilities,
        *manifest.optional_capabilities,
        *manifest.capability_factories,
    }
    declared_interfaces = {
        *(item.name for item in manifest.provides_interfaces),
        *(item.name for item in manifest.requires_interfaces),
    }
    operational_check_ids = {
        item.check_id for item in manifest.operational_check_providers
    }
    documentation_topic_ids = {item.id for item in manifest.documentation}
    ownership_resource_types = {
        item.resource_type for item in manifest.ownership_providers
    }
    provider_authority_modes: set[str] = set()

    for declaration in manifest.external_providers:
        if declaration.module_id != manifest.id:
            raise RegistryError(
                f"Provider declaration {declaration.id!r} belongs to "
                f"{declaration.module_id!r}, not module {manifest.id!r}"
            )
        if declaration.id in provider_ids:
            raise RegistryError(
                f"Module {manifest.id!r} declares duplicate external provider "
                f"{declaration.id!r}"
            )
        provider_ids.add(declaration.id)
        provider_authority_modes.update(declaration.authority_modes)
        _validate_provider_references(
            manifest.id,
            declaration,
            declared_capabilities=declared_capabilities,
            declared_interfaces=declared_interfaces,
            operational_check_ids=operational_check_ids,
            documentation_topic_ids=documentation_topic_ids,
            ownership_resource_types=ownership_resource_types,
        )

    state_provider_ids: set[str] = set()
    for registration in manifest.external_provider_state_providers:
        if registration.module_id != manifest.id:
            raise RegistryError(
                f"Provider state registration {registration.provider_id!r} belongs "
                f"to {registration.module_id!r}, not module {manifest.id!r}"
            )
        if registration.provider_id not in provider_ids:
            raise RegistryError(
                f"Module {manifest.id!r} registers runtime state for undeclared "
                f"provider {registration.provider_id!r}"
            )
        if registration.provider_id in state_provider_ids:
            raise RegistryError(
                f"Module {manifest.id!r} registers duplicate runtime state for "
                f"provider {registration.provider_id!r}"
            )
        state_provider_ids.add(registration.provider_id)

    missing_state_provider_ids = provider_ids - state_provider_ids
    if missing_state_provider_ids:
        raise RegistryError(
            f"Module {manifest.id!r} external providers require sanitized runtime "
            "state providers: " + ", ".join(sorted(missing_state_provider_ids))
        )

    if architecture is None:
        if manifest.external_providers or manifest.external_provider_state_providers:
            raise RegistryError(
                f"Module {manifest.id!r} declares external providers without a "
                "module architecture declaration"
            )
        return
    missing_modes = provider_authority_modes - set(
        architecture.supported_authority_modes
    )
    if missing_modes:
        raise RegistryError(
            f"Module {manifest.id!r} provider authority modes are not declared "
            "by its architecture metadata: " + ", ".join(sorted(missing_modes))
        )
    unknown_target_providers = set(
        architecture.target_tested_providers
    ) - provider_ids
    if unknown_target_providers:
        raise RegistryError(
            f"Module {manifest.id!r} target-tested providers are not declared: "
            + ", ".join(sorted(unknown_target_providers))
        )


def _validate_provider_references(
    module_id: str,
    declaration: ExternalProviderDeclaration,
    *,
    declared_capabilities: set[str],
    declared_interfaces: set[str],
    operational_check_ids: set[str],
    documentation_topic_ids: set[str],
    ownership_resource_types: set[str],
) -> None:
    references = (
        (
            "capabilities",
            set(declaration.capability_names),
            declared_capabilities,
        ),
        (
            "interfaces",
            set(declaration.interface_names),
            declared_interfaces,
        ),
        (
            "operational checks",
            set(declaration.operational_check_ids),
            operational_check_ids,
        ),
        (
            "documentation topics",
            set(declaration.documentation_topic_ids),
            documentation_topic_ids,
        ),
        (
            "ownership resource types",
            set(declaration.ownership_resource_types),
            ownership_resource_types,
        ),
    )
    for label, requested, available in references:
        missing = requested - available
        if missing:
            raise RegistryError(
                f"Module {module_id!r} provider {declaration.id!r} references "
                f"undeclared {label}: " + ", ".join(sorted(missing))
            )


def _validate_workflow_definition_contributions(
    manifest: ModuleManifest,
) -> None:
    seen_keys: set[str] = set()
    for contribution in manifest.workflow_definitions:
        if contribution.origin_module_id != manifest.id:
            raise RegistryError(
                f"Workflow contribution {contribution.definition_key!r} in "
                f"module {manifest.id!r} declares origin module "
                f"{contribution.origin_module_id!r}"
            )
        if contribution.origin_module_version != manifest.version:
            raise RegistryError(
                f"Workflow contribution {contribution.definition_key!r} in "
                f"module {manifest.id!r} declares origin version "
                f"{contribution.origin_module_version!r}, expected "
                f"{manifest.version!r}"
            )
        if not re.match(
            r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,119}$",
            contribution.definition_key,
        ):
            raise RegistryError(
                f"Workflow contribution key is invalid: "
                f"{contribution.definition_key!r}"
            )
        if contribution.definition_key in seen_keys:
            raise RegistryError(
                f"Module {manifest.id!r} declares duplicate Workflow "
                f"contribution {contribution.definition_key!r}"
            )
        seen_keys.add(contribution.definition_key)
        if contribution.scope_type not in {"system", "tenant"}:
            raise RegistryError(
                f"Workflow contribution {contribution.definition_key!r} has "
                f"unsupported scope {contribution.scope_type!r}"
            )
        expected_hash = workflow_definition_contribution_hash(contribution)
        if (
            contribution.content_hash is not None
            and contribution.content_hash != expected_hash
        ):
            raise RegistryError(
                f"Workflow contribution {contribution.definition_key!r} has "
                "a content hash that does not match its canonical content"
            )


def _validate_documentation_extensions(manifest: ModuleManifest) -> None:
    for capability, metadata in manifest.capability_documentation.items():
        if capability not in manifest.capability_factories:
            raise RegistryError(
                f"Module {manifest.id!r} documents capability {capability!r} "
                "but does not provide it"
            )
        if not metadata.label.strip():
            raise RegistryError(
                f"Module {manifest.id!r} capability {capability!r} "
                "documentation must have a label"
            )
        if not metadata.summary.strip():
            raise RegistryError(
                f"Module {manifest.id!r} capability {capability!r} "
                "documentation must have a summary"
            )
        if metadata.contract_version is not None and not metadata.contract_version.strip():
            raise RegistryError(
                f"Module {manifest.id!r} capability {capability!r} "
                "documentation contract version must not be empty"
            )

    provider_keys: set[str] = set()
    for registration in manifest.documentation_configuration_providers:
        if not registration.keys:
            raise RegistryError(
                f"Module {manifest.id!r} documentation configuration provider must declare keys"
            )
        for raw_key in registration.keys:
            key = raw_key.strip()
            if not key:
                raise RegistryError(
                    f"Module {manifest.id!r} documentation configuration provider declares an empty key"
                )
            if key in provider_keys:
                raise RegistryError(
                    f"Module {manifest.id!r} declares duplicate documentation configuration key {key!r}"
                )
            provider_keys.add(key)

    source_ids: set[str] = set()
    for source in manifest.documentation_sources:
        if not _INTERFACE_NAME_RE.match(source.id):
            raise RegistryError(
                f"Module {manifest.id!r} documentation source id must be namespaced: {source.id!r}"
            )
        if source.id in source_ids:
            raise RegistryError(
                f"Module {manifest.id!r} declares duplicate documentation source {source.id!r}"
            )
        source_ids.add(source.id)
        if not source.label.strip():
            raise RegistryError(
                f"Module {manifest.id!r} documentation source {source.id!r} must have a label"
            )


def _validate_manifest_identity(manifest: ModuleManifest) -> None:
    if not _MODULE_ID_RE.match(manifest.id):
        raise RegistryError(f"Module manifest id must match {_MODULE_ID_RE.pattern}: {manifest.id!r}")
    if not manifest.name.strip():
        raise RegistryError(f"Module {manifest.id!r} must declare a non-empty name")
    if not manifest.version.strip():
        raise RegistryError(f"Module {manifest.id!r} must declare a non-empty version")
    if (
        manifest.permission_namespace is not None
        and not _MODULE_ID_RE.match(manifest.permission_namespace)
    ):
        raise RegistryError(
            f"Module {manifest.id!r} has invalid permission namespace "
            f"{manifest.permission_namespace!r}"
        )
    if manifest.compatibility.manifest_contract_version != SUPPORTED_MANIFEST_CONTRACT_VERSION:
        raise RegistryError(
            f"Module {manifest.id!r} uses unsupported manifest contract version "
            f"{manifest.compatibility.manifest_contract_version!r}; supported version is "
            f"{SUPPORTED_MANIFEST_CONTRACT_VERSION!r}"
        )


def _validate_manifest_contract_lists(manifest: ModuleManifest) -> None:
    _validate_dependency_list(manifest.id, "dependencies", manifest.dependencies)
    _validate_dependency_list(manifest.id, "optional_dependencies", manifest.optional_dependencies)
    _validate_capability_list(manifest.id, "required_capabilities", manifest.required_capabilities)
    _validate_capability_list(manifest.id, "optional_capabilities", manifest.optional_capabilities)
    _validate_interface_providers(manifest.id, manifest.provides_interfaces)
    _validate_interface_requirements(manifest.id, manifest.requires_interfaces)
    provider_ids: set[str] = set()
    for registration in manifest.search_providers:
        if not _INTERFACE_NAME_RE.match(registration.id):
            raise RegistryError(
                f"Module {manifest.id!r} search provider id must be namespaced: "
                f"{registration.id!r}"
            )
        if registration.id in provider_ids:
            raise RegistryError(
                f"Module {manifest.id!r} declares duplicate search provider "
                f"{registration.id!r}"
            )
        provider_ids.add(registration.id)
    source_ids: set[str] = set()
    for registration in manifest.search_sources:
        if not _INTERFACE_NAME_RE.match(registration.id):
            raise RegistryError(
                f"Module {manifest.id!r} search source id must be "
                f"namespaced: {registration.id!r}"
            )
        if registration.id in source_ids:
            raise RegistryError(
                f"Module {manifest.id!r} declares duplicate search source "
                f"{registration.id!r}"
            )
        source_ids.add(registration.id)


def _validate_manifest_overlaps(manifest: ModuleManifest) -> None:
    overlap = set(manifest.dependencies) & set(manifest.optional_dependencies)
    if overlap:
        joined = ", ".join(sorted(overlap))
        raise RegistryError(f"Module {manifest.id!r} lists dependencies as both required and optional: {joined}")
    capability_overlap = set(manifest.required_capabilities) & set(manifest.optional_capabilities)
    if capability_overlap:
        joined = ", ".join(sorted(capability_overlap))
        raise RegistryError(f"Module {manifest.id!r} lists capabilities as both required and optional: {joined}")


def _validate_manifest_migration_spec(manifest: ModuleManifest) -> None:
    if manifest.migration_spec is not None:
        if manifest.migration_spec.module_id != manifest.id:
            raise RegistryError(f"Module {manifest.id!r} has migration spec for {manifest.migration_spec.module_id!r}")
        if manifest.migration_spec.metadata is None and not manifest.migration_spec.script_location:
            raise RegistryError(f"Module {manifest.id!r} migration spec must declare metadata or script location")


def _validate_manifest_frontend(manifest: ModuleManifest) -> None:
    if manifest.frontend is None:
        return
    frontend = manifest.frontend
    if frontend.module_id != manifest.id:
        raise RegistryError(f"Module {manifest.id!r} has frontend metadata for {frontend.module_id!r}")
    if frontend.asset_manifest_contract_version != SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION:
        raise RegistryError(
            f"Module {manifest.id!r} uses unsupported frontend asset manifest contract version "
            f"{frontend.asset_manifest_contract_version!r}; supported version is "
            f"{SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION!r}"
        )
    if frontend.package_name is not None and not _NPM_PACKAGE_RE.match(frontend.package_name):
        raise RegistryError(f"Module {manifest.id!r} has invalid frontend package name {frontend.package_name!r}")
    for route in (*frontend.routes, *frontend.settings_routes, *frontend.public_routes):
        _validate_frontend_route(manifest.id, route.path, route.component)
    for route in (*frontend.routes, *frontend.settings_routes):
        if route.surface_id is not None:
            _validate_view_surface_id(manifest.id, route.surface_id)
    for item in frontend.nav_items:
        _validate_nav_item(manifest.id, item)
        if item.surface_id is not None:
            _validate_view_surface_id(manifest.id, item.surface_id)
    _validate_view_surfaces(manifest)


def _validate_view_surfaces(manifest: ModuleManifest) -> None:
    frontend = manifest.frontend
    if frontend is None:
        return
    root_id = module_view_surface_id(manifest.id)
    known_ids = {root_id}
    _validate_view_surface_id(manifest.id, root_id)
    for item in frontend.nav_items:
        surface_id = item.surface_id or navigation_view_surface_id(manifest.id, item.path)
        _register_view_surface_id(manifest.id, surface_id, known_ids)
    for route in (*frontend.routes, *frontend.settings_routes):
        surface_id = route.surface_id or route_view_surface_id(manifest.id, route.path)
        _register_view_surface_id(manifest.id, surface_id, known_ids)
    for surface in frontend.view_surfaces:
        _validate_declared_view_surface(manifest.id, surface)
        _register_view_surface_id(manifest.id, surface.id, known_ids)
    for surface in frontend.view_surfaces:
        _validate_view_surface_definition(
            manifest.id,
            surface,
            root_id=root_id,
            known_ids=known_ids,
        )
    parent_by_id = {
        surface.id: surface.parent_id or root_id
        for surface in frontend.view_surfaces
    }
    _validate_view_surface_hierarchy(manifest.id, parent_by_id)


def _register_view_surface_id(
    module_id: str,
    surface_id: str,
    known_ids: set[str],
) -> None:
    _validate_view_surface_id(module_id, surface_id)
    if surface_id in known_ids:
        raise RegistryError(
            f"Duplicate view surface id {surface_id!r} in module {module_id!r}"
        )
    known_ids.add(surface_id)


def _validate_declared_view_surface(
    module_id: str,
    surface: ViewSurface,
) -> None:
    if surface.module_id != module_id:
        raise RegistryError(
            f"View surface {surface.id!r} belongs to {surface.module_id!r}, "
            f"not module {module_id!r}"
        )


def _validate_view_surface_definition(
    module_id: str,
    surface: ViewSurface,
    *,
    root_id: str,
    known_ids: set[str],
) -> None:
    parent_id = surface.parent_id or root_id
    if parent_id not in known_ids:
        raise RegistryError(
            f"View surface {surface.id!r} in module {module_id!r} "
            f"references unknown parent {parent_id!r}"
        )
    if surface.required and not surface.default_visible:
        raise RegistryError(
            f"Required view surface {surface.id!r} in module {module_id!r} "
            "must be visible by default"
        )


def _validate_view_surface_hierarchy(
    module_id: str,
    parent_by_id: Mapping[str, str],
) -> None:
    for surface_id in parent_by_id:
        path: set[str] = set()
        current_id: str | None = surface_id
        while current_id in parent_by_id:
            if current_id in path:
                raise RegistryError(
                    f"View surface hierarchy in module {module_id!r} "
                    f"contains a cycle at {current_id!r}"
                )
            path.add(current_id)
            current_id = parent_by_id[current_id]


def _validate_view_surface_id(module_id: str, surface_id: str) -> None:
    if not validate_view_surface_id(surface_id):
        raise RegistryError(
            f"View surface id for module {module_id!r} is invalid: {surface_id!r}"
        )
    if not surface_id.startswith(f"{module_id}."):
        raise RegistryError(
            f"View surface id for module {module_id!r} must start with "
            f"{module_id!r} followed by '.': {surface_id!r}"
        )


def _validate_frontend_route(module_id: str, path: str, component: str) -> None:
    if not path.startswith("/"):
        raise RegistryError(f"Frontend route for module {module_id!r} must start with '/': {path!r}")
    if not component.strip():
        raise RegistryError(f"Frontend route {path!r} for module {module_id!r} must declare a component")


def _validate_public_frontend_route_uniqueness(
    manifests: tuple[ModuleManifest, ...],
) -> None:
    owners: dict[str, tuple[str, PublicFrontendRoute]] = {}
    for manifest in manifests:
        if manifest.frontend is None:
            continue
        for route in manifest.frontend.public_routes:
            previous = owners.get(route.path)
            if previous is not None:
                previous_module, _previous_route = previous
                raise RegistryError(
                    f"Duplicate public frontend route {route.path!r} in modules "
                    f"{previous_module!r} and {manifest.id!r}"
                )
            owners[route.path] = (manifest.id, route)


def _validate_interface_closure(manifests: tuple[ModuleManifest, ...]) -> None:
    providers: dict[str, list[tuple[ModuleManifest, ModuleInterfaceProvider]]] = defaultdict(list)
    for manifest in manifests:
        for provided in manifest.provides_interfaces:
            providers[provided.name].append((manifest, provided))

    for manifest in manifests:
        for requirement in manifest.requires_interfaces:
            available = providers.get(requirement.name, [])
            matching = [
                (provider_manifest, provided)
                for provider_manifest, provided in available
                if version_satisfies_range(
                    provided.version,
                    version_min=requirement.version_min,
                    version_max_exclusive=requirement.version_max_exclusive,
                )
            ]
            if matching:
                continue
            version_range = format_version_range(
                version_min=requirement.version_min,
                version_max_exclusive=requirement.version_max_exclusive,
            )
            if not available:
                if requirement.optional:
                    continue
                raise RegistryError(
                    f"Module {manifest.id!r} requires unavailable interface "
                    f"{requirement.name!r} ({version_range})"
                )
            available_versions = ", ".join(
                f"{provider_manifest.id}@{provided.version}"
                for provider_manifest, provided in available
            )
            raise RegistryError(
                f"Module {manifest.id!r} requires interface {requirement.name!r} "
                f"({version_range}) but available providers are {available_versions}"
            )


def _validate_dependency_list(module_id: str, field_name: str, dependencies: tuple[str, ...]) -> None:
    if len(dependencies) != len(set(dependencies)):
        raise RegistryError(f"Module {module_id!r} has duplicate {field_name}")
    for dependency in dependencies:
        if dependency == module_id:
            raise RegistryError(f"Module {module_id!r} cannot depend on itself")
        if not _MODULE_ID_RE.match(dependency):
            raise RegistryError(f"Module {module_id!r} has invalid dependency id {dependency!r}")


def _validate_capability_list(module_id: str, field_name: str, capabilities: tuple[str, ...]) -> None:
    if len(capabilities) != len(set(capabilities)):
        raise RegistryError(f"Module {module_id!r} has duplicate {field_name}")
    for capability in capabilities:
        if not capability.strip() or any(part == "" for part in capability.split(".")):
            raise RegistryError(f"Module {module_id!r} has invalid capability name {capability!r}")


def _validate_interface_providers(module_id: str, providers: tuple[ModuleInterfaceProvider, ...]) -> None:
    names = [provider.name for provider in providers]
    if len(names) != len(set(names)):
        raise RegistryError(f"Module {module_id!r} has duplicate provided interface names")
    for provider in providers:
        _validate_interface_name(module_id, provider.name)
        if not provider.version.strip():
            raise RegistryError(f"Module {module_id!r} provides interface {provider.name!r} without a version")


def _validate_interface_requirements(module_id: str, requirements: tuple[ModuleInterfaceRequirement, ...]) -> None:
    names = [requirement.name for requirement in requirements]
    if len(names) != len(set(names)):
        raise RegistryError(f"Module {module_id!r} has duplicate required interface names")
    for requirement in requirements:
        _validate_interface_name(module_id, requirement.name)
        if requirement.version_min is not None and not requirement.version_min.strip():
            raise RegistryError(f"Module {module_id!r} has blank minimum version for interface {requirement.name!r}")
        if requirement.version_max_exclusive is not None and not requirement.version_max_exclusive.strip():
            raise RegistryError(f"Module {module_id!r} has blank maximum version for interface {requirement.name!r}")
        if not version_range_is_valid(
            version_min=requirement.version_min,
            version_max_exclusive=requirement.version_max_exclusive,
        ):
            version_range = format_version_range(
                version_min=requirement.version_min,
                version_max_exclusive=requirement.version_max_exclusive,
            )
            raise RegistryError(f"Module {module_id!r} has invalid interface range {version_range!r}")


def _validate_interface_name(module_id: str, name: str) -> None:
    if not _INTERFACE_NAME_RE.match(name):
        raise RegistryError(f"Module {module_id!r} has invalid interface name {name!r}")


def _validate_nav_item(module_id: str, item: NavItem) -> None:
    if not item.path.startswith("/"):
        raise RegistryError(f"Navigation item for module {module_id!r} must start with '/': {item.path!r}")
    if not item.label.strip():
        raise RegistryError(f"Navigation item {item.path!r} for module {module_id!r} must declare a label")
    if item.icon is not None and not item.icon.strip():
        raise RegistryError(f"Navigation item {item.path!r} for module {module_id!r} has an empty icon name")
