# app/modules/service.py
"""
Module service for platform module operations.

Provides methods to check module enablement, get enabled modules,
and filter menu items based on module configuration.

Module configuration is stored in the PlatformModule junction table,
which provides auditability, per-module config, and explicit state tracking.
"""

import logging
from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.modules.base import ModuleDefinition
from app.modules.enums import FrontendType
from app.modules.registry import (
    MODULES,
    get_core_module_codes,
    get_menu_item_module,
    get_module,
)
from app.modules.tenancy.models import Platform, PlatformModule

logger = logging.getLogger(__name__)


class ModuleService:
    """
    Service for platform module operations.

    Handles module enablement checking, module listing, and menu item filtering
    based on enabled modules.

    Module configuration is stored in the PlatformModule junction table,
    which provides auditability, per-module config, and explicit state tracking.

    If no PlatformModule records exist for a platform, no optional modules are
    enabled (only core modules). Use seed scripts or the admin API to configure
    module enablement for each platform.

    Example PlatformModule records:
        PlatformModule(platform_id=1, module_code="billing", is_enabled=True, config={"stripe_mode": "live"})
        PlatformModule(platform_id=1, module_code="inventory", is_enabled=True, config={"low_stock_threshold": 10})
    """

    # =========================================================================
    # Module Enablement
    # =========================================================================

    def get_platform_modules(
        self,
        db: Session,
        platform_id: int,
    ) -> list[ModuleDefinition]:
        """
        Get all enabled modules for a platform.

        Args:
            db: Database session
            platform_id: Platform ID

        Returns:
            List of enabled ModuleDefinition objects (always includes core)
        """
        enabled_codes = self._get_enabled_module_codes(db, platform_id)
        return [MODULES[code] for code in enabled_codes if code in MODULES]

    def get_enabled_module_codes(
        self,
        db: Session,
        platform_id: int,
    ) -> set[str]:
        """
        Get set of enabled module codes for a platform.

        Args:
            db: Database session
            platform_id: Platform ID

        Returns:
            Set of enabled module codes (always includes core modules)
        """
        return self._get_enabled_module_codes(db, platform_id)

    def is_module_enabled(
        self,
        db: Session,
        platform_id: int,
        module_code: str,
    ) -> bool:
        """
        Check if a specific module is enabled for a platform.

        Core modules are always enabled.

        Args:
            db: Database session
            platform_id: Platform ID
            module_code: Module code to check

        Returns:
            True if module is enabled
        """
        # Core modules are always enabled
        if module_code in get_core_module_codes():
            return True

        enabled_codes = self._get_enabled_module_codes(db, platform_id)
        return module_code in enabled_codes

    def _get_enabled_module_codes(
        self,
        db: Session,
        platform_id: int,
    ) -> set[str]:
        """
        Get enabled module codes for a platform.

        Uses the PlatformModule junction table exclusively. If no records exist,
        returns only core modules (empty set of optional modules).
        Always includes core modules.

        Args:
            db: Database session
            platform_id: Platform ID

        Returns:
            Set of enabled module codes
        """
        platform = db.query(Platform).filter(Platform.id == platform_id).first()
        if not platform:
            logger.warning(f"Platform {platform_id} not found, returning core modules only")
            return get_core_module_codes()

        # Query junction table for enabled modules
        platform_modules = (
            db.query(PlatformModule)
            .filter(PlatformModule.platform_id == platform_id)
            .all()
        )

        enabled_set = {pm.module_code for pm in platform_modules if pm.is_enabled}

        # Always include core modules
        core_codes = get_core_module_codes()
        enabled_set = enabled_set | core_codes

        # Resolve dependencies - add required modules
        enabled_set = self._resolve_dependencies(enabled_set)

        logger.debug(
            f"[MODULES] Platform '{platform.code}' (id={platform_id}) has "
            f"{len(enabled_set)} modules enabled: {sorted(enabled_set)}"
        )

        return enabled_set

    def _resolve_dependencies(self, enabled_codes: set[str]) -> set[str]:
        """
        Resolve module dependencies by adding required modules.

        If module A requires module B, and A is enabled, B must also be enabled.

        Args:
            enabled_codes: Set of explicitly enabled module codes

        Returns:
            Set of enabled module codes including dependencies
        """
        resolved = set(enabled_codes)
        changed = True

        while changed:
            changed = False
            for code in list(resolved):
                module = get_module(code)
                if module:
                    for required in module.requires:
                        if required not in resolved:
                            resolved.add(required)
                            changed = True
                            logger.debug(
                                f"Module '{code}' requires '{required}', auto-enabling"
                            )

        return resolved

    # =========================================================================
    # Menu Item Filtering
    # =========================================================================

    def get_module_menu_items(
        self,
        db: Session,
        platform_id: int,
        frontend_type: FrontendType,
    ) -> set[str]:
        """
        Get all menu item IDs available for enabled modules.

        Args:
            db: Database session
            platform_id: Platform ID
            frontend_type: Which frontend (admin or store)

        Returns:
            Set of menu item IDs from enabled modules
        """
        enabled_modules = self.get_platform_modules(db, platform_id)
        menu_items = set()

        for module in enabled_modules:
            menu_items.update(module.get_menu_items(frontend_type))

        return menu_items

    def is_menu_item_module_enabled(
        self,
        db: Session,
        platform_id: int,
        menu_item_id: str,
        frontend_type: FrontendType,
    ) -> bool:
        """
        Check if the module providing a menu item is enabled.

        Args:
            db: Database session
            platform_id: Platform ID
            menu_item_id: Menu item ID
            frontend_type: Which frontend (admin or store)

        Returns:
            True if the module providing this menu item is enabled,
            or if menu item is not associated with any module.
        """
        module_code = get_menu_item_module(menu_item_id, frontend_type)

        # If menu item isn't associated with any module, allow it
        if module_code is None:
            return True

        return self.is_module_enabled(db, platform_id, module_code)

    def filter_menu_items_by_modules(
        self,
        db: Session,
        platform_id: int,
        menu_item_ids: set[str],
        frontend_type: FrontendType,
    ) -> set[str]:
        """
        Filter menu items to only those from enabled modules.

        Args:
            db: Database session
            platform_id: Platform ID
            menu_item_ids: Set of menu item IDs to filter
            frontend_type: Which frontend (admin or store)

        Returns:
            Filtered set of menu item IDs
        """
        available_items = self.get_module_menu_items(db, platform_id, frontend_type)

        # Items that are in available_items, OR items not associated with any module
        filtered = set()
        for item_id in menu_item_ids:
            module_code = get_menu_item_module(item_id, frontend_type)
            if module_code is None or item_id in available_items:
                filtered.add(item_id)

        return filtered

    # =========================================================================
    # Module Configuration
    # =========================================================================

    def get_module_config(
        self,
        db: Session,
        platform_id: int,
        module_code: str,
    ) -> dict:
        """
        Get module-specific configuration for a platform.

        Uses the PlatformModule junction table for configuration storage.

        Args:
            db: Database session
            platform_id: Platform ID
            module_code: Module code

        Returns:
            Module configuration dict (empty if not configured)
        """
        platform_module = (
            db.query(PlatformModule)
            .filter(
                PlatformModule.platform_id == platform_id,
                PlatformModule.module_code == module_code,
            )
            .first()
        )

        if platform_module:
            return platform_module.config or {}

        return {}

    def set_module_config(
        self,
        db: Session,
        platform_id: int,
        module_code: str,
        config: dict,
    ) -> bool:
        """
        Set module-specific configuration for a platform.

        Uses junction table for persistence. Creates record if doesn't exist.

        Args:
            db: Database session
            platform_id: Platform ID
            module_code: Module code
            config: Configuration dict to set

        Returns:
            True if successful
        """
        if module_code not in MODULES:
            logger.error(f"Unknown module: {module_code}")
            return False

        platform = db.query(Platform).filter(Platform.id == platform_id).first()
        if not platform:
            logger.error(f"Platform {platform_id} not found")
            return False

        # Get or create junction table record
        platform_module = (
            db.query(PlatformModule)
            .filter(
                PlatformModule.platform_id == platform_id,
                PlatformModule.module_code == module_code,
            )
            .first()
        )

        if platform_module:
            platform_module.config = config
        else:
            # Create new record with config
            platform_module = PlatformModule(
                platform_id=platform_id,
                module_code=module_code,
                is_enabled=True,  # Default to enabled
                config=config,
            )
            db.add(platform_module)

        logger.info(f"Updated config for module '{module_code}' on platform {platform_id}")
        return True

    def set_enabled_modules(
        self,
        db: Session,
        platform_id: int,
        module_codes: list[str],
        user_id: int | None = None,
    ) -> bool:
        """
        Set the enabled modules for a platform.

        Core modules are automatically included.
        Dependencies are automatically resolved.
        Uses junction table for auditability.

        Args:
            db: Database session
            platform_id: Platform ID
            module_codes: List of module codes to enable
            user_id: ID of user making the change (for audit)

        Returns:
            True if successful, False if platform not found
        """
        platform = db.query(Platform).filter(Platform.id == platform_id).first()
        if not platform:
            logger.error(f"Platform {platform_id} not found")
            return False

        # Validate module codes
        valid_codes = set(MODULES.keys())
        invalid = [code for code in module_codes if code not in valid_codes]
        if invalid:
            logger.warning(f"Invalid module codes ignored: {invalid}")
            module_codes = [code for code in module_codes if code in valid_codes]

        # Always include core modules
        core_codes = get_core_module_codes()
        enabled_set = set(module_codes) | core_codes

        # Resolve dependencies
        enabled_set = self._resolve_dependencies(enabled_set)

        now = datetime.now(UTC)

        # Update junction table for all modules
        for code in MODULES:
            platform_module = (
                db.query(PlatformModule)
                .filter(
                    PlatformModule.platform_id == platform_id,
                    PlatformModule.module_code == code,
                )
                .first()
            )

            should_enable = code in enabled_set

            if platform_module:
                # Update existing record
                if should_enable and not platform_module.is_enabled:
                    platform_module.is_enabled = True
                    platform_module.enabled_at = now
                    platform_module.enabled_by_user_id = user_id
                elif not should_enable and platform_module.is_enabled:
                    platform_module.is_enabled = False
                    platform_module.disabled_at = now
                    platform_module.disabled_by_user_id = user_id
            else:
                # Create new record
                platform_module = PlatformModule(
                    platform_id=platform_id,
                    module_code=code,
                    is_enabled=should_enable,
                    enabled_at=now if should_enable else None,
                    enabled_by_user_id=user_id if should_enable else None,
                    disabled_at=None if should_enable else now,
                    disabled_by_user_id=None if should_enable else user_id,
                    config={},
                )
                db.add(platform_module)  # noqa: PERF006

        logger.info(
            f"Updated enabled modules for platform {platform_id}: {sorted(enabled_set)}"
        )
        return True

    def enable_module(
        self,
        db: Session,
        platform_id: int,
        module_code: str,
        user_id: int | None = None,
    ) -> bool:
        """
        Enable a single module for a platform.

        Also enables required dependencies.
        Uses the PlatformModule junction table for auditability.

        Args:
            db: Database session
            platform_id: Platform ID
            module_code: Module code to enable
            user_id: ID of user enabling the module (for audit)

        Returns:
            True if successful
        """
        if module_code not in MODULES:
            logger.error(f"Unknown module: {module_code}")
            return False

        platform = db.query(Platform).filter(Platform.id == platform_id).first()
        if not platform:
            logger.error(f"Platform {platform_id} not found")
            return False

        now = datetime.now(UTC)

        # Enable this module and its dependencies
        modules_to_enable = {module_code}
        module = get_module(module_code)
        if module:
            for required in module.requires:
                modules_to_enable.add(required)

        for code in modules_to_enable:
            # Check if junction table record exists
            platform_module = (
                db.query(PlatformModule)
                .filter(
                    PlatformModule.platform_id == platform_id,
                    PlatformModule.module_code == code,
                )
                .first()
            )

            if platform_module:
                # Update existing record
                platform_module.is_enabled = True
                platform_module.enabled_at = now
                platform_module.enabled_by_user_id = user_id
            else:
                # Create new record
                platform_module = PlatformModule(
                    platform_id=platform_id,
                    module_code=code,
                    is_enabled=True,
                    enabled_at=now,
                    enabled_by_user_id=user_id,
                    config={},
                )
                db.add(platform_module)  # noqa: PERF006

        logger.info(f"Enabled module '{module_code}' for platform {platform_id}")
        return True

    def disable_module(
        self,
        db: Session,
        platform_id: int,
        module_code: str,
        user_id: int | None = None,
    ) -> bool:
        """
        Disable a single module for a platform.

        Core modules cannot be disabled.
        Also disables modules that depend on this one.
        Uses the PlatformModule junction table for auditability.

        Args:
            db: Database session
            platform_id: Platform ID
            module_code: Module code to disable
            user_id: ID of user disabling the module (for audit)

        Returns:
            True if successful, False if core or not found
        """
        if module_code not in MODULES:
            logger.error(f"Unknown module: {module_code}")
            return False

        module = MODULES[module_code]
        if module.is_core:
            logger.warning(f"Cannot disable core module: {module_code}")
            return False

        platform = db.query(Platform).filter(Platform.id == platform_id).first()
        if not platform:
            logger.error(f"Platform {platform_id} not found")
            return False

        now = datetime.now(UTC)

        # Get modules to disable (this one + dependents)
        modules_to_disable = {module_code}
        dependents = self._get_dependent_modules(module_code)
        modules_to_disable.update(dependents)

        for code in modules_to_disable:
            # Check if junction table record exists
            platform_module = (
                db.query(PlatformModule)
                .filter(
                    PlatformModule.platform_id == platform_id,
                    PlatformModule.module_code == code,
                )
                .first()
            )

            if platform_module:
                # Update existing record
                platform_module.is_enabled = False
                platform_module.disabled_at = now
                platform_module.disabled_by_user_id = user_id
            else:
                # Create disabled record for tracking
                platform_module = PlatformModule(
                    platform_id=platform_id,
                    module_code=code,
                    is_enabled=False,
                    disabled_at=now,
                    disabled_by_user_id=user_id,
                    config={},
                )
                db.add(platform_module)  # noqa: PERF006

            if code != module_code:
                logger.info(
                    f"Also disabled '{code}' (depends on '{module_code}')"
                )

        logger.info(f"Disabled module '{module_code}' for platform {platform_id}")
        return True

    def _get_dependent_modules(self, module_code: str) -> set[str]:
        """
        Get modules that depend on a given module.

        Args:
            module_code: Module code to find dependents for

        Returns:
            Set of module codes that require the given module
        """
        dependents = set()
        for code, module in MODULES.items():
            if module_code in module.requires:
                dependents.add(code)
                # Recursively find dependents of dependents
                dependents.update(self._get_dependent_modules(code))
        return dependents

    # =========================================================================
    # Platform Code Helpers
    # =========================================================================

    def get_platform_modules_by_code(
        self,
        db: Session,
        platform_code: str,
    ) -> list[ModuleDefinition]:
        """
        Get enabled modules for a platform by code.

        Args:
            db: Database session
            platform_code: Platform code (e.g., "oms", "loyalty")

        Returns:
            List of enabled ModuleDefinition objects
        """
        platform = db.query(Platform).filter(Platform.code == platform_code).first()
        if not platform:
            logger.warning(f"Platform '{platform_code}' not found, returning core modules only")
            core_codes = get_core_module_codes()
            return [MODULES[code] for code in core_codes if code in MODULES]

        return self.get_platform_modules(db, platform.id)

    def is_module_enabled_by_code(
        self,
        db: Session,
        platform_code: str,
        module_code: str,
    ) -> bool:
        """
        Check if a module is enabled for a platform by code.

        Args:
            db: Database session
            platform_code: Platform code (e.g., "oms", "loyalty")
            module_code: Module code to check

        Returns:
            True if module is enabled
        """
        platform = db.query(Platform).filter(Platform.code == platform_code).first()
        if not platform:
            logger.warning(f"Platform '{platform_code}' not found, assuming enabled")
            return True

        return self.is_module_enabled(db, platform.id, module_code)


# Singleton instance
module_service = ModuleService()


__all__ = [
    "ModuleService",
    "module_service",
]
