"""
Enrichment Credit Bucket Manager

Implements a 30-day rollover credit system using FEFO (First-Expiring-First-Out) consumption.

Key Features:
- Monthly tier buckets with 30-day rollover
- FEFO consumption to minimize credit waste
- Rollover caps to prevent infinite accumulation
- Automatic expiration of old credits
- Full audit trail via event-sourced ledger

Architecture:
- `enrichment_credit_buckets`: Stores credit issuances with expiration dates
- `enrichment_credit_ledger`: Event-sourced log of all credit movements

Usage:
1. `mint_monthly_tier_credits()`: Create monthly tier bucket (called by Stripe webhook)
2. `consume_credits_fefo()`: Consume credits for enrichment job (called before PDL API)
3. `apply_rollover_cap()`: Clip prior month's bucket to cap (called by monthly cron)
4. `expire_old_buckets()`: Expire buckets past expiration date (called by daily cron)
"""

import logging
from datetime import date, timedelta
from typing import Dict, List, Optional
from dateutil.relativedelta import relativedelta
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError

from ..models import (
    EnrichmentCreditBucket,
    EnrichmentCreditLedger,
    CreditLedgerKind,
)

logger = logging.getLogger(__name__)


# ====================================================================
# Custom Exceptions
# ====================================================================


class InsufficientCreditsError(Exception):
    """Raised when account doesn't have enough credits."""

    pass


# ====================================================================
# Tier Configuration
# ====================================================================

# Import tier limits from centralized config
try:
    from ..middleware.quota_checker import TIER_LIMITS

    TIER_ALLOWANCES = {
        tier: limits.get("enrichment_credits", 0)
        for tier, limits in TIER_LIMITS.items()
    }

    TIER_ROLLOVER_CAPS = {
        tier: limits.get("enrichment_credits", 0)  # Rollover cap = monthly allowance
        for tier, limits in TIER_LIMITS.items()
    }
except ImportError:
    # Fallback for development/testing (should never happen in production)
    TIER_ALLOWANCES = {
        "free": 0,
        "pro": 25,  # Fallback to v3 values
        "scale": 75,
    }

    TIER_ROLLOVER_CAPS = {
        "free": 0,
        "pro": 25,
        "scale": 75,
    }


# ====================================================================
# Date Utilities
# ====================================================================


def get_month_boundaries(dt: date) -> tuple[date, date]:
    """
    Get the first and last day of the month for the given date.

    Args:
        dt: Any date in the target month

    Returns:
        Tuple of (first_day, last_day) as date objects
    """
    first_day = dt.replace(day=1)
    # Get last day by going to first of next month and subtracting 1 day
    next_month = first_day + relativedelta(months=1)
    last_day = next_month - timedelta(days=1)
    return first_day, last_day


def get_expiration_date(valid_to: date, rollover_days: int = 30) -> date:
    """
    Calculate expiration date by adding rollover_days to valid_to date.

    Args:
        valid_to: Last day of validity period
        rollover_days: Number of days to add for rollover (default 30)

    Returns:
        Expiration date
    """
    return valid_to + timedelta(days=rollover_days)


# ====================================================================
# Core Functions
# ====================================================================


