# app/modules/core/services/menu_service.py
"""
Menu service for platform-specific menu configuration.

Provides:
- Menu visibility checking based on platform/user configuration
- Module-based filtering (menu items only shown if module is enabled)
- Filtered menu rendering for frontends
- Menu configuration management (super admin only)
- Mandatory item enforcement

Menu Resolution Order:
1. Module enablement: Is the module providing this item enabled?
2. Visibility config: Is this item explicitly shown/hidden?
3. Mandatory status: Is this item mandatory (always visible)?

Usage:
    from app.modules.core.services import menu_service

    # Check if menu item is accessible
    if menu_service.can_access_menu_item(db, FrontendType.ADMIN, "inventory", platform_id=1):
        ...

    # Get filtered menu for rendering
    menu = menu_service.get_menu_for_rendering(db, FrontendType.ADMIN, platform_id=1)

    # Update menu visibility (super admin)
    menu_service.update_menu_visibility(db, FrontendType.ADMIN, "inventory", False, platform_id=1)
"""

import logging
from dataclasses import dataclass

from sqlalchemy.orm import Session

from app.modules.core.models import AdminMenuConfig
from app.modules.core.services.menu_discovery_service import menu_discovery_service
from app.modules.enums import FrontendType
from app.modules.service import module_service

logger = logging.getLogger(__name__)


@dataclass
class MenuItemConfig:
    """Menu item configuration for admin UI."""

    id: str
    label: str
    icon: str
    url: str
    section_id: str
    section_label: str | None
    is_visible: bool
    is_mandatory: bool
    is_super_admin_only: bool
    is_module_enabled: bool = True  # Whether the module providing this item is enabled
    module_code: str | None = None  # Module that provides this item


