# app/modules/billing/services/feature_service.py
"""
Feature-agnostic billing service for merchant-level access control.

Zero knowledge of what features exist. Works with:
- TierFeatureLimit (tier -> feature mappings)
- MerchantFeatureOverride (per-merchant exceptions)
- FeatureAggregatorService (discovers features from modules)

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

    # Check if merchant has feature
    if feature_service.has_feature(db, merchant_id, platform_id, "analytics_dashboard"):
        ...

    # Check quantitative limit
    allowed, msg = feature_service.check_resource_limit(
        db, "products_limit", store_id=store_id
    )

    # Get feature summary for merchant portal
    summary = feature_service.get_merchant_features_summary(db, merchant_id, platform_id)
"""

import logging
import time
from dataclasses import dataclass

from sqlalchemy.orm import Session, joinedload

from app.modules.billing.models import (
    MerchantFeatureOverride,
    MerchantSubscription,
    SubscriptionTier,
)
from app.modules.billing.models.tier_feature_limit import TierFeatureLimit
from app.modules.contracts.features import FeatureType

logger = logging.getLogger(__name__)


@dataclass
class FeatureSummary:
    """Summary of a feature for merchant portal display."""

    code: str
    name_key: str
    description_key: str
    category: str
    feature_type: str  # "binary" or "quantitative"
    scope: str  # "store" or "merchant"
    enabled: bool
    limit: int | None  # For quantitative: effective limit
    current: int | None  # For quantitative: current usage
    remaining: int | None  # For quantitative: remaining capacity
    percent_used: float | None  # For quantitative: usage percentage
    is_override: bool  # Whether an override is applied
    unit_key: str | None
    ui_icon: str | None


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

    Caches (merchant_id, platform_id) -> set of feature codes with TTL.
    """

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

    def get(self, merchant_id: int, platform_id: int) -> set[str] | None:
        key = (merchant_id, platform_id)
        if key not in self._cache:
            return None
        features, timestamp = self._cache[key]
        if time.time() - timestamp > self._ttl:
            del self._cache[key]
            return None
        return features

    def set(self, merchant_id: int, platform_id: int, features: set[str]) -> None:
        self._cache[(merchant_id, platform_id)] = (features, time.time())

    def invalidate(self, merchant_id: int, platform_id: int) -> None:
        self._cache.pop((merchant_id, platform_id), None)

    def invalidate_all(self) -> None:
        self._cache.clear()


class FeatureService:
    """
    Feature-agnostic service for merchant-level billing.

    Resolves feature access through:
    1. MerchantSubscription -> SubscriptionTier -> TierFeatureLimit
    2. MerchantFeatureOverride (admin-set exceptions)
    3. FeatureAggregator (for usage counts and declarations)
    """

    def __init__(self):
        self._cache = FeatureCache(ttl_seconds=300)

    # =========================================================================
    # Store -> Merchant Resolution
    # =========================================================================

    def _get_merchant_for_store(
        self, db: Session, store_id: int, platform_id: int | None = None
    ) -> tuple[int | None, int | None]:
        """
        Resolve store_id to (merchant_id, platform_id).

        Args:
            db: Database session
            store_id: Store ID
            platform_id: Platform ID from JWT. When provided, skips DB lookup.

        Returns:
            Tuple of (merchant_id, platform_id), either may be None
        """
        from app.modules.tenancy.services.platform_service import platform_service
        from app.modules.tenancy.services.store_service import store_service

        store = store_service.get_store_by_id_optional(db, store_id)
        if not store:
            return None, None

        merchant_id = store.merchant_id
        if platform_id is None:
            platform_id = platform_service.get_first_active_platform_id_for_store(db, store_id)

        return merchant_id, platform_id

    def _get_merchant_and_platforms_for_store(
        self, db: Session, store_id: int
    ) -> tuple[int | None, list[int]]:
        """
        Resolve store_id to (merchant_id, [platform_ids]).

        Returns all active platform IDs for the store's merchant,
        ordered with the primary platform first.
        """
        from app.modules.tenancy.services.platform_service import platform_service
        from app.modules.tenancy.services.store_service import store_service

        store = store_service.get_store_by_id_optional(db, store_id)
        if not store:
            return None, []

        platform_ids = platform_service.get_active_platform_ids_for_store(db, store_id)
        return store.merchant_id, platform_ids

    def _get_subscription(
        self, db: Session, merchant_id: int, platform_id: int
    ) -> MerchantSubscription | None:
        """Get merchant subscription for a platform."""
        return (
            db.query(MerchantSubscription)
            .options(joinedload(MerchantSubscription.tier).joinedload(SubscriptionTier.feature_limits))
            .filter(
                MerchantSubscription.merchant_id == merchant_id,
                MerchantSubscription.platform_id == platform_id,
            )
            .first()
        )

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

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

        Checks:
        1. MerchantFeatureOverride (force enable/disable)
        2. TierFeatureLimit (tier assignment)

        Args:
            db: Database session
            merchant_id: Merchant ID
            platform_id: Platform ID
            feature_code: Feature code to check

        Returns:
            True if merchant has access to the feature
        """
        # Check override first
        override = (
            db.query(MerchantFeatureOverride)
            .filter(
                MerchantFeatureOverride.merchant_id == merchant_id,
                MerchantFeatureOverride.platform_id == platform_id,
                MerchantFeatureOverride.feature_code == feature_code,
            )
            .first()
        )
        if override is not None:
            return override.is_enabled

        # Check tier assignment
        subscription = self._get_subscription(db, merchant_id, platform_id)
        if not subscription or not subscription.is_active or not subscription.tier:
            return False

        return subscription.tier.has_feature(feature_code)

    def has_feature_for_store(
        self, db: Session, store_id: int, feature_code: str,
        platform_id: int | None = None,
    ) -> bool:
        """
        Convenience method that resolves the store -> merchant -> platform
        hierarchy and checks whether the merchant has access to a feature.

        Args:
            db: Database session.
            store_id: The store ID to resolve.
            feature_code: The feature code to check.
            platform_id: Platform ID from JWT. When provided, skips DB lookup.

        Returns:
            True if the resolved merchant has access to the feature,
            False if the store/merchant cannot be resolved or lacks access.
        """
        merchant_id, resolved_platform_id = self._get_merchant_for_store(
            db, store_id, platform_id=platform_id
        )
        if merchant_id is None or resolved_platform_id is None:
            return False
        return self.has_feature(db, merchant_id, resolved_platform_id, feature_code)

    def get_merchant_feature_codes(
        self, db: Session, merchant_id: int, platform_id: int
    ) -> set[str]:
        """Get all feature codes available to merchant on a platform."""
        # Check cache
        cached = self._cache.get(merchant_id, platform_id)
        if cached is not None:
            return cached

        features: set[str] = set()

        # Get tier features
        subscription = self._get_subscription(db, merchant_id, platform_id)
        if subscription and subscription.is_active and subscription.tier:
            features = subscription.tier.get_feature_codes()

        # Apply overrides
        overrides = (
            db.query(MerchantFeatureOverride)
            .filter(
                MerchantFeatureOverride.merchant_id == merchant_id,
                MerchantFeatureOverride.platform_id == platform_id,
            )
            .all()
        )
        for override in overrides:
            if override.is_enabled:
                features.add(override.feature_code)
            else:
                features.discard(override.feature_code)

        self._cache.set(merchant_id, platform_id, features)
        return features

    # =========================================================================
    # Effective Limits
    # =========================================================================

    def get_effective_limit(
        self, db: Session, merchant_id: int, platform_id: int, feature_code: str
    ) -> int | None:
        """
        Get the effective limit for a feature (override or tier default).

        Returns:
            Limit value, or None for unlimited
        """
        # Check override first
        override = (
            db.query(MerchantFeatureOverride)
            .filter(
                MerchantFeatureOverride.merchant_id == merchant_id,
                MerchantFeatureOverride.platform_id == platform_id,
                MerchantFeatureOverride.feature_code == feature_code,
            )
            .first()
        )
        if override is not None:
            return override.limit_value

        # Get from tier
        subscription = self._get_subscription(db, merchant_id, platform_id)
        if not subscription or not subscription.tier:
            return 0  # No subscription = no access

        return subscription.tier.get_limit_for_feature(feature_code)

    # =========================================================================
    # Resource Limit Checks
    # =========================================================================

    def check_resource_limit(
        self,
        db: Session,
        feature_code: str,
        store_id: int | None = None,
        merchant_id: int | None = None,
        platform_id: int | None = None,
    ) -> tuple[bool, str | None]:
        """
        Check if a resource limit allows adding more items.

        Resolves store -> merchant if needed. Gets the declaration to
        determine scope, then checks usage against limit.

        Args:
            db: Database session
            feature_code: Feature code (e.g., "products_limit")
            store_id: Store ID (if checking per-store)
            merchant_id: Merchant ID (if already known)
            platform_id: Platform ID (if already known, e.g. from JWT)

        Returns:
            (allowed, error_message) tuple
        """
        from app.modules.billing.services.feature_aggregator import feature_aggregator

        # Resolve store -> merchant if needed
        if merchant_id is None and store_id is not None:
            merchant_id, platform_id = self._get_merchant_for_store(
                db, store_id, platform_id=platform_id
            )

        if merchant_id is None or platform_id is None:
            return False, "No subscription found"

        # Check subscription is active
        subscription = self._get_subscription(db, merchant_id, platform_id)
        if not subscription or not subscription.is_active:
            return False, "Subscription is not active"

        # Get feature declaration
        decl = feature_aggregator.get_declaration(feature_code)
        if decl is None:
            logger.warning(f"Unknown feature code: {feature_code}")
            return True, None  # Unknown features are allowed by default

        # Binary features: just check if enabled
        if decl.feature_type == FeatureType.BINARY:
            if self.has_feature(db, merchant_id, platform_id, feature_code):
                return True, None
            return False, f"Feature '{feature_code}' requires an upgrade"

        # Quantitative: check usage against limit
        limit = self.get_effective_limit(db, merchant_id, platform_id, feature_code)
        if limit is None:  # Unlimited
            return True, None

        # Get current usage based on scope
        usage = feature_aggregator.get_usage_for_feature(
            db, feature_code,
            store_id=store_id,
            merchant_id=merchant_id,
            platform_id=platform_id,
        )
        current = usage.current_count if usage else 0

        if current >= limit:
            return False, (
                f"Limit reached ({current}/{limit} {decl.unit_key or feature_code}). "
                f"Upgrade to increase your limit."
            )

        return True, None

    # =========================================================================
    # Feature Summary
    # =========================================================================

    def get_merchant_features_summary(
        self, db: Session, merchant_id: int, platform_id: int
    ) -> list[FeatureSummary]:
        """
        Get complete feature summary for merchant portal display.

        Returns all features with current status, limits, and usage.
        """
        from app.modules.billing.services.feature_aggregator import feature_aggregator

        declarations = feature_aggregator.get_all_declarations()
        merchant_features = self.get_merchant_feature_codes(db, merchant_id, platform_id)

        # Preload overrides
        overrides = {
            o.feature_code: o
            for o in db.query(MerchantFeatureOverride).filter(
                MerchantFeatureOverride.merchant_id == merchant_id,
                MerchantFeatureOverride.platform_id == platform_id,
            ).all()
        }

        # Get all usage at once
        merchant_usage = feature_aggregator.get_merchant_usage(db, merchant_id, platform_id)

        summaries = []
        for code, decl in sorted(declarations.items(), key=lambda x: (x[1].category, x[1].display_order)):
            enabled = code in merchant_features
            is_override = code in overrides
            limit = self.get_effective_limit(db, merchant_id, platform_id, code) if decl.feature_type == FeatureType.QUANTITATIVE else None

            current = None
            remaining = None
            percent_used = None

            if decl.feature_type == FeatureType.QUANTITATIVE:
                usage_data = merchant_usage.get(code)
                current = usage_data.current_count if usage_data else 0
                if limit is not None:
                    remaining = max(0, limit - current)
                    percent_used = min(100.0, (current / limit * 100)) if limit > 0 else 0.0

            summaries.append(FeatureSummary(
                code=code,
                name_key=decl.name_key,
                description_key=decl.description_key,
                category=decl.category,
                feature_type=decl.feature_type.value,
                scope=decl.scope.value,
                enabled=enabled,
                limit=limit,
                current=current,
                remaining=remaining,
                percent_used=percent_used,
                is_override=is_override,
                unit_key=decl.unit_key,
                ui_icon=decl.ui_icon,
            ))

        return summaries

    # =========================================================================
    # Tier Feature Limit Management
    # =========================================================================

    def get_tier_feature_limits(self, db: Session, tier_id: int) -> list:
        """Get feature limits for a tier."""
        return (
            db.query(TierFeatureLimit)
            .filter(TierFeatureLimit.tier_id == tier_id)
            .order_by(TierFeatureLimit.feature_code)
            .all()
        )

    def upsert_tier_feature_limits(self, db: Session, tier_id: int, entries: list[dict]) -> list:
        """Replace feature limits for a tier. Returns list of new TierFeatureLimit objects."""
        db.query(TierFeatureLimit).filter(TierFeatureLimit.tier_id == tier_id).delete()
        new_rows = []
        for entry in entries:
            if not entry.get("enabled", True):
                continue
            row = TierFeatureLimit(
                tier_id=tier_id,
                feature_code=entry["feature_code"],
                limit_value=entry.get("limit_value"),
            )
            db.add(row)  # noqa: PERF006
            new_rows.append(row)
        return new_rows

    # =========================================================================
    # Merchant Feature Override Management
    # =========================================================================

    def get_merchant_overrides(self, db: Session, merchant_id: int) -> list:
        """Get feature overrides for a merchant."""
        return (
            db.query(MerchantFeatureOverride)
            .filter(MerchantFeatureOverride.merchant_id == merchant_id)
            .order_by(MerchantFeatureOverride.feature_code)
            .all()
        )

    def upsert_merchant_overrides(
        self, db: Session, merchant_id: int, platform_id: int, entries: list[dict]
    ) -> list:
        """Upsert feature overrides for a merchant."""
        results = []
        for entry in entries:
            existing = (
                db.query(MerchantFeatureOverride)
                .filter(
                    MerchantFeatureOverride.merchant_id == merchant_id,
                    MerchantFeatureOverride.platform_id == platform_id,
                    MerchantFeatureOverride.feature_code == entry["feature_code"],
                )
                .first()
            )
            if existing:
                existing.limit_value = entry.get("limit_value")
                existing.is_enabled = entry.get("is_enabled", True)
                existing.reason = entry.get("reason")
                results.append(existing)
            else:
                row = MerchantFeatureOverride(
                    merchant_id=merchant_id,
                    platform_id=platform_id,
                    feature_code=entry["feature_code"],
                    limit_value=entry.get("limit_value"),
                    is_enabled=entry.get("is_enabled", True),
                    reason=entry.get("reason"),
                )
                db.add(row)  # noqa: PERF006
                results.append(row)
        return results

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

    def invalidate_cache(self, merchant_id: int, platform_id: int) -> None:
        """Invalidate cache for a specific merchant/platform."""
        self._cache.invalidate(merchant_id, platform_id)

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


# Singleton instance
feature_service = FeatureService()

__all__ = [
    "feature_service",
    "FeatureService",
    "FeatureSummary",
]