async def mint_monthly_tier_credits(
    account_id: str,
    tier: str,
    invoice_id: Optional[str] = None,
    db: Optional[AsyncSession] = None,
) -> EnrichmentCreditBucket:
    """
    Create a monthly tier bucket with 30-day rollover.

    Called by Stripe webhook when invoice is paid.

    Args:
        account_id: The account ID
        tier: Tier level ("free", "pro", "scale")
        invoice_id: Optional Stripe invoice ID for reference
        db: Async database session (if None, creates new session)

    Returns:
        The created EnrichmentCreditBucket object

    Raises:
        ValueError: If tier is invalid
    """
    # Validate tier
    tier_lower = tier.lower()
    if tier_lower not in TIER_ALLOWANCES:
        raise ValueError(
            f"Invalid tier '{tier}'. Must be one of: {list(TIER_ALLOWANCES.keys())}"
        )

    allowance = TIER_ALLOWANCES[tier_lower]

    # If no credits for this tier, return early (but still log)
    if allowance == 0:
        logger.info(
            f"Tier '{tier}' has 0 credits. No bucket created for account {account_id}"
        )
        # Still create a bucket for audit trail purposes

    # Get current month boundaries
    today = date.today()
    valid_from, valid_to = get_month_boundaries(today)

    # Calculate expiration date (valid_to + 30 days)
    expires_at = get_expiration_date(valid_to, rollover_days=30)

    # Determine if we need to manage our own session
    should_close = False
    if db is None:
        from ..db import AsyncSessionLocal

        db = AsyncSessionLocal()
        should_close = True

    try:
        # IDEMPOTENCY CHECK: bail out if bucket already exists for this account + month
        existing_stmt = select(EnrichmentCreditBucket).where(
            EnrichmentCreditBucket.account_id == account_id,
            EnrichmentCreditBucket.source == "tier",
            EnrichmentCreditBucket.valid_from == valid_from,
        )
        existing_result = await db.execute(existing_stmt)
        existing_bucket = existing_result.scalar_one_or_none()

        if existing_bucket:
            logger.warning(
                f"Bucket already exists for account {account_id} for month {valid_from.strftime('%Y-%m')} "
                f"(bucket_id={existing_bucket.id}). Skipping duplicate mint."
            )
            await db.refresh(existing_bucket)
            return existing_bucket

        # Create new bucket + ledger entry
        bucket = EnrichmentCreditBucket(
            account_id=account_id,
            source="tier",
            source_ref=invoice_id,
            valid_from=valid_from,
            valid_to=valid_to,
            expires_at=expires_at,
            quantity_total=allowance,
            quantity_remaining=allowance,
            status="active",
            notes=f"Monthly tier credits for {tier} ({valid_from.strftime('%Y-%m')})",
        )

        db.add(bucket)
        await db.flush()  # Flush to get bucket.id

        # Log "issue" event to ledger
        ledger_entry = EnrichmentCreditLedger(
            account_id=account_id,
            kind=CreditLedgerKind.ISSUE,
            bucket_id=bucket.id,
            quantity=allowance,
            balance_after=None,  # Will be calculated by caller if needed
            notes=f"Monthly tier credits issued for {tier} tier ({valid_from.strftime('%Y-%m')})",
            event_metadata={
                "tier": tier,
                "invoice_id": invoice_id,
                "valid_from": valid_from.isoformat(),
                "valid_to": valid_to.isoformat(),
                "expires_at": expires_at.isoformat(),
            },
        )
        db.add(ledger_entry)

        await db.commit()
        await db.refresh(bucket)

        logger.info(
            f"Minted {allowance} credits for account {account_id} (tier={tier}, "
            f"bucket_id={bucket.id}, expires={expires_at})"
        )

        return bucket
    except IntegrityError as conflict_error:
        await db.rollback()
        logger.warning(
            "Unique constraint prevented duplicate tier bucket for account %s (%s): %s",
            account_id,
            valid_from.strftime("%Y-%m"),
            conflict_error,
        )
        retry_result = await db.execute(existing_stmt)
        existing_bucket = retry_result.scalar_one_or_none()
        if existing_bucket is None:
            logger.error(
                "Constraint triggered but existing bucket not found for account %s (%s)",
                account_id,
                valid_from.strftime("%Y-%m"),
            )
            raise
        await db.refresh(existing_bucket)
        return existing_bucket
    except Exception as e:
        await db.rollback()
        logger.error(
            f"Failed to mint credits for account {account_id}: {e}", exc_info=True
        )
        raise
    finally:
        if should_close:
            await db.close()


