"""Balance and VIP-level logic.

Balances are derived from the user's :class:`~main.models.Transaction` rows
(no stored running total), and the VIP level is derived from the user's
cumulative deposits — it is "automatically upgraded by the system when a
certain deposit amount is reached", as described on the Level page.
"""
from decimal import Decimal

from django.db.models import Q, Sum

from .models import Transaction, TicketBooking, PremiumTicket


# VIP tiers, lowest first. ``min_deposit`` is the cumulative deposit total
# (in euros) at which the tier unlocks. Commission rate and daily ticketing
# match the figures shown on the Level page.
LEVELS = [
    {'code': 'VIP',  'name': 'VIP',  'icon': '🥉', 'min_deposit': Decimal('0'),
     'commission_rate': Decimal('0.008'), 'daily_ticketing': 32},
    {'code': 'VVIP', 'name': 'VVIP', 'icon': '🦅', 'min_deposit': Decimal('1000'),
     'commission_rate': Decimal('0.018'), 'daily_ticketing': 37},
    {'code': 'SVIP', 'name': 'SVIP', 'icon': '👑', 'min_deposit': Decimal('5000'),
     'commission_rate': Decimal('0.03'), 'daily_ticketing': 37},
]


def _sum(user, **filters):
    agg = Transaction.objects.filter(user=user, **filters).aggregate(total=Sum('amount'))
    return agg['total'] or Decimal('0')


def user_balance(user):
    """Current wallet balance: admin override, else signed transaction sum."""
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.balance_override is not None:
        return profile.balance_override
    return _sum(user)


def total_deposits(user):
    """Cumulative deposits, used to determine the VIP level."""
    return _sum(user, kind=Transaction.DEPOSIT)


def total_earnings(user):
    """Wallet earnings: admin override, else total commission earned."""
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.earnings_override is not None:
        return profile.earnings_override
    return _sum(user, kind=Transaction.COMMISSION)


def current_level(user):
    """The highest tier the user's cumulative deposits qualify for."""
    total = total_deposits(user)
    chosen = LEVELS[0]
    for level in LEVELS:
        if total >= level['min_deposit']:
            chosen = level
    return chosen


def next_level(user):
    """The tier above the current one, or ``None`` if already at the top."""
    code = current_level(user)['code']
    for index, level in enumerate(LEVELS):
        if level['code'] == code:
            return LEVELS[index + 1] if index + 1 < len(LEVELS) else None
    return None


def format_eur(amount):
    """Format a Decimal/number as a euro string, e.g. ``€1,000.00``."""
    return '€{:,.2f}'.format(amount or Decimal('0'))


def _movie_seed(movie_id):
    """A stable integer seed derived from a movie id (int or string)."""
    try:
        return int(movie_id)
    except (TypeError, ValueError):
        return sum(ord(c) for c in str(movie_id or ''))


def movie_ticket_price(movie_id):
    """Deterministic ticket price (in euros) for a movie.

    Derived from the movie id so the price shown on the ticket-ordering
    screen is exactly the amount charged/returned when the rating is
    submitted. Computed server-side (never trusted from the client) and
    stable per movie. Ranges roughly €12.00–€45.99.
    """
    seed = _movie_seed(movie_id)
    euros = 12 + (seed % 34)            # 12 .. 45
    cents = (seed * 37) % 100           # 0 .. 99
    return (Decimal(euros) + Decimal(cents) / Decimal('100')).quantize(Decimal('0.01'))


def movie_display_rating(movie_id):
    """A deterministic 0–10 rating fallback when the provider omits one."""
    seed = _movie_seed(movie_id)
    return round(6.0 + (seed % 40) / 10.0, 1)  # 6.0 .. 9.9


# Ticketing requirement: a user rates TICKETS_PER_SET movies to complete one
# "set", and must complete REQUIRED_SETS full sets (REQUIRED_TICKETS movies in
# total) before they are allowed to withdraw their commission.
TICKETS_PER_SET = 32
REQUIRED_SETS = 3
REQUIRED_TICKETS = TICKETS_PER_SET * REQUIRED_SETS  # 96


def completed_tickets(user):
    """How many tickets the user has fully completed (counts toward set progress).

    An admin can pin this via ``Profile.tickets_override``; when set it replaces
    the live booking count so the user's set/progress can be configured directly.
    """
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.tickets_override is not None:
        return profile.tickets_override
    return TicketBooking.objects.filter(user=user, status=TicketBooking.COMPLETED).count()


def withdraw_unlocked(user):
    """True when an admin has flagged this user to withdraw regardless of the
    set/ticket progress requirement."""
    profile = getattr(user, 'profile', None)
    return bool(profile and profile.withdraw_unlocked)


def withdraw_locked(user):
    """True when an admin has blocked withdrawals for tax / fee processing."""
    profile = getattr(user, 'profile', None)
    return bool(profile and profile.withdraw_locked)


def withdraw_lock_reason(user):
    profile = getattr(user, 'profile', None)
    if profile is None or not profile.withdraw_locked:
        return ''
    return profile.withdraw_lock_reason or 'Withdrawal is temporarily locked while tax and government fees are processed.'


