<?php

namespace App\Livewire\Portal;

use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Document\Models\Document;
use App\Domain\Financial\Models\Installment;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Enums\SessionStatus;
use App\Domain\Training\Models\TrainingSession;
use App\Livewire\Portal\Concerns\PortalScreen;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;

/**
 * The balance, the next session, and one action list.
 *
 * What is owed is the headline; قسط مستحق and مستند منتهي follow it. A member
 * opens this app to find out whether anything needs doing, and everything that
 * does is on one surface rather than spread across five tabs waiting to be
 * discovered.
 */
#[Layout('layouts.portal')]
#[Title('الرئيسية')]
class PortalHome extends Component
{
    use PortalScreen;

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

    public function render()
    {
        $portal = $this->portal();
        $activeId = $this->activeParticipantId();
        $familyIds = $this->familyParticipantIds();
        $active = $portal->activeParticipant();

        $groupIds = $active
            ? $active->activeEnrollments()->pluck('training_group_id')->all()
            : [];

        // Money is family-scoped: a guardian asking what is owed is asking
        // about the household, not about whichever child is selected. One
        // query, read twice — the headline and the tone of it are the same
        // fact, and asking the database the same question twice invited them
        // to disagree.
        $openInvoices = Invoice::whereIn('billable_id', $familyIds)
            ->where('billable_type', Participant::class)
            ->whereIn('status', ['sent', 'partially_paid', 'overdue'])
            ->get(['id', 'status', 'due_amount']);

        return view('livewire.portal.portal-home', [
            'greeting' => now()->hour < 12 ? __('صباح الخير') : __('مساء الخير'),
            'activeChild' => $active,
            'nextSession' => $this->nextSession($groupIds),
            'attendanceRate' => $this->attendanceRateThisMonth($activeId),
            'streak' => $this->currentStreak($activeId),
            'outstanding' => (int) $openInvoices->sum('due_amount'),
            'outstandingCount' => $openInvoices->count(),
            'duesOverdue' => $openInvoices->contains(
                fn (Invoice $invoice) => ($invoice->status instanceof \BackedEnum
                    ? $invoice->status->value
                    : (string) $invoice->status) === 'overdue'
            ),
            'actions' => $this->actionList($activeId, $familyIds),
        ]);
    }

    private function nextSession(array $groupIds): ?TrainingSession
    {
        if ($groupIds === []) {
            return null;
        }

        return TrainingSession::whereIn('training_group_id', $groupIds)
            ->where('status', SessionStatus::Scheduled)
            ->where(function ($q) {
                $q->where('session_date', '>', now()->toDateString())
                    ->orWhere(function ($q2) {
                        $q2->where('session_date', now()->toDateString())
                            ->where('start_time', '>=', now()->format('H:i:s'));
                    });
            })
            ->with(['group.program', 'facility', 'trainer.person'])
            ->orderBy('session_date')
            ->orderBy('start_time')
            ->first();
    }

    private function attendanceRateThisMonth(int $participantId): ?float
    {
        $from = now()->startOfMonth()->toDateString();
        $to = now()->endOfMonth()->toDateString();

        $base = fn () => AttendanceRecord::where('subject_type', Participant::class)
            ->where('subject_id', $participantId)
            ->whereHas('session', fn ($q) => $q->whereBetween('session_date', [$from, $to]));

        // Cancelled and exempt sessions are not a chance to attend, so counting
        // them would report a rate the member could not have changed.
        $total = $base()->whereNotIn('status', ['cancelled', 'exempt'])->count();

        if ($total === 0) {
            return null;
        }

        $positive = $base()->whereIn('status', ['present', 'late', 'partial'])->count();

        return round($positive / $total * 100, 1);
    }

    /** Consecutive attended sessions, most recent first, stopping at the first miss. */
    private function currentStreak(int $participantId): int
    {
        $recent = AttendanceRecord::where('subject_type', Participant::class)
            ->where('subject_id', $participantId)
            ->whereIn('status', ['present', 'late', 'absent', 'no_show'])
            ->orderByDesc('marked_at')
            ->limit(40)
            ->pluck('status');

        $streak = 0;

        foreach ($recent as $status) {
            $value = $status instanceof \BackedEnum ? $status->value : (string) $status;

            if (! in_array($value, ['present', 'late'], true)) {
                break;
            }

            $streak++;
        }

        return $streak;
    }

    /**
     * Everything that needs the member to do something, newest problem first.
     *
     * @return array<int, array{tone: string, label: string, detail: string, route: ?string}>
     */
    private function actionList(int $activeId, array $familyIds): array
    {
        $actions = [];

        // The outstanding balance is not in this list any more: it is the
        // headline of the screen. It was appearing twice — once as an action
        // and once as a statistic — with the same number in both, which reads
        // as two problems rather than one.

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

        if ($nextInstallment) {
            $actions[] = [
                'tone' => $nextInstallment->due_date?->isPast() ? 'danger' : 'info',
                'label' => __('قسط مستحق'),
                'detail' => format_money((int) $nextInstallment->amount) . ' — ' . $nextInstallment->due_date?->translatedFormat('j M'),
                'route' => route('portal.payments'),
            ];
        }

        // A medical certificate expires at 06:00 on a nightly job and the
        // member had no in-product way to fix it. This is the first half of
        // that: telling them before a coach turns them away at the gate.
        $expiring = Document::where('documentable_type', Participant::class)
            ->whereIn('documentable_id', $familyIds)
            ->whereNotNull('expires_at')
            ->where('expires_at', '<=', now()->addDays(30)->toDateString())
            ->orderBy('expires_at')
            ->first();

        if ($expiring) {
            $expired = $expiring->expires_at?->isPast();

            $actions[] = [
                'tone' => $expired ? 'danger' : 'warning',
                'label' => $expired ? __('مستند منتهي') : __('مستند على وشك الانتهاء'),
                'detail' => $expiring->document_type->label() . ' — ' . $expiring->expires_at?->translatedFormat('j M Y'),
                'route' => route('portal.account'),
            ];
        }

        return $actions;
    }
}
