# app/modules/base.py
"""
Base module definition class.

A Module is a self-contained unit of functionality that can be enabled/disabled
per platform. Each module contains:
- Features: Granular capabilities for tier-based access control
- Menu items: Sidebar entries per frontend type
- Routes: API and page routes (future: dynamically registered)
- Services: Business logic (optional, for self-contained modules)
- Models: Database models (optional, for self-contained modules)
- Schemas: Pydantic schemas (optional, for self-contained modules)
- Templates: Jinja2 templates (optional, for self-contained modules)
- Migrations: Database migrations (optional, for self-contained modules)

Module Classification:
- Core modules: Always enabled, cannot be disabled (core, tenancy, cms, customers)
- Optional modules: Can be enabled/disabled per platform
- Internal modules: Admin-only tools, not customer-facing (dev-tools, monitoring)

Self-Contained Module Structure:
    app/modules/<code>/
    ├── __init__.py
    ├── definition.py        # ModuleDefinition instance
    ├── config.py            # Module configuration schema (optional)
    ├── exceptions.py        # Module-specific exceptions (optional)
    ├── routes/              # FastAPI routers
    ├── services/            # Business logic
    ├── tasks/               # Celery background tasks (optional)
    │   └── __init__.py      # Task module discovery marker
    ├── models/              # SQLAlchemy models
    ├── schemas/             # Pydantic schemas
    ├── migrations/          # Alembic migrations for this module
    │   └── versions/        # Migration scripts
    ├── templates/           # Jinja2 templates (namespaced)
    └── locales/             # Translation files
"""

from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from fastapi import APIRouter
    from pydantic import BaseModel

    from app.modules.contracts.audit import AuditProviderProtocol
    from app.modules.contracts.cms import MediaUsageProviderProtocol
    from app.modules.contracts.features import FeatureProviderProtocol
    from app.modules.contracts.metrics import MetricsProviderProtocol
    from app.modules.contracts.onboarding import OnboardingProviderProtocol
    from app.modules.contracts.widgets import DashboardWidgetProviderProtocol

from app.modules.enums import FrontendType

# =============================================================================
# Menu Item Definitions
# =============================================================================


@dataclass
class MenuItemDefinition:
    """
    Definition of a single menu item within a section.

    Attributes:
        id: Unique identifier (e.g., "catalog.products", "orders.list")
        label_key: i18n key for the menu item label
        icon: Lucide icon name (e.g., "box", "shopping-cart")
        route: URL path (can include placeholders like {store_code})
        order: Sort order within section (lower = higher priority)
        is_mandatory: If True, cannot be hidden by user preferences
        requires_permission: Permission code required to see this item
        badge_source: Key for dynamic badge count (e.g., "pending_orders_count")
        is_super_admin_only: Only visible to super admins (admin frontend only)

    Example:
        MenuItemDefinition(
            id="catalog.products",
            label_key="catalog.menu.products",
            icon="box",
            route="/admin/catalog/products",
            order=10,
            is_mandatory=True
        )
    """

    id: str
    label_key: str
    icon: str
    route: str
    order: int = 100
    is_mandatory: bool = False
    requires_permission: str | None = None
    badge_source: str | None = None
    is_super_admin_only: bool = False
    header_template: str | None = None  # Optional partial for custom header rendering


@dataclass
class MenuSectionDefinition:
    """
    Definition of a menu section containing related menu items.

    Sections group related menu items together in the sidebar.
    A section can be collapsed/expanded by the user.

    Attributes:
        id: Unique section identifier (e.g., "catalog", "orders")
        label_key: i18n key for section header (None for headerless sections)
        icon: Lucide icon name for section (optional)
        order: Sort order among sections (lower = higher priority)
        items: List of menu items in this section
        is_super_admin_only: Only visible to super admins
        is_collapsible: Whether section can be collapsed

    Example:
        MenuSectionDefinition(
            id="catalog",
            label_key="catalog.menu.section",
            icon="package",
            order=20,
            items=[
                MenuItemDefinition(
                    id="catalog.products",
                    label_key="catalog.menu.products",
                    icon="box",
                    route="/admin/catalog/products",
                    order=10
                ),
            ]
        )
    """

    id: str
    label_key: str | None
    icon: str | None = None
    order: int = 100
    items: list[MenuItemDefinition] = field(default_factory=list)
    is_super_admin_only: bool = False
    is_collapsible: bool = True