# Every new user is credited a one-time welcome bonus at registration. It funds
# a free TRAINING round that familiarises the user with the system. The training
# round is SEPARATE from the 3-set/96-ticket game: its tickets do NOT count
# toward the withdrawal unlock. Once the training round (TRAINING_TICKETS) is
# complete the bonus is clawed back, leaving only the commission the user earned
# — which is withdrawable before they join the real 3-set game.
WELCOME_BONUS = Decimal('300')
TRAINING_TICKETS = TICKETS_PER_SET  # 32 — the free training round, before the game


def welcome_bonus_granted(user):
    """True if this user received the welcome bonus at registration."""
    return _sum(user, kind=Transaction.WELCOME_BONUS) > 0


def welcome_bonus_reversed(user):
    """True once the welcome bonus has been clawed back (after training)."""
    return Transaction.objects.filter(
        user=user, kind=Transaction.BONUS_REVERSAL).exists()


def training_offset(user):
    """Number of completed tickets that count as the free trial round (and so do
    NOT count toward the 3-set game). Normally ``TRAINING_TICKETS``; zero for a
    user an admin has reset to skip the trial (Profile.skip_trial)."""
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.skip_trial:
        return 0
    return TRAINING_TICKETS


def training_complete(user):
    """True once the user has finished the free training round of 32 tickets.
    A user who skips the trial has no training round, so this is always True."""
    return completed_tickets(user) >= training_offset(user)


def game_tickets(user):
    """Completed tickets that count toward the 3-set/96 unlock — i.e. those
    booked AFTER the training round. The first ``training_offset`` are excluded
    (zero for a skip-trial user, so all completions count immediately)."""
    return max(0, completed_tickets(user) - training_offset(user))


def welcome_bonus_active(user):
    """True while the welcome bonus is live: granted, not yet reversed, and the
    training round is still in progress."""
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.bonus_override is not None:
        return profile.bonus_override > 0
    return (welcome_bonus_granted(user)
            and not welcome_bonus_reversed(user)
            and not training_complete(user))


def wallet_bonus(user):
    """Bonus amount shown in the wallet: admin override, else live trial bonus."""
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.bonus_override is not None:
        return profile.bonus_override
    return WELCOME_BONUS if welcome_bonus_active(user) else Decimal('0')


def processing_total(user):
    """Principal currently held in processing tickets: admin override, else live."""
    profile = getattr(user, 'profile', None)
    if profile is not None and profile.processing_override is not None:
        return profile.processing_override
    agg = TicketBooking.objects.filter(
        user=user, status=TicketBooking.PROCESSING).aggregate(total=Sum('amount'))
    return agg['total'] or Decimal('0')


# Ticket pricing: a normal ticket costs 10% of the user's current balance, and
# commission is a flat 3% of the ticket price for every movie rated.
TICKET_PRICE_RATE = Decimal('0.10')
COMMISSION_RATE = Decimal('0.03')

# After the free trial round, a user must hold at least this balance (in euros)
# to book a ticket; below it they're asked to deposit / contact support. The
# same floor is the minimum accepted deposit.
MIN_TICKET_BALANCE = Decimal('50')


def ticket_price_for(user):
    """Normal ticket price: 10% of the user's current balance."""
    return (user_balance(user) * TICKET_PRICE_RATE).quantize(Decimal('0.01'))


def ticket_commission(price):
    """Commission for rating a movie: a flat 3% of the ticket price."""
    return (Decimal(price) * COMMISSION_RATE).quantize(Decimal('0.01'))


def current_set_number(user):
    """Current 1-based set number within the 3-set game (the training round is
    excluded), for premium targeting and set-approval gating."""
    return game_tickets(user) // TICKETS_PER_SET + 1


def approved_set(user):
    """The highest set the user is approved (by an admin) to work on."""
    profile = getattr(user, 'profile', None)
    return profile.approved_set if profile else 1


def needs_set_approval(user):
    """True when the user has finished their current set and must wait for admin
    approval before starting the next one."""
    return current_set_number(user) > approved_set(user)


def next_ticket_position(user):
    """1-based position (within the current game set) of the ticket the user is
    about to reserve next, e.g. 5 means they are booking the 5th ticket of the
    set. Caps within ``TICKETS_PER_SET``."""
    return game_tickets(user) % TICKETS_PER_SET + 1


def tickets_in_set(user):
    """Completed tickets within the user's current round, capped at
    ``TICKETS_PER_SET`` (32). This restarts at the beginning of each set — and
    of the free trial round — instead of growing without bound, so the wallet
    shows e.g. ``18`` rather than ``50`` after an admin reset to the next set.
    """
    completed = completed_tickets(user)
    offset = training_offset(user)              # 0 for a skip-trial user
    if completed < offset:
        return completed                        # free trial round, 0..31
    game = completed - offset                   # tickets booked after training
    in_set = game % TICKETS_PER_SET
    # On an exact set boundary a full set has just been completed; show 32
    # (the finished set) rather than 0 for the not-yet-started next set.
    if in_set == 0 and game > 0:
        return TICKETS_PER_SET
    return in_set


