# app/modules/billing/services/feature_service.py
"""
Feature service for tier-based access control.

Provides:
- Feature availability checking with caching
- Vendor feature listing for API/UI
- Feature metadata for upgrade prompts
- Cache invalidation on subscription changes

Usage:
    from app.modules.billing.services.feature_service import feature_service

    # Check if vendor has feature
    if feature_service.has_feature(db, vendor_id, FeatureCode.ANALYTICS_DASHBOARD):
        ...

    # Get all features available to vendor
    features = feature_service.get_vendor_features(db, vendor_id)

    # Get feature info for upgrade prompt
    info = feature_service.get_feature_upgrade_info(db, "analytics_dashboard")
"""

import logging
import time
from dataclasses import dataclass
from functools import lru_cache

from sqlalchemy.orm import Session, joinedload

from app.modules.billing.exceptions import (
    FeatureNotFoundError,
    InvalidFeatureCodesError,
    TierNotFoundError,
)
from app.modules.billing.models import Feature, FeatureCode
from app.modules.billing.models import SubscriptionTier, VendorSubscription

logger = logging.getLogger(__name__)


@dataclass
class FeatureInfo:
    """Feature information for API responses."""

    code: str
    name: str
    description: str | None
    category: str
    ui_location: str | None
    ui_icon: str | None
    ui_route: str | None
    ui_badge_text: str | None
    is_available: bool
    minimum_tier_code: str | None
    minimum_tier_name: str | None


@dataclass
class FeatureUpgradeInfo:
    """Information for upgrade prompts."""

    feature_code: str
    feature_name: str
    feature_description: str | None
    required_tier_code: str
    required_tier_name: str
    required_tier_price_monthly_cents: int


class FeatureCache:
    """
    In-memory cache for vendor features.

    Caches vendor_id -> set of feature codes with TTL.
    Invalidated when subscription changes.
    """

    def __init__(self, ttl_seconds: int = 300):
        self._cache: dict[int, tuple[set[str], float]] = {}
        self._ttl = ttl_seconds

    def get(self, vendor_id: int) -> set[str] | None:
        """Get cached features for vendor, or None if not cached/expired."""
        if vendor_id not in self._cache:
            return None

        features, timestamp = self._cache[vendor_id]
        if time.time() - timestamp > self._ttl:
            del self._cache[vendor_id]
            return None

        return features

    def set(self, vendor_id: int, features: set[str]) -> None:
        """Cache features for vendor."""
        self._cache[vendor_id] = (features, time.time())

    def invalidate(self, vendor_id: int) -> None:
        """Invalidate cache for vendor."""
        self._cache.pop(vendor_id, None)

    def invalidate_all(self) -> None:
        """Invalidate entire cache."""
        self._cache.clear()