async def consume_credits_fefo(
    account_id: str,
    quantity_needed: int,
    job_id: str,
    user_email: str,
    db: AsyncSession,
) -> List[Dict]:
    """
    Consume credits using FEFO (First-Expiring-First-Out) strategy.

    Called by enrichment job before calling PDL API.

    Args:
        account_id: The account ID
        quantity_needed: Number of credits to consume
        job_id: Job ID for audit trail
        user_email: User email for audit trail
        db: Async database session

    Returns:
        List of dicts with consumption details:
        [{"bucket_id": int, "source": str, "amount": int, "expires_at": date}, ...]

    Raises:
        InsufficientCreditsError: If not enough credits available
    """
    if quantity_needed <= 0:
        logger.warning(
            f"consume_credits_fefo called with quantity_needed={quantity_needed}"
        )
        return []

    # Check account tier to determine if pack credits can be used
    from sqlalchemy import text as sql_text
    tier_result = await db.execute(
        sql_text("SELECT tier FROM usage_quotas WHERE account_id = :aid"),
        {"aid": str(account_id)},
    )
    tier_row = tier_result.first()
    account_tier = tier_row[0].lower() if tier_row and tier_row[0] else "free"
    can_use_pack_credits = account_tier in ("pro", "scale")

    # Query active buckets ordered by expires_at ASC (FEFO)
    # Use .with_for_update() for row locking (concurrency safety)
    stmt = (
        select(EnrichmentCreditBucket)
        .where(
            EnrichmentCreditBucket.account_id == account_id,
            EnrichmentCreditBucket.status == "active",
            EnrichmentCreditBucket.quantity_remaining > 0,
        )
        .order_by(EnrichmentCreditBucket.expires_at.asc())
        .with_for_update()  # Lock rows to prevent race conditions
    )

    result = await db.execute(stmt)
    all_buckets = result.scalars().all()

    if not all_buckets:
        raise InsufficientCreditsError(
            f"Account {account_id} has no active credit buckets"
        )

    # Filter buckets: exclude pack credits if account doesn't have active subscription
    buckets = []
    blocked_pack_credits = 0
    for bucket in all_buckets:
        if bucket.source == "pack" and not can_use_pack_credits:
            blocked_pack_credits += bucket.quantity_remaining
            logger.warning(
                f"Account {account_id} has {bucket.quantity_remaining} pack credits (bucket {bucket.id}) "
                f"but tier={account_tier} - pack credits require active Pro or Scale subscription"
            )
            continue
        buckets.append(bucket)

    if not buckets:
        if blocked_pack_credits > 0:
            raise InsufficientCreditsError(
                f"Account {account_id} has {blocked_pack_credits} pack credits, but pack credits require "
                f"an active Pro or Scale subscription. Current tier: {account_tier}. "
                f"Upgrade to use your purchased credits."
            )
        raise InsufficientCreditsError(
            f"Account {account_id} has no active credit buckets"
        )

    # Calculate total available credits (after filtering)
    total_available = sum(b.quantity_remaining for b in buckets)

    if total_available < quantity_needed:
        raise InsufficientCreditsError(
            f"Account {account_id} needs {quantity_needed} credits but only has {total_available} available"
        )

    # Consume credits using FEFO
    consumption_log = []
    remaining_need = quantity_needed

    for bucket in buckets:
        if remaining_need <= 0:
            break

        # Take min(quantity_remaining, remaining_need) from this bucket
        amount_to_take = min(bucket.quantity_remaining, remaining_need)

        # Update bucket
        bucket.quantity_remaining -= amount_to_take
        remaining_need -= amount_to_take

        # If bucket is now empty, mark as consumed
        if bucket.quantity_remaining == 0:
            bucket.status = "consumed"

        # Log consumption to ledger
        ledger_entry = EnrichmentCreditLedger(
            account_id=account_id,
            kind=CreditLedgerKind.CONSUME,
            bucket_id=bucket.id,
            job_id=job_id,
            user_email=user_email,
            quantity=-amount_to_take,  # Negative for consumption
            balance_after=None,  # Will be calculated by caller if needed
            notes=f"Credits consumed for job {job_id}",
            event_metadata={
                "bucket_source": bucket.source,
                "bucket_expires_at": bucket.expires_at.isoformat()
                if isinstance(bucket.expires_at, date)
                else str(bucket.expires_at),
                "amount_consumed": amount_to_take,
            },
        )
        db.add(ledger_entry)

        # Add to consumption log
        consumption_log.append(
            {
                "bucket_id": bucket.id,
                "source": bucket.source,
                "amount": amount_to_take,
                "expires_at": bucket.expires_at,
            }
        )

        logger.debug(
            f"Consumed {amount_to_take} credits from bucket {bucket.id} "
            f"(source={bucket.source}, remaining={bucket.quantity_remaining}, "
            f"status={bucket.status})"
        )

    # Commit transaction
    try:
        await db.commit()
        logger.info(
            f"Successfully consumed {quantity_needed} credits for account {account_id} "
            f"(job={job_id}, user={user_email}, buckets_used={len(consumption_log)})"
        )
    except Exception as e:
        await db.rollback()
        logger.error(
            f"Failed to consume credits for account {account_id}: {e}", exc_info=True
        )
        raise

    return consumption_log


