"""Plan service for subscription and usage management."""

from datetime import date
from typing import Any
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from identity_plan_kit.config import IdentityPlanKitConfig
from identity_plan_kit.plans.domain.entities import Plan, UserPlan
from identity_plan_kit.plans.domain.exceptions import (
    FeatureNotAvailableError,
    FeatureNotFoundError,
    PlanExpiredError,
    PlanNotFoundError,
    QuotaExceededError,
    UserPlanNotFoundError,
)
from identity_plan_kit.plans.dto.usage import UsageInfo
from identity_plan_kit.plans.repositories.usage_repo import QuotaExceededInRepoError
from identity_plan_kit.plans.uow import PlansUnitOfWork
from identity_plan_kit.shared.logging import get_logger

logger = get_logger(__name__)


class PlanService:
    """Service for plan and usage operations."""

    def __init__(
        self,
        config: IdentityPlanKitConfig,
        session_factory: async_sessionmaker[AsyncSession],
    ) -> None:
        self._config = config
        self._session_factory = session_factory

    def _create_uow(
        self,
        session: AsyncSession | None = None,
    ) -> PlansUnitOfWork:
        """
        Create a new Unit of Work instance.

        Args:
            session: Optional external session for transaction participation.
        """
        return PlansUnitOfWork(self._session_factory, session=session)

    async def get_user_plan(
        self,
        user_id: UUID,
        session: AsyncSession | None = None,
    ) -> UserPlan:
        """
        Get user's active plan.

        Args:
            user_id: User UUID
            session: Optional external session for transaction participation

        Returns:
            UserPlan entity

        Raises:
            UserPlanNotFoundError: If user has no active plan
        """
        async with self._create_uow(session=session) as uow:
            user_plan = await uow.plans.get_user_active_plan(user_id)

            if user_plan is None:
                raise UserPlanNotFoundError()

            return user_plan

    async def get_plan(self, plan_code: str) -> Plan:
        """
        Get plan by code.

        Args:
            plan_code: Plan code

        Returns:
            Plan entity
        """
        async with self._create_uow() as uow:
            plan = await uow.plans.get_plan_by_code(plan_code)

            if plan is None:
                raise PlanNotFoundError(plan_code)

            return plan

    async def check_feature_access(
        self,
        user_id: UUID,
        feature_code: str,
        session: AsyncSession | None = None,
    ) -> bool:
        """
        Check if user has access to a feature.

        Args:
            user_id: User UUID
            feature_code: Feature code to check
            session: Optional external session for transaction participation

        Returns:
            True if user has access, False otherwise

        Note:
            This method does NOT swallow unexpected exceptions.
            Database errors and other infrastructure failures will propagate.
        """
        try:
            async with self._create_uow(session=session) as uow:
                user_plan = await uow.plans.get_user_active_plan(user_id)
                if user_plan is None:
                    return False

                limit = await uow.plans.get_plan_limit(user_plan.plan_id, feature_code)
                return limit is not None

        except (UserPlanNotFoundError, FeatureNotAvailableError):
            # Expected business exceptions - user doesn't have access
            return False
        # Other exceptions (DB errors, etc.) propagate to caller

    async def check_and_consume_quota(
        self,
        user_id: UUID,
        feature_code: str,
        amount: int = 1,
        session: AsyncSession | None = None,
    ) -> UsageInfo:
        """
        Check if user has quota and consume it atomically.

        P0 FIX: Uses atomic check-and-consume to prevent TOCTOU race conditions.
        Two concurrent requests cannot both pass the quota check anymore.

        Args:
            user_id: User UUID
            feature_code: Feature code
            amount: Amount to consume
            session: Optional external session for transaction participation

        Returns:
            UsageInfo with updated usage

        Raises:
            UserPlanNotFoundError: If user has no active plan
            PlanExpiredError: If plan has expired
            FeatureNotAvailableError: If feature not in plan
            QuotaExceededError: If quota would be exceeded
        """
        async with self._create_uow(session=session) as uow:
            # Get user's active plan
            user_plan = await uow.plans.get_user_active_plan(user_id)
            if user_plan is None:
                raise UserPlanNotFoundError()

            if user_plan.is_expired:
                raise PlanExpiredError()

            # Get feature limit
            limit = await uow.plans.get_plan_limit(user_plan.plan_id, feature_code)
            if limit is None:
                raise FeatureNotAvailableError(feature_code, user_plan.plan_code)

            # Check for custom limit override
            custom_limit = user_plan.get_custom_limit(feature_code)
            effective_limit = custom_limit if custom_limit is not None else limit.limit

            # Atomic check-and-consume (fixes TOCTOU race condition)
            try:
                new_usage = await uow.usage.atomic_check_and_consume(
                    user_plan_id=user_plan.id,
                    feature_id=limit.feature_id,
                    amount=amount,
                    limit=effective_limit,
                    period=limit.period,
                )
            except QuotaExceededInRepoError as e:
                logger.warning(
                    "quota_exceeded",
                    user_id=str(user_id),
                    feature=feature_code,
                    used=e.current_usage,
                    limit=e.limit,
                    requested=e.requested,
                )
                raise QuotaExceededError(
                    feature_code=feature_code,
                    limit=e.limit,
                    used=e.current_usage,
                    period=limit.period.value if limit.period else None,
                ) from e

            # UoW commits automatically
            if effective_limit == -1:
                logger.debug(
                    "quota_consumed_unlimited",
                    user_id=str(user_id),
                    feature=feature_code,
                    amount=amount,
                )
                remaining = -1
            else:
                logger.info(
                    "quota_consumed",
                    user_id=str(user_id),
                    feature=feature_code,
                    amount=amount,
                    used=new_usage,
                    limit=effective_limit,
                )
                remaining = effective_limit - new_usage

            return UsageInfo(
                feature_code=feature_code,
                used=new_usage,
                limit=effective_limit,
                period=limit.period.value if limit.period else None,
                remaining=remaining,
            )

    async def get_usage_info(
        self,
        user_id: UUID,
        feature_code: str,
    ) -> UsageInfo:
        """
        Get current usage info for a feature.

        Args:
            user_id: User UUID
            feature_code: Feature code

        Returns:
            UsageInfo with current usage
        """
        async with self._create_uow() as uow:
            user_plan = await uow.plans.get_user_active_plan(user_id)
            if user_plan is None:
                raise UserPlanNotFoundError()

            limit = await uow.plans.get_plan_limit(user_plan.plan_id, feature_code)
            if limit is None:
                raise FeatureNotAvailableError(feature_code, user_plan.plan_code)

            current_usage = await uow.usage.get_current_usage(
                user_plan.id,
                limit.feature_id,
                limit.period,
            )

            effective_limit = user_plan.get_custom_limit(feature_code) or limit.limit

            remaining = -1 if effective_limit == -1 else effective_limit - current_usage

            return UsageInfo(
                feature_code=feature_code,
                used=current_usage,
                limit=effective_limit,
                period=limit.period.value if limit.period else None,
                remaining=remaining,
            )

    # =========================================================================
    # Plan Management Methods (for webhook/payment integration)
    # =========================================================================

    async def assign_plan(
        self,
        user_id: UUID,
        plan_code: str,
        started_at: date | None = None,
        ends_at: date | None = None,
        custom_limits: dict[str, Any] | None = None,
        expire_current: bool = True,
        session: AsyncSession | None = None,
    ) -> UserPlan:
        """
        Assign a user to a plan.

        Use this when a payment webhook indicates a new subscription or upgrade.
        Optionally expires the current plan to prevent overlap.

        Args:
            user_id: User UUID
            plan_code: Plan code to assign (e.g., "pro", "enterprise")
            started_at: Plan start date (defaults to today)
            ends_at: Plan end date (defaults to 100 years for "lifetime")
            custom_limits: Optional custom limits override (e.g., {"api_calls": 5000})
            expire_current: If True, expires any current plan before assigning new one
            session: Optional external session for transaction participation

        Returns:
            Created UserPlan entity

        Raises:
            PlanNotFoundError: If plan_code doesn't exist

        Example:
            # Stripe webhook: subscription created
            user_plan = await kit.plan_service.assign_plan(
                user_id=user.id,
                plan_code="pro",
                ends_at=datetime.fromisoformat(event.current_period_end).date(),
            )
        """
        async with self._create_uow(session=session) as uow:
            # Verify plan exists
            plan = await uow.plans.get_plan_by_code(plan_code)
            if plan is None:
                raise PlanNotFoundError(plan_code)

            # Optionally expire current plan
            if expire_current:
                current_plan = await uow.plans.get_user_active_plan(user_id)
                if current_plan is not None:
                    await uow.plans.expire_user_plan(current_plan.id)
                    logger.info(
                        "previous_plan_expired",
                        user_id=str(user_id),
                        previous_plan_id=str(current_plan.id),
                    )

            # Create new plan assignment
            user_plan = await uow.plans.create_user_plan(
                user_id=user_id,
                plan_id=plan.id,
                started_at=started_at,
                ends_at=ends_at,
                custom_limits=custom_limits,
            )

            logger.info(
                "plan_assigned",
                user_id=str(user_id),
                plan_code=plan_code,
                ends_at=str(ends_at) if ends_at else "lifetime",
            )

            return user_plan

    async def cancel_plan(
        self,
        user_id: UUID,
        immediate: bool = False,
        session: AsyncSession | None = None,
    ) -> bool:
        """
        Cancel user's current plan.

        Use this when a payment webhook indicates a subscription cancellation.

        Args:
            user_id: User UUID
            immediate: If True, plan becomes inactive immediately.
                      If False, plan remains active until its current ends_at.
            session: Optional external session for transaction participation

        Returns:
            True if a plan was cancelled, False if no active plan found

        Example:
            # Stripe webhook: subscription deleted (immediate cancellation)
            await kit.plan_service.cancel_plan(user_id, immediate=True)

            # Stripe webhook: subscription scheduled for cancellation
            await kit.plan_service.cancel_plan(user_id, immediate=False)
        """
        async with self._create_uow(session=session) as uow:
            result = await uow.plans.cancel_user_plan(user_id, immediate=immediate)

            if result:
                logger.info(
                    "plan_cancelled",
                    user_id=str(user_id),
                    immediate=immediate,
                )

            return result

    async def extend_plan(
        self,
        user_id: UUID,
        new_ends_at: date,
        session: AsyncSession | None = None,
    ) -> UserPlan:
        """
        Extend user's current plan end date.

        Use this when a payment webhook indicates subscription renewal.

        Args:
            user_id: User UUID
            new_ends_at: New plan end date
            session: Optional external session for transaction participation

        Returns:
            Updated UserPlan entity

        Raises:
            UserPlanNotFoundError: If user has no active plan

        Example:
            # Stripe webhook: invoice paid (renewal)
            await kit.plan_service.extend_plan(
                user_id=user.id,
                new_ends_at=datetime.fromisoformat(event.current_period_end).date(),
            )
        """
        async with self._create_uow(session=session) as uow:
            user_plan = await uow.plans.get_user_active_plan(user_id)
            if user_plan is None:
                raise UserPlanNotFoundError()

            updated_plan = await uow.plans.update_user_plan(
                user_plan_id=user_plan.id,
                ends_at=new_ends_at,
            )

            if updated_plan is None:
                raise UserPlanNotFoundError()

            logger.info(
                "plan_extended",
                user_id=str(user_id),
                user_plan_id=str(user_plan.id),
                new_ends_at=str(new_ends_at),
            )

            return updated_plan

    async def update_plan_limits(
        self,
        user_id: UUID,
        custom_limits: dict[str, int],
        session: AsyncSession | None = None,
    ) -> UserPlan:
        """
        Update custom limits for user's current plan.

        Use this when you need to override plan limits for specific users
        (e.g., promotional offers, enterprise contracts).

        Args:
            user_id: User UUID
            custom_limits: Custom limits override (e.g., {"api_calls": 10000})
            session: Optional external session for transaction participation

        Returns:
            Updated UserPlan entity

        Raises:
            UserPlanNotFoundError: If user has no active plan

        Example:
            # Give a user extra API calls
            await kit.plan_service.update_plan_limits(
                user_id=user.id,
                custom_limits={"api_calls": 10000, "ai_generation": 500},
            )
        """
        async with self._create_uow(session=session) as uow:
            user_plan = await uow.plans.get_user_active_plan(user_id)
            if user_plan is None:
                raise UserPlanNotFoundError()

            # Merge with existing custom limits
            merged_limits = {**user_plan.custom_limits, **custom_limits}

            updated_plan = await uow.plans.update_user_plan(
                user_plan_id=user_plan.id,
                custom_limits=merged_limits,
            )

            if updated_plan is None:
                raise UserPlanNotFoundError()

            logger.info(
                "plan_limits_updated",
                user_id=str(user_id),
                user_plan_id=str(user_plan.id),
                custom_limits=custom_limits,
            )

            return updated_plan

    async def reset_usage(
        self,
        user_id: UUID,
        feature_code: str | None = None,
        session: AsyncSession | None = None,
    ) -> None:
        """
        Reset usage counters for a user.

        Use this when:
        - A new billing period starts
        - Manually resetting usage (e.g., customer support)
        - Testing

        Args:
            user_id: User UUID
            feature_code: Specific feature to reset (None = reset all features)
            session: Optional external session for transaction participation

        Raises:
            UserPlanNotFoundError: If user has no active plan
            FeatureNotFoundError: If feature_code doesn't exist

        Example:
            # Reset all usage for a user
            await kit.plan_service.reset_usage(user_id)

            # Reset specific feature usage
            await kit.plan_service.reset_usage(user_id, "api_calls")
        """
        async with self._create_uow(session=session) as uow:
            user_plan = await uow.plans.get_user_active_plan(user_id)
            if user_plan is None:
                raise UserPlanNotFoundError()

            if feature_code is not None:
                # Reset specific feature
                feature = await uow.plans.get_feature_by_code(feature_code)
                if feature is None:
                    raise FeatureNotFoundError(feature_code)

                await uow.usage.reset_usage(user_plan.id, feature.id)

                logger.info(
                    "usage_reset",
                    user_id=str(user_id),
                    feature_code=feature_code,
                )
            else:
                # Reset all usage for this user plan in a single query
                reset_count = await uow.usage.reset_all_usage(user_plan.id)

                logger.info(
                    "all_usage_reset",
                    user_id=str(user_id),
                    features_reset=reset_count,
                )

    async def get_user_plan_or_none(
        self,
        user_id: UUID,
        session: AsyncSession | None = None,
    ) -> UserPlan | None:
        """
        Get user's active plan without raising an exception.

        Unlike get_user_plan(), returns None instead of raising
        UserPlanNotFoundError if the user has no active plan.

        Args:
            user_id: User UUID
            session: Optional external session for transaction participation

        Returns:
            UserPlan entity or None
        """
        async with self._create_uow(session=session) as uow:
            return await uow.plans.get_user_active_plan(user_id)
