# app/modules/loyalty/models/staff_pin.py
"""
Staff PIN database model.

Merchant-based staff PINs:
- PINs belong to a merchant's loyalty program
- Each store (location) has its own set of staff PINs
- Staff can only use PINs at their assigned location

Provides fraud prevention by requiring staff to authenticate
before performing stamp/points operations. Includes:
- Secure PIN hashing with bcrypt
- Failed attempt tracking
- Automatic lockout after too many failures
"""

from datetime import UTC, datetime

import bcrypt
from sqlalchemy import (
    Boolean,
    Column,
    DateTime,
    ForeignKey,
    Index,
    Integer,
    String,
)
from sqlalchemy.orm import relationship

from app.core.database import Base
from models.database.base import TimestampMixin


class StaffPin(Base, TimestampMixin):
    """
    Staff PIN for loyalty operations.

    Each staff member can have their own PIN to authenticate
    stamp/points operations. PINs are hashed with bcrypt and
    include lockout protection.

    PINs are scoped to a specific store (location) within the
    merchant's loyalty program.
    """

    __tablename__ = "staff_pins"

    id = Column(Integer, primary_key=True, index=True)

    # Merchant association
    merchant_id = Column(
        Integer,
        ForeignKey("merchants.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
        comment="Merchant that owns the loyalty program",
    )

    # Program and store relationships
    program_id = Column(
        Integer,
        ForeignKey("loyalty_programs.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
    )
    store_id = Column(
        Integer,
        ForeignKey("stores.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
        comment="Store (location) where this staff member works",
    )

    # Staff identity
    name = Column(
        String(100),
        nullable=False,
        comment="Staff member name",
    )
    staff_id = Column(
        String(50),
        nullable=True,
        index=True,
        comment="Optional staff ID/employee number",
    )

    # PIN authentication
    pin_hash = Column(
        String(255),
        nullable=False,
        comment="bcrypt hash of PIN",
    )

    # Security tracking
    failed_attempts = Column(
        Integer,
        default=0,
        nullable=False,
        comment="Consecutive failed PIN attempts",
    )
    locked_until = Column(
        DateTime(timezone=True),
        nullable=True,
        comment="Lockout expires at this time",
    )
    last_used_at = Column(
        DateTime(timezone=True),
        nullable=True,
        comment="Last successful use of PIN",
    )

    # Status
    is_active = Column(
        Boolean,
        default=True,
        nullable=False,
        index=True,
    )

    # =========================================================================
    # Relationships
    # =========================================================================
    merchant = relationship("Merchant", backref="staff_pins")
    program = relationship("LoyaltyProgram", back_populates="staff_pins")
    store = relationship("Store", backref="staff_pins")

    # Indexes
    __table_args__ = (
        Index("idx_staff_pin_merchant_active", "merchant_id", "is_active"),
        Index("idx_staff_pin_store_active", "store_id", "is_active"),
        Index("idx_staff_pin_program_active", "program_id", "is_active"),
    )

    def __repr__(self) -> str:
        return f"<StaffPin(id={self.id}, name='{self.name}', store_id={self.store_id}, active={self.is_active})>"

    # =========================================================================
    # PIN Operations
    # =========================================================================

    def set_pin(self, plain_pin: str) -> None:
        """
        Hash and store a PIN.

        Args:
            plain_pin: The plain text PIN (typically 4-6 digits)
        """
        salt = bcrypt.gensalt()
        self.pin_hash = bcrypt.hashpw(plain_pin.encode("utf-8"), salt).decode("utf-8")

    def verify_pin(self, plain_pin: str) -> bool:
        """
        Verify a PIN against the stored hash.

        Args:
            plain_pin: The plain text PIN to verify

        Returns:
            True if PIN matches, False otherwise
        """
        if not self.pin_hash:
            return False
        return bcrypt.checkpw(plain_pin.encode("utf-8"), self.pin_hash.encode("utf-8"))

    # =========================================================================
    # Lockout Management
    # =========================================================================

    @property
    def is_locked(self) -> bool:
        """Check if PIN is currently locked out."""
        if not self.locked_until:
            return False
        return datetime.now(UTC) < self.locked_until

    def record_failed_attempt(self, max_attempts: int = 5, lockout_minutes: int = 30) -> bool:
        """
        Record a failed PIN attempt.

        Args:
            max_attempts: Maximum failed attempts before lockout
            lockout_minutes: Duration of lockout in minutes

        Returns:
            True if account is now locked
        """
        self.failed_attempts += 1

        if self.failed_attempts >= max_attempts:
            from datetime import timedelta

            self.locked_until = datetime.now(UTC) + timedelta(minutes=lockout_minutes)
            return True

        return False

    def record_success(self) -> None:
        """Record a successful PIN verification."""
        self.failed_attempts = 0
        self.locked_until = None
        self.last_used_at = datetime.now(UTC)

    def unlock(self) -> None:
        """Manually unlock a PIN (admin action)."""
        self.failed_attempts = 0
        self.locked_until = None

    # =========================================================================
    # Properties
    # =========================================================================

    @property
    def remaining_attempts(self) -> int:
        """Get remaining attempts before lockout (assuming max 5)."""
        return max(0, 5 - self.failed_attempts)

    @property
    def lockout_remaining_seconds(self) -> int | None:
        """Get seconds remaining in lockout, or None if not locked."""
        if not self.locked_until:
            return None
        remaining = (self.locked_until - datetime.now(UTC)).total_seconds()
        return max(0, int(remaining))