async def apply_rollover_cap(
    account_id: str, tier: str, db: AsyncSession
) -> Optional[int]:
    """
    Clip prior month's tier bucket to 1x monthly allowance.

    Called by monthly cron at start of billing cycle.

    Args:
        account_id: The account ID
        tier: Current tier level ("free", "pro", "scale")
        db: Async database session

    Returns:
        Amount clipped (or None if no clipping needed)

    Raises:
        ValueError: If tier is invalid
    """
    # Validate tier
    tier_lower = tier.lower()
    if tier_lower not in TIER_ROLLOVER_CAPS:
        raise ValueError(
            f"Invalid tier '{tier}'. Must be one of: {list(TIER_ROLLOVER_CAPS.keys())}"
        )

    cap = TIER_ROLLOVER_CAPS[tier_lower]

    # Get prior month's first day
    today = date.today()
    first_of_this_month = today.replace(day=1)
    first_of_last_month = first_of_this_month - relativedelta(months=1)

    # Find prior month's tier bucket
    stmt = (
        select(EnrichmentCreditBucket)
        .where(
            EnrichmentCreditBucket.account_id == account_id,
            EnrichmentCreditBucket.source == "tier",
            EnrichmentCreditBucket.valid_from == first_of_last_month,
            EnrichmentCreditBucket.status == "active",
        )
        .with_for_update()  # Lock for update
    )

    result = await db.execute(stmt)
    bucket = result.scalar_one_or_none()

    if not bucket:
        logger.info(
            f"No active tier bucket found for account {account_id} from {first_of_last_month}. "
            "Skipping rollover cap."
        )
        return None

    # Check if clipping is needed
    if bucket.quantity_remaining <= cap:
        logger.info(
            f"Bucket {bucket.id} for account {account_id} has {bucket.quantity_remaining} credits "
            f"(<= cap of {cap}). No clipping needed."
        )
        return None

    # Calculate amount to clip
    amount_clipped = bucket.quantity_remaining - cap

    # Update bucket
    old_quantity = bucket.quantity_remaining
    bucket.quantity_remaining = cap

    # Log "adjust" event to ledger
    ledger_entry = EnrichmentCreditLedger(
        account_id=account_id,
        kind=CreditLedgerKind.ADJUST,
        bucket_id=bucket.id,
        quantity=-amount_clipped,  # Negative for reduction
        balance_after=None,  # Will be calculated by caller if needed
        notes=f"Rollover cap applied: reduced from {old_quantity} to {cap}",
        event_metadata={
            "tier": tier,
            "cap": cap,
            "amount_clipped": amount_clipped,
            "prior_month": first_of_last_month.isoformat(),
        },
    )
    db.add(ledger_entry)

    try:
        await db.commit()
        logger.info(
            f"Applied rollover cap to bucket {bucket.id} for account {account_id}: "
            f"clipped {amount_clipped} credits (from {old_quantity} to {cap})"
        )
    except Exception as e:
        await db.rollback()
        logger.error(
            f"Failed to apply rollover cap for account {account_id}: {e}", exc_info=True
        )
        raise

    return amount_clipped