# =============================================================================
# Permission Definitions
# =============================================================================


@dataclass
class PermissionDefinition:
    """
    Definition of a permission that a module exposes.

    Permissions are granular capabilities that can be assigned to roles/users.
    Each module defines its own permissions, which are then discovered and
    aggregated by the tenancy module for role assignment.

    Attributes:
        id: Unique identifier in format "resource.action" (e.g., "products.view")
        label_key: i18n key for the permission label
        description_key: i18n key for permission description
        category: Grouping category for UI organization (e.g., "products", "orders")
        is_owner_only: If True, only store owners can have this permission

    Example:
        PermissionDefinition(
            id="products.view",
            label_key="catalog.permissions.products_view",
            description_key="catalog.permissions.products_view_desc",
            category="products",
        )
    """

    id: str
    label_key: str
    description_key: str = ""
    category: str = "general"
    is_owner_only: bool = False


# =============================================================================
# Scheduled Task Definitions
# =============================================================================


@dataclass
class ScheduledTask:
    """
    Definition of a Celery Beat scheduled task.

    Used in ModuleDefinition to declare scheduled tasks that should be
    registered with Celery Beat when the module is loaded.

    Attributes:
        name: Unique name for the schedule entry (e.g., "billing.reset_counters")
        task: Full Python path to the task (e.g., "app.modules.billing.tasks.subscription.reset_period_counters")
        schedule: Cron expression string or crontab dict
            - String format: "minute hour day_of_month month day_of_week" (e.g., "5 0 * * *")
            - Dict format: {"minute": 5, "hour": 0} for crontab kwargs
        args: Positional arguments to pass to the task
        kwargs: Keyword arguments to pass to the task
        options: Celery task options (e.g., {"queue": "scheduled"})

    Example:
        ScheduledTask(
            name="billing.reset_period_counters",
            task="app.modules.billing.tasks.subscription.reset_period_counters",
            schedule="5 0 * * *",  # Daily at 00:05
            options={"queue": "scheduled"},
        )
    """

    name: str
    task: str
    schedule: str | dict[str, Any]
    args: tuple = ()
    kwargs: dict[str, Any] = field(default_factory=dict)
    options: dict[str, Any] = field(default_factory=dict)


