"""Wallet, payments, invoices — shared shipper/transporter settlement."""
from __future__ import annotations

import logging
import uuid
from decimal import Decimal

from django.conf import settings
from django.db import transaction
from django.db.models import F
from django.utils import timezone

from accounts.models import ShipperProfile
from billing.models import Invoice, LedgerEntry, Payment, WalletAccount, WithdrawalRequest
from core.models import Bid, Trip

logger = logging.getLogger('api')


def _currency():
    return getattr(settings, 'DEFAULT_WALLET_CURRENCY', 'USD')


def settlement_amount_for_bid(bid: Bid):
    if bid.counter_amount is not None:
        return bid.counter_amount
    return bid.amount


def latest_non_failed_payment_for_trip(trip: Trip) -> Payment | None:
    prefetched = getattr(trip, '_prefetched_objects_cache', None)
    if prefetched is not None and 'payments' in prefetched:
        for payment in trip.payments.all():
            return payment
        return None
    return (
        Payment.objects.filter(trip=trip)
        .exclude(status=Payment.Status.FAILED)
        .order_by('-id')
        .first()
    )


def trip_payment_settlement_for_transporter(trip: Trip) -> dict:
    """
    High-level payment state for transporter UIs (completed trips list).
    status: pending | completed
    """
    payment = latest_non_failed_payment_for_trip(trip)
    expected_amount = None
    bid = getattr(trip, 'accepted_bid', None)
    if bid is not None:
        expected_amount = str(settlement_amount_for_bid(bid))

    if payment is None:
        return {
            'status': 'pending',
            'raw_status': None,
            'method': None,
            'amount': expected_amount,
            'payment_id': None,
            'can_confirm_cash': False,
            'can_confirm_card': False,
            'awaiting_shipper_payment': True,
        }

    completed = payment.status == Payment.Status.CAPTURED
    return {
        'status': 'completed' if completed else 'pending',
        'raw_status': payment.status,
        'method': payment.method,
        'amount': str(payment.amount),
        'payment_id': payment.id,
        'can_confirm_cash': (
            payment.method == Payment.Method.CASH
            and payment.status == Payment.Status.PENDING_COD
        ),
        'can_confirm_card': (
            payment.method == Payment.Method.CARD
            and payment.status in (
                Payment.Status.REQUIRES_ACTION,
                Payment.Status.PROCESSING,
            )
        ),
        'awaiting_shipper_payment': _awaiting_shipper_payment(payment),
    }


def _awaiting_shipper_payment(payment: Payment) -> bool:
    """False once shipper has chosen a method awaiting driver/transporter confirmation."""
    if payment.method == Payment.Method.CASH and payment.status == Payment.Status.PENDING_COD:
        return False
    if payment.method == Payment.Method.CARD and payment.status in (
        Payment.Status.REQUIRES_ACTION,
        Payment.Status.PROCESSING,
    ):
        return False
    return payment.status in (
        Payment.Status.PENDING,
        Payment.Status.PROCESSING,
        Payment.Status.REQUIRES_ACTION,
    )


def get_or_create_wallet(user):
    w, _ = WalletAccount.objects.get_or_create(user=user, defaults={'currency': _currency()})
    return w


def generate_invoice_number():
    return f'INV-{timezone.now().strftime("%Y%m%d")}-{uuid.uuid4().hex[:10].upper()}'


def _issue_invoice_for_trip(*, trip: Trip, shipper, payment: Payment | None, line_items, tax: Decimal, total: Decimal, status):
    inv = Invoice.objects.create(
        number=generate_invoice_number(),
        trip=trip,
        shipper=shipper,
        line_items=line_items,
        tax=tax,
        total=total,
        status=status,
        payment=payment,
    )
    return inv


def _build_trip_invoice_line_items(trip: Trip, amount: Decimal):
    return [{'description': f'Shipment #{trip.shipment_id} / Trip #{trip.id}', 'amount': str(amount)}]