async def expire_old_buckets(db: AsyncSession) -> int:
    """
    Expire buckets past their expiration date.

    Called by daily cron at midnight UTC.

    Args:
        db: Async database session

    Returns:
        Number of buckets expired
    """
    today = date.today()

    # Find all buckets where status="active" AND expires_at < today
    stmt = (
        select(EnrichmentCreditBucket)
        .where(
            EnrichmentCreditBucket.status == "active",
            EnrichmentCreditBucket.expires_at < today,
        )
        .with_for_update()  # Lock for update
    )

    result = await db.execute(stmt)
    expired_buckets = result.scalars().all()

    if not expired_buckets:
        logger.info("No buckets to expire")
        return 0

    expired_count = 0

    for bucket in expired_buckets:
        # Log "expire" event to ledger
        ledger_entry = EnrichmentCreditLedger(
            account_id=bucket.account_id,
            kind=CreditLedgerKind.EXPIRE,
            bucket_id=bucket.id,
            quantity=-bucket.quantity_remaining,  # Negative for expiration
            balance_after=None,  # Will be calculated by caller if needed
            notes=f"Bucket expired: {bucket.quantity_remaining} credits forfeited",
            event_metadata={
                "expired_at": today.isoformat(),
                "original_expires_at": bucket.expires_at.isoformat()
                if isinstance(bucket.expires_at, date)
                else str(bucket.expires_at),
                "quantity_forfeited": bucket.quantity_remaining,
                "source": bucket.source,
            },
        )
        db.add(ledger_entry)

        # Update bucket status
        bucket.status = "expired"
        bucket.quantity_remaining = 0

        expired_count += 1

        logger.debug(
            f"Expired bucket {bucket.id} for account {bucket.account_id}: "
            f"forfeited {bucket.quantity_remaining} credits (source={bucket.source}, "
            f"expires_at={bucket.expires_at})"
        )

    try:
        await db.commit()
        logger.info(f"Successfully expired {expired_count} buckets")
    except Exception as e:
        await db.rollback()
        logger.error(f"Failed to expire buckets: {e}", exc_info=True)
        raise

    return expired_count


# ====================================================================
# Helper Functions
# ====================================================================


async def get_account_balance(account_id: str, db: AsyncSession) -> Dict[str, int]:
    """
    Get the current credit balance for an account by source.

    Args:
        account_id: The account ID
        db: Async database session

    Returns:
        Dict with balance by source:
        {
            "tier": int,
            "pack": int,
            "adjustment": int,
            "total": int
        }
    """
    stmt = select(EnrichmentCreditBucket).where(
        EnrichmentCreditBucket.account_id == account_id,
        EnrichmentCreditBucket.status == "active",
        EnrichmentCreditBucket.quantity_remaining > 0,
    )

    result = await db.execute(stmt)
    buckets = result.scalars().all()

    balance = {
        "tier": 0,
        "pack": 0,
        "adjustment": 0,
        "total": 0,
    }

    for bucket in buckets:
        balance[bucket.source] += bucket.quantity_remaining
        balance["total"] += bucket.quantity_remaining

    return balance


async def get_expiring_soon(
    account_id: str, days: int, db: AsyncSession
) -> List[EnrichmentCreditBucket]:
    """
    Get buckets that will expire within the specified number of days.

    Args:
        account_id: The account ID
        days: Number of days to look ahead
        db: Async database session

    Returns:
        List of EnrichmentCreditBucket objects expiring soon
    """
    today = date.today()
    cutoff_date = today + timedelta(days=days)

    stmt = (
        select(EnrichmentCreditBucket)
        .where(
            EnrichmentCreditBucket.account_id == account_id,
            EnrichmentCreditBucket.status == "active",
            EnrichmentCreditBucket.quantity_remaining > 0,
            EnrichmentCreditBucket.expires_at <= cutoff_date,
            EnrichmentCreditBucket.expires_at >= today,
        )
        .order_by(EnrichmentCreditBucket.expires_at.asc())
    )

    result = await db.execute(stmt)
    return result.scalars().all()