@dataclass
class ModuleDefinition:
    """
    Definition of a platform module.

    A module groups related functionality that can be enabled/disabled per platform.
    Core modules cannot be disabled and are always available.

    Self-contained modules include their own services, models, schemas, templates,
    and migrations. The path attributes describe where these components are located.

    Attributes:
        # Identity
        code: Unique identifier (e.g., "billing", "marketplace")
        name: Display name (e.g., "Billing & Subscriptions")
        description: Description of what this module provides
        version: Semantic version of the module (e.g., "1.0.0")

        # Dependencies
        requires: List of module codes this module depends on

        # Components
        features: List of feature codes this module provides
        menu_items: Dict mapping FrontendType to list of menu item IDs
        permissions: List of permission codes this module defines

        # Classification
        is_core: Core modules cannot be disabled
        is_internal: Internal modules are admin-only (not customer-facing)

        # Configuration
        config_schema: Pydantic model class for module configuration
        default_config: Default configuration values

        # Routes
        admin_router: FastAPI router for admin routes
        store_router: FastAPI router for store routes

        # Lifecycle hooks
        on_enable: Called when module is enabled for a platform
        on_disable: Called when module is disabled for a platform
        on_startup: Called when application starts (for enabled modules)
        health_check: Called to check module health status

        # Self-contained module paths (optional)
        is_self_contained: Whether module uses self-contained structure
        services_path: Path to services subpackage
        models_path: Path to models subpackage
        schemas_path: Path to schemas subpackage
        templates_path: Path to templates directory (relative to module)
        exceptions_path: Path to exceptions module
        locales_path: Path to locales directory (relative to module)
        migrations_path: Path to migrations directory (relative to module)

    Example (traditional thin wrapper):
        billing_module = ModuleDefinition(
            code="billing",
            name="Billing & Subscriptions",
            description="Subscription tiers, billing history, and payment processing",
            features=["subscription_management", "billing_history", "stripe_integration"],
            menu_items={
                FrontendType.ADMIN: ["subscription-tiers", "subscriptions", "billing-history"],
                FrontendType.STORE: ["billing"],
            },
        )

    Example (self-contained module with configuration):
        from pydantic import BaseModel, Field

        class CMSConfig(BaseModel):
            max_pages: int = Field(default=100, ge=1)
            enable_seo: bool = True

        cms_module = ModuleDefinition(
            code="cms",
            name="Content Management",
            version="1.0.0",
            features=["cms_basic", "cms_custom_pages"],
            config_schema=CMSConfig,
            default_config={"max_pages": 100, "enable_seo": True},
            is_self_contained=True,
            services_path="app.modules.cms.services",
            models_path="app.modules.cms.models",
            migrations_path="migrations",
            health_check=lambda: {"status": "healthy"},
        )
    """

    # =========================================================================
    # Identity
    # =========================================================================
    code: str
    name: str
    description: str = ""
    version: str = "1.0.0"

    # =========================================================================
    # Dependencies
    # =========================================================================
    requires: list[str] = field(default_factory=list)

    # =========================================================================
    # Components
    # =========================================================================
    features: list[str] = field(default_factory=list)
    menu_items: dict[FrontendType, list[str]] = field(default_factory=dict)
    permissions: list[PermissionDefinition] = field(default_factory=list)

    # =========================================================================
    # Menu Definitions (Module-Driven Menus)
    # =========================================================================
    # NEW: Full menu definitions per frontend type. When set, these take
    # precedence over menu_items for menu rendering. This enables modules
    # to fully define their own menu structure with icons, routes, and labels.
    menus: dict[FrontendType, list[MenuSectionDefinition]] = field(default_factory=dict)

    # =========================================================================
    # Classification
    # =========================================================================
    is_core: bool = False
    is_internal: bool = False

    # =========================================================================
    # Configuration
    # =========================================================================
    config_schema: "type[BaseModel] | None" = None
    default_config: dict[str, Any] = field(default_factory=dict)

    # =========================================================================
    # Routes (registered dynamically)
    # =========================================================================
    admin_router: "APIRouter | None" = None
    store_router: "APIRouter | None" = None

    # =========================================================================
    # Lifecycle Hooks
    # =========================================================================
    on_enable: Callable[[int], None] | None = None  # Called with platform_id
    on_disable: Callable[[int], None] | None = None  # Called with platform_id
    on_startup: Callable[[], None] | None = None  # Called on app startup
    health_check: Callable[[], dict[str, Any]] | None = None  # Returns health status

    # =========================================================================
    # Self-Contained Module Paths (optional)
    # =========================================================================
    is_self_contained: bool = False
    services_path: str | None = None
    models_path: str | None = None
    schemas_path: str | None = None
    templates_path: str | None = None  # Relative to module directory
    exceptions_path: str | None = None
    locales_path: str | None = None  # Relative to module directory
    migrations_path: str | None = None  # Relative to module directory, e.g., "migrations"

    # =========================================================================
    # Celery Tasks (optional)
    # =========================================================================
    tasks_path: str | None = None  # Python import path, e.g., "app.modules.billing.tasks"
    scheduled_tasks: list[ScheduledTask] = field(default_factory=list)

    # =========================================================================
    # Context Providers (Module-Driven Page Context)
    # =========================================================================
    # Callables that provide context data for page templates per frontend type.
    # Each provider receives (request, db, platform) and returns a dict.
    # This enables modules to contribute context only when enabled for a platform.
    #
    # Example:
    #     def get_billing_platform_context(request, db, platform):
    #         from app.modules.billing.models import TIER_LIMITS
    #         return {"tiers": get_tiers_data()}
    #
    #     billing_module = ModuleDefinition(
    #         code="billing",
    #         context_providers={
    #             FrontendType.PLATFORM: get_billing_platform_context,
    #         },
    #     )
    context_providers: dict[FrontendType, Callable[..., dict[str, Any]]] = field(default_factory=dict)

    # =========================================================================
    # Metrics Provider (Module-Driven Statistics)
    # =========================================================================
    # Callable that returns a MetricsProviderProtocol implementation.
    # Use a callable (factory function) to enable lazy loading and avoid
    # circular imports. Each module can provide its own metrics for dashboards.
    #
    # Example:
    #     def _get_metrics_provider():
    #         from app.modules.orders.services.order_metrics import order_metrics_provider
    #         return order_metrics_provider
    #
    #     orders_module = ModuleDefinition(
    #         code="orders",
    #         metrics_provider=_get_metrics_provider,
    #     )
    #
    # The provider will be discovered by core's StatsAggregator service.
    metrics_provider: "Callable[[], MetricsProviderProtocol] | None" = None

    # =========================================================================
    # Widget Provider (Module-Driven Dashboard Widgets)
    # =========================================================================
    # Callable that returns a DashboardWidgetProviderProtocol implementation.
    # Use a callable (factory function) to enable lazy loading and avoid
    # circular imports. Each module can provide its own widgets for dashboards.
    #
    # Example:
    #     def _get_widget_provider():
    #         from app.modules.orders.services.order_widgets import order_widget_provider
    #         return order_widget_provider
    #
    #     orders_module = ModuleDefinition(
    #         code="orders",
    #         widget_provider=_get_widget_provider,
    #     )
    #
    # The provider will be discovered by core's WidgetAggregator service.
    widget_provider: "Callable[[], DashboardWidgetProviderProtocol] | None" = None

    # =========================================================================
    # Audit Provider (Module-Driven Audit Logging)
    # =========================================================================
    # Callable that returns an AuditProviderProtocol implementation.
    # Use a callable (factory function) to enable lazy loading and avoid
    # circular imports. Modules can provide audit logging backends.
    #
    # Example:
    #     def _get_audit_provider():
    #         from app.modules.monitoring.services.audit_provider import audit_provider
    #         return audit_provider
    #
    #     monitoring_module = ModuleDefinition(
    #         code="monitoring",
    #         audit_provider=_get_audit_provider,
    #     )
    #
    # The provider will be discovered by core's AuditAggregator service.
    audit_provider: "Callable[[], AuditProviderProtocol] | None" = None

    # =========================================================================
    # Feature Provider (Module-Driven Billable Features)
    # =========================================================================
    # Callable that returns a FeatureProviderProtocol implementation.
    # Use a callable (factory function) to enable lazy loading and avoid
    # circular imports. Each module can declare its billable features
    # and provide usage tracking for limit enforcement.
    #
    # Example:
    #     def _get_feature_provider():
    #         from app.modules.catalog.services.catalog_features import catalog_feature_provider
    #         return catalog_feature_provider
    #
    #     catalog_module = ModuleDefinition(
    #         code="catalog",
    #         feature_provider=_get_feature_provider,
    #     )
    #
    # The provider will be discovered by billing's FeatureAggregator service.
    feature_provider: "Callable[[], FeatureProviderProtocol] | None" = None

    # =========================================================================
    # Media Usage Provider (Module-Driven Media Usage Tracking)
    # =========================================================================
    # Callable that returns a MediaUsageProviderProtocol implementation.
    # Modules that use media files (catalog, etc.) can register a provider
    # to report where media is being used.
    media_usage_provider: "Callable[[], MediaUsageProviderProtocol] | None" = None

    # =========================================================================
    # Onboarding Provider (Module-Driven Post-Signup Onboarding)
    # =========================================================================
    # Callable that returns an OnboardingProviderProtocol implementation.
    # Modules declare onboarding steps (what needs to be configured after signup)
    # and provide completion checks. The core module's OnboardingAggregator
    # discovers and aggregates all providers into a dashboard checklist banner.
    #
    # Example:
    #     def _get_onboarding_provider():
    #         from app.modules.marketplace.services.marketplace_onboarding_service import (
    #             marketplace_onboarding_provider,
    #         )
    #         return marketplace_onboarding_provider
    #
    #     marketplace_module = ModuleDefinition(
    #         code="marketplace",
    #         onboarding_provider=_get_onboarding_provider,
    #     )
    #
    # The provider will be discovered by core's OnboardingAggregator service.
    onboarding_provider: "Callable[[], OnboardingProviderProtocol] | None" = None

    # =========================================================================
    # Menu Item Methods (Legacy - uses menu_items dict of IDs)
    # =========================================================================

    def get_menu_items(self, frontend_type: FrontendType) -> list[str]:
        """Get menu item IDs for a specific frontend type (legacy)."""
        return self.menu_items.get(frontend_type, [])

    def get_all_menu_items(self) -> set[str]:
        """Get all menu item IDs across all frontend types (legacy)."""
        all_items = set()
        for items in self.menu_items.values():
            all_items.update(items)
        return all_items

    def has_menu_item(self, menu_item_id: str) -> bool:
        """Check if this module provides a specific menu item."""
        return menu_item_id in self.get_all_menu_items()

    # =========================================================================
    # Menu Definition Methods (New - uses menus dict of full definitions)
    # =========================================================================

    def get_menu_sections(self, frontend_type: FrontendType) -> list[MenuSectionDefinition]:
        """
        Get menu section definitions for a specific frontend type.

        Args:
            frontend_type: The frontend type to get menus for

        Returns:
            List of MenuSectionDefinition objects, sorted by order
        """
        sections = self.menus.get(frontend_type, [])
        return sorted(sections, key=lambda s: s.order)

    def get_all_menu_definitions(self) -> dict[FrontendType, list[MenuSectionDefinition]]:
        """
        Get all menu definitions for all frontend types.

        Returns:
            Dict mapping FrontendType to list of MenuSectionDefinition
        """
        return self.menus

    def has_menus_for_frontend(self, frontend_type: FrontendType) -> bool:
        """Check if this module has menu definitions for a frontend type."""
        return frontend_type in self.menus and len(self.menus[frontend_type]) > 0

    def get_mandatory_menu_item_ids(self, frontend_type: FrontendType) -> set[str]:
        """
        Get IDs of all mandatory menu items for a frontend type.

        Returns:
            Set of menu item IDs that are marked as is_mandatory=True
        """
        mandatory_ids = set()
        for section in self.menus.get(frontend_type, []):
            for item in section.items:
                if item.is_mandatory:
                    mandatory_ids.add(item.id)
        return mandatory_ids

    # =========================================================================
    # Feature Methods
    # =========================================================================

    def has_feature(self, feature_code: str) -> bool:
        """Check if this module provides a specific feature."""
        return feature_code in self.features

    def has_permission(self, permission_id: str) -> bool:
        """Check if this module defines a specific permission."""
        return any(p.id == permission_id for p in self.permissions)

    def get_permission_ids(self) -> set[str]:
        """Get all permission IDs defined by this module."""
        return {p.id for p in self.permissions}

    # =========================================================================
    # Dependency Methods
    # =========================================================================

    def check_dependencies(self, enabled_modules: set[str]) -> list[str]:
        """
        Check if all required modules are enabled.

        Args:
            enabled_modules: Set of enabled module codes

        Returns:
            List of missing required module codes
        """
        return [req for req in self.requires if req not in enabled_modules]

    # =========================================================================
    # Configuration Methods
    # =========================================================================

    def validate_config(self, config: dict[str, Any]) -> dict[str, Any]:
        """
        Validate configuration against the schema.

        Args:
            config: Configuration dict to validate

        Returns:
            Validated configuration dict

        Raises:
            ValidationError: If configuration is invalid
        """
        if self.config_schema is None:
            return config

        # Merge with defaults
        merged = {**self.default_config, **config}
        # Validate using Pydantic model
        validated = self.config_schema(**merged)
        return validated.model_dump()

    def get_default_config(self) -> dict[str, Any]:
        """Get the default configuration for this module."""
        if self.config_schema is None:
            return self.default_config.copy()

        # Use Pydantic model defaults
        return self.config_schema().model_dump()

    # =========================================================================
    # Lifecycle Methods
    # =========================================================================

    def run_on_enable(self, platform_id: int) -> None:
        """Run the on_enable hook if defined."""
        if self.on_enable:
            self.on_enable(platform_id)

    def run_on_disable(self, platform_id: int) -> None:
        """Run the on_disable hook if defined."""
        if self.on_disable:
            self.on_disable(platform_id)

    def run_on_startup(self) -> None:
        """Run the on_startup hook if defined."""
        if self.on_startup:
            self.on_startup()

    def get_health_status(self) -> dict[str, Any]:
        """
        Get the health status of this module.

        Returns:
            Dict with at least a 'status' key ('healthy', 'degraded', 'unhealthy')
        """
        if self.health_check:
            return self.health_check()
        return {"status": "healthy", "message": "No health check defined"}

    # =========================================================================
    # Self-Contained Module Methods
    # =========================================================================

    def get_module_dir(self) -> Path:
        """
        Get the filesystem path to this module's directory.

        Returns:
            Path to app/modules/<code>/
        """
        # Handle module codes with hyphens (e.g., dev-tools -> dev_tools)
        dir_name = self.code.replace("-", "_")
        return Path(__file__).parent / dir_name

    def get_templates_dir(self) -> Path | None:
        """
        Get the filesystem path to this module's templates directory.

        Returns:
            Path to templates directory, or None if not self-contained
        """
        if not self.is_self_contained or not self.templates_path:
            return None
        return self.get_module_dir() / self.templates_path

    def get_locales_dir(self) -> Path | None:
        """
        Get the filesystem path to this module's locales directory.

        Returns:
            Path to locales directory, or None if not configured
        """
        if not self.is_self_contained or not self.locales_path:
            return None
        return self.get_module_dir() / self.locales_path

    def get_migrations_dir(self) -> Path | None:
        """
        Get the filesystem path to this module's migrations directory.

        Returns:
            Path to migrations directory, or None if not configured
        """
        if not self.migrations_path:
            return None
        return self.get_module_dir() / self.migrations_path

    def get_import_path(self, component: str) -> str | None:
        """
        Get the Python import path for a module component.

        Args:
            component: One of "services", "models", "schemas", "exceptions", "tasks"

        Returns:
            Import path string, or None if not configured
        """
        paths = {
            "services": self.services_path,
            "models": self.models_path,
            "schemas": self.schemas_path,
            "exceptions": self.exceptions_path,
            "tasks": self.tasks_path,
        }
        return paths.get(component)

    def get_tasks_module(self) -> str | None:
        """
        Get the Python import path for this module's tasks.

        Returns the explicitly configured tasks_path, or infers it from
        the module code if the module is self-contained.

        Returns:
            Import path string (e.g., "app.modules.billing.tasks"), or None
        """
        if self.tasks_path:
            return self.tasks_path
        if self.is_self_contained:
            dir_name = self.code.replace("-", "_")
            return f"app.modules.{dir_name}.tasks"
        return None

    def get_tasks_dir(self) -> Path | None:
        """
        Get the filesystem path to this module's tasks directory.

        Returns:
            Path to tasks directory, or None if not configured
        """
        tasks_module = self.get_tasks_module()
        if not tasks_module:
            return None
        return self.get_module_dir() / "tasks"

    def has_tasks(self) -> bool:
        """
        Check if this module has a tasks directory.

        Returns:
            True if tasks directory exists and contains __init__.py
        """
        tasks_dir = self.get_tasks_dir()
        if not tasks_dir:
            return False
        return tasks_dir.exists() and (tasks_dir / "__init__.py").exists()

    def validate_structure(self) -> list[str]:
        """
        Validate that self-contained module has expected directory structure.

        Returns:
            List of missing or invalid paths (empty if valid)
        """
        if not self.is_self_contained:
            return []

        issues = []
        module_dir = self.get_module_dir()

        if not module_dir.exists():
            issues.append(f"Module directory not found: {module_dir}")
            return issues

        # Check optional directories
        expected_dirs = []
        if self.services_path:
            expected_dirs.append("services")
        if self.models_path:
            expected_dirs.append("models")
        if self.schemas_path:
            expected_dirs.append("schemas")
        if self.templates_path:
            expected_dirs.append(self.templates_path)
        if self.locales_path:
            expected_dirs.append(self.locales_path)
        if self.migrations_path:
            expected_dirs.append(self.migrations_path)

        for dir_name in expected_dirs:
            dir_path = module_dir / dir_name
            if not dir_path.exists():
                issues.append(f"Missing directory: {dir_path}")

        return issues

    # =========================================================================
    # Classification Methods
    # =========================================================================

    def get_tier(self) -> str:
        """
        Get the tier classification of this module.

        Returns:
            'core', 'internal', or 'optional'
        """
        if self.is_core:
            return "core"
        if self.is_internal:
            return "internal"
        return "optional"

    # =========================================================================
    # Context Provider Methods
    # =========================================================================

    def has_context_provider(self, frontend_type: FrontendType) -> bool:
        """Check if this module has a context provider for a frontend type."""
        return frontend_type in self.context_providers

    def get_context_contribution(
        self,
        frontend_type: FrontendType,
        request: Any,
        db: Any,
        platform: Any,
    ) -> dict[str, Any]:
        """
        Get context contribution from this module for a frontend type.

        Args:
            frontend_type: The frontend type (PLATFORM, ADMIN, STORE, STOREFRONT)
            request: FastAPI Request object
            db: Database session
            platform: Platform object (may be None for some contexts)

        Returns:
            Dict of context variables, or empty dict if no provider

        Note:
            Exceptions are caught and logged by the caller. This method
            may raise if the provider fails.
        """
        provider = self.context_providers.get(frontend_type)
        if provider is None:
            return {}
        return provider(request, db, platform)

    def get_supported_frontend_types(self) -> list[FrontendType]:
        """Get list of frontend types this module provides context for."""
        return list(self.context_providers.keys())

    # =========================================================================
    # Metrics Provider Methods
    # =========================================================================

    def has_metrics_provider(self) -> bool:
        """Check if this module has a metrics provider."""
        return self.metrics_provider is not None

    def get_metrics_provider_instance(self) -> "MetricsProviderProtocol | None":
        """
        Get the metrics provider instance for this module.

        Calls the metrics_provider factory function to get the provider.
        Returns None if no provider is configured.

        Returns:
            MetricsProviderProtocol instance, or None
        """
        if self.metrics_provider is None:
            return None
        return self.metrics_provider()

    # =========================================================================
    # Widget Provider Methods
    # =========================================================================

    def has_widget_provider(self) -> bool:
        """Check if this module has a widget provider."""
        return self.widget_provider is not None

    def get_widget_provider_instance(self) -> "DashboardWidgetProviderProtocol | None":
        """
        Get the widget provider instance for this module.

        Calls the widget_provider factory function to get the provider.
        Returns None if no provider is configured.

        Returns:
            DashboardWidgetProviderProtocol instance, or None
        """
        if self.widget_provider is None:
            return None
        return self.widget_provider()

    # =========================================================================
    # Audit Provider Methods
    # =========================================================================

    def has_audit_provider(self) -> bool:
        """Check if this module has an audit provider."""
        return self.audit_provider is not None

    def get_audit_provider_instance(self) -> "AuditProviderProtocol | None":
        """
        Get the audit provider instance for this module.

        Calls the audit_provider factory function to get the provider.
        Returns None if no provider is configured.

        Returns:
            AuditProviderProtocol instance, or None
        """
        if self.audit_provider is None:
            return None
        return self.audit_provider()

    # =========================================================================
    # Feature Provider Methods
    # =========================================================================

    def has_feature_provider(self) -> bool:
        """Check if this module has a feature provider."""
        return self.feature_provider is not None

    def get_feature_provider_instance(self) -> "FeatureProviderProtocol | None":
        """
        Get the feature provider instance for this module.

        Calls the feature_provider factory function to get the provider.
        Returns None if no provider is configured.

        Returns:
            FeatureProviderProtocol instance, or None
        """
        if self.feature_provider is None:
            return None
        return self.feature_provider()

    # =========================================================================
    # Media Usage Provider Methods
    # =========================================================================

    def has_media_usage_provider(self) -> bool:
        """Check if this module has a media usage provider."""
        return self.media_usage_provider is not None

    def get_media_usage_provider_instance(self) -> "MediaUsageProviderProtocol | None":
        """Get the media usage provider instance for this module.

        Returns:
            MediaUsageProviderProtocol instance, or None
        """
        if self.media_usage_provider is None:
            return None
        return self.media_usage_provider()

    # =========================================================================
    # Onboarding Provider Methods
    # =========================================================================

    def has_onboarding_provider(self) -> bool:
        """Check if this module has an onboarding provider."""
        return self.onboarding_provider is not None

    def get_onboarding_provider_instance(self) -> "OnboardingProviderProtocol | None":
        """Get the onboarding provider instance for this module.

        Returns:
            OnboardingProviderProtocol instance, or None
        """
        if self.onboarding_provider is None:
            return None
        return self.onboarding_provider()

    # =========================================================================
    # Magic Methods
    # =========================================================================

    def __hash__(self) -> int:
        return hash(self.code)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, ModuleDefinition):
            return self.code == other.code
        return False

    def __repr__(self) -> str:
        tier = self.get_tier()
        sc = ", self_contained" if self.is_self_contained else ""
        return f"<Module({self.code}, tier={tier}{sc})>"
