# app/modules/loyalty/services/stamp_service.py
"""
Stamp service.

Handles stamp operations including:
- Adding stamps with anti-fraud checks
- Redeeming stamps for rewards
- Daily limit tracking
"""

import logging
from datetime import UTC, datetime, timedelta

from sqlalchemy.orm import Session

from app.modules.loyalty.config import config
from app.modules.loyalty.exceptions import (
    DailyStampLimitException,
    InsufficientStampsException,
    LoyaltyCardInactiveException,
    LoyaltyProgramInactiveException,
    StampCooldownException,
    StaffPinRequiredException,
)
from app.modules.loyalty.models import LoyaltyCard, LoyaltyTransaction, TransactionType
from app.modules.loyalty.services.card_service import card_service
from app.modules.loyalty.services.pin_service import pin_service

logger = logging.getLogger(__name__)


class StampService:
    """Service for stamp operations."""

    def add_stamp(
        self,
        db: Session,
        *,
        card_id: int | None = None,
        qr_code: str | None = None,
        card_number: str | None = None,
        staff_pin: str | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
        notes: str | None = None,
    ) -> dict:
        """
        Add a stamp to a loyalty card.

        Performs all anti-fraud checks:
        - Staff PIN verification (if required)
        - Cooldown period check
        - Daily limit check

        Args:
            db: Database session
            card_id: Card ID
            qr_code: QR code data
            card_number: Card number
            staff_pin: Staff PIN for verification
            ip_address: Request IP for audit
            user_agent: Request user agent for audit
            notes: Optional notes

        Returns:
            Dict with operation result

        Raises:
            LoyaltyCardNotFoundException: Card not found
            LoyaltyCardInactiveException: Card is inactive
            LoyaltyProgramInactiveException: Program is inactive
            StaffPinRequiredException: PIN required but not provided
            InvalidStaffPinException: PIN is invalid
            StampCooldownException: Cooldown period not elapsed
            DailyStampLimitException: Daily limit reached
        """
        # Look up the card
        card = card_service.lookup_card(
            db,
            card_id=card_id,
            qr_code=qr_code,
            card_number=card_number,
        )

        # Validate card and program
        if not card.is_active:
            raise LoyaltyCardInactiveException(card.id)

        program = card.program
        if not program.is_active:
            raise LoyaltyProgramInactiveException(program.id)

        # Check if stamps are enabled
        if not program.is_stamps_enabled:
            logger.warning(f"Stamp attempted on points-only program {program.id}")
            raise LoyaltyCardInactiveException(card.id)

        # Verify staff PIN if required
        verified_pin = None
        if program.require_staff_pin:
            if not staff_pin:
                raise StaffPinRequiredException()
            verified_pin = pin_service.verify_pin(db, program.id, staff_pin)

        # Check cooldown
        now = datetime.now(UTC)
        if card.last_stamp_at:
            cooldown_ends = card.last_stamp_at + timedelta(minutes=program.cooldown_minutes)
            if now < cooldown_ends:
                raise StampCooldownException(
                    cooldown_ends.isoformat(),
                    program.cooldown_minutes,
                )

        # Check daily limit
        stamps_today = card_service.get_stamps_today(db, card.id)
        if stamps_today >= program.max_daily_stamps:
            raise DailyStampLimitException(program.max_daily_stamps, stamps_today)

        # Add the stamp
        card.stamp_count += 1
        card.total_stamps_earned += 1
        card.last_stamp_at = now

        # Check if reward earned
        reward_earned = card.stamp_count >= program.stamps_target

        # Create transaction
        transaction = LoyaltyTransaction(
            card_id=card.id,
            vendor_id=card.vendor_id,
            staff_pin_id=verified_pin.id if verified_pin else None,
            transaction_type=TransactionType.STAMP_EARNED.value,
            stamps_delta=1,
            stamps_balance_after=card.stamp_count,
            points_balance_after=card.points_balance,
            ip_address=ip_address,
            user_agent=user_agent,
            notes=notes,
            transaction_at=now,
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        stamps_today += 1

        logger.info(
            f"Added stamp to card {card.id} "
            f"(stamps: {card.stamp_count}/{program.stamps_target}, "
            f"today: {stamps_today}/{program.max_daily_stamps})"
        )

        # Calculate next stamp availability
        next_stamp_at = now + timedelta(minutes=program.cooldown_minutes)

        return {
            "success": True,
            "message": "Stamp added successfully",
            "card_id": card.id,
            "card_number": card.card_number,
            "stamp_count": card.stamp_count,
            "stamps_target": program.stamps_target,
            "stamps_until_reward": max(0, program.stamps_target - card.stamp_count),
            "reward_earned": reward_earned,
            "reward_description": program.stamps_reward_description if reward_earned else None,
            "next_stamp_available_at": next_stamp_at,
            "stamps_today": stamps_today,
            "stamps_remaining_today": max(0, program.max_daily_stamps - stamps_today),
        }

    def redeem_stamps(
        self,
        db: Session,
        *,
        card_id: int | None = None,
        qr_code: str | None = None,
        card_number: str | None = None,
        staff_pin: str | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
        notes: str | None = None,
    ) -> dict:
        """
        Redeem stamps for a reward.

        Args:
            db: Database session
            card_id: Card ID
            qr_code: QR code data
            card_number: Card number
            staff_pin: Staff PIN for verification
            ip_address: Request IP for audit
            user_agent: Request user agent for audit
            notes: Optional notes

        Returns:
            Dict with operation result

        Raises:
            LoyaltyCardNotFoundException: Card not found
            InsufficientStampsException: Not enough stamps
            StaffPinRequiredException: PIN required but not provided
        """
        # Look up the card
        card = card_service.lookup_card(
            db,
            card_id=card_id,
            qr_code=qr_code,
            card_number=card_number,
        )

        # Validate card and program
        if not card.is_active:
            raise LoyaltyCardInactiveException(card.id)

        program = card.program
        if not program.is_active:
            raise LoyaltyProgramInactiveException(program.id)

        # Check if enough stamps
        if card.stamp_count < program.stamps_target:
            raise InsufficientStampsException(card.stamp_count, program.stamps_target)

        # Verify staff PIN if required
        verified_pin = None
        if program.require_staff_pin:
            if not staff_pin:
                raise StaffPinRequiredException()
            verified_pin = pin_service.verify_pin(db, program.id, staff_pin)

        # Redeem stamps
        now = datetime.now(UTC)
        stamps_redeemed = program.stamps_target
        card.stamp_count -= stamps_redeemed
        card.stamps_redeemed += 1
        card.last_redemption_at = now

        # Create transaction
        transaction = LoyaltyTransaction(
            card_id=card.id,
            vendor_id=card.vendor_id,
            staff_pin_id=verified_pin.id if verified_pin else None,
            transaction_type=TransactionType.STAMP_REDEEMED.value,
            stamps_delta=-stamps_redeemed,
            stamps_balance_after=card.stamp_count,
            points_balance_after=card.points_balance,
            reward_description=program.stamps_reward_description,
            ip_address=ip_address,
            user_agent=user_agent,
            notes=notes,
            transaction_at=now,
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        logger.info(
            f"Redeemed stamps from card {card.id} "
            f"(reward: {program.stamps_reward_description}, "
            f"total redemptions: {card.stamps_redeemed})"
        )

        return {
            "success": True,
            "message": "Reward redeemed successfully",
            "card_id": card.id,
            "card_number": card.card_number,
            "stamp_count": card.stamp_count,
            "stamps_target": program.stamps_target,
            "reward_description": program.stamps_reward_description,
            "total_redemptions": card.stamps_redeemed,
        }


# Singleton instance
stamp_service = StampService()