@transaction.atomic
def ensure_invoice_for_completed_trip(*, trip: Trip) -> Invoice:
    """
    Requirement sequence: invoice is generated after trip completion and before payment.
    Idempotent: reuses latest invoice for the trip if one already exists.
    """
    existing = Invoice.objects.select_for_update().filter(trip=trip).order_by('-issued_at', '-id').first()
    if existing:
        return existing
    if trip.status != Trip.Status.COMPLETED:
        raise ValueError('Invoice can only be generated when trip status is COMPLETED.')
    amount = settlement_amount_for_bid(trip.accepted_bid)
    return _issue_invoice_for_trip(
        trip=trip,
        shipper=trip.shipment.shipper,
        payment=None,
        line_items=_build_trip_invoice_line_items(trip, amount),
        tax=Decimal('0'),
        total=amount,
        status=Invoice.Status.ISSUED,
    )


@transaction.atomic
def archive_invoice_after_payment(*, invoice: Invoice):
    if invoice.is_archived:
        return invoice
    invoice.is_archived = True
    invoice.archived_at = timezone.now()
    invoice.save(update_fields=['is_archived', 'archived_at'])
    return invoice


@transaction.atomic
def _apply_wallet_transfer(*, payer, payee, amount: Decimal, currency: str, trip: Trip, payment: Payment, idem_parts: str):
    """Debit payer wallet, credit payee wallet; idempotent ledger rows."""
    get_or_create_wallet(payer)
    get_or_create_wallet(payee)
    payer_wallet = WalletAccount.objects.select_for_update().get(user=payer)
    payee_wallet = WalletAccount.objects.select_for_update().get(user=payee)
    if payer_wallet.balance < amount:
        raise ValueError('Insufficient wallet balance.')

    debit_key = f'pay-debit-{payment.id}-{idem_parts}'
    credit_key = f'pay-credit-{payment.id}-{idem_parts}'

    if not LedgerEntry.objects.filter(user=payer, idempotency_key=debit_key).exists():
        LedgerEntry.objects.create(
            user=payer,
            amount=-amount,
            entry_type=LedgerEntry.EntryType.WALLET_DEBIT_PAYMENT,
            trip=trip,
            payment=payment,
            idempotency_key=debit_key,
            note='Shipment payment',
        )
        WalletAccount.objects.filter(pk=payer_wallet.pk).update(balance=F('balance') - amount)

    if not LedgerEntry.objects.filter(user=payee, idempotency_key=credit_key).exists():
        LedgerEntry.objects.create(
            user=payee,
            amount=amount,
            entry_type=LedgerEntry.EntryType.WALLET_CREDIT_EARNING,
            trip=trip,
            payment=payment,
            idempotency_key=credit_key,
            note='Trip earning',
        )
        WalletAccount.objects.filter(pk=payee_wallet.pk).update(balance=F('balance') + amount)


@transaction.atomic
def _apply_in_person_collection_earning(
    *, payee, amount: Decimal, trip: Trip, payment: Payment, idem_parts: str, note: str,
):
    """Transporter credited after in-person cash/card collected; shipper wallet is not debited."""
    get_or_create_wallet(payee)
    payee_wallet = WalletAccount.objects.select_for_update().get(user=payee)
    credit_key = f'in-person-earn-{payment.id}-{idem_parts}'
    if LedgerEntry.objects.filter(user=payee, idempotency_key=credit_key).exists():
        return
    LedgerEntry.objects.create(
        user=payee,
        amount=amount,
        entry_type=LedgerEntry.EntryType.WALLET_CREDIT_EARNING,
        trip=trip,
        payment=payment,
        idempotency_key=credit_key,
        note=note,
    )
    WalletAccount.objects.filter(pk=payee_wallet.pk).update(balance=F('balance') + amount)


@transaction.atomic
def _apply_credit_earning_only(*, payee, amount: Decimal, trip: Trip, payment: Payment, idem_parts: str):
    """Transporter credited on credit terms; no shipper wallet movement."""
    get_or_create_wallet(payee)
    payee_wallet = WalletAccount.objects.select_for_update().get(user=payee)
    credit_key = f'credit-earn-{payment.id}-{idem_parts}'
    if LedgerEntry.objects.filter(user=payee, idempotency_key=credit_key).exists():
        return
    LedgerEntry.objects.create(
        user=payee,
        amount=amount,
        entry_type=LedgerEntry.EntryType.CREDIT_EARNING,
        trip=trip,
        payment=payment,
        idempotency_key=credit_key,
        note='Credit terms earning',
    )
    WalletAccount.objects.filter(pk=payee_wallet.pk).update(balance=F('balance') + amount)


