"""Financial flows: translate deal transitions into balanced ledger journals.

Each flow is idempotent (keyed off the deal) so a retried request or a replayed
provider callback can never double-post. Fees come from the effective-dated fee
schedule, never hard-coded. Chart of accounts (see ledger/seed.py):
  1000 settlement/custody asset · 2000 escrow liability
  2100 seller payable · 4000 fee revenue
"""

from decimal import Decimal

from django.db import transaction
from django.utils import timezone

from ledger.services import post_journal

from .fees import compute_fee
from .models import PaymentAttempt, Payout, Refund
from .providers import get_provider
from .providers.base import DisbursementInstruction


def _d(v) -> Decimal:
    return v if isinstance(v, Decimal) else Decimal(str(v))


@transaction.atomic
def record_funding(deal, *, operator="system", provider="manual", provider_ref=None):
    """Provider-confirmed collection: cash in, escrow liability up."""
    amount = _d(deal.amount)
    post_journal(
        reference=f"FUND-{deal.code}",
        description=f"Escrow funding for deal {deal.code}",
        deal=deal,
        created_by=operator,
        idempotency_key=f"fund:{deal.id}",
        lines=[
            {"account": "1000", "debit": amount, "memo": "Settlement in"},
            {"account": "2000", "credit": amount, "memo": "Customer escrow liability"},
        ],
    )
    PaymentAttempt.objects.get_or_create(
        provider_ref=provider_ref or f"MANUAL-FUND-{deal.code}",
        defaults={
            "deal": deal, "amount": amount, "currency": "TZS",
            "status": PaymentAttempt.Status.SUCCEEDED, "provider": provider,
        },
    )


@transaction.atomic
def record_release_and_payout(deal, *, operator="system"):
    """Release escrow to seller (net of fee), then attempt the payout via the
    active payment provider (settings.PAYMENT_PROVIDER).

    The release journal always posts — the seller becomes owed the money the
    moment the deal completes, regardless of the payout rail. The payout
    journal (cash actually leaving settlement) only posts once the provider
    confirms the disbursement succeeded, so a failed/pending AzamPay transfer
    leaves the seller's payable balance intact instead of falsely recording
    money as paid out; the Payout row is left FAILED_RETRYABLE for ops to
    retry (calling this function again) or reconcile manually.
    """
    amount = _d(deal.amount)
    fee = compute_fee(amount)
    net = amount - fee

    deal.fee = fee
    deal.net_amount = net
    deal.save(update_fields=["fee", "net_amount"])

    release_lines = [
        {"account": "2000", "debit": amount, "memo": "Release escrow"},
        {"account": "2100", "credit": net, "memo": "Seller payable"},
    ]
    if fee > 0:
        release_lines.append({"account": "4000", "credit": fee, "memo": "Platform fee"})
    post_journal(
        reference=f"RELEASE-{deal.code}",
        description=f"Release for deal {deal.code}",
        deal=deal, created_by=operator, idempotency_key=f"release:{deal.id}",
        lines=release_lines,
    )

    _disburse_seller_payout(
        deal, net, operator=operator,
        journal_reference=f"PAYOUT-{deal.code}", idempotency_key=f"payout:{deal.id}",
    )
    return fee, net


def _post_payout_journal(deal, net, *, operator, journal_reference, idempotency_key):
    post_journal(
        reference=journal_reference,
        description=f"Seller payout for deal {deal.code}",
        deal=deal, created_by=operator, idempotency_key=idempotency_key,
        lines=[
            {"account": "2100", "debit": net, "memo": "Seller payout"},
            {"account": "1000", "credit": net, "memo": "Settlement out"},
        ],
    )


def resolve_payout_destination(user):
    """Where a user's payouts (deal-release or anything else that pays them
    out) should land — one unified answer whether they're acting as buyer or
    seller, per the merged payment-methods model (core.models.PaymentMethod):

      1. Their default saved payment method, if they have one.
      2. Else their most recently saved one (covers the edge case where
         is_default somehow isn't set on any row).
      3. Else the legacy single payout_provider/payout_phone fields on User
         (pre-merge accounts that set a payout method before PaymentMethod
         existed) — kept read-only for backward compatibility; nothing
         writes to these fields anymore.
      4. Else their login phone, with no provider — the adapter resolves an
         MNO from the phone itself.

    Returns (phone, provider).
    """
    from core.models import PaymentMethod

    default = PaymentMethod.objects.filter(user=user, is_default=True).first()
    if default is None:
        default = PaymentMethod.objects.filter(user=user).order_by("-created_at").first()
    if default is not None:
        return default.phone, default.provider
    if user.payout_phone:
        return user.payout_phone, user.payout_provider
    return user.phone, ""


