# app/modules/loyalty/services/pin_service.py
"""
Staff PIN service.

Handles PIN operations including:
- PIN creation and management
- PIN verification with lockout
- PIN security (failed attempts, lockout)
"""

import logging
from datetime import UTC, datetime

from sqlalchemy.orm import Session

from app.modules.loyalty.config import config
from app.modules.loyalty.exceptions import (
    InvalidStaffPinException,
    StaffPinLockedException,
    StaffPinNotFoundException,
)
from app.modules.loyalty.models import StaffPin
from app.modules.loyalty.schemas.pin import PinCreate, PinUpdate

logger = logging.getLogger(__name__)


class PinService:
    """Service for staff PIN operations."""

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

    def get_pin(self, db: Session, pin_id: int) -> StaffPin | None:
        """Get a staff PIN by ID."""
        return db.query(StaffPin).filter(StaffPin.id == pin_id).first()

    def get_pin_by_staff_id(
        self,
        db: Session,
        program_id: int,
        staff_id: str,
    ) -> StaffPin | None:
        """Get a staff PIN by employee ID."""
        return (
            db.query(StaffPin)
            .filter(
                StaffPin.program_id == program_id,
                StaffPin.staff_id == staff_id,
            )
            .first()
        )

    def require_pin(self, db: Session, pin_id: int) -> StaffPin:
        """Get a PIN or raise exception if not found."""
        pin = self.get_pin(db, pin_id)
        if not pin:
            raise StaffPinNotFoundException(str(pin_id))
        return pin

    def list_pins(
        self,
        db: Session,
        program_id: int,
        *,
        is_active: bool | None = None,
    ) -> list[StaffPin]:
        """List all staff PINs for a program."""
        query = db.query(StaffPin).filter(StaffPin.program_id == program_id)

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

        return query.order_by(StaffPin.name).all()

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

    def create_pin(
        self,
        db: Session,
        program_id: int,
        vendor_id: int,
        data: PinCreate,
    ) -> StaffPin:
        """
        Create a new staff PIN.

        Args:
            db: Database session
            program_id: Program ID
            vendor_id: Vendor ID
            data: PIN creation data

        Returns:
            Created PIN
        """
        pin = StaffPin(
            program_id=program_id,
            vendor_id=vendor_id,
            name=data.name,
            staff_id=data.staff_id,
        )
        pin.set_pin(data.pin)

        db.add(pin)
        db.commit()
        db.refresh(pin)

        logger.info(f"Created staff PIN {pin.id} for '{pin.name}' in program {program_id}")

        return pin

    def update_pin(
        self,
        db: Session,
        pin_id: int,
        data: PinUpdate,
    ) -> StaffPin:
        """
        Update a staff PIN.

        Args:
            db: Database session
            pin_id: PIN ID
            data: Update data

        Returns:
            Updated PIN
        """
        pin = self.require_pin(db, pin_id)

        if data.name is not None:
            pin.name = data.name

        if data.staff_id is not None:
            pin.staff_id = data.staff_id

        if data.pin is not None:
            pin.set_pin(data.pin)
            # Reset lockout when PIN is changed
            pin.failed_attempts = 0
            pin.locked_until = None

        if data.is_active is not None:
            pin.is_active = data.is_active

        db.commit()
        db.refresh(pin)

        logger.info(f"Updated staff PIN {pin_id}")

        return pin

    def delete_pin(self, db: Session, pin_id: int) -> None:
        """Delete a staff PIN."""
        pin = self.require_pin(db, pin_id)
        program_id = pin.program_id

        db.delete(pin)
        db.commit()

        logger.info(f"Deleted staff PIN {pin_id} from program {program_id}")

    def unlock_pin(self, db: Session, pin_id: int) -> StaffPin:
        """Unlock a locked staff PIN."""
        pin = self.require_pin(db, pin_id)
        pin.unlock()
        db.commit()
        db.refresh(pin)

        logger.info(f"Unlocked staff PIN {pin_id}")

        return pin

    # =========================================================================
    # Verification
    # =========================================================================

    def verify_pin(
        self,
        db: Session,
        program_id: int,
        plain_pin: str,
    ) -> StaffPin:
        """
        Verify a staff PIN.

        Checks all active PINs for the program and returns the matching one.

        Args:
            db: Database session
            program_id: Program ID
            plain_pin: Plain text PIN to verify

        Returns:
            Verified StaffPin object

        Raises:
            InvalidStaffPinException: PIN is invalid
            StaffPinLockedException: PIN is locked
        """
        # Get all active PINs for the program
        pins = self.list_pins(db, program_id, is_active=True)

        if not pins:
            raise InvalidStaffPinException()

        # Try each PIN
        for pin in pins:
            # Check if locked
            if pin.is_locked:
                continue

            # Verify PIN
            if pin.verify_pin(plain_pin):
                # Success - record it
                pin.record_success()
                db.commit()

                logger.debug(f"PIN verified for '{pin.name}' in program {program_id}")

                return pin

        # No match found - record failed attempt on all unlocked PINs
        # This is a simplified approach; in production you might want to
        # track which PIN was attempted based on additional context
        locked_pin = None
        remaining = None

        for pin in pins:
            if not pin.is_locked:
                is_now_locked = pin.record_failed_attempt(
                    max_attempts=config.pin_max_failed_attempts,
                    lockout_minutes=config.pin_lockout_minutes,
                )
                if is_now_locked:
                    locked_pin = pin
                else:
                    remaining = pin.remaining_attempts

        db.commit()

        # If a PIN just got locked, raise that specific error
        if locked_pin:
            raise StaffPinLockedException(locked_pin.locked_until.isoformat())

        raise InvalidStaffPinException(remaining)

    def find_matching_pin(
        self,
        db: Session,
        program_id: int,
        plain_pin: str,
    ) -> StaffPin | None:
        """
        Find a matching PIN without recording attempts.

        Useful for checking PIN validity without side effects.

        Args:
            db: Database session
            program_id: Program ID
            plain_pin: Plain text PIN to check

        Returns:
            Matching StaffPin or None
        """
        pins = self.list_pins(db, program_id, is_active=True)

        for pin in pins:
            if not pin.is_locked and pin.verify_pin(plain_pin):
                return pin

        return None


# Singleton instance
pin_service = PinService()
