from decimal import Decimal, ROUND_FLOOR
from uuid import UUID

from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.models.customer import Customer
from app.models.loyalty import LoyaltySettings, LoyaltyTransaction
from app.models.sale import Sale
from app.services.audit import record_audit


def get_settings(db: Session, business_id: UUID, create: bool = False) -> LoyaltySettings | None:
    settings = db.scalar(select(LoyaltySettings).where(LoyaltySettings.business_id == business_id))
    if settings is None and create:
        settings = LoyaltySettings(business_id=business_id)
        db.add(settings); db.flush()
    return settings


def balance(db: Session, business_id: UUID, customer_id: UUID) -> int:
    return int(db.scalar(select(func.coalesce(func.sum(LoyaltyTransaction.points), 0)).where(LoyaltyTransaction.business_id == business_id, LoyaltyTransaction.customer_id == customer_id)) or 0)


def customer_or_error(db: Session, business_id: UUID, customer_id: UUID, lock: bool = False) -> Customer:
    query = select(Customer).where(Customer.id == customer_id, Customer.business_id == business_id)
    if lock:
        query = query.with_for_update()
    customer = db.scalar(query)
    if customer is None:
        raise LookupError("Customer not found.")
    return customer


def earn_for_sale(db: Session, business_id: UUID, sale_id: UUID, user_id: UUID) -> tuple[LoyaltyTransaction | None, bool]:
    settings = get_settings(db, business_id, create=True)
    if not settings.enabled:
        raise ValueError("Loyalty is disabled for this business.")
    sale = db.scalar(select(Sale).where(Sale.id == sale_id, Sale.business_id == business_id))
    if sale is None:
        raise LookupError("Sale not found.")
    if sale.customer_id is None:
        raise ValueError("A customer is required to earn loyalty points.")
    if sale.lifecycle_status != "COMPLETED":
        raise ValueError("Only completed sales can earn loyalty points.")
    existing = db.scalar(select(LoyaltyTransaction).where(LoyaltyTransaction.sale_id == sale_id))
    if existing is not None:
        return existing, False
    points = int((sale.total / settings.earn_amount * settings.earn_points).to_integral_value(rounding=ROUND_FLOOR))
    if points <= 0:
        return None, False
    transaction = LoyaltyTransaction(business_id=business_id, customer_id=sale.customer_id, sale_id=sale.id, transaction_type="EARN", points=points, created_by=user_id, reference=sale.receipt_number)
    db.add(transaction)
    try:
        db.commit(); db.refresh(transaction)
        return transaction, True
    except IntegrityError:
        db.rollback()
        return db.scalar(select(LoyaltyTransaction).where(LoyaltyTransaction.sale_id == sale_id)), False


def add_debit_or_adjustment(db: Session, business_id: UUID, customer_id: UUID, user_id: UUID, transaction_type: str, points: int, reference: str | None, notes: str | None) -> LoyaltyTransaction:
    customer_or_error(db, business_id, customer_id, lock=True)
    previous_balance = balance(db, business_id, customer_id)
    if previous_balance + points < 0:
        raise ValueError("Insufficient loyalty points.")
    transaction = LoyaltyTransaction(business_id=business_id, customer_id=customer_id, transaction_type=transaction_type, points=points, reference=reference, notes=notes, created_by=user_id)
    db.add(transaction); db.flush()
    record_audit(db, business_id, 'LOYALTY_REDEEMED' if transaction_type == 'REDEEM' else 'LOYALTY_ADJUSTED', 'loyalty_transaction', transaction.id, user_id, None, {
        'customer_id': str(customer_id), 'points': points, 'previous_balance': previous_balance,
        'new_balance': previous_balance + points, 'reference': reference,
    })
    db.commit(); db.refresh(transaction)
    return transaction


def summary(db: Session, business_id: UUID, customer_id: UUID) -> dict:
    customer_or_error(db, business_id, customer_id)
    earned = int(db.scalar(select(func.coalesce(func.sum(LoyaltyTransaction.points), 0)).where(LoyaltyTransaction.business_id == business_id, LoyaltyTransaction.customer_id == customer_id, LoyaltyTransaction.points > 0)) or 0)
    redeemed = int(db.scalar(select(func.coalesce(func.sum(-LoyaltyTransaction.points), 0)).where(LoyaltyTransaction.business_id == business_id, LoyaltyTransaction.customer_id == customer_id, LoyaltyTransaction.transaction_type == "REDEEM")) or 0)
    settings = get_settings(db, business_id, create=True)
    db.commit()
    return {"customer_id": customer_id, "enabled": settings.enabled, "current_points": balance(db, business_id, customer_id), "lifetime_earned": earned, "lifetime_redeemed": redeemed}
