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

Merchant-based points operations:
- Points earned at any store count toward merchant total
- Points can be redeemed at any store within the merchant
- Supports voiding points for returns

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

import logging
from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.modules.loyalty.exceptions import (
    InsufficientPointsException,
    InvalidRewardException,
    LoyaltyCardInactiveException,
    LoyaltyException,
    LoyaltyProgramInactiveException,
    OrderReferenceRequiredException,
    StaffPinRequiredException,
)
from app.modules.loyalty.models import 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,
        *,
        store_id: int,
        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,
        category_ids: list[int] | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
        notes: str | None = None,
        acting_terminal_device_id: int | None = None,
    ) -> dict:
        """
        Earn points from a purchase.

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

        Args:
            db: Database session
            store_id: Store ID (where purchase is being made)
            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 (validates it belongs to store's merchant)
        card = card_service.lookup_card_for_store(
            db,
            store_id,
            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)

        # Check if order reference is required
        from app.modules.loyalty.services.program_service import program_service
        settings = program_service.get_merchant_settings(db, card.merchant_id)
        if settings and settings.require_order_reference and not order_reference:
            raise OrderReferenceRequiredException()

        # Category is mandatory when the store has categories configured
        if not category_ids:
            from app.modules.loyalty.services.category_service import category_service

            store_categories = category_service.list_categories(
                db, store_id, active_only=True
            )
            if store_categories:
                raise LoyaltyException(
                    message="Please select a product category",
                    error_code="CATEGORY_REQUIRED",
                )

        # Idempotency guard: if same order_reference already earned points on this card, return existing result
        if order_reference:
            existing_tx = (
                db.query(LoyaltyTransaction)
                .filter(
                    LoyaltyTransaction.card_id == card.id,
                    LoyaltyTransaction.order_reference == order_reference,
                    LoyaltyTransaction.transaction_type == TransactionType.POINTS_EARNED.value,
                )
                .first()
            )
            if existing_tx:
                return {
                    "success": True,
                    "message": "Points already earned for this order",
                    "points_earned": existing_tx.points_delta,
                    "points_per_euro": program.points_per_euro,
                    "purchase_amount_cents": existing_tx.purchase_amount_cents or purchase_amount_cents,
                    "card_id": card.id,
                    "card_number": card.card_number,
                    "points_balance": card.points_balance,
                    "total_points_earned": card.total_points_earned,
                    "store_id": existing_tx.store_id,
                }

        # Check minimum purchase amount
        if program.minimum_purchase_cents > 0 and purchase_amount_cents < program.minimum_purchase_cents:
            return {
                "success": True,
                "message": f"Purchase below minimum of €{program.minimum_purchase_cents/100:.2f}",
                "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,
            }

        # Verify staff PIN if required.
        # Paired terminal devices ARE the principal — the cashier already
        # bcrypt-verified locally against the device's cached hash list.
        # Web-terminal user JWTs still require the per-action PIN.
        verified_pin = None
        if program.require_staff_pin and acting_terminal_device_id is None:
            if not staff_pin:
                raise StaffPinRequiredException()
            verified_pin = pin_service.verify_pin(db, program.id, staff_pin, store_id=store_id)

        # 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,
            }

        # Re-fetch with row lock to prevent concurrent modification
        card = card_service.get_card_for_update(db, card.id)

        # Add points
        now = datetime.now(UTC)
        card.points_balance += points_earned
        card.total_points_earned += points_earned
        card.last_points_at = now
        card.last_activity_at = now

        # Create transaction
        transaction = LoyaltyTransaction(
            merchant_id=card.merchant_id,
            card_id=card.id,
            store_id=store_id,
            staff_pin_id=verified_pin.id if verified_pin else None,
            acting_terminal_device_id=acting_terminal_device_id,
            category_ids=category_ids,
            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)

        # Sync wallet passes with updated points balance
        from app.modules.loyalty.services.wallet_service import wallet_service

        wallet_service.sync_card_to_wallets(db, card)

        logger.info(
            f"Added {points_earned} points to card {card.id} at store {store_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,
            "store_id": store_id,
        }

    def redeem_points(
        self,
        db: Session,
        *,
        store_id: int,
        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,
        acting_terminal_device_id: int | None = None,
    ) -> dict:
        """
        Redeem points for a reward.

        Args:
            db: Database session
            store_id: Store ID (where redemption is happening)
            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 (validates it belongs to store's merchant)
        card = card_service.lookup_card_for_store(
            db,
            store_id,
            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 minimum redemption
        if points_required < program.minimum_redemption_points:
            raise InvalidRewardException(reward_id)

        # Paired-device principal bypasses the per-action PIN; see earn_points.
        verified_pin = None
        if program.require_staff_pin and acting_terminal_device_id is None:
            if not staff_pin:
                raise StaffPinRequiredException()
            verified_pin = pin_service.verify_pin(db, program.id, staff_pin, store_id=store_id)

        # Re-fetch with row lock to prevent concurrent modification
        card = card_service.get_card_for_update(db, card.id)

        # Check balance AFTER acquiring lock to prevent TOCTOU race
        if card.points_balance < points_required:
            raise InsufficientPointsException(card.points_balance, points_required)

        # Redeem points
        now = datetime.now(UTC)
        card.points_balance -= points_required
        card.points_redeemed += points_required
        card.last_redemption_at = now
        card.last_activity_at = now

        # Create transaction
        transaction = LoyaltyTransaction(
            merchant_id=card.merchant_id,
            card_id=card.id,
            store_id=store_id,
            staff_pin_id=verified_pin.id if verified_pin else None,
            acting_terminal_device_id=acting_terminal_device_id,
            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)

        # Sync wallet passes with updated points balance
        from app.modules.loyalty.services.wallet_service import wallet_service

        wallet_service.sync_card_to_wallets(db, card)

        logger.info(
            f"Redeemed {points_required} points from card {card.id} at store {store_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,
            "store_id": store_id,
        }

    def void_points(
        self,
        db: Session,
        *,
        store_id: int,
        card_id: int | None = None,
        qr_code: str | None = None,
        card_number: str | None = None,
        points_to_void: int | None = None,
        original_transaction_id: int | None = None,
        order_reference: str | None = None,
        staff_pin: str | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
        notes: str | None = None,
        acting_terminal_device_id: int | None = None,
    ) -> dict:
        """
        Void points for a return.

        Args:
            db: Database session
            store_id: Store ID
            card_id: Card ID
            qr_code: QR code data
            card_number: Card number
            points_to_void: Number of points to void (if not using original_transaction_id)
            original_transaction_id: ID of original earn transaction to void
            order_reference: Order reference (to find original transaction)
            staff_pin: Staff PIN for verification
            ip_address: Request IP for audit
            user_agent: Request user agent for audit
            notes: Reason for voiding

        Returns:
            Dict with operation result
        """
        # Look up the card
        card = card_service.lookup_card_for_store(
            db,
            store_id,
            card_id=card_id,
            qr_code=qr_code,
            card_number=card_number,
        )

        program = card.program

        # Check if void transactions are allowed
        from app.modules.loyalty.services.program_service import program_service
        settings = program_service.get_merchant_settings(db, card.merchant_id)
        if settings and not settings.allow_void_transactions:
            raise LoyaltyCardInactiveException(card.id)

        # Paired-device principal bypasses the per-action PIN; see earn_points.
        verified_pin = None
        if program.require_staff_pin and acting_terminal_device_id is None:
            if not staff_pin:
                raise StaffPinRequiredException()
            verified_pin = pin_service.verify_pin(db, program.id, staff_pin, store_id=store_id)

        # Determine points to void
        original_transaction = None
        if original_transaction_id:
            original_transaction = (
                db.query(LoyaltyTransaction)
                .filter(
                    LoyaltyTransaction.id == original_transaction_id,
                    LoyaltyTransaction.card_id == card.id,
                    LoyaltyTransaction.transaction_type == TransactionType.POINTS_EARNED.value,
                )
                .first()
            )
            if original_transaction:
                points_to_void = original_transaction.points_delta
        elif order_reference:
            original_transaction = (
                db.query(LoyaltyTransaction)
                .filter(
                    LoyaltyTransaction.order_reference == order_reference,
                    LoyaltyTransaction.card_id == card.id,
                    LoyaltyTransaction.transaction_type == TransactionType.POINTS_EARNED.value,
                )
                .first()
            )
            if original_transaction:
                points_to_void = original_transaction.points_delta

        if not points_to_void or points_to_void <= 0:
            return {
                "success": False,
                "message": "No points to void",
                "card_id": card.id,
                "card_number": card.card_number,
                "points_balance": card.points_balance,
            }

        # Re-fetch with row lock to prevent concurrent modification
        card = card_service.get_card_for_update(db, card.id)

        # Void the points (can reduce balance below what was earned)
        now = datetime.now(UTC)
        actual_voided = min(points_to_void, card.points_balance)
        card.points_balance = max(0, card.points_balance - points_to_void)
        card.total_points_voided += actual_voided
        card.last_activity_at = now

        # Create void transaction
        transaction = LoyaltyTransaction(
            merchant_id=card.merchant_id,
            card_id=card.id,
            store_id=store_id,
            staff_pin_id=verified_pin.id if verified_pin else None,
            acting_terminal_device_id=acting_terminal_device_id,
            transaction_type=TransactionType.POINTS_VOIDED.value,
            points_delta=-actual_voided,
            stamps_balance_after=card.stamp_count,
            points_balance_after=card.points_balance,
            related_transaction_id=original_transaction.id if original_transaction else None,
            order_reference=order_reference,
            ip_address=ip_address,
            user_agent=user_agent,
            notes=notes or "Points voided for return",
            transaction_at=now,
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        # Sync wallet passes with updated points balance
        from app.modules.loyalty.services.wallet_service import wallet_service

        wallet_service.sync_card_to_wallets(db, card)

        logger.info(
            f"Voided {actual_voided} points from card {card.id} at store {store_id} "
            f"(balance: {card.points_balance})"
        )

        return {
            "success": True,
            "message": "Points voided successfully",
            "points_voided": actual_voided,
            "card_id": card.id,
            "card_number": card.card_number,
            "points_balance": card.points_balance,
            "store_id": store_id,
        }

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

        Args:
            db: Database session
            card_id: Card ID
            points_delta: Points to add (positive) or remove (negative)
            store_id: Store ID
            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 and store provided
        verified_pin = None
        if program.require_staff_pin and staff_pin and store_id:
            verified_pin = pin_service.verify_pin(db, program.id, staff_pin, store_id=store_id)

        # Re-fetch with row lock to prevent concurrent modification
        card = card_service.get_card_for_update(db, card.id)

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

        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(
            merchant_id=card.merchant_id,
            card_id=card.id,
            store_id=store_id,
            staff_pin_id=verified_pin.id if verified_pin else None,
            acting_terminal_device_id=acting_terminal_device_id,
            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)

        # Sync wallet passes with updated points balance
        from app.modules.loyalty.services.wallet_service import wallet_service

        wallet_service.sync_card_to_wallets(db, 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()