def pending_special_ticket(user):
    """The unused admin-set ticket (premium/golden egg/special) due for the
    user's current set, if any.

    An admin may pin a ticket to a specific position within the set via
    ``ticket_number`` (1–32); that ticket only becomes due once the user reaches
    that position. Tickets with a blank ``ticket_number`` apply on the next
    ticket the user reserves (legacy behaviour).
    """
    position = next_ticket_position(user)
    candidates = PremiumTicket.objects.filter(
        user=user, is_used=False, set_number__lte=current_set_number(user),
    ).filter(
        Q(ticket_number__isnull=True) | Q(ticket_number__lte=position)
    )
    return candidates.order_by('set_number', 'ticket_number', 'created_at').first()


def ticketing_progress(user):
    """Set/withdrawal progress for the task and withdrawal screens.

    One set = ``TICKETS_PER_SET`` rated movies; the user must finish
    ``REQUIRED_SETS`` sets (``REQUIRED_TICKETS`` total) to unlock withdrawals.
    """
    completed = completed_tickets(user)
    offset = training_offset(user)            # 0 for a skip-trial user
    in_training = completed < offset
    training_done = min(completed, offset)
    game = game_tickets(user)                 # tickets booked after training
    capped = min(game, REQUIRED_TICKETS)
    main_complete = game >= REQUIRED_TICKETS

    # Withdrawal: the training winnings are withdrawable after the training round
    # and BEFORE the user joins the 3-set game (game == 0). Once the game starts,
    # the previous logic applies: all 3 sets (96 tickets) must be completed. A
    # skip-trial user has no trial winnings (offset 0), so this branch never
    # applies to them and they must complete all 3 sets.
    # An admin can also unlock withdrawals directly (Profile.withdraw_unlocked),
    # bypassing the set requirement entirely.
    locked = withdraw_locked(user)
    can_withdraw = (not locked
                    and (withdraw_unlocked(user)
                    or main_complete
                    or (game == 0 and welcome_bonus_granted(user)
                        and offset > 0 and completed >= offset)))

    if main_complete:
        current_set, in_set, sets_completed = REQUIRED_SETS, TICKETS_PER_SET, REQUIRED_SETS
    else:
        sets_completed = capped // TICKETS_PER_SET   # fully finished game sets
        in_set = capped % TICKETS_PER_SET            # progress within current set
        current_set = sets_completed + 1             # 1-based set in progress

    return {
        'tickets_per_set': TICKETS_PER_SET,
        'required_sets': REQUIRED_SETS,
        'required_tickets': REQUIRED_TICKETS,
        'completed': game,
        'completed_capped': capped,
        'sets_completed': sets_completed,
        'current_set': current_set,
        'in_set': in_set,
        'set_remaining': TICKETS_PER_SET - in_set if not main_complete else 0,
        'remaining': max(0, REQUIRED_TICKETS - game),
        'can_withdraw': can_withdraw,
        'withdraw_locked': locked,
        'withdraw_lock_reason': withdraw_lock_reason(user),
        'welcome_bonus_active': welcome_bonus_active(user),
        'welcome_bonus_amount': WELCOME_BONUS,
        # Training round (separate from the game).
        'in_training': in_training,
        'training_done': training_done,
        'training_total': TRAINING_TICKETS,
        'training_percent': int(training_done * 100 / TRAINING_TICKETS) if TRAINING_TICKETS else 0,
        # Whole-game percentage (0–100) for the 3-set progress bar.
        'percent': int(capped * 100 / REQUIRED_TICKETS) if REQUIRED_TICKETS else 0,
    }


def set_progress_label(user):
    """Short, live label of where a user sits in the trial / 3-set game, for the
    admin user list and agent portal:

    * ``Trial 5/32`` while the free trial round is in progress,
    * ``Awaiting reset (set 1 done)`` once a set is finished and the user needs
      an admin to reset/approve the next one,
    * ``Set 2/3 · 18/32`` mid-game, and
    * ``Complete 96/96`` when all three sets are done.
    """
    p = ticketing_progress(user)
    if p['in_training']:
        return 'Trial {}/{}'.format(p['training_done'], p['training_total'])
    if p['sets_completed'] >= p['required_sets']:
        return 'Complete {0}/{0}'.format(p['required_tickets'])
    if needs_set_approval(user):
        return 'Awaiting reset (set {} done)'.format(current_set_number(user) - 1)
    return 'Set {}/{} · {}/{}'.format(
        p['current_set'], p['required_sets'], p['in_set'], p['tickets_per_set'])


def level_rows(user):
    """Display-ready rows for the Level page tier table."""
    current_code = current_level(user)['code']
    rows = []
    for level in LEVELS:
        rows.append({
            'code': level['code'],
            'name': level['name'],
            'icon': level['icon'],
            'price': format_eur(level['min_deposit']),
            'commission': '{:.1f}%'.format(level['commission_rate'] * 100),
            'daily_ticketing': level['daily_ticketing'],
            'is_current': level['code'] == current_code,
        })
    return rows
