<?php

namespace App\Livewire\Groups;

use App\Domain\Attendance\Enums\AttendanceStatus;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Financial\Enums\InvoiceStatus;
use App\Domain\Financial\Enums\SubscriptionPaymentCase;
use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Financial\Models\PaymentPlan;
use App\Domain\Inventory\Models\Product;
use App\Domain\Financial\Services\ParticipantBillingService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Enums\SessionStatus;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;

#[Layout('layouts.app')]
#[Title('تفاصيل المجموعة')]
class GroupShow extends Component
{
    use UsesBranchScope;

    /**
     * Route-model bound, so BranchScope resolves it — another branch's group
     * 404s rather than opening. Locked so the browser cannot swap the id on a
     * later round-trip and carry assignTrainer()/removeTrainer() onto a group
     * this screen never rendered.
     */
    #[Locked]
    public TrainingGroup $group;

    public string $activeTab = 'overview';
    public ?int $assignTrainerId = null;
    public string $enrollSortBy = 'name_ar';
    public string $enrollSortDir = 'asc';

    public function sortEnrollments(string $column): void
    {
        if ($this->enrollSortBy === $column) {
            $this->enrollSortDir = $this->enrollSortDir === 'asc' ? 'desc' : 'asc';
        } else {
            $this->enrollSortBy = $column;
            $this->enrollSortDir = 'asc';
        }
    }

    /**
     * The billing cycle that contains today, as [start, end) in Y-m-d.
     *
     * Derived from the programme's own cycle rather than the calendar month:
     * "this month" is only the same thing while billing_day is 1, and a roster
     * that silently disagrees with the invoice run is worse than no roster.
     */
    private function currentCycleWindow(): array
    {
        $program = $this->group->program;

        $months = match ($program?->billing_cycle) {
            'quarterly' => 3,
            'semi_annual' => 6,
            'annual' => 12,
            default => 1,
        };

        $day = max(1, (int) ($program?->billing_day ?? 1));

        $today = now();
        $anchor = $today->copy()->startOfMonth();
        $start = $anchor->copy()->addDays(min($day, $anchor->daysInMonth) - 1);

        // Before this month's billing day, the live cycle is the previous one.
        if ($today->lt($start)) {
            $start = $start->subMonths($months);
        }

        return [$start->toDateString(), $start->copy()->addMonths($months)->toDateString()];
    }

    public function mount(TrainingGroup $group): void
    {
        $this->authorize('groups.list');

        $this->group = $group->load([
            'program',
            'branch',
            'headTrainer',
            'creator',
            'schedules',
            'schedules.facility',
            'schedules.trainer',
        ]);
    }

    /**
     * The coaches this branch may assign to a group.
     *
     * users has no branch_id — a user's branch lives on the role_user pivot —
     * so BranchScope cannot reach this query and the filter has to be written
     * out. A null pivot branch means the user is not pinned to any branch and
     * works for all of them, the same reading Trainer uses for its shared rows;
     * hard-filtering those would empty the picker in every academy that never
     * filled the pivot in.
     */
    private function branchTrainers(): Builder
    {
        $branchId = $this->getActiveBranchId();

        return User::query()
            ->where('status', 'active')
            ->whereHas('roles', function ($q) use ($branchId) {
                $q->whereIn('slug', ['trainer', 'head_trainer']);

                if ($branchId !== null) {
                    $q->where(fn ($w) => $w->where('role_user.branch_id', $branchId)
                        ->orWhereNull('role_user.branch_id'));
                }
            });
    }

    public function assignTrainer(): void
    {
        $this->authorize('groups.update');

        if (!$this->assignTrainerId) {
            return;
        }

        // $assignTrainerId is settable from the browser, so the id is checked
        // against the same branch-filtered set the <select> was built from
        // rather than trusted. Without this any user id in the academy could be
        // attached to this group as a coach.
        $this->validate(
            ['assignTrainerId' => ['required', 'integer', Rule::in($this->branchTrainers()->pluck('id')->all())]],
            ['assignTrainerId.in' => __('المدرب المختار غير متاح في هذا الفرع')]
        );

        $exists = Assignment::where('assignable_type', TrainingGroup::class)
            ->where('assignable_id', $this->group->id)
            ->where('user_id', $this->assignTrainerId)
            ->where('status', 'active')
            ->exists();

        if ($exists) {
            session()->flash('error', __('هذا المدرب مُعيَّن بالفعل لهذه المجموعة'));
            return;
        }

        Assignment::create([
            'academy_id' => $this->group->academy_id,
            // The group owns the assignment. Stamping from the request context
            // instead would file it against the operator's own branch whenever
            // they are working across branches.
            'branch_id' => $this->group->branch_id,
            'user_id' => $this->assignTrainerId,
            'assignable_type' => TrainingGroup::class,
            'assignable_id' => $this->group->id,
            'role_label' => 'مدرب',
            'scope' => 'full',
            'status' => 'active',
            'is_primary' => false,
            'start_date' => now()->toDateString(),
            'created_by' => auth()->id(),
        ]);

        $this->assignTrainerId = null;
        session()->flash('success', __('تم تعيين المدرب بنجاح'));
    }