def _idem_key(request, suffix: str) -> str:
    raw = ''
    if request is not None:
        raw = (request.META.get('HTTP_IDEMPOTENCY_KEY') or '').strip()
    if not raw:
        raw = f'server-{suffix}'
    return raw


def user_can_confirm_trip_payment(*, trip: Trip, user) -> bool:
    """Only the trip transporter (bid winner / fleet owner or individual driver) may confirm."""
    return user.id == trip.transporter_id


def process_shipper_trip_payment(*, trip: Trip, shipper, method: str, request=None) -> dict:
    """
    Create/update payment for trip (WALLET, CARD, CASH, CREDIT — ledger in database).
    CARD and CASH require trip transporter confirmation after shipper selects the method.
    Returns dict with keys: payment, invoice (optional).
    Raises ValueError on business rule violations.
    """
    if trip.shipment.shipper_id != shipper.id:
        raise ValueError('Trip does not belong to this shipper.')
    if trip.status != Trip.Status.COMPLETED:
        raise ValueError('Payment can only be processed after trip is COMPLETED.')
    payee = trip.transporter
    bid = trip.accepted_bid
    amount = settlement_amount_for_bid(bid)
    currency = _currency()
    idem = _idem_key(request, f'{trip.id}-{method}')
    invoice = ensure_invoice_for_completed_trip(trip=trip)
    if invoice.status in (Invoice.Status.PAID, Invoice.Status.VOID):
        raise ValueError('Invoice is already settled for this trip.')

    existing = (
        Payment.objects.filter(trip=trip, payer=shipper, idempotency_key=idem)
        .exclude(status=Payment.Status.FAILED)
        .first()
    )
    if existing and existing.status in (
        Payment.Status.CAPTURED,
        Payment.Status.PENDING_COD,
        Payment.Status.PROCESSING,
        Payment.Status.REQUIRES_ACTION,
    ):
        return {
            'payment': existing,
            'invoice': Invoice.objects.filter(payment=existing).order_by('-issued_at').first() or invoice,
        }

    blocking = Payment.objects.filter(
        trip=trip,
        status__in=(
            Payment.Status.CAPTURED,
            Payment.Status.PENDING_COD,
            Payment.Status.REQUIRES_ACTION,
            Payment.Status.PROCESSING,
        ),
    ).exclude(idempotency_key=idem)
    if blocking.exists():
        raise ValueError('A payment already exists for this trip.')

    if method == Payment.Method.CREDIT:
        profile = ShipperProfile.objects.filter(user=shipper).first()
        if not profile or profile.account_type != ShipperProfile.AccountType.BUSINESS or not profile.credit_approved:
            raise ValueError('Credit payment is not available for this account.')

    with transaction.atomic():
        payment = Payment.objects.create(
            trip=trip,
            payer=shipper,
            payee=payee,
            method=method,
            amount=amount,
            currency=currency,
            status=Payment.Status.PENDING,
            idempotency_key=idem,
        )

        if method == Payment.Method.WALLET:
            payment.status = Payment.Status.CAPTURED
            payment.save(update_fields=['status', 'updated_at'])
            _apply_wallet_transfer(
                payer=shipper, payee=payee, amount=amount, currency=currency,
                trip=trip, payment=payment, idem_parts=idem,
            )
            invoice.payment = payment
            invoice.status = Invoice.Status.PAID
            invoice.save(update_fields=['payment', 'status'])
            archive_invoice_after_payment(invoice=invoice)
            return {'payment': payment, 'invoice': invoice}

        if method == Payment.Method.CASH:
            payment.status = Payment.Status.PENDING_COD
            payment.save(update_fields=['status', 'updated_at'])
            invoice.payment = payment
            invoice.save(update_fields=['payment'])
            return {'payment': payment, 'invoice': invoice}

        if method == Payment.Method.CREDIT:
            payment.status = Payment.Status.CAPTURED
            payment.save(update_fields=['status', 'updated_at'])
            _apply_credit_earning_only(payee=payee, amount=amount, trip=trip, payment=payment, idem_parts=idem)
            invoice.payment = payment
            invoice.status = Invoice.Status.PAID
            invoice.save(update_fields=['payment', 'status'])
            archive_invoice_after_payment(invoice=invoice)
            return {'payment': payment, 'invoice': invoice}

        if method == Payment.Method.CARD:
            payment.status = Payment.Status.REQUIRES_ACTION
            payment.save(update_fields=['status', 'updated_at'])
            invoice.payment = payment
            invoice.save(update_fields=['payment'])
            return {'payment': payment, 'invoice': invoice}

    raise ValueError('Unsupported payment method.')


