# app/modules/loyalty/services/points_service.py
"""
Points service.

Handles points operations including:
- Earning points from purchases
- Redeeming points for rewards
- Points balance management
"""

import logging
from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.modules.loyalty.exceptions import (
    InsufficientPointsException,
    InvalidRewardException,
    LoyaltyCardInactiveException,
    LoyaltyProgramInactiveException,
    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 PointsService:
    """Service for points operations."""

    def earn_points(
        self,
        db: Session,
        *,
        card_id: int | None = None,
        qr_code: str | None = None,
        card_number: str | None = None,
        purchase_amount_cents: int,
        order_reference: str | None = None,
        staff_pin: str | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
        notes: str | None = None,
    ) -> dict:
        """
        Earn points from a purchase.

        Points are calculated based on the program's points_per_euro rate.

        Args:
            db: Database session
            card_id: Card ID
            qr_code: QR code data
            card_number: Card number
            purchase_amount_cents: Purchase amount in cents
            order_reference: Order reference for tracking
            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
        """
        # 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 points are enabled
        if not program.is_points_enabled:
            logger.warning(f"Points attempted on stamps-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)

        # Calculate points
        # points_per_euro is per full euro, so divide cents by 100
        purchase_euros = purchase_amount_cents / 100
        points_earned = int(purchase_euros * program.points_per_euro)

        if points_earned <= 0:
            return {
                "success": True,
                "message": "Purchase too small to earn points",
                "points_earned": 0,
                "points_per_euro": program.points_per_euro,
                "purchase_amount_cents": purchase_amount_cents,
                "card_id": card.id,
                "card_number": card.card_number,
                "points_balance": card.points_balance,
                "total_points_earned": card.total_points_earned,
            }

        # Add points
        now = datetime.now(UTC)
        card.points_balance += points_earned
        card.total_points_earned += points_earned
        card.last_points_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.POINTS_EARNED.value,
            points_delta=points_earned,
            stamps_balance_after=card.stamp_count,
            points_balance_after=card.points_balance,
            purchase_amount_cents=purchase_amount_cents,
            order_reference=order_reference,
            ip_address=ip_address,
            user_agent=user_agent,
            notes=notes,
            transaction_at=now,
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        logger.info(
            f"Added {points_earned} points to card {card.id} "
            f"(purchase: €{purchase_euros:.2f}, balance: {card.points_balance})"
        )

        return {
            "success": True,
            "message": "Points earned successfully",
            "points_earned": points_earned,
            "points_per_euro": program.points_per_euro,
            "purchase_amount_cents": purchase_amount_cents,
            "card_id": card.id,
            "card_number": card.card_number,
            "points_balance": card.points_balance,
            "total_points_earned": card.total_points_earned,
        }

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

        Args:
            db: Database session
            card_id: Card ID
            qr_code: QR code data
            card_number: Card number
            reward_id: ID of the reward to redeem
            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:
            InvalidRewardException: Reward not found or inactive
            InsufficientPointsException: Not enough points
        """
        # 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)

        # Find the reward
        reward = program.get_points_reward(reward_id)
        if not reward:
            raise InvalidRewardException(reward_id)

        if not reward.get("is_active", True):
            raise InvalidRewardException(reward_id)

        points_required = reward["points_required"]
        reward_name = reward["name"]

        # Check if enough points
        if card.points_balance < points_required:
            raise InsufficientPointsException(card.points_balance, points_required)

        # 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 points
        now = datetime.now(UTC)
        card.points_balance -= points_required
        card.points_redeemed += points_required
        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.POINTS_REDEEMED.value,
            points_delta=-points_required,
            stamps_balance_after=card.stamp_count,
            points_balance_after=card.points_balance,
            reward_id=reward_id,
            reward_description=reward_name,
            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 {points_required} points from card {card.id} "
            f"(reward: {reward_name}, balance: {card.points_balance})"
        )

        return {
            "success": True,
            "message": "Reward redeemed successfully",
            "reward_id": reward_id,
            "reward_name": reward_name,
            "points_spent": points_required,
            "card_id": card.id,
            "card_number": card.card_number,
            "points_balance": card.points_balance,
            "total_points_redeemed": card.points_redeemed,
        }

    def adjust_points(
        self,
        db: Session,
        card_id: int,
        points_delta: int,
        *,
        reason: str,
        staff_pin: str | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
    ) -> dict:
        """
        Manually adjust points (admin operation).

        Args:
            db: Database session
            card_id: Card ID
            points_delta: Points to add (positive) or remove (negative)
            reason: Reason for adjustment
            staff_pin: Staff PIN for verification
            ip_address: Request IP for audit
            user_agent: Request user agent for audit

        Returns:
            Dict with operation result
        """
        card = card_service.require_card(db, card_id)
        program = card.program

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

        # Apply adjustment
        now = datetime.now(UTC)
        card.points_balance += points_delta

        if points_delta > 0:
            card.total_points_earned += points_delta
        else:
            # Negative adjustment - don't add to redeemed, just reduce balance
            pass

        # Ensure balance doesn't go negative
        if card.points_balance < 0:
            card.points_balance = 0

        # 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.POINTS_ADJUSTMENT.value,
            points_delta=points_delta,
            stamps_balance_after=card.stamp_count,
            points_balance_after=card.points_balance,
            notes=reason,
            ip_address=ip_address,
            user_agent=user_agent,
            transaction_at=now,
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        logger.info(
            f"Adjusted points for card {card.id} by {points_delta:+d} "
            f"(reason: {reason}, balance: {card.points_balance})"
        )

        return {
            "success": True,
            "message": "Points adjusted successfully",
            "points_delta": points_delta,
            "card_id": card.id,
            "card_number": card.card_number,
            "points_balance": card.points_balance,
        }


# Singleton instance
points_service = PointsService()
