# app/modules/loyalty/services/card_service.py
"""
Loyalty card service.

Handles card operations including:
- Customer enrollment
- Card lookup (by ID, QR code, card number)
- Card management (activation, deactivation)
"""

import logging
from datetime import UTC, datetime

from sqlalchemy.orm import Session, joinedload

from app.modules.loyalty.exceptions import (
    LoyaltyCardAlreadyExistsException,
    LoyaltyCardNotFoundException,
    LoyaltyProgramInactiveException,
    LoyaltyProgramNotFoundException,
)
from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram, LoyaltyTransaction, TransactionType

logger = logging.getLogger(__name__)


class CardService:
    """Service for loyalty card operations."""

    # =========================================================================
    # Read Operations
    # =========================================================================

    def get_card(self, db: Session, card_id: int) -> LoyaltyCard | None:
        """Get a loyalty card by ID."""
        return (
            db.query(LoyaltyCard)
            .options(joinedload(LoyaltyCard.program))
            .filter(LoyaltyCard.id == card_id)
            .first()
        )

    def get_card_by_qr_code(self, db: Session, qr_code: str) -> LoyaltyCard | None:
        """Get a loyalty card by QR code data."""
        return (
            db.query(LoyaltyCard)
            .options(joinedload(LoyaltyCard.program))
            .filter(LoyaltyCard.qr_code_data == qr_code)
            .first()
        )

    def get_card_by_number(self, db: Session, card_number: str) -> LoyaltyCard | None:
        """Get a loyalty card by card number."""
        return (
            db.query(LoyaltyCard)
            .options(joinedload(LoyaltyCard.program))
            .filter(LoyaltyCard.card_number == card_number)
            .first()
        )

    def get_card_by_customer_and_program(
        self,
        db: Session,
        customer_id: int,
        program_id: int,
    ) -> LoyaltyCard | None:
        """Get a customer's card for a specific program."""
        return (
            db.query(LoyaltyCard)
            .options(joinedload(LoyaltyCard.program))
            .filter(
                LoyaltyCard.customer_id == customer_id,
                LoyaltyCard.program_id == program_id,
            )
            .first()
        )

    def require_card(self, db: Session, card_id: int) -> LoyaltyCard:
        """Get a card or raise exception if not found."""
        card = self.get_card(db, card_id)
        if not card:
            raise LoyaltyCardNotFoundException(str(card_id))
        return card

    def lookup_card(
        self,
        db: Session,
        *,
        card_id: int | None = None,
        qr_code: str | None = None,
        card_number: str | None = None,
    ) -> LoyaltyCard:
        """
        Look up a card by any identifier.

        Args:
            db: Database session
            card_id: Card ID
            qr_code: QR code data
            card_number: Card number

        Returns:
            Found card

        Raises:
            LoyaltyCardNotFoundException: If no card found
        """
        card = None

        if card_id:
            card = self.get_card(db, card_id)
        elif qr_code:
            card = self.get_card_by_qr_code(db, qr_code)
        elif card_number:
            card = self.get_card_by_number(db, card_number)

        if not card:
            identifier = card_id or qr_code or card_number or "unknown"
            raise LoyaltyCardNotFoundException(str(identifier))

        return card

    def list_cards(
        self,
        db: Session,
        vendor_id: int,
        *,
        skip: int = 0,
        limit: int = 50,
        is_active: bool | None = None,
        search: str | None = None,
    ) -> tuple[list[LoyaltyCard], int]:
        """
        List loyalty cards for a vendor.

        Args:
            db: Database session
            vendor_id: Vendor ID
            skip: Pagination offset
            limit: Pagination limit
            is_active: Filter by active status
            search: Search by card number or customer email

        Returns:
            (cards, total_count)
        """
        from app.modules.customers.models.customer import Customer

        query = (
            db.query(LoyaltyCard)
            .options(joinedload(LoyaltyCard.customer))
            .filter(LoyaltyCard.vendor_id == vendor_id)
        )

        if is_active is not None:
            query = query.filter(LoyaltyCard.is_active == is_active)

        if search:
            query = query.join(Customer).filter(
                (LoyaltyCard.card_number.ilike(f"%{search}%"))
                | (Customer.email.ilike(f"%{search}%"))
                | (Customer.first_name.ilike(f"%{search}%"))
                | (Customer.last_name.ilike(f"%{search}%"))
            )

        total = query.count()
        cards = (
            query.order_by(LoyaltyCard.created_at.desc())
            .offset(skip)
            .limit(limit)
            .all()
        )

        return cards, total

    def list_customer_cards(
        self,
        db: Session,
        customer_id: int,
    ) -> list[LoyaltyCard]:
        """List all loyalty cards for a customer."""
        return (
            db.query(LoyaltyCard)
            .options(joinedload(LoyaltyCard.program))
            .filter(LoyaltyCard.customer_id == customer_id)
            .all()
        )

    # =========================================================================
    # Write Operations
    # =========================================================================

    def enroll_customer(
        self,
        db: Session,
        customer_id: int,
        vendor_id: int,
        *,
        program_id: int | None = None,
    ) -> LoyaltyCard:
        """
        Enroll a customer in a loyalty program.

        Args:
            db: Database session
            customer_id: Customer ID
            vendor_id: Vendor ID
            program_id: Optional program ID (defaults to vendor's program)

        Returns:
            Created loyalty card

        Raises:
            LoyaltyProgramNotFoundException: If no program exists
            LoyaltyProgramInactiveException: If program is inactive
            LoyaltyCardAlreadyExistsException: If customer already enrolled
        """
        # Get the program
        if program_id:
            program = (
                db.query(LoyaltyProgram)
                .filter(LoyaltyProgram.id == program_id)
                .first()
            )
        else:
            program = (
                db.query(LoyaltyProgram)
                .filter(LoyaltyProgram.vendor_id == vendor_id)
                .first()
            )

        if not program:
            raise LoyaltyProgramNotFoundException(f"vendor:{vendor_id}")

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

        # Check if customer already has a card
        existing = self.get_card_by_customer_and_program(db, customer_id, program.id)
        if existing:
            raise LoyaltyCardAlreadyExistsException(customer_id, program.id)

        # Create the card
        card = LoyaltyCard(
            customer_id=customer_id,
            program_id=program.id,
            vendor_id=vendor_id,
        )

        db.add(card)
        db.flush()  # Get the card ID

        # Create enrollment transaction
        transaction = LoyaltyTransaction(
            card_id=card.id,
            vendor_id=vendor_id,
            transaction_type=TransactionType.CARD_CREATED.value,
            transaction_at=datetime.now(UTC),
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        logger.info(
            f"Enrolled customer {customer_id} in loyalty program {program.id} "
            f"(card: {card.card_number})"
        )

        return card

    def deactivate_card(self, db: Session, card_id: int) -> LoyaltyCard:
        """Deactivate a loyalty card."""
        card = self.require_card(db, card_id)
        card.is_active = False

        # Create deactivation transaction
        transaction = LoyaltyTransaction(
            card_id=card.id,
            vendor_id=card.vendor_id,
            transaction_type=TransactionType.CARD_DEACTIVATED.value,
            transaction_at=datetime.now(UTC),
        )
        db.add(transaction)

        db.commit()
        db.refresh(card)

        logger.info(f"Deactivated loyalty card {card_id}")

        return card

    def reactivate_card(self, db: Session, card_id: int) -> LoyaltyCard:
        """Reactivate a deactivated loyalty card."""
        card = self.require_card(db, card_id)
        card.is_active = True
        db.commit()
        db.refresh(card)

        logger.info(f"Reactivated loyalty card {card_id}")

        return card

    # =========================================================================
    # Helpers
    # =========================================================================

    def get_stamps_today(self, db: Session, card_id: int) -> int:
        """Get number of stamps earned today for a card."""
        from sqlalchemy import func

        today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)

        count = (
            db.query(func.count(LoyaltyTransaction.id))
            .filter(
                LoyaltyTransaction.card_id == card_id,
                LoyaltyTransaction.transaction_type == TransactionType.STAMP_EARNED.value,
                LoyaltyTransaction.transaction_at >= today_start,
            )
            .scalar()
        )

        return count or 0

    def get_card_transactions(
        self,
        db: Session,
        card_id: int,
        *,
        skip: int = 0,
        limit: int = 50,
    ) -> tuple[list[LoyaltyTransaction], int]:
        """Get transaction history for a card."""
        query = (
            db.query(LoyaltyTransaction)
            .filter(LoyaltyTransaction.card_id == card_id)
            .order_by(LoyaltyTransaction.transaction_at.desc())
        )

        total = query.count()
        transactions = query.offset(skip).limit(limit).all()

        return transactions, total


# Singleton instance
card_service = CardService()