@transaction.atomic
def _finalize_in_person_payment(*, trip: Trip, payment: Payment, idem_suffix: str, earning_note: str):
    """Mark payment captured, credit payee wallet, settle and archive invoice."""
    shipper = trip.shipment.shipper
    amount = payment.amount
    idem = payment.idempotency_key or str(payment.id)
    payment.status = Payment.Status.CAPTURED
    payment.save(update_fields=['status', 'updated_at'])
    _apply_in_person_collection_earning(
        payee=trip.transporter,
        amount=amount,
        trip=trip,
        payment=payment,
        idem_parts=f'{idem}-{idem_suffix}',
        note=earning_note,
    )
    inv = Invoice.objects.filter(trip=trip, payment=payment).first()
    if inv:
        inv.status = Invoice.Status.PAID
        inv.save(update_fields=['status'])
        archive_invoice_after_payment(invoice=inv)
    else:
        issued = _issue_invoice_for_trip(
            trip=trip, shipper=shipper, payment=payment,
            line_items=_build_trip_invoice_line_items(trip, amount),
            tax=Decimal('0'),
            total=amount,
            status=Invoice.Status.PAID,
        )
        archive_invoice_after_payment(invoice=issued)
    return payment


@transaction.atomic
def confirm_cash_collection(*, trip: Trip, transporter_user):
    """Trip transporter confirms physical cash received; credit transporter platform wallet only."""
    if not user_can_confirm_trip_payment(trip=trip, user=transporter_user):
        raise ValueError('Not your trip.')
    payment = (
        Payment.objects.select_for_update()
        .filter(trip=trip, method=Payment.Method.CASH, status=Payment.Status.PENDING_COD)
        .order_by('-id')
        .first()
    )
    if not payment:
        raise ValueError('No pending cash payment for this trip.')
    return _finalize_in_person_payment(
        trip=trip, payment=payment, idem_suffix='cod', earning_note='Cash on delivery collected',
    )


@transaction.atomic
def confirm_card_collection(*, trip: Trip, transporter_user):
    """Trip transporter confirms in-person card payment; credit transporter platform wallet only."""
    if not user_can_confirm_trip_payment(trip=trip, user=transporter_user):
        raise ValueError('Not your trip.')
    payment = (
        Payment.objects.select_for_update()
        .filter(
            trip=trip,
            method=Payment.Method.CARD,
            status__in=(Payment.Status.REQUIRES_ACTION, Payment.Status.PROCESSING),
        )
        .order_by('-id')
        .first()
    )
    if not payment:
        raise ValueError('No pending card payment for this trip.')
    return _finalize_in_person_payment(
        trip=trip, payment=payment, idem_suffix='card', earning_note='Card payment collected',
    )


def create_withdrawal_request(*, user, amount: Decimal, note: str = '') -> WithdrawalRequest:
    """Record a payout request; balance is unchanged until admin/process marks PAID (out of band)."""
    wallet = get_or_create_wallet(user)
    if amount <= 0:
        raise ValueError('Amount must be positive.')
    if wallet.balance < amount:
        raise ValueError('Insufficient wallet balance.')
    return WithdrawalRequest.objects.create(
        user=user, amount=amount, currency=wallet.currency, note=note, status=WithdrawalRequest.Status.PENDING
    )
