<?php

namespace App\Livewire\Portal;

use App\Domain\Financial\Models\Installment;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Wallet;
use App\Domain\Financial\Services\PaymentService;
use App\Domain\Financial\Services\WalletService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Enums\WaitlistResponse;
use App\Domain\Training\Models\Waitlist;
use App\Livewire\Portal\Concerns\PortalScreen;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\Attributes\Url;

/**
 * The three things the product already sends a push about every day and gave
 * the member nowhere to act on.
 *
 *  - **Instalments (B3).** `reminders:installments` and `push:installment-due`
 *    fire daily, and the only payment path ever built charged the whole
 *    `due_amount`. The push said pay and the app could not.
 *  - **Waitlist offers (B5).** `waitlists.notified_at/expires_at/response`,
 *    `WaitlistSpotAvailable` and `SendWaitlistSpotPush` all exist with no
 *    accept surface anywhere: the push fires, the offer expires, the place goes
 *    to nobody and the revenue with it.
 *  - **Renewals (B7).** `enrollments:generate-renewals` bills daily and
 *    `RenewalPolicy::ManualRenew` explicitly means a human decides — with
 *    nowhere to decide it.
 *
 * Paying from the wallet is here too, because a balance a member can see and
 * cannot spend is worse than no balance at all.
 */
#[Layout('layouts.portal')]
#[Title('المستحقات')]
class PortalDues extends Component
{
    use PortalScreen;

    #[Url(as: 'tab')]
    public string $tab = 'installments';

    #[Locked]
    public ?int $payingInstallmentId = null;

    public function mount(): void
    {
        $this->authorizePortal();
    }

    public function updatedTab(): void
    {
        if (! in_array($this->tab, ['installments', 'offers'], true)) {
            $this->tab = 'installments';
        }
    }

    /**
     * Settle one instalment from the member's wallet.
     *
     * Only from the wallet: a card gateway is feature-flagged off and an
     * InstaPay transfer is a proof, not an instant payment. What this does is
     * move money that is already the academy's, held on the member's behalf —
     * which is why it can be immediate.
     */
    public function payInstallmentFromWallet(int $installmentId, PaymentService $payments, WalletService $wallets): void
    {
        $this->authorize('portal.pay');

        $installment = Installment::whereHas('paymentPlan.invoice', function ($q) {
            $q->whereIn('billable_id', $this->familyParticipantIds())
                ->where('billable_type', Participant::class);
        })->findOrFail($installmentId);

        $invoice = $installment->paymentPlan?->invoice;

        if (! $invoice) {
            $this->addError('pay', __('لا توجد فاتورة مرتبطة بهذا القسط'));

            return;
        }

        // can_authorize_payment has existed since 2024 with zero readers. This
        // is one of the two places it finally gates something.
        if (! $this->portal()->mayPayFor((int) $invoice->billable_id)) {
            $this->addError('pay', __('لا تملك صلاحية الدفع عن هذا العضو'));

            return;
        }

        $participant = Participant::find($invoice->billable_id);

        if (! $participant) {
            return;
        }

        $wallet = Wallet::where('owner_type', Participant::class)
            ->where('owner_id', $participant->id)
            ->first();

        $amount = min((int) $installment->amount, (int) $invoice->due_amount);

        if (! $wallet || (int) $wallet->balance < $amount) {
            $this->addError('pay', __('رصيد المحفظة لا يكفي لسداد هذا القسط'));

            return;
        }

        try {
            \Illuminate\Support\Facades\DB::transaction(function () use ($wallets, $payments, $wallet, $amount, $invoice, $installment, $participant) {
                $wallets->withdraw($wallet, $amount, 'سداد قسط من المحفظة: ' . $invoice->number, $invoice, auth()->user());

                $payment = $payments->recordPayment([
                    'academy_id' => $invoice->academy_id,
                    // Branch at the service boundary: a payment with no branch
                    // lands in the all-branches total and in no branch.
                    'branch_id' => $invoice->branch_id ?: $participant->branch_id,
                    'invoice_id' => $invoice->id,
                    'amount' => $amount,
                    'method' => 'wallet',
                    'direction' => 'inbound',
                    'currency' => $invoice->currency ?: 'EGP',
                    'payment_date' => now()->toDateString(),
                    'metadata' => ['source' => 'portal_wallet', 'installment_id' => $installment->id],
                ], auth()->user());

                Installment::where('id', $installment->id)
                    ->whereIn('status', ['pending', 'overdue'])
                    ->update(['status' => 'paid', 'paid_at' => now(), 'payment_id' => $payment->id, 'updated_at' => now()]);
            });
        } catch (DomainException $e) {
            $this->addError('pay', $e->getMessage());

            return;
        }

        session()->flash('success', __('تم سداد القسط من المحفظة'));
    }

    /**
     * Accept or decline a waitlist place.
     *
     * The conditional UPDATE is the control: an offer that has expired, or that
     * somebody already answered, matches zero rows rather than being answered
     * twice.
     */
    public function respondToOffer(int $waitlistId, string $response): void
    {
        if (! in_array($response, ['accepted', 'declined'], true)) {
            return;
        }

        $updated = Waitlist::where('id', $waitlistId)
            ->whereIn('participant_id', $this->familyParticipantIds())
            ->whereNotNull('notified_at')
            ->whereNull('responded_at')
            ->where(function ($q) {
                $q->whereNull('expires_at')->orWhere('expires_at', '>', now());
            })
            ->update([
                'response' => $response,
                'responded_at' => now(),
                'updated_at' => now(),
            ]);

        if ($updated === 0) {
            $this->addError('offer', __('انتهت صلاحية هذا العرض أو تم الرد عليه بالفعل'));

            return;
        }

        session()->flash('success', $response === 'accepted'
            // Accepting reserves the intent, not the place: enrolling is a
            // staff action with money attached, and pretending otherwise would
            // create an enrolment nobody has paid for.
            ? __('تم تسجيل موافقتك — ستتواصل معك الإدارة لإتمام التسجيل')
            : __('تم تسجيل اعتذارك عن المقعد'));
    }

    public function render()
    {
        $familyIds = $this->familyParticipantIds();

        $installments = Installment::whereHas('paymentPlan.invoice', function ($q) use ($familyIds) {
            $q->whereIn('billable_id', $familyIds)->where('billable_type', Participant::class);
        })
            ->whereIn('status', ['pending', 'overdue'])
            ->with('paymentPlan.invoice')
            ->orderBy('due_date')
            ->get();

        $walletBalances = Wallet::where('owner_type', Participant::class)
            ->whereIn('owner_id', $familyIds)
            ->pluck('balance', 'owner_id');

        return view('livewire.portal.portal-dues', [
            'installments' => $installments,
            'walletBalances' => $walletBalances,
            'payableIds' => $this->portal()->payableParticipantIds(),
            'offers' => Waitlist::whereIn('participant_id', $familyIds)
                ->whereNotNull('notified_at')
                ->whereNull('responded_at')
                ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
                ->with(['group.program', 'participant.person'])
                ->get(),
            'renewals' => Invoice::whereIn('billable_id', $familyIds)
                ->where('billable_type', Participant::class)
                ->whereIn('status', ['sent', 'overdue', 'partially_paid'])
                ->where('metadata->source', 'renewal')
                ->get(),
        ]);
    }
}