class MenuService:
    """
    Service for menu visibility configuration and rendering.

    Menu visibility is an opt-in model:
    - All items are hidden by default (except mandatory)
    - Database stores explicitly shown items (is_visible=True)
    - Mandatory items are always visible and cannot be hidden
    """

    # =========================================================================
    # Menu Access Checking
    # =========================================================================

    def can_access_menu_item(
        self,
        db: Session,
        frontend_type: FrontendType,
        menu_item_id: str,
        platform_id: int | None = None,
        user_id: int | None = None,
    ) -> bool:
        """
        Check if a menu item is accessible for a given scope.

        Checks in order:
        1. Menu item exists in registry
        2. Module providing this item is enabled (if platform_id given)
        3. Mandatory status
        4. Visibility configuration

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            menu_item_id: Menu item identifier
            platform_id: Platform ID (for platform admins and stores)
            user_id: User ID (for super admins only)

        Returns:
            True if menu item is visible/accessible
        """
        # Validate menu item exists in registry
        all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        if menu_item_id not in all_items:
            logger.warning(
                f"Unknown menu item: {menu_item_id} for {frontend_type.value}"
            )
            return False

        # Check module enablement if platform is specified
        if platform_id:
            if not module_service.is_menu_item_module_enabled(
                db, platform_id, menu_item_id, frontend_type
            ):
                return False

        # Mandatory items are always accessible (if module is enabled)
        mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
        if menu_item_id in mandatory_items:
            return True

        # No scope specified - show all by default (fallback for unconfigured)
        if not platform_id and not user_id:
            return True

        # Get visibility from database (opt-in: must be explicitly shown)
        shown_items = self._get_shown_items(db, frontend_type, platform_id, user_id)

        # If no configuration exists, show all items (first-time setup)
        if shown_items is None:
            return True

        return menu_item_id in shown_items

    def get_visible_menu_items(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int | None = None,
        user_id: int | None = None,
    ) -> set[str]:
        """
        Get set of visible menu item IDs for a scope.

        Filters by:
        1. Module enablement (if platform_id given)
        2. Visibility configuration
        3. Mandatory status

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            platform_id: Platform ID (for platform admins and stores)
            user_id: User ID (for super admins only)

        Returns:
            Set of visible menu item IDs
        """
        all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)

        # Filter by module enablement if platform is specified
        if platform_id:
            module_service.get_module_menu_items(db, platform_id, frontend_type)
            # Only keep items from enabled modules (or items not associated with any module)
            all_items = module_service.filter_menu_items_by_modules(
                db, platform_id, all_items, frontend_type
            )
            # Mandatory items from enabled modules only
            mandatory_items = mandatory_items & all_items

        # No scope specified - return all items (fallback)
        if not platform_id and not user_id:
            return all_items

        shown_items = self._get_shown_items(db, frontend_type, platform_id, user_id)

        # If no configuration exists yet, show all items (first-time setup)
        if shown_items is None:
            return all_items

        # Shown items plus mandatory (mandatory are always visible)
        # But only if module is enabled
        visible = (shown_items | mandatory_items) & all_items
        return visible

    def _get_shown_items(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int | None = None,
        user_id: int | None = None,
    ) -> set[str] | None:
        """
        Get set of shown menu item IDs from database.

        Returns:
            Set of shown item IDs, or None if no configuration exists.
        """
        query = db.query(AdminMenuConfig).filter(
            AdminMenuConfig.frontend_type == frontend_type,
        )

        if platform_id:
            query = query.filter(AdminMenuConfig.platform_id == platform_id)
        elif user_id:
            query = query.filter(AdminMenuConfig.user_id == user_id)
        else:
            return None

        # Check if any config exists for this scope
        configs = query.all()
        if not configs:
            return None  # No config = use defaults (all visible)

        # Return only items marked as visible
        return {c.menu_item_id for c in configs if c.is_visible}

    # =========================================================================
    # Menu Rendering
    # =========================================================================

    def get_menu_for_rendering(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int | None = None,
        user_id: int | None = None,
        is_super_admin: bool = False,
        store_code: str | None = None,
        enabled_module_codes: set[str] | None = None,
        user_permissions: list[str] | None = None,
    ) -> list:
        """
        Get filtered menu structure for frontend rendering.

        Uses MenuDiscoveryService to aggregate menus from all enabled modules.

        Filters by:
        1. Module enablement (items from disabled modules are removed)
        2. Visibility configuration
        3. Super admin status
        4. User permissions (items with requires_permission hidden if user lacks it)

        Args:
            db: Database session
            frontend_type: Which frontend (admin, store, or merchant)
            platform_id: Platform ID (for platform admins and stores)
            user_id: User ID (for super admins only)
            is_super_admin: Whether user is super admin (affects admin-only sections)
            store_code: Store code for URL placeholder replacement (store frontend)
            enabled_module_codes: If provided, overrides single-platform lookup
                for module enablement. Used by merchant portal where a merchant
                may have subscriptions across multiple platforms.
            user_permissions: List of permission IDs the user has. If provided,
                items with requires_permission will be hidden unless the user
                has the permission. None = no permission filtering.

        Returns:
            List of DiscoveredMenuSection ready for rendering
        """
        return menu_discovery_service.get_menu_for_frontend(
            db=db,
            frontend_type=frontend_type,
            platform_id=platform_id,
            user_id=user_id,
            is_super_admin=is_super_admin,
            store_code=store_code,
            enabled_module_codes=enabled_module_codes,
            user_permissions=user_permissions,
        )

    # =========================================================================
    # Merchant Menu
    # =========================================================================

    def get_merchant_menu_by_platform(
        self,
        db: Session,
        merchant_id: int,
    ) -> tuple[list, list]:
        """
        Get merchant menu items grouped by platform.

        Returns two lists of DiscoveredMenuSection:
        - core_sections: items from core modules (always visible, no platform grouping)
        - platform_sections: items from non-core modules, with platform metadata

        Each platform section has its label_key replaced with the platform name
        and a `platform_code` attribute added for frontend identification.

        Args:
            db: Database session
            merchant_id: Merchant ID

        Returns:
            Tuple of (core_sections, platform_sections)
        """
        from app.modules.billing.services.subscription_service import (
            subscription_service,
        )
        from app.modules.registry import MODULES
        from app.modules.tenancy.services.platform_service import platform_service

        core_codes = {code for code, mod in MODULES.items() if mod.is_core}

        # Get all sections from core modules (no platform filtering needed)
        core_sections = menu_discovery_service.get_menu_sections_for_frontend(
            db,
            FrontendType.MERCHANT,
            enabled_module_codes=core_codes,
        )
        # Filter to only items from core modules
        for section in core_sections:
            section.items = [i for i in section.items if i.module_code in core_codes]
        core_sections = [s for s in core_sections if s.items]

        # Get platform-specific sections
        platform_sections = []
        platform_ids = subscription_service.get_active_subscription_platform_ids(
            db, merchant_id
        )

        for pid in platform_ids:
            platform = platform_service.get_platform_by_id(db, pid)
            if not platform:
                continue

            # Get modules enabled on this platform (excluding core — already at root)
            platform_module_codes = module_service.get_enabled_module_codes(db, pid)
            non_core_codes = platform_module_codes - core_codes

            if not non_core_codes:
                continue  # No platform-specific items

            # Get sections for this platform's non-core modules
            sections = menu_discovery_service.get_menu_sections_for_frontend(
                db,
                FrontendType.MERCHANT,
                enabled_module_codes=non_core_codes,
            )
            # Filter to only items from non-core modules
            for section in sections:
                section.items = [
                    i for i in section.items if i.module_code in non_core_codes
                ]

            # Flatten all platform sections into one section per platform
            all_items = []
            for section in sections:
                all_items.extend(section.items)

            if not all_items:
                continue

            # Create a single section for this platform
            from app.modules.core.services.menu_discovery_service import (
                DiscoveredMenuSection,
            )

            platform_section = DiscoveredMenuSection(
                id=f"platform-{platform.code}",
                label_key=platform.name,  # Use platform name directly
                icon=getattr(platform, "icon", None) or "globe-alt",
                order=30 + platform_ids.index(pid),
                is_super_admin_only=False,
                is_collapsible=True,
                items=sorted(all_items, key=lambda i: (i.section_order, i.order)),
            )
            platform_sections.append(platform_section)

        return core_sections, platform_sections

    def get_merchant_enabled_module_codes(
        self,
        db: Session,
        merchant_id: int,
    ) -> set[str]:
        """
        Get the union of enabled module codes across all platforms the merchant
        has an active subscription on.

        Core modules (those with is_core=True) are always included.

        Args:
            db: Database session
            merchant_id: Merchant ID

        Returns:
            Set of enabled module codes
        """
        from app.modules.billing.services.subscription_service import (
            subscription_service,
        )
        from app.modules.registry import MODULES

        # Always include core modules
        core_codes = {code for code, mod in MODULES.items() if mod.is_core}

        # Find all platform IDs where merchant has active/trial subscriptions
        platform_ids = set(
            subscription_service.get_active_subscription_platform_ids(db, merchant_id)
        )

        if not platform_ids:
            return core_codes

        # Union enabled module codes across all subscribed platforms
        all_enabled = set(core_codes)
        for platform_id in platform_ids:
            platform_codes = module_service.get_enabled_module_codes(db, platform_id)
            all_enabled |= platform_codes

        return all_enabled

    def get_merchant_primary_platform_id(
        self,
        db: Session,
        merchant_id: int,
    ) -> int | None:
        """
        Get the primary platform ID for a merchant's visibility config.

        Resolution order:
        1. First active StorePlatform for the merchant's stores (by joined_at)
        2. First active subscription's platform (fallback)

        Args:
            db: Database session
            merchant_id: Merchant ID

        Returns:
            Platform ID or None if no active subscriptions
        """
        from app.modules.billing.services.subscription_service import (
            subscription_service,
        )
        from app.modules.tenancy.services.platform_service import platform_service
        from app.modules.tenancy.services.store_service import store_service

        # Get merchant's active stores and find the primary platform
        stores = store_service.get_stores_by_merchant_id(
            db, merchant_id, active_only=True
        )

        # Try primary store platform first
        for store in stores:
            pid = platform_service.get_first_active_platform_id_for_store(db, store.id)
            if pid is not None:
                # Verify merchant has active subscription on this platform
                active_pids = subscription_service.get_active_subscription_platform_ids(
                    db, merchant_id
                )
                if pid in active_pids:
                    return pid

        # Fallback: first active subscription's platform
        active_pids = subscription_service.get_active_subscription_platform_ids(
            db, merchant_id
        )
        return active_pids[0] if active_pids else None

    def get_store_fallback_platform_id(
        self,
        db: Session,
        store_id: int,
    ) -> int | None:
        """
        Get the fallback platform ID for a store's menu visibility config.

        Returns the first active StorePlatform ordered by joined_at.
        Used only when platform_id is not available from JWT context.

        Args:
            db: Database session
            store_id: Store ID

        Returns:
            Platform ID or None if no active store-platform link
        """
        from app.modules.tenancy.services.platform_service import platform_service

        return platform_service.get_first_active_platform_id_for_store(db, store_id)

    def get_merchant_for_menu(
        self,
        db: Session,
        user_id: int,
    ):
        """
        Get the active merchant owned by a user, for menu rendering.

        Args:
            db: Database session
            user_id: Owner user ID

        Returns:
            Merchant ORM object or None
        """
        from app.modules.tenancy.services.merchant_service import merchant_service

        return merchant_service.get_merchant_by_owner_id(db, user_id)

    # =========================================================================
    # Menu Configuration (Super Admin)
    # =========================================================================

    def get_platform_menu_config(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int,
    ) -> list[MenuItemConfig]:
        """
        Get full menu configuration for a platform (for admin UI).

        Returns all menu items with their visibility status and module info.
        Items from disabled modules are marked with is_module_enabled=False.

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

        Returns:
            List of MenuItemConfig with current visibility state and module info
        """
        shown_items = self._get_shown_items(db, frontend_type, platform_id=platform_id)
        mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)

        # Get module-available items
        all_item_ids = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        module_available_items = module_service.filter_menu_items_by_modules(
            db, platform_id, all_item_ids, frontend_type
        )

        # Get all menu items from discovery service
        all_items = menu_discovery_service.get_all_menu_items(frontend_type)

        result = []
        for item in all_items:
            # Check if module is enabled for this item
            is_module_enabled = item.id in module_available_items

            # If no config exists (shown_items is None), show all by default
            # Otherwise, item is visible if in shown_items or mandatory
            # Note: visibility config is independent of module enablement
            is_visible = (
                shown_items is None
                or item.id in shown_items
                or item.id in mandatory_items
            )

            result.append(
                MenuItemConfig(
                    id=item.id,
                    label=item.label_key,
                    icon=item.icon,
                    url=item.route,
                    section_id=item.section_id,
                    section_label=item.section_label_key,
                    is_visible=is_visible,
                    is_mandatory=item.id in mandatory_items,
                    is_super_admin_only=item.is_super_admin_only,
                    is_module_enabled=is_module_enabled,
                    module_code=item.module_code,
                )
            )

        return result

    def update_menu_visibility(
        self,
        db: Session,
        frontend_type: FrontendType,
        menu_item_id: str,
        is_visible: bool,
        platform_id: int | None = None,
        user_id: int | None = None,
    ) -> None:
        """
        Update visibility for a menu item (opt-in model).

        In the opt-in model:
        - is_visible=True: Create/update record to show item
        - is_visible=False: Remove record (item hidden by default)

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            menu_item_id: Menu item identifier
            is_visible: Whether the item should be visible
            platform_id: Platform ID (for platform-scoped config)
            user_id: User ID (for user-scoped config, admin frontend only)

        Raises:
            ValueError: If menu item is mandatory or doesn't exist
            ValueError: If neither platform_id nor user_id is provided
            ValueError: If user_id is provided for store frontend
        """
        # Validate menu item exists
        all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        if menu_item_id not in all_items:
            raise ValueError(f"Unknown menu item: {menu_item_id}")

        # Check if mandatory - mandatory items are always visible, no need to store
        mandatory = menu_discovery_service.get_mandatory_item_ids(frontend_type)
        if menu_item_id in mandatory:
            if not is_visible:
                raise ValueError(f"Cannot hide mandatory menu item: {menu_item_id}")
            # Mandatory items don't need explicit config, they're always visible
            return

        # Validate scope
        if not platform_id and not user_id:
            raise ValueError("Either platform_id or user_id must be provided")

        if user_id and frontend_type == FrontendType.STORE:
            raise ValueError("User-scoped config not supported for store frontend")

        # Find existing config
        query = db.query(AdminMenuConfig).filter(
            AdminMenuConfig.frontend_type == frontend_type,
            AdminMenuConfig.menu_item_id == menu_item_id,
        )

        if platform_id:
            query = query.filter(AdminMenuConfig.platform_id == platform_id)
        else:
            query = query.filter(AdminMenuConfig.user_id == user_id)

        config = query.first()

        if is_visible:
            # Opt-in: Create or update config to visible (explicitly show)
            if config:
                config.is_visible = True
            else:
                config = AdminMenuConfig(
                    frontend_type=frontend_type,
                    platform_id=platform_id,
                    user_id=user_id,
                    menu_item_id=menu_item_id,
                    is_visible=True,
                )
                db.add(config)
            logger.info(
                f"Set menu config visible: {frontend_type.value}/{menu_item_id} "
                f"(platform_id={platform_id}, user_id={user_id})"
            )
        else:
            # Opt-in: Remove config to hide (hidden is default)
            if config:
                db.delete(config)
                logger.info(
                    f"Removed menu config (hidden): {frontend_type.value}/{menu_item_id} "
                    f"(platform_id={platform_id}, user_id={user_id})"
                )

    def bulk_update_menu_visibility(
        self,
        db: Session,
        frontend_type: FrontendType,
        visibility_map: dict[str, bool],
        platform_id: int | None = None,
        user_id: int | None = None,
    ) -> None:
        """
        Update visibility for multiple menu items at once.

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            visibility_map: Dict of menu_item_id -> is_visible
            platform_id: Platform ID (for platform-scoped config)
            user_id: User ID (for user-scoped config, admin frontend only)
        """
        for menu_item_id, is_visible in visibility_map.items():
            try:
                self.update_menu_visibility(
                    db, frontend_type, menu_item_id, is_visible, platform_id, user_id
                )
            except ValueError as e:
                logger.warning(f"Skipping {menu_item_id}: {e}")

    def reset_platform_menu_config(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int,
    ) -> None:
        """
        Reset menu configuration for a platform to defaults (all hidden except mandatory).

        In opt-in model, reset means hide everything so user can opt-in to what they want.

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            platform_id: Platform ID
        """
        # Delete all existing records
        deleted = (
            db.query(AdminMenuConfig)
            .filter(
                AdminMenuConfig.frontend_type == frontend_type,
                AdminMenuConfig.platform_id == platform_id,
            )
            .delete()
        )
        logger.info(
            f"Reset menu config for platform {platform_id} ({frontend_type.value}): "
            f"deleted {deleted} rows"
        )

        # Create records with is_visible=False for all non-mandatory items
        # This makes "reset" mean "hide everything except mandatory"
        all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)

        configs = [
            AdminMenuConfig(
                frontend_type=frontend_type,
                platform_id=platform_id,
                user_id=None,
                menu_item_id=item_id,
                is_visible=False,
            )
            for item_id in all_items
            if item_id not in mandatory_items
        ]
        db.add_all(configs)

        logger.info(
            f"Created {len(all_items) - len(mandatory_items)} hidden records for platform {platform_id}"
        )

    def show_all_platform_menu_config(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int,
    ) -> None:
        """
        Show all menu items for a platform.

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            platform_id: Platform ID
        """
        # Delete all existing records
        deleted = (
            db.query(AdminMenuConfig)
            .filter(
                AdminMenuConfig.frontend_type == frontend_type,
                AdminMenuConfig.platform_id == platform_id,
            )
            .delete()
        )
        logger.info(
            f"Show all menu config for platform {platform_id} ({frontend_type.value}): "
            f"deleted {deleted} rows"
        )

        # Create records with is_visible=True for all non-mandatory items
        all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)

        configs = [
            AdminMenuConfig(
                frontend_type=frontend_type,
                platform_id=platform_id,
                user_id=None,
                menu_item_id=item_id,
                is_visible=True,
            )
            for item_id in all_items
            if item_id not in mandatory_items
        ]
        db.add_all(configs)

        logger.info(
            f"Created {len(all_items) - len(mandatory_items)} visible records for platform {platform_id}"
        )

    def initialize_menu_config(
        self,
        db: Session,
        frontend_type: FrontendType,
        platform_id: int | None = None,
        user_id: int | None = None,
    ) -> bool:
        """
        Initialize menu configuration with all items visible.

        Called when first customizing a menu. Creates records for all items
        so the user can then toggle individual items off.

        Args:
            db: Database session
            frontend_type: Which frontend (admin or store)
            platform_id: Platform ID (for platform-scoped config)
            user_id: User ID (for user-scoped config)

        Returns:
            True if initialized, False if config already exists with visible items
        """
        if not platform_id and not user_id:
            return False  # No scope specified

        # Helper to build a fresh query for this scope
        def scope_query():
            q = db.query(AdminMenuConfig).filter(
                AdminMenuConfig.frontend_type == frontend_type,
            )
            if platform_id:
                return q.filter(AdminMenuConfig.platform_id == platform_id)
            return q.filter(AdminMenuConfig.user_id == user_id)

        # Check if any visible records exist (valid opt-in config)
        visible_count = (
            scope_query()
            .filter(
                AdminMenuConfig.is_visible == True  # noqa: E712
            )
            .count()
        )
        if visible_count > 0:
            logger.debug(
                f"Config already exists with {visible_count} visible items, skipping init"
            )
            return False  # Already initialized

        # Check if ANY records exist (even is_visible=False from old opt-out model)
        total_count = scope_query().count()
        if total_count > 0:
            # Clean up old records first
            deleted = scope_query().delete(synchronize_session="fetch")
            db.flush()  # Ensure deletes are applied before inserts
            logger.info(
                f"Cleaned up {deleted} old menu config records before initialization"
            )

        # Get all menu items for this frontend
        all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
        mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)

        # Create visible records for all non-mandatory items
        configs = [
            AdminMenuConfig(
                frontend_type=frontend_type,
                platform_id=platform_id,
                user_id=user_id,
                menu_item_id=item_id,
                is_visible=True,
            )
            for item_id in all_items
            if item_id not in mandatory_items
        ]
        db.add_all(configs)

        logger.info(
            f"Initialized menu config with {len(all_items) - len(mandatory_items)} items "
            f"(platform_id={platform_id}, user_id={user_id})"
        )
        return True


# Singleton instance
menu_service = MenuService()


__all__ = [
    "menu_service",
    "MenuService",
    "MenuItemConfig",
]