class FeatureService:
    """
    Service for feature-based access control.

    Provides methods to check feature availability and get feature metadata.
    Uses in-memory caching with TTL for performance.
    """

    def __init__(self):
        self._cache = FeatureCache(ttl_seconds=300)  # 5 minute cache
        self._feature_registry_cache: dict[str, Feature] | None = None
        self._feature_registry_timestamp: float = 0

    # =========================================================================
    # Feature Availability
    # =========================================================================

    def has_feature(self, db: Session, vendor_id: int, feature_code: str) -> bool:
        """
        Check if vendor has access to a specific feature.

        Args:
            db: Database session
            vendor_id: Vendor ID
            feature_code: Feature code (use FeatureCode constants)

        Returns:
            True if vendor has access to the feature
        """
        vendor_features = self._get_vendor_feature_codes(db, vendor_id)
        return feature_code in vendor_features

    def get_vendor_feature_codes(self, db: Session, vendor_id: int) -> set[str]:
        """
        Get set of feature codes available to vendor.

        Args:
            db: Database session
            vendor_id: Vendor ID

        Returns:
            Set of feature codes the vendor has access to
        """
        return self._get_vendor_feature_codes(db, vendor_id)

    def _get_vendor_feature_codes(self, db: Session, vendor_id: int) -> set[str]:
        """Internal method with caching."""
        # Check cache first
        cached = self._cache.get(vendor_id)
        if cached is not None:
            return cached

        # Get subscription with tier relationship
        subscription = (
            db.query(VendorSubscription)
            .options(joinedload(VendorSubscription.tier_obj))
            .filter(VendorSubscription.vendor_id == vendor_id)
            .first()
        )

        if not subscription:
            logger.warning(f"No subscription found for vendor {vendor_id}")
            return set()

        # Get features from tier
        tier = subscription.tier_obj
        if tier and tier.features:
            features = set(tier.features)
        else:
            # Fallback: query tier by code
            tier = (
                db.query(SubscriptionTier)
                .filter(SubscriptionTier.code == subscription.tier)
                .first()
            )
            features = set(tier.features) if tier and tier.features else set()

        # Cache and return
        self._cache.set(vendor_id, features)
        return features

    # =========================================================================
    # Feature Listing
    # =========================================================================

    def get_vendor_features(
        self,
        db: Session,
        vendor_id: int,
        category: str | None = None,
        include_unavailable: bool = True,
    ) -> list[FeatureInfo]:
        """
        Get all features with availability status for vendor.

        Args:
            db: Database session
            vendor_id: Vendor ID
            category: Optional category filter
            include_unavailable: Include features not available to vendor

        Returns:
            List of FeatureInfo with is_available flag
        """
        vendor_features = self._get_vendor_feature_codes(db, vendor_id)

        # Query all active features
        query = db.query(Feature).filter(Feature.is_active == True)  # noqa: E712

        if category:
            query = query.filter(Feature.category == category)

        if not include_unavailable:
            # Only return features the vendor has
            query = query.filter(Feature.code.in_(vendor_features))

        features = (
            query.options(joinedload(Feature.minimum_tier))
            .order_by(Feature.category, Feature.display_order)
            .all()
        )

        result = []
        for feature in features:
            result.append(
                FeatureInfo(
                    code=feature.code,
                    name=feature.name,
                    description=feature.description,
                    category=feature.category,
                    ui_location=feature.ui_location,
                    ui_icon=feature.ui_icon,
                    ui_route=feature.ui_route,
                    ui_badge_text=feature.ui_badge_text,
                    is_available=feature.code in vendor_features,
                    minimum_tier_code=feature.minimum_tier.code if feature.minimum_tier else None,
                    minimum_tier_name=feature.minimum_tier.name if feature.minimum_tier else None,
                )
            )

        return result

    def get_available_feature_codes(self, db: Session, vendor_id: int) -> list[str]:
        """
        Get list of feature codes available to vendor (for frontend).

        Simple list for x-feature directive checks.
        """
        return list(self._get_vendor_feature_codes(db, vendor_id))

    # =========================================================================
    # Feature Metadata
    # =========================================================================

    def get_feature_by_code(self, db: Session, feature_code: str) -> Feature | None:
        """Get feature by code."""
        return (
            db.query(Feature)
            .options(joinedload(Feature.minimum_tier))
            .filter(Feature.code == feature_code)
            .first()
        )

    def get_feature_upgrade_info(
        self, db: Session, feature_code: str
    ) -> FeatureUpgradeInfo | None:
        """
        Get upgrade information for a feature.

        Used for upgrade prompts when a feature is not available.
        """
        feature = self.get_feature_by_code(db, feature_code)

        if not feature or not feature.minimum_tier:
            return None

        tier = feature.minimum_tier
        return FeatureUpgradeInfo(
            feature_code=feature.code,
            feature_name=feature.name,
            feature_description=feature.description,
            required_tier_code=tier.code,
            required_tier_name=tier.name,
            required_tier_price_monthly_cents=tier.price_monthly_cents,
        )

    def get_all_features(
        self,
        db: Session,
        category: str | None = None,
        active_only: bool = True,
    ) -> list[Feature]:
        """Get all features (for admin)."""
        query = db.query(Feature).options(joinedload(Feature.minimum_tier))

        if active_only:
            query = query.filter(Feature.is_active == True)  # noqa: E712

        if category:
            query = query.filter(Feature.category == category)

        return query.order_by(Feature.category, Feature.display_order).all()

    def get_features_by_tier(self, db: Session, tier_code: str) -> list[str]:
        """Get feature codes for a specific tier."""
        tier = db.query(SubscriptionTier).filter(SubscriptionTier.code == tier_code).first()

        if not tier or not tier.features:
            return []

        return tier.features

    # =========================================================================
    # Feature Categories
    # =========================================================================

    def get_categories(self, db: Session) -> list[str]:
        """Get all unique feature categories."""
        result = (
            db.query(Feature.category)
            .filter(Feature.is_active == True)  # noqa: E712
            .distinct()
            .order_by(Feature.category)
            .all()
        )
        return [row[0] for row in result]

    def get_features_grouped_by_category(
        self, db: Session, vendor_id: int
    ) -> dict[str, list[FeatureInfo]]:
        """Get features grouped by category with availability."""
        features = self.get_vendor_features(db, vendor_id, include_unavailable=True)

        grouped: dict[str, list[FeatureInfo]] = {}
        for feature in features:
            if feature.category not in grouped:
                grouped[feature.category] = []
            grouped[feature.category].append(feature)

        return grouped

    # =========================================================================
    # Cache Management
    # =========================================================================

    def invalidate_vendor_cache(self, vendor_id: int) -> None:
        """
        Invalidate cache for a specific vendor.

        Call this when:
        - Vendor's subscription tier changes
        - Tier features are updated (for all vendors on that tier)
        """
        self._cache.invalidate(vendor_id)
        logger.debug(f"Invalidated feature cache for vendor {vendor_id}")

    def invalidate_all_cache(self) -> None:
        """
        Invalidate entire cache.

        Call this when tier features are modified in admin.
        """
        self._cache.invalidate_all()
        logger.debug("Invalidated all feature caches")

    # =========================================================================
    # Admin Operations
    # =========================================================================

    def get_all_tiers_with_features(self, db: Session) -> list[SubscriptionTier]:
        """Get all active tiers with their features for admin."""
        return (
            db.query(SubscriptionTier)
            .filter(SubscriptionTier.is_active == True)  # noqa: E712
            .order_by(SubscriptionTier.display_order)
            .all()
        )

    def get_tier_by_code(self, db: Session, tier_code: str) -> SubscriptionTier:
        """
        Get tier by code, raising exception if not found.

        Raises:
            TierNotFoundError: If tier not found
        """
        tier = db.query(SubscriptionTier).filter(SubscriptionTier.code == tier_code).first()
        if not tier:
            raise TierNotFoundError(tier_code)
        return tier

    def get_tier_features_with_details(
        self, db: Session, tier_code: str
    ) -> tuple[SubscriptionTier, list[Feature]]:
        """
        Get tier with full feature details.

        Returns:
            Tuple of (tier, list of Feature objects)

        Raises:
            TierNotFoundError: If tier not found
        """
        tier = self.get_tier_by_code(db, tier_code)
        feature_codes = tier.features or []

        features = (
            db.query(Feature)
            .filter(Feature.code.in_(feature_codes))
            .order_by(Feature.category, Feature.display_order)
            .all()
        )

        return tier, features

    def update_tier_features(
        self, db: Session, tier_code: str, feature_codes: list[str]
    ) -> SubscriptionTier:
        """
        Update features for a tier (admin operation).

        Args:
            db: Database session
            tier_code: Tier code
            feature_codes: List of feature codes to assign

        Returns:
            Updated tier

        Raises:
            TierNotFoundError: If tier not found
            InvalidFeatureCodesError: If any feature codes are invalid
        """
        tier = db.query(SubscriptionTier).filter(SubscriptionTier.code == tier_code).first()

        if not tier:
            raise TierNotFoundError(tier_code)

        # Validate feature codes exist
        # noqa: SVC-005 - Features are platform-level, not vendor-scoped
        valid_codes = {
            f.code for f in db.query(Feature.code).filter(Feature.is_active == True).all()  # noqa: E712
        }
        invalid = set(feature_codes) - valid_codes
        if invalid:
            raise InvalidFeatureCodesError(invalid)

        tier.features = feature_codes

        # Invalidate all caches since tier features changed
        self.invalidate_all_cache()

        logger.info(f"Updated features for tier {tier_code}: {len(feature_codes)} features")
        return tier

    def update_feature(
        self,
        db: Session,
        feature_code: str,
        name: str | None = None,
        description: str | None = None,
        category: str | None = None,
        ui_location: str | None = None,
        ui_icon: str | None = None,
        ui_route: str | None = None,
        ui_badge_text: str | None = None,
        minimum_tier_code: str | None = None,
        is_active: bool | None = None,
        is_visible: bool | None = None,
        display_order: int | None = None,
    ) -> Feature:
        """
        Update feature metadata.

        Args:
            db: Database session
            feature_code: Feature code to update
            ... other optional fields to update

        Returns:
            Updated feature

        Raises:
            FeatureNotFoundError: If feature not found
            TierNotFoundError: If minimum_tier_code provided but not found
        """
        feature = (
            db.query(Feature)
            .options(joinedload(Feature.minimum_tier))
            .filter(Feature.code == feature_code)
            .first()
        )

        if not feature:
            raise FeatureNotFoundError(feature_code)

        # Update fields if provided
        if name is not None:
            feature.name = name
        if description is not None:
            feature.description = description
        if category is not None:
            feature.category = category
        if ui_location is not None:
            feature.ui_location = ui_location
        if ui_icon is not None:
            feature.ui_icon = ui_icon
        if ui_route is not None:
            feature.ui_route = ui_route
        if ui_badge_text is not None:
            feature.ui_badge_text = ui_badge_text
        if is_active is not None:
            feature.is_active = is_active
        if is_visible is not None:
            feature.is_visible = is_visible
        if display_order is not None:
            feature.display_order = display_order

        # Update minimum tier if provided
        if minimum_tier_code is not None:
            if minimum_tier_code == "":
                feature.minimum_tier_id = None
            else:
                tier = (
                    db.query(SubscriptionTier)
                    .filter(SubscriptionTier.code == minimum_tier_code)
                    .first()
                )
                if not tier:
                    raise TierNotFoundError(minimum_tier_code)
                feature.minimum_tier_id = tier.id

        logger.info(f"Updated feature {feature_code}")
        return feature

    def update_feature_minimum_tier(
        self, db: Session, feature_code: str, tier_code: str | None
    ) -> Feature:
        """
        Update minimum tier for a feature (for upgrade prompts).

        Args:
            db: Database session
            feature_code: Feature code
            tier_code: Tier code or None

        Raises:
            FeatureNotFoundError: If feature not found
            TierNotFoundError: If tier_code provided but not found
        """
        feature = db.query(Feature).filter(Feature.code == feature_code).first()

        if not feature:
            raise FeatureNotFoundError(feature_code)

        if tier_code:
            tier = db.query(SubscriptionTier).filter(SubscriptionTier.code == tier_code).first()
            if not tier:
                raise TierNotFoundError(tier_code)
            feature.minimum_tier_id = tier.id
        else:
            feature.minimum_tier_id = None

        logger.info(f"Updated minimum tier for feature {feature_code}: {tier_code}")
        return feature


# Singleton instance
feature_service = FeatureService()


# ============================================================================
# Convenience Exports
# ============================================================================
# Re-export FeatureCode for easy imports

__all__ = [
    "feature_service",
    "FeatureService",
    "FeatureInfo",
    "FeatureUpgradeInfo",
    "FeatureCode",
]