def _disburse_seller_payout(deal, net, *, operator, journal_reference, idempotency_key):
    """Attempt to pay `net` out to the seller via the active provider.

    Some providers (the dev ManualProvider) confirm success synchronously;
    AzamPay's real disbursement is async — create_disbursement only submits
    the transfer ("processing"), and the payout journal is posted later, once
    confirmed, either here (synchronous "succeeded") or from
    payments.views.azampay_disbursement_callback (async confirmation).
    Returns the Payout row."""
    payout, _ = Payout.objects.get_or_create(
        deal=deal, seller=deal.seller,
        defaults={
            "amount": net, "currency": "TZS", "status": Payout.Status.QUEUED,
            "journal_reference": journal_reference, "idempotency_key": idempotency_key,
        },
    )
    if payout.status in {Payout.Status.SUCCEEDED, Payout.Status.PROCESSING}:
        # Already disbursed, or already submitted and awaiting the async
        # callback — don't resubmit a second transfer for the same payout.
        return payout

    payee_phone, payee_provider = resolve_payout_destination(deal.seller)
    provider = get_provider()
    result = provider.create_disbursement(
        DisbursementInstruction(
            payee_phone=payee_phone,
            provider=payee_provider,
            amount=net, currency="TZS", reference=journal_reference,
        )
    )
    payout.provider = provider.name
    payout.provider_ref = result.provider_ref or payout.provider_ref
    if result.status == "succeeded":
        _post_payout_journal(deal, net, operator=operator, journal_reference=journal_reference, idempotency_key=idempotency_key)
        payout.status = Payout.Status.SUCCEEDED
    elif result.status == "processing":
        payout.status = Payout.Status.PROCESSING  # journal posts later, from the disbursement callback
    else:
        payout.retry_count += 1
        payout.status = Payout.Status.FAILED_RETRYABLE
    payout.save(update_fields=["provider", "provider_ref", "status", "retry_count", "updated_at"])
    return payout


@transaction.atomic
def retry_payout(payout, *, operator="system"):
    """Re-attempt a FAILED_RETRYABLE payout (admin action). No-op if it's
    already succeeded or a submission is already in flight — see the guard
    in _disburse_seller_payout."""
    return _disburse_seller_payout(
        payout.deal, payout.amount, operator=operator,
        journal_reference=payout.journal_reference, idempotency_key=payout.idempotency_key,
    )


@transaction.atomic
def record_refund(deal, amount, *, operator="system", reason="", provider="manual", suffix="full"):
    """Refund escrow back to the buyer."""
    amount = _d(amount)
    post_journal(
        reference=f"REFUND-{deal.code}-{suffix}",
        description=f"Refund for deal {deal.code}",
        deal=deal, created_by=operator, idempotency_key=f"refund:{deal.id}:{suffix}",
        lines=[
            {"account": "2000", "debit": amount, "memo": "Reverse escrow"},
            {"account": "1000", "credit": amount, "memo": "Settlement out (refund)"},
        ],
    )
    Refund.objects.get_or_create(
        deal=deal, buyer=deal.buyer,
        defaults={"amount": amount, "currency": "TZS", "status": Refund.Status.COMPLETED,
                  "reason": reason[:255], "provider": provider,
                  "provider_ref": f"MANUAL-REFUND-{deal.code}"},
    )


@transaction.atomic
def record_split(deal, buyer_amount, seller_amount, *, operator="system", provider="manual"):
    """Split outcome: partial refund to buyer + partial payout to seller (fee on seller share)."""
    buyer_amount = _d(buyer_amount)
    seller_amount = _d(seller_amount)
    total = buyer_amount + seller_amount
    fee = compute_fee(seller_amount) if seller_amount > 0 else Decimal("0.00")
    seller_net = seller_amount - fee

    deal.fee = fee
    deal.net_amount = seller_net
    deal.save(update_fields=["fee", "net_amount"])

    lines = [{"account": "2000", "debit": total, "memo": "Release escrow (split)"}]
    if seller_net > 0:
        lines.append({"account": "2100", "credit": seller_net, "memo": "Seller payable"})
    if fee > 0:
        lines.append({"account": "4000", "credit": fee, "memo": "Platform fee"})
    if buyer_amount > 0:
        lines.append({"account": "1000", "credit": buyer_amount, "memo": "Buyer refund out"})
    post_journal(
        reference=f"SPLIT-{deal.code}",
        description=f"Split resolution for deal {deal.code}",
        deal=deal, created_by=operator, idempotency_key=f"split:{deal.id}",
        lines=lines,
    )
    if seller_net > 0:
        _disburse_seller_payout(
            deal, seller_net, operator=operator,
            journal_reference=f"SPLIT-PAYOUT-{deal.code}", idempotency_key=f"split-payout:{deal.id}",
        )
    if buyer_amount > 0:
        Refund.objects.get_or_create(
            deal=deal, buyer=deal.buyer,
            defaults={"amount": buyer_amount, "currency": "TZS", "status": Refund.Status.COMPLETED,
                      "reason": "split", "provider": provider,
                      "provider_ref": f"MANUAL-SPLITREFUND-{deal.code}"},
        )
    return fee, seller_net
