"""
Payout orchestration. Money movements always go through the ledger; the wallet
is locked with select_for_update during reservation so concurrent requests
cannot overspend the balance.
"""
from __future__ import annotations

import secrets
from decimal import Decimal

from django.db import transaction as db_transaction
from django.utils import timezone
from rest_framework.exceptions import PermissionDenied, ValidationError

from apps.ledger import services as ledger
from apps.ledger.models import Account, Transaction

from .models import Payout

ZERO = Decimal('0.00')


@db_transaction.atomic
def request_payout(*, user, amount, payout_method: str, provider: str,
                   account_name: str, account_number: str = '',
                   idempotency_key: str | None = None) -> Payout:
    """Reserve funds and create a pending payout. Safe to retry via the key."""
    idempotency_key = idempotency_key or secrets.token_hex(16)

    # Idempotent: an identical request returns the existing payout.
    existing = Payout.objects.filter(idempotency_key=idempotency_key).first()
    if existing:
        return existing

    amount = Decimal(str(amount)).quantize(Decimal('0.01'))
    if amount <= ZERO:
        raise ValidationError('Amount must be greater than zero')

    # Fraud control: KYC must be verified before money leaves the platform.
    if not user.kyc_verified:
        raise PermissionDenied('Complete KYC verification before requesting a payout')

    # Lock the wallet row so the available-balance check is race-free.
    wallet = ledger.get_user_account(user)
    Account.objects.select_for_update().get(pk=wallet.pk)
    available = ledger.account_balance(wallet)
    if amount > available:
        raise ValidationError('Insufficient balance for this payout')

    payout = Payout.objects.create(
        user=user, amount=amount, payout_method=payout_method, provider=provider,
        account_name=account_name, account_number=account_number,
        idempotency_key=idempotency_key, status=Payout.Status.PENDING)

    clearing = ledger.get_system_account(Account.Kind.PAYOUTS_CLEARING)
    reserve = ledger.post_transaction(
        kind=Transaction.Kind.PAYOUT_RESERVE,
        lines=[(wallet, -amount), (clearing, amount)],
        idempotency_key=f'payout_reserve:{payout.id}',
        reference_type='payout', reference_id=payout.id,
        memo=f'Reserve for payout #{payout.id}', created_by=user)
    payout.reserve_txn = reserve
    payout.save(update_fields=['reserve_txn'])
    return payout


@db_transaction.atomic
def settle_payout(*, payout: Payout, provider_reference: str = '', actor=None) -> Payout:
    """Mark a reserved payout as paid and move clearing → cash (idempotent)."""
    payout = Payout.objects.select_for_update().get(pk=payout.pk)
    if payout.status == Payout.Status.PAID:
        return payout
    if payout.status not in (Payout.Status.PENDING, Payout.Status.PROCESSING):
        raise ValidationError(f'Cannot settle a {payout.status} payout')

    clearing = ledger.get_system_account(Account.Kind.PAYOUTS_CLEARING)
    cash = ledger.get_system_account(Account.Kind.CASH)
    settle = ledger.post_transaction(
        kind=Transaction.Kind.PAYOUT_SETTLE,
        lines=[(clearing, -payout.amount), (cash, payout.amount)],
        idempotency_key=f'payout_settle:{payout.id}',
        reference_type='payout', reference_id=payout.id,
        memo=f'Settle payout #{payout.id}', created_by=actor)

    payout.status = Payout.Status.PAID
    payout.provider_reference = provider_reference or payout.provider_reference
    payout.settle_txn = settle
    payout.paid_at = timezone.now()
    payout.processed_by = actor
    payout.save(update_fields=['status', 'provider_reference', 'settle_txn',
                               'paid_at', 'processed_by', 'updated_at'])
    return payout


@db_transaction.atomic
def reverse_payout(*, payout: Payout, reason: str = '',
                   final_status: str = Payout.Status.FAILED, actor=None) -> Payout:
    """Return reserved funds to the wallet and mark the payout failed/rejected."""
    payout = Payout.objects.select_for_update().get(pk=payout.pk)
    if payout.status in (Payout.Status.FAILED, Payout.Status.REJECTED):
        return payout
    if payout.status == Payout.Status.PAID:
        raise ValidationError('Cannot reverse a settled payout')

    wallet = ledger.get_user_account(payout.user)
    clearing = ledger.get_system_account(Account.Kind.PAYOUTS_CLEARING)
    ledger.post_transaction(
        kind=Transaction.Kind.PAYOUT_REVERSE,
        lines=[(clearing, -payout.amount), (wallet, payout.amount)],
        idempotency_key=f'payout_reverse:{payout.id}',
        reference_type='payout', reference_id=payout.id,
        memo=f'Reverse payout #{payout.id}: {reason}'[:255], created_by=actor)

    payout.status = final_status
    payout.failure_reason = reason
    payout.processed_by = actor
    payout.save(update_fields=['status', 'failure_reason', 'processed_by', 'updated_at'])
    return payout