    public function removeTrainer(int $assignmentId): void
    {
        $this->authorize('groups.update');

        Assignment::where('id', $assignmentId)
            ->where('assignable_type', TrainingGroup::class)
            ->where('assignable_id', $this->group->id)
            ->update(['status' => 'cancelled']);

        session()->flash('success', __('تم إلغاء تعيين المدرب'));
    }

    public function render()
    {
        // Active enrollments with participant details
        $enrollQuery = Enrollment::where('enrollments.training_group_id', $this->group->id)
            ->where('enrollments.status', 'active')
            ->with(['participant.person']);

        if ($this->enrollSortBy === 'name_ar') {
            $enrollQuery->join('participants', 'enrollments.participant_id', '=', 'participants.id')
                ->join('people', 'participants.person_id', '=', 'people.id')
                ->orderBy('people.name_ar', $this->enrollSortDir)
                ->select('enrollments.*');
        } elseif ($this->enrollSortBy === 'enrollment_date') {
            $enrollQuery->orderBy('enrollment_date', $this->enrollSortDir);
        } elseif ($this->enrollSortBy === 'payment_status') {
            $enrollQuery->orderBy('payment_status', $this->enrollSortDir);
        } else {
            $enrollQuery->orderBy('enrollment_date', 'asc');
        }

        $activeEnrollments = $enrollQuery->get();

        $participantIds = $activeEnrollments->pluck('participant_id')->toArray();

        // ---- Subscription, for the billing cycle we are actually in --------
        //
        // The old version answered a different question in each place: the red
        // "unpaid" flag matched invoice descriptions with ilike '%اشتراك%' plus
        // the programme name, while the amount beside it was a lifetime total.
        // The two disagreed constantly — a row could be flagged unpaid and show
        // a green figure, because they were never the same calculation. One
        // computation now feeds the flag, the amount and the header counts.
        $billing = app(ParticipantBillingService::class);

        // The service reads invoices through the raw query builder, which no
        // global scope reaches — so the branch has to be handed over rather
        // than assumed. The roster is already one branch's, but a player who
        // transferred in carries invoices from where they came from, and those
        // are not this branch's money.
        $billingBranchId = $this->getActiveBranchId();

        [$cycleStart, $cycleEnd] = $this->currentCycleWindow();
        $cycleFacts = $billing->subscriptionForPeriod($participantIds, $cycleStart, $cycleEnd, $billingBranchId);

        $subscriptionCases = [];
        foreach ($activeEnrollments as $enrollment) {
            $pid = $enrollment->participant_id;
            $facts = $cycleFacts[$pid] ?? null;

            $subscriptionCases[$pid] = [
                'case' => SubscriptionPaymentCase::classify($facts, (bool) $enrollment->participant?->is_free),
                'facts' => $facts,
            ];
        }

        // Legend entries, in precedence order, limited to cases actually present
        // so the roster never explains a colour nobody can see.
        $presentCases = array_values(array_unique(array_map(
            fn ($row) => $row['case'],
            $subscriptionCases
        ), SORT_REGULAR));

        $caseLegend = array_values(array_filter(
            SubscriptionPaymentCase::PRECEDENCE,
            fn ($case) => in_array($case, $presentCases, true)
        ));

        $countOf = fn (array $cases) => count(array_filter(
            $subscriptionCases,
            fn ($row) => in_array($row['case'], $cases, true)
        ));

        $freeCount = $countOf([SubscriptionPaymentCase::Free]);
        $unpaidCount = $countOf([
            SubscriptionPaymentCase::Unpaid,
            SubscriptionPaymentCase::Partial,
            SubscriptionPaymentCase::NotBilled,
        ]);
        $paidCount = count($subscriptionCases) - $freeCount - $unpaidCount;

        // Essential products: who bought / didn't buy each one.
        // No branch clause: Product carries BranchScope in shared mode, so this
        // is the branch's own products plus the academy-wide catalogue. Hard-
        // filtering on branch_id here would drop every shared product.
        $essentialProducts = Product::where('is_essential', true)
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->get(['id', 'name_ar', 'name']);

        $essentialProductStats = [];
        if ($essentialProducts->isNotEmpty() && $participantIds) {
            $productClass = Product::class;
            foreach ($essentialProducts as $product) {
                $buyerIds = InvoiceItem::where('itemable_type', $productClass)
                    ->where('itemable_id', $product->id)
                    ->whereHas('invoice', function ($q) use ($participantIds) {
                        $q->where('billable_type', Participant::class)
                            ->whereIn('billable_id', $participantIds)
                            ->whereNull('deleted_at')
                            ->whereNotIn('status', [InvoiceStatus::Cancelled]);
                    })
                    ->join('invoices', 'invoice_items.invoice_id', '=', 'invoices.id')
                    ->pluck('invoices.billable_id')
                    ->unique()
                    ->toArray();

                $essentialProductStats[] = [
                    'product' => $product,
                    'bought_count' => count($buyerIds),
                    'not_bought_count' => count($participantIds) - count($buyerIds),
                ];
            }
        }

        // Per-participant installment info (active payment plans)
        $participantInstallments = [];
        if ($participantIds) {
            $plansWithItems = PaymentPlan::whereIn('status', ['active', 'partial'])
                ->whereHas('invoice', function ($q) use ($participantIds) {
                    $q->where('billable_type', Participant::class)
                        ->whereIn('billable_id', $participantIds)
                        ->whereNull('deleted_at');
                })
                ->with(['invoice.items.itemable', 'invoice'])
                ->get();

            foreach ($plansWithItems as $plan) {
                $pid = $plan->invoice->billable_id;
                $itemName = $plan->invoice->items->first()?->itemable?->name_ar
                    ?? $plan->invoice->items->first()?->description
                    ?? __('اشتراك');

                $participantInstallments[$pid][] = [
                    'name' => $itemName,
                    'paid' => $plan->paid_installments,
                    'remaining' => $plan->total_installments - $plan->paid_installments,
                    'total' => $plan->total_installments,
                ];
            }
        }

        // All enrollments count (any status)
        $totalEnrollments = Enrollment::where('training_group_id', $this->group->id)
            ->whereNotIn('status', ['cancelled'])
            ->count();

        // Sessions: recent completed + upcoming scheduled
        $recentSessions = TrainingSession::where('training_group_id', $this->group->id)
            ->where('session_date', '<', now()->toDateString())
            ->orderByDesc('session_date')
            ->limit(20)
            ->get();

        $upcomingSessions = TrainingSession::where('training_group_id', $this->group->id)
            ->where('session_date', '>=', now()->toDateString())
            ->orderBy('session_date')
            ->limit(10)
            ->get();

        // Session stats
        $totalSessions = TrainingSession::where('training_group_id', $this->group->id)->count();
        $completedSessions = TrainingSession::where('training_group_id', $this->group->id)
            ->where('status', SessionStatus::Completed)
            ->count();

        // Attendance stats for the group (across all sessions)
        $sessionIds = TrainingSession::where('training_group_id', $this->group->id)
            ->pluck('id');

        $attendanceQuery = AttendanceRecord::whereIn('training_session_id', $sessionIds);

        $totalAttendanceRecords = (clone $attendanceQuery)->count();
        $presentCount = (clone $attendanceQuery)->where('status', AttendanceStatus::Present)->count();
        $lateCount = (clone $attendanceQuery)->where('status', AttendanceStatus::Late)->count();
        $absentCount = (clone $attendanceQuery)->whereIn('status', [AttendanceStatus::Absent, AttendanceStatus::NoShow])->count();
        $partialCount = (clone $attendanceQuery)->where('status', AttendanceStatus::Partial)->count();
        $excusedCount = (clone $attendanceQuery)->where('status', AttendanceStatus::Excused)->count();
        $cancelledExempt = (clone $attendanceQuery)->whereIn('status', [AttendanceStatus::Cancelled, AttendanceStatus::Exempt])->count();

        $attendanceDenominator = $totalAttendanceRecords - $cancelledExempt;
        $attendanceRate = $attendanceDenominator > 0
            ? round(($presentCount + $lateCount + $partialCount) / $attendanceDenominator * 100, 1)
            : 0;

        // Capacity percentage
        $capacityPercent = $this->group->max_capacity > 0
            ? round(($this->group->current_count / $this->group->max_capacity) * 100, 1)
            : 0;

        // Per-participant attendance rates for enrollments tab
        $participantAttendanceRates = [];
        if ($activeEnrollments->isNotEmpty() && $sessionIds->isNotEmpty()) {
            $participantClass = \App\Domain\Participant\Models\Participant::class;
            foreach ($activeEnrollments as $enrollment) {
                $pQuery = AttendanceRecord::whereIn('training_session_id', $sessionIds)
                    ->where('subject_type', $participantClass)
                    ->where('subject_id', $enrollment->participant_id);

                $pTotal = (clone $pQuery)->count();
                $pExcluded = (clone $pQuery)->whereIn('status', [AttendanceStatus::Cancelled, AttendanceStatus::Exempt])->count();
                $pPositive = (clone $pQuery)->whereIn('status', [AttendanceStatus::Present, AttendanceStatus::Late, AttendanceStatus::Partial])->count();

                $pDenom = $pTotal - $pExcluded;
                $participantAttendanceRates[$enrollment->participant_id] = $pDenom > 0
                    ? round(($pPositive / $pDenom) * 100, 1)
                    : null;
            }
        }

        // Waitlist count
        $waitlistCount = $this->group->waitlist_count ?? 0;

        // Trainer assignments for this group
        $trainerAssignments = Assignment::where('assignable_type', TrainingGroup::class)
            ->where('assignable_id', $this->group->id)
            ->where('status', 'active')
            ->with('user')
            ->get();

        $availableTrainers = $this->branchTrainers()
            ->whereNotIn('id', $trainerAssignments->pluck('user_id')->toArray())
            ->orderBy('name_ar')
            ->get(['id', 'name_ar', 'name']);

        // ---- Money actually collected, per participant -------------------
        //
        // Invoices are routinely bundled (subscription + registration card on
        // one invoice), so a payment cannot simply be read off a line. The
        // billing service allocates each payment across the lines it covers.
        //
        // Products this programme requires. Falls back to nothing when the
        // programme has no bundle configured, so groups are unaffected until
        // someone sets one up.
        $bundledProducts = $this->group->program
            ? $this->group->program->bundledProducts()->get()
            : collect();

        // A programme requiring exactly one product is the only case where an
        // instalment line that names nothing ("القسط الاول") can be read: there
        // is one thing here paid in instalments, so that is what it is paying
        // off. With two required products the same line is a coin flip and is
        // left where it was.
        $singleRequirement = $bundledProducts
            ->filter(fn ($p) => (bool) ($p->pivot->is_required ?? true))
            ->count() === 1;

        $participantsById = $activeEnrollments->pluck('participant', 'participant_id');

        $bundleColumns = [];
        foreach ($bundledProducts as $product) {
            $facts = $billing->bundledProductForParticipants(
                $participantIds,
                $product->id,
                (string) $product->name_ar,
                $billingBranchId,
                $singleRequirement && (bool) ($product->pivot->is_required ?? true),
            );

            $rows = [];
            foreach ($participantIds as $pid) {
                $row = $facts[$pid] ?? null;
                $billedAmount = (int) ($row['billed'] ?? 0);
                $paidAmount = (int) ($row['paid'] ?? 0);

                // What they owe in total. A real product sale froze the agreed
                // price on the invoice, so that is the figure. Money typed by
                // hand is only the instalments taken so far — showing 2,500 of
                // 2,500 would call a third of a card "paid in full" — so the
                // expected total is what this player would be charged for it.
                $expected = $billedAmount;
                $estimated = false;
                if ($billedAmount > 0 && ! ($row['from_product_lines'] ?? false)) {
                    $tier = $participantsById[$pid]?->membership_type?->value ?? 'non_member';
                    $price = (int) $product->priceForTier($tier);
                    if ($price > $billedAmount) {
                        $expected = $price;
                        $estimated = true;
                    }
                }

                $owned = $billedAmount > 0 || $paidAmount > 0;

                $rows[$pid] = [
                    'owned' => $owned,
                    // A free player is never billed for the bundle either, so
                    // the header count must not read them as missing it — the
                    // cell already shows a dash rather than a warning.
                    'exempt' => (bool) $participantsById[$pid]?->is_free,
                    'billed' => $expected,
                    'paid' => $paidAmount,
                    // Percentage of the product's price actually settled.
                    'percent' => $expected > 0
                        ? min(100, (int) round($paidAmount * 100 / $expected))
                        : 0,
                    'fully_paid' => $owned && $paidAmount >= $expected,
                    // The card is being paid off rather than bought outright:
                    // the roster says so instead of flagging a shortfall.
                    'installment' => $owned && $paidAmount > 0 && $paidAmount < $expected,
                    'plan' => $row['plan'] ?? null,
                    'estimated_total' => $estimated,
                    'from_text' => (bool) ($row['from_text'] ?? false),
                ];
            }

            $bundleColumns[] = [
                'label' => (string) $product->name_ar,
                'product' => $product,
                'required' => (bool) ($product->pivot->is_required ?? true),
                'rows' => $rows,
                'missing_count' => count(array_filter($rows, fn ($r) => ! $r['owned'] && ! $r['exempt'])),
            ];
        }

        // Required kits, alongside the required products. A kit is only ever
        // sold as a kit, so what was billed IS the obligation — there is no
        // hand-typed instalment line to read and no price to estimate from.
        $bundledKits = $this->group->program
            ? $this->group->program->bundledKits()->get()
            : collect();

        foreach ($bundledKits as $kit) {
            $facts = $billing->bundledKitForParticipants($participantIds, $kit->id, $billingBranchId);

            $rows = [];
            foreach ($participantIds as $pid) {
                $row = $facts[$pid] ?? null;
                $billedAmount = (int) ($row['billed'] ?? 0);
                $paidAmount = (int) ($row['paid'] ?? 0);
                $owned = $billedAmount > 0 || $paidAmount > 0;

                $rows[$pid] = [
                    'owned' => $owned,
                    'exempt' => (bool) $participantsById[$pid]?->is_free,
                    'billed' => $billedAmount,
                    'paid' => $paidAmount,
                    'percent' => $billedAmount > 0
                        ? min(100, (int) round($paidAmount * 100 / $billedAmount))
                        : 0,
                    'fully_paid' => $owned && $paidAmount >= $billedAmount,
                    'installment' => $owned && $paidAmount > 0 && $paidAmount < $billedAmount,
                    'plan' => $row['plan'] ?? null,
                    'estimated_total' => false,
                    'from_text' => false,
                ];
            }

            $bundleColumns[] = [
                'label' => (string) $kit->name_ar,
                'kit' => $kit,
                'required' => (bool) ($kit->pivot->is_required ?? true),
                'rows' => $rows,
                'missing_count' => count(array_filter($rows, fn ($r) => ! $r['owned'] && ! $r['exempt'])),
            ];
        }

        // Collected for this group, for the header summary. Only shown to users
        // allowed to see money.
        // Subscription money for THIS cycle only; product money is one-off, so
        // it is counted in full rather than pinned to a month.
        $groupCollected = array_sum(array_column($cycleFacts, 'paid'));
        foreach ($bundleColumns as $col) {
            $groupCollected += array_sum(array_column($col['rows'], 'paid'));
        }

        $canSeeFinancials = auth()->user()?->can('invoices.list') ?? false;

        return view('livewire.groups.group-show', [
            'activeEnrollments' => $activeEnrollments,
            'subscriptionCases' => $subscriptionCases,
            'caseLegend' => $caseLegend,
            'cycleStart' => $cycleStart,
            'cycleEnd' => $cycleEnd,
            'paidCount' => $paidCount,
            'unpaidCount' => $unpaidCount,
            'freeCount' => $freeCount,
            'essentialProductStats' => $essentialProductStats,
            'participantInstallments' => $participantInstallments,
            'bundleColumns' => $bundleColumns,
            'groupCollected' => $groupCollected,
            'canSeeFinancials' => $canSeeFinancials,
            'totalEnrollments' => $totalEnrollments,
            'recentSessions' => $recentSessions,
            'upcomingSessions' => $upcomingSessions,
            'totalSessions' => $totalSessions,
            'completedSessions' => $completedSessions,
            'totalAttendanceRecords' => $totalAttendanceRecords,
            'presentCount' => $presentCount,
            'lateCount' => $lateCount,
            'absentCount' => $absentCount,
            'partialCount' => $partialCount,
            'excusedCount' => $excusedCount,
            'attendanceRate' => $attendanceRate,
            'capacityPercent' => $capacityPercent,
            'participantAttendanceRates' => $participantAttendanceRates,
            'waitlistCount' => $waitlistCount,
            'trainerAssignments' => $trainerAssignments,
            'availableTrainers' => $availableTrainers,
        ]);
    }
}
