# app/modules/loyalty/services/program_service.py
"""
Loyalty program service.

Handles CRUD operations for loyalty programs including:
- Program creation and configuration
- Program updates
- Program activation/deactivation
- Statistics retrieval
"""

import logging
from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.modules.loyalty.exceptions import (
    LoyaltyProgramAlreadyExistsException,
    LoyaltyProgramNotFoundException,
)
from app.modules.loyalty.models import LoyaltyProgram, LoyaltyType
from app.modules.loyalty.schemas.program import (
    ProgramCreate,
    ProgramUpdate,
)

logger = logging.getLogger(__name__)


class ProgramService:
    """Service for loyalty program operations."""

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

    def get_program(self, db: Session, program_id: int) -> LoyaltyProgram | None:
        """Get a loyalty program by ID."""
        return (
            db.query(LoyaltyProgram)
            .filter(LoyaltyProgram.id == program_id)
            .first()
        )

    def get_program_by_vendor(self, db: Session, vendor_id: int) -> LoyaltyProgram | None:
        """Get a vendor's loyalty program."""
        return (
            db.query(LoyaltyProgram)
            .filter(LoyaltyProgram.vendor_id == vendor_id)
            .first()
        )

    def get_active_program_by_vendor(self, db: Session, vendor_id: int) -> LoyaltyProgram | None:
        """Get a vendor's active loyalty program."""
        return (
            db.query(LoyaltyProgram)
            .filter(
                LoyaltyProgram.vendor_id == vendor_id,
                LoyaltyProgram.is_active == True,
            )
            .first()
        )

    def require_program(self, db: Session, program_id: int) -> LoyaltyProgram:
        """Get a program or raise exception if not found."""
        program = self.get_program(db, program_id)
        if not program:
            raise LoyaltyProgramNotFoundException(str(program_id))
        return program

    def require_program_by_vendor(self, db: Session, vendor_id: int) -> LoyaltyProgram:
        """Get a vendor's program or raise exception if not found."""
        program = self.get_program_by_vendor(db, vendor_id)
        if not program:
            raise LoyaltyProgramNotFoundException(f"vendor:{vendor_id}")
        return program

    def list_programs(
        self,
        db: Session,
        *,
        skip: int = 0,
        limit: int = 100,
        is_active: bool | None = None,
    ) -> tuple[list[LoyaltyProgram], int]:
        """List all loyalty programs (admin)."""
        query = db.query(LoyaltyProgram)

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

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

        return programs, total

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

    def create_program(
        self,
        db: Session,
        vendor_id: int,
        data: ProgramCreate,
    ) -> LoyaltyProgram:
        """
        Create a new loyalty program for a vendor.

        Args:
            db: Database session
            vendor_id: Vendor ID
            data: Program configuration

        Returns:
            Created program

        Raises:
            LoyaltyProgramAlreadyExistsException: If vendor already has a program
        """
        # Check if vendor already has a program
        existing = self.get_program_by_vendor(db, vendor_id)
        if existing:
            raise LoyaltyProgramAlreadyExistsException(vendor_id)

        # Convert points_rewards to dict list for JSON storage
        points_rewards_data = [r.model_dump() for r in data.points_rewards]

        program = LoyaltyProgram(
            vendor_id=vendor_id,
            loyalty_type=data.loyalty_type,
            # Stamps
            stamps_target=data.stamps_target,
            stamps_reward_description=data.stamps_reward_description,
            stamps_reward_value_cents=data.stamps_reward_value_cents,
            # Points
            points_per_euro=data.points_per_euro,
            points_rewards=points_rewards_data,
            # Anti-fraud
            cooldown_minutes=data.cooldown_minutes,
            max_daily_stamps=data.max_daily_stamps,
            require_staff_pin=data.require_staff_pin,
            # Branding
            card_name=data.card_name,
            card_color=data.card_color,
            card_secondary_color=data.card_secondary_color,
            logo_url=data.logo_url,
            hero_image_url=data.hero_image_url,
            # Terms
            terms_text=data.terms_text,
            privacy_url=data.privacy_url,
            # Status
            is_active=True,
            activated_at=datetime.now(UTC),
        )

        db.add(program)
        db.commit()
        db.refresh(program)

        logger.info(
            f"Created loyalty program {program.id} for vendor {vendor_id} "
            f"(type: {program.loyalty_type})"
        )

        return program

    def update_program(
        self,
        db: Session,
        program_id: int,
        data: ProgramUpdate,
    ) -> LoyaltyProgram:
        """
        Update a loyalty program.

        Args:
            db: Database session
            program_id: Program ID
            data: Update data

        Returns:
            Updated program
        """
        program = self.require_program(db, program_id)

        update_data = data.model_dump(exclude_unset=True)

        # Handle points_rewards specially (convert to dict list)
        if "points_rewards" in update_data and update_data["points_rewards"] is not None:
            update_data["points_rewards"] = [
                r.model_dump() if hasattr(r, "model_dump") else r
                for r in update_data["points_rewards"]
            ]

        for field, value in update_data.items():
            setattr(program, field, value)

        db.commit()
        db.refresh(program)

        logger.info(f"Updated loyalty program {program_id}")

        return program

    def activate_program(self, db: Session, program_id: int) -> LoyaltyProgram:
        """Activate a loyalty program."""
        program = self.require_program(db, program_id)
        program.activate()
        db.commit()
        db.refresh(program)
        logger.info(f"Activated loyalty program {program_id}")
        return program

    def deactivate_program(self, db: Session, program_id: int) -> LoyaltyProgram:
        """Deactivate a loyalty program."""
        program = self.require_program(db, program_id)
        program.deactivate()
        db.commit()
        db.refresh(program)
        logger.info(f"Deactivated loyalty program {program_id}")
        return program

    def delete_program(self, db: Session, program_id: int) -> None:
        """Delete a loyalty program and all associated data."""
        program = self.require_program(db, program_id)
        vendor_id = program.vendor_id

        db.delete(program)
        db.commit()

        logger.info(f"Deleted loyalty program {program_id} for vendor {vendor_id}")

    # =========================================================================
    # Statistics
    # =========================================================================

    def get_program_stats(self, db: Session, program_id: int) -> dict:
        """
        Get statistics for a loyalty program.

        Returns dict with:
        - total_cards, active_cards
        - total_stamps_issued, total_stamps_redeemed
        - total_points_issued, total_points_redeemed
        - etc.
        """
        from datetime import timedelta

        from sqlalchemy import func

        from app.modules.loyalty.models import LoyaltyCard, LoyaltyTransaction

        program = self.require_program(db, program_id)

        # Card counts
        total_cards = (
            db.query(func.count(LoyaltyCard.id))
            .filter(LoyaltyCard.program_id == program_id)
            .scalar()
            or 0
        )
        active_cards = (
            db.query(func.count(LoyaltyCard.id))
            .filter(
                LoyaltyCard.program_id == program_id,
                LoyaltyCard.is_active == True,
            )
            .scalar()
            or 0
        )

        # Stamp totals from cards
        stamp_stats = (
            db.query(
                func.sum(LoyaltyCard.total_stamps_earned),
                func.sum(LoyaltyCard.stamps_redeemed),
            )
            .filter(LoyaltyCard.program_id == program_id)
            .first()
        )
        total_stamps_issued = stamp_stats[0] or 0
        total_stamps_redeemed = stamp_stats[1] or 0

        # Points totals from cards
        points_stats = (
            db.query(
                func.sum(LoyaltyCard.total_points_earned),
                func.sum(LoyaltyCard.points_redeemed),
            )
            .filter(LoyaltyCard.program_id == program_id)
            .first()
        )
        total_points_issued = points_stats[0] or 0
        total_points_redeemed = points_stats[1] or 0

        # This month's activity
        month_start = datetime.now(UTC).replace(day=1, hour=0, minute=0, second=0, microsecond=0)

        stamps_this_month = (
            db.query(func.count(LoyaltyTransaction.id))
            .join(LoyaltyCard)
            .filter(
                LoyaltyCard.program_id == program_id,
                LoyaltyTransaction.transaction_type == "stamp_earned",
                LoyaltyTransaction.transaction_at >= month_start,
            )
            .scalar()
            or 0
        )

        redemptions_this_month = (
            db.query(func.count(LoyaltyTransaction.id))
            .join(LoyaltyCard)
            .filter(
                LoyaltyCard.program_id == program_id,
                LoyaltyTransaction.transaction_type == "stamp_redeemed",
                LoyaltyTransaction.transaction_at >= month_start,
            )
            .scalar()
            or 0
        )

        # 30-day active cards
        thirty_days_ago = datetime.now(UTC) - timedelta(days=30)
        cards_with_activity_30d = (
            db.query(func.count(func.distinct(LoyaltyTransaction.card_id)))
            .join(LoyaltyCard)
            .filter(
                LoyaltyCard.program_id == program_id,
                LoyaltyTransaction.transaction_at >= thirty_days_ago,
            )
            .scalar()
            or 0
        )

        # Averages
        avg_stamps = total_stamps_issued / total_cards if total_cards > 0 else 0
        avg_points = total_points_issued / total_cards if total_cards > 0 else 0

        # Estimated liability (unredeemed value)
        current_stamps = (
            db.query(func.sum(LoyaltyCard.stamp_count))
            .filter(LoyaltyCard.program_id == program_id)
            .scalar()
            or 0
        )
        stamp_value = program.stamps_reward_value_cents or 0
        current_points = (
            db.query(func.sum(LoyaltyCard.points_balance))
            .filter(LoyaltyCard.program_id == program_id)
            .scalar()
            or 0
        )
        # Rough estimate: assume 100 points = €1
        points_value_cents = current_points // 100 * 100

        estimated_liability = (
            (current_stamps * stamp_value // program.stamps_target) + points_value_cents
        )

        return {
            "total_cards": total_cards,
            "active_cards": active_cards,
            "total_stamps_issued": total_stamps_issued,
            "total_stamps_redeemed": total_stamps_redeemed,
            "stamps_this_month": stamps_this_month,
            "redemptions_this_month": redemptions_this_month,
            "total_points_issued": total_points_issued,
            "total_points_redeemed": total_points_redeemed,
            "cards_with_activity_30d": cards_with_activity_30d,
            "average_stamps_per_card": round(avg_stamps, 2),
            "average_points_per_card": round(avg_points, 2),
            "estimated_liability_cents": estimated_liability,
        }


# Singleton instance
program_service = ProgramService()
