<?php

namespace App\Livewire\Facilities;

use App\Domain\Facility\Models\Facility;
use App\Domain\Facility\Models\SpaceLayout;
use App\Domain\Facility\Models\SpaceReservation;
use App\Domain\Facility\Services\SpaceCollisionService;
use App\Domain\HR\Services\TrainerWorkloadService;
use App\Domain\Scheduling\Services\ScheduleConflictService;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingSchedule;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;

#[Layout('layouts.app')]
#[Title('جدولة المنشآت')]
class VisualScheduleBuilder extends Component
{
    use UsesBranchScope;

    #[Url]
    public string $facility_id = '';

    #[Url]
    public string $selectedDay = '';

    // Flexible time (to the minute). The two selected_* values are what the
    // save writes into training_schedules and space_reservations; they are set
    // by openSlot()/openCustomTime() after the operating-hours check and are
    // never bound in the view, so they are locked rather than left settable.
    #[Locked]
    public string $selectedSlotStart = '';

    #[Locked]
    public string $selectedSlotEnd = '';

    public string $customStartTime = '';
    public string $customEndTime = '';
    public bool $useCustomTime = false;

    // Grid state. All four are built on the server from branch-scoped queries
    // and none is bound in the view, so #[Locked] keeps them that way. Without
    // it $assignments is a public array the browser can rewrite, and
    // saveAssignments() persists group_id, trainer_id and segment_ids straight
    // out of it.
    #[Locked]
    public array $gridSegments = [];

    #[Locked]
    public array $assignments = [];

    #[Locked]
    public array $conflicts = [];

    #[Locked]
    public array $warnings = [];

    // Recurring booking
    public bool $showRecurring = false;
    public int $recurringWeeks = 8;
    public array $recurringDays = [];
    public bool $skipConflicts = true;

    // Trainer availability cache (for the sidebar indicators)
    #[Locked]
    public array $trainerAvailability = [];

    #[Locked]
    public array $groupAvailability = [];

    public function mount(): void
    {
        $this->authorize('schedules.manage');

        if (!$this->selectedDay) {
            $this->selectedDay = now()->toDateString();
        }

        // A ?facility_id= carried over from another branch (bookmark, back
        // button, shared link) must not survive the branch scope. This is a
        // tidy-up of the picker, not the security check — that lives in
        // resolveFacility(), which every read and write goes through.
        if ($this->facility_id && ! $this->resolveFacility()) {
            $this->facility_id = '';
        }
    }

    /**
     * The facility the builder is pointed at, or null.
     *
     * facility_id is wire:model'd from the picker, so it is re-settable on
     * every round-trip; checking it once in mount() and filtering nowhere else
     * is the "validate in mount, query in render" IDOR. Every read and every
     * write therefore resolves through here, where BranchScope applies the
     * branch condition at the point the query is built. Another branch's id
     * comes back null and the screen stays empty rather than rendering — or
     * booking — someone else's building.
     */
    private function resolveFacility(array $with = []): ?Facility
    {
        if (! $this->facility_id) {
            return null;
        }

        return Facility::query()
            ->when($with !== [], fn ($q) => $q->with($with))
            ->whereKey($this->facility_id)
            ->first();
    }

    /**
     * Switching facility drops an id that does not resolve in this branch, and
     * clears the grid — a slot and its pending assignments were picked against
     * one court's layout and must not be saved against another's.
     */
    public function updatedFacilityId(): void
    {
        if ($this->facility_id && ! $this->resolveFacility()) {
            $this->facility_id = '';
        }

        $this->resetGrid();
        $this->refreshAvailability();
    }

    /**
     * Trainers this branch may schedule.
     *
     * 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 trainer is not pinned anywhere and
     * works for every branch, the same reading Trainer uses for its own shared
     * rows; hard-filtering those would empty this sidebar for every academy
     * that never filled the pivot in.
     */
    private function branchTrainers(): Builder
    {
        $branchId = $this->getActiveBranchId();

        return User::query()
            ->where('status', 'active')
            ->whereHas('primaryRole', fn ($q) => $q->whereIn('slug', ['trainer', 'head_trainer']))
            ->when($branchId, fn ($q) => $q->whereHas(
                'roles',
                fn ($r) => $r->where(
                    fn ($w) => $w->where('role_user.branch_id', $branchId)
                        ->orWhereNull('role_user.branch_id')
                )
            ));
    }

    private function isBranchTrainer(int $trainerId): bool
    {
        return $this->branchTrainers()->whereKey($trainerId)->exists();
    }

    /**
     * The branch this screen is booking into — the facility's own, never the
     * reader's, because a pitch belongs to exactly one branch and that is where
     * the booking lands.
     *
     * It is handed to ScheduleConflictService, which uses it to look for
     * trainer clashes across every branch and then redact the other branch's
     * group names. That read is deliberately cross-branch: a coach cannot be in
     * two branches at once, so a scoped check would report a trainer free while
     * they are teaching elsewhere and book them twice. Without it the sidebar
     * badge and the drop check disagreed with validateAssignment(), which has
     * defaulted this from the facility all along.
     */
    private function schedulingBranchId(): ?int
    {
        return $this->resolveFacility()?->branch_id;
    }

    public function render()
    {
        // Only this branch's facilities. Showing every branch's courts on a
        // branch-scoped screen is how a receptionist books the wrong building.
        // No explicit branch clause: BranchScope narrows this, and forBranch()
        // would be worse than redundant here — it strips the scope it restates.
        $facilities = Facility::active()
            ->with('branch')
            ->orderBy('name_ar')
            ->get();

        $facility = $this->resolveFacility(['activeLayout']);

        $timeSlots = $this->generateTimeSlots($facility);
        $existingReservations = $this->getExistingReservations($facility);
        $groups = $this->getAvailableGroups();
        $trainers = $this->getAvailableTrainers();

        return view('livewire.facilities.visual-schedule-builder', [
            'facilities' => $facilities,
            'facility' => $facility,
            'timeSlots' => $timeSlots,
            'existingReservations' => $existingReservations,
            'groups' => $groups,
            'trainers' => $trainers,
            'dayOfWeek' => $this->selectedDay ? Carbon::parse($this->selectedDay)->dayOfWeek : now()->dayOfWeek,
        ]);
    }

    // ─── Navigation ──────────────────────────────────────────────

    public function selectDay(string $date): void
    {
        $this->selectedDay = $date;
        $this->resetGrid();
        $this->refreshAvailability();
    }

    public function openSlot(string $start, string $end): void
    {
        $this->selectedSlotStart = $start;
        $this->selectedSlotEnd = $end;
        $this->customStartTime = $start;
        $this->customEndTime = $end;
        $this->useCustomTime = false;
        $this->loadGridForSlot();
        $this->refreshAvailability();
    }

    public function openCustomTime(): void
    {
        if (!$this->customStartTime || !$this->customEndTime) {
            $this->addError('time', __('يجب تحديد وقت البداية والنهاية'));
            return;
        }

        if ($this->customStartTime >= $this->customEndTime) {
            $this->addError('time', __('وقت البداية يجب أن يكون قبل وقت النهاية'));
            return;
        }

        $facility = $this->resolveFacility();
        if ($facility) {
            $opStart = $facility->operating_start ?? '00:00';
            $opEnd = $facility->operating_end ?? '23:59';

            if ($this->customStartTime < $opStart || $this->customEndTime > $opEnd) {
                $this->addError('time', __('الوقت المحدد خارج ساعات عمل المنشأة') . " ({$opStart} - {$opEnd})");
                return;
            }
        }

        $this->selectedSlotStart = $this->customStartTime;
        $this->selectedSlotEnd = $this->customEndTime;
        $this->useCustomTime = true;
        $this->loadGridForSlot();
        $this->refreshAvailability();
    }

    public function closeSlot(): void
    {
        $this->selectedSlotStart = '';
        $this->selectedSlotEnd = '';
        $this->customStartTime = '';
        $this->customEndTime = '';
        $this->useCustomTime = false;
        $this->gridSegments = [];
        $this->assignments = [];
        $this->conflicts = [];
        $this->warnings = [];
    }

    // ─── Assignment Actions ──────────────────────────────────────

    public function assignGroupToSegments(int $groupId, array $segmentIds): void
    {
        if (empty($segmentIds) || !$this->facility_id || !$this->selectedSlotStart) {
            return;
        }

        $this->conflicts = [];
        $this->warnings = [];

        $facility = $this->resolveFacility();
        if (!$facility) {
            $this->addError('assignment', __('المنشأة غير متاحة في هذا الفرع'));
            return;
        }

        // The group id comes from a drag handler, so it is browser-supplied.
        // Resolve it before it reaches the conflict services, which take a bare
        // id and would happily report another branch's clashes back.
        $group = TrainingGroup::with(['program', 'headTrainer'])
            ->withCount(['schedules as active_schedules_count' => fn ($q) => $q->where('is_active', true)])
            ->find($groupId);

        if (!$group) {
            $this->addError('assignment', __('المجموعة غير متاحة في هذا الفرع'));
            return;
        }

        // 1. Check space collision (physical segments overlap)
        $collisionService = app(SpaceCollisionService::class);
        $spaceConflicts = $collisionService->check(
            $facility->id,
            $this->selectedDay,
            $this->selectedSlotStart,
            $this->selectedSlotEnd,
            $segmentIds
        );

        if (!empty($spaceConflicts)) {
            $this->conflicts = array_map(fn ($c) => array_merge($c, ['category' => 'space']), $spaceConflicts);
            return;
        }

        // 2. Check segments aren't already claimed by another group (group-to-group overlap is blocked)
        //    Trainers CAN overlap with groups (supervision), but two groups cannot share cells.
        foreach ($this->assignments as $assignment) {
            $assignType = $assignment['type'] ?? '';
            if ($assignType === 'trainer') continue; // trainers don't block groups

            $occupiedSegments = $assignment['segment_ids'] ?? [];
            $overlap = array_intersect($segmentIds, $occupiedSegments);

            if (!empty($overlap)) {
                $ownerName = $assignment['group_name'] ?? $assignment['title'] ?? __('مجموعة أخرى');
                $this->conflicts = [[
                    'category' => 'segment_taken',
                    'title' => $ownerName,
                    'message' => __('الخلايا المحددة مشغولة بالفعل بواسطة') . ' "' . $ownerName . '" — ' . __('لا يمكن لمجموعتين مشاركة نفس المساحة'),
                    'time' => $this->selectedSlotStart . ' - ' . $this->selectedSlotEnd,
                ]];
                return;
            }
        }

        // 3. Check same group isn't already assigned in this slot
        foreach ($this->assignments as $assignment) {
            if (($assignment['group_id'] ?? null) === $groupId && ($assignment['type'] ?? '') !== 'existing') {
                $this->addError('assignment', __('هذه المجموعة معيّنة بالفعل في هذا الوقت'));
                return;
            }
        }

        // 3. Check group conflicts (can't be in two places at once)
        $conflictService = app(ScheduleConflictService::class);
        $validation = $conflictService->validateAssignment(
            'group',
            $group->id,
            $facility->id,
            $this->selectedDay,
            $this->selectedSlotStart,
            $this->selectedSlotEnd,
            $segmentIds
        );

        if (!$validation['valid']) {
            $this->conflicts = array_map(fn ($c) => array_merge($c, ['category' => 'group']), $validation['errors']);
            return;
        }

        // Warnings are non-blocking but shown to user
        if (!empty($validation['warnings'])) {
            $this->warnings = $validation['warnings'];
        }

        // 4. Sessions-per-week soft warning
        if ($group->program) {
            $target = $group->program->sessions_per_week;
            $afterAssign = $group->active_schedules_count + 1;
            if ($afterAssign > $target) {
                $this->warnings[] = [
                    'message' => __('هذه المجموعة تجاوزت عدد الحصص المطلوبة') . " ({$afterAssign}/{$target} " . __('حصص أسبوعياً') . ")",
                ];
            }
        }

        // 5. Add to pending assignments. headTrainer is a belongsTo onto users,
        // which carries no branch scope, so it resolves — but it stays
        // null-safe because a group may simply have no coach assigned yet.
        $this->assignments[] = [
            'type' => 'group',
            'group_id' => $group->id,
            'group_name' => $group->name_ar ?? '',
            'segment_ids' => $segmentIds,
            'implied_trainer' => $group->headTrainer?->name_ar ?? $group->headTrainer?->name,
            'implied_trainer_id' => $group->head_trainer_id,
        ];
    }

    public function assignTrainerToSegments(int $trainerId, array $segmentIds): void
    {
        if (empty($segmentIds) || !$this->facility_id || !$this->selectedSlotStart) {
            return;
        }

        $this->conflicts = [];
        $this->warnings = [];

        // The trainer id comes straight from the drag handler. users has no
        // branch column for BranchScope to catch, so this is the only thing
        // stopping another branch's coach being pinned to this branch's slot.
        if (!$this->isBranchTrainer($trainerId)) {
            $this->addError('assignment', __('المدرب غير متاح في هذا الفرع'));
            return;
        }

        // Check trainer conflicts, across branches: see schedulingBranchId().
        $conflictService = app(ScheduleConflictService::class);
        $trainerConflicts = $conflictService->checkTrainerConflicts(
            $trainerId,
            $this->selectedDay,
            $this->selectedSlotStart,
            $this->selectedSlotEnd,
            null,
            null,
            $this->schedulingBranchId()
        );

        if (!empty($trainerConflicts)) {
            $this->conflicts = array_map(fn ($c) => array_merge($c, ['category' => 'trainer']), $trainerConflicts);
            return;
        }

        $trainer = $this->branchTrainers()->find($trainerId);
        $this->assignments[] = [
            'type' => 'trainer',
            'trainer_id' => $trainerId,
            'trainer_name' => $trainer?->name_ar ?? $trainer?->name ?? '',
            'segment_ids' => $segmentIds,
        ];
    }

    public function removeAssignment(int $index): void
    {
        unset($this->assignments[$index]);
        $this->assignments = array_values($this->assignments);
        $this->conflicts = [];
        $this->warnings = [];
    }

    public function dismissWarnings(): void
    {
        $this->warnings = [];
    }

    public function dismissConflicts(): void
    {
        $this->conflicts = [];
    }

    // ─── Save Operations ─────────────────────────────────────────

    public function saveAssignments(): void
    {
        $newAssignments = collect($this->assignments)->filter(fn ($a) => ($a['type'] ?? '') !== 'existing');

        if ($newAssignments->isEmpty() || !$this->facility_id || !$this->selectedSlotStart) {
            $this->addError('save', __('لا يوجد تعيينات جديدة للحفظ'));
            return;
        }

        $facility = $this->resolveFacility();
        if (!$facility) {
            $this->addError('save', __('المنشأة غير متاحة في هذا الفرع'));
            return;
        }

        $layout = $this->resolveLayout($facility);
        $dayOfWeek = Carbon::parse($this->selectedDay)->dayOfWeek;
        $conflictService = app(ScheduleConflictService::class);

        // Final validation pass before commit
        foreach ($newAssignments as $assignment) {
            if ($assignment['type'] === 'group') {
                $validation = $conflictService->validateAssignment(
                    'group',
                    $assignment['group_id'],
                    $facility->id,
                    $this->selectedDay,
                    $this->selectedSlotStart,
                    $this->selectedSlotEnd,
                    $assignment['segment_ids']
                );
                if (!$validation['valid']) {
                    $this->conflicts = $validation['errors'];
                    return;
                }
            } elseif ($assignment['type'] === 'trainer') {
                $trainerConflicts = $conflictService->checkTrainerConflicts(
                    $assignment['trainer_id'],
                    $this->selectedDay,
                    $this->selectedSlotStart,
                    $this->selectedSlotEnd,
                    null,
                    null,
                    $facility->branch_id
                );
                if (!empty($trainerConflicts)) {
                    $this->conflicts = $trainerConflicts;
                    return;
                }
            }
        }

        DB::transaction(function () use ($facility, $layout, $dayOfWeek, $newAssignments) {
            foreach ($newAssignments as $assignment) {
                if ($assignment['type'] === 'group') {
                    $this->persistGroupAssignment($assignment, $facility, $layout, $dayOfWeek);
                } elseif ($assignment['type'] === 'trainer') {
                    $this->persistTrainerAssignment($assignment, $facility, $dayOfWeek);
                }
            }
        });

        $this->dispatch('assignment-saved');
        session()->flash('success', __('تم حفظ الجدول بنجاح'));
        $this->loadGridForSlot();
        $this->refreshAvailability();
    }

    public function saveRecurring(): void
    {
        $newAssignments = collect($this->assignments)->filter(fn ($a) => ($a['type'] ?? '') !== 'existing');

        if ($newAssignments->isEmpty() || !$this->facility_id || !$this->selectedSlotStart) {
            return;
        }

        $facility = $this->resolveFacility();
        if (!$facility) {
            $this->addError('save', __('المنشأة غير متاحة في هذا الفرع'));
            return;
        }

        $layout = $this->resolveLayout($facility);
        $startDate = Carbon::parse($this->selectedDay);
        $dayOfWeek = $startDate->dayOfWeek;
        $collisionService = app(SpaceCollisionService::class);
        $conflictService = app(ScheduleConflictService::class);

        $days = !empty($this->recurringDays) ? array_map('intval', $this->recurringDays) : [$dayOfWeek];

        // recurringWeeks is wire:model'd from a range input, so the 1..52 the
        // slider enforces is a client-side bound only. This loop creates a
        // reservation row per week per day inside one transaction.
        $this->recurringWeeks = max(1, min(52, (int) $this->recurringWeeks));

        $bookedCount = 0;
        $conflictDates = [];
        $trainerConflictDates = [];

        DB::transaction(function () use (
            $facility, $layout, $startDate, $days,
            $collisionService, $conflictService,
            $newAssignments, &$bookedCount, &$conflictDates, &$trainerConflictDates
        ) {
            foreach ($newAssignments as $assignment) {
                if ($assignment['type'] !== 'group') continue;

                $group = TrainingGroup::find($assignment['group_id']);
                if (!$group) continue;

                foreach ($days as $targetDay) {
                    $schedule = TrainingSchedule::updateOrCreate(
                        [
                            'academy_id' => $group->academy_id,
                            'training_group_id' => $group->id,
                            'day_of_week' => $targetDay,
                            'start_time' => $this->selectedSlotStart,
                        ],
                        [
                            'end_time' => $this->selectedSlotEnd,
                            'facility_id' => $facility->id,
                            'space_reservation_template' => $assignment['segment_ids'],
                            'effective_from' => $startDate->toDateString(),
                            'effective_until' => $startDate->copy()->addWeeks($this->recurringWeeks)->toDateString(),
                            'is_active' => true,
                        ]
                    );

                    for ($week = 0; $week < $this->recurringWeeks; $week++) {
                        $date = $startDate->copy()->addWeeks($week);
                        while ($date->dayOfWeek !== (int) $targetDay) {
                            $date->addDay();
                        }

                        $dateStr = $date->toDateString();

                        // Space collision check
                        $spaceConflicts = $collisionService->check(
                            $facility->id,
                            $dateStr,
                            $this->selectedSlotStart,
                            $this->selectedSlotEnd,
                            $assignment['segment_ids']
                        );

                        if (!empty($spaceConflicts)) {
                            if ($this->skipConflicts) {
                                $conflictDates[] = $dateStr;
                                continue;
                            }
                            $conflictDates[] = $dateStr;
                            continue;
                        }

                        // Group conflict check (can't be in two places)
                        $groupConflicts = $conflictService->checkGroupConflicts(
                            $group->id,
                            $dateStr,
                            $this->selectedSlotStart,
                            $this->selectedSlotEnd,
                            $schedule->id
                        );

                        if (!empty($groupConflicts)) {
                            $conflictDates[] = $dateStr;
                            continue;
                        }

                        // Trainer conflict check (implied trainer from group)
                        if ($group->head_trainer_id) {
                            $tc = $conflictService->checkTrainerConflicts(
                                $group->head_trainer_id,
                                $dateStr,
                                $this->selectedSlotStart,
                                $this->selectedSlotEnd,
                                $schedule->id,
                                null,
                                $facility->branch_id
                            );
                            if (!empty($tc)) {
                                $trainerConflictDates[] = $dateStr;
                                // Don't block — warn only. Trainer conflicts on recurring are soft.
                            }
                        }

                        SpaceReservation::create([
                            'academy_id' => $group->academy_id,
                            'facility_id' => $facility->id,
                            'space_layout_id' => $layout?->id,
                            'status' => 'confirmed',
                            'reservation_date' => $dateStr,
                            'start_time' => $this->selectedSlotStart,
                            'end_time' => $this->selectedSlotEnd,
                            'segment_ids' => $assignment['segment_ids'],
                            'reservable_type' => TrainingSchedule::class,
                            'reservable_id' => $schedule->id,
                            'title' => $group->name_ar,
                            'is_recurring' => true,
                            'recurrence_pattern' => 'weekly',
                            'recurrence_end_date' => $startDate->copy()->addWeeks($this->recurringWeeks)->toDateString(),
                            'created_by' => auth()->id(),
                        ]);

                        $bookedCount++;
                    }
                }
            }
        });

        $msg = __('تم حجز') . " {$bookedCount} " . __('جلسة بنجاح');
        if (!empty($conflictDates)) {
            $msg .= '. ' . __('تم تخطي') . ' ' . count($conflictDates) . ' ' . __('تاريخ بسبب تعارضات مكانية');
        }
        if (!empty($trainerConflictDates)) {
            $msg .= '. ' . __('تحذير: تعارض مدرب في') . ' ' . count($trainerConflictDates) . ' ' . __('تاريخ');
        }

        session()->flash('success', $msg);
        $this->showRecurring = false;
        $this->loadGridForSlot();
        $this->refreshAvailability();
    }

    // ─── Cancel Operations ───────────────────────────────────────

    /**
     * Both ids arrive from wire:click. SpaceReservation and TrainingSchedule
     * (through its group) are branch-scoped, so a foreign id already resolves
     * to null; pinning both to the open facility keeps the button from reaching
     * a booking this screen never rendered.
     */
    public function cancelReservation(?int $reservationId = null, ?int $scheduleId = null): void
    {
        if (! $this->facility_id) {
            return;
        }

        if ($reservationId) {
            $reservation = SpaceReservation::where('facility_id', $this->facility_id)
                ->find($reservationId);
            if ($reservation) {
                $reservation->update([
                    'status' => 'cancelled',
                    'cancelled_by' => auth()->id(),
                    'cancelled_at' => now(),
                ]);
            }
        }

        if ($scheduleId) {
            $schedule = TrainingSchedule::where('facility_id', $this->facility_id)
                ->find($scheduleId);
            if ($schedule) {
                $schedule->update(['is_active' => false, 'effective_until' => now()->toDateString()]);
            }
        }

        $this->loadGridForSlot();
        $this->refreshAvailability();
        session()->flash('success', __('تم إلغاء الحجز'));
    }

    public function cancelSeries(int $reservationId): void
    {
        if (! $this->facility_id) {
            return;
        }

        $reservation = SpaceReservation::where('facility_id', $this->facility_id)
            ->find($reservationId);
        if (!$reservation || !$reservation->reservable_id) return;

        // The bulk update below deliberately carries no facility clause: a
        // schedule that moved court leaves older rows on the old facility_id,
        // and those still belong to the series being cancelled. Branch is still
        // enforced — BranchScope applies to this update as it does to any other
        // Eloquent query.
        $count = SpaceReservation::where('reservable_type', $reservation->reservable_type)
            ->where('reservable_id', $reservation->reservable_id)
            ->where('reservation_date', '>=', now()->toDateString())
            ->where('status', 'confirmed')
            ->count();

        SpaceReservation::where('reservable_type', $reservation->reservable_type)
            ->where('reservable_id', $reservation->reservable_id)
            ->where('reservation_date', '>=', now()->toDateString())
            ->where('status', 'confirmed')
            ->update([
                'status' => 'cancelled',
                'cancelled_by' => auth()->id(),
                'cancelled_at' => now(),
            ]);

        // Deactivate the schedule if all future reservations are cancelled
        if ($reservation->reservable_type === TrainingSchedule::class) {
            $schedule = TrainingSchedule::find($reservation->reservable_id);
            // Scoped through its group; another branch's schedule is not found.
            if ($schedule) {
                $schedule->update(['is_active' => false, 'effective_until' => now()->toDateString()]);
            }
        }

        $this->loadGridForSlot();
        $this->refreshAvailability();
        session()->flash('success', __('تم إلغاء') . " {$count} " . __('حجز مستقبلي'));
    }

    // ─── Availability Check (for sidebar indicators) ─────────────

    /**
     * Public Livewire methods, so any id can be posted at them and Livewire
     * ships the return value back to the browser. The ids are checked against
     * this branch's lists before anything is looked up.
     */
    public function checkTrainerAvailability(int $trainerId): array
    {
        if (!$this->selectedSlotStart || !$this->selectedDay) return [];
        if (!$this->isBranchTrainer($trainerId)) return [];

        $conflictService = app(ScheduleConflictService::class);
        return $conflictService->checkTrainerConflicts(
            $trainerId,
            $this->selectedDay,
            $this->selectedSlotStart,
            $this->selectedSlotEnd,
            null,
            null,
            $this->schedulingBranchId()
        );
    }

    public function getTrainerScheduleForDay(int $trainerId): array
    {
        if (!$this->isBranchTrainer($trainerId)) return [];

        $conflictService = app(ScheduleConflictService::class);
        return $conflictService->getTrainerOccupiedSlots($trainerId, $this->selectedDay);
    }

    public function getGroupScheduleForDay(int $groupId): array
    {
        if (!TrainingGroup::whereKey($groupId)->exists()) return [];

        $conflictService = app(ScheduleConflictService::class);
        return $conflictService->getGroupOccupiedSlots($groupId, $this->selectedDay);
    }

    // ─── Private Helpers ─────────────────────────────────────────

    private function refreshAvailability(): void
    {
        if (!$this->selectedSlotStart || !$this->selectedDay) {
            $this->trainerAvailability = [];
            $this->groupAvailability = [];
            return;
        }

        $conflictService = app(ScheduleConflictService::class);

        // Check each trainer this branch may schedule — same list the sidebar
        // renders, so the availability badges line up with what is offerable.
        $trainers = $this->branchTrainers()->pluck('id');

        // Resolved once: the badge has to answer the same question the drop
        // check answers, or the sidebar shows a coach free and the assignment
        // is then refused for a clash the user was never shown.
        $schedulingBranchId = $this->schedulingBranchId();

        $this->trainerAvailability = [];
        foreach ($trainers as $trainerId) {
            $conflicts = $conflictService->checkTrainerConflicts(
                $trainerId,
                $this->selectedDay,
                $this->selectedSlotStart,
                $this->selectedSlotEnd,
                null,
                null,
                $schedulingBranchId
            );
            $this->trainerAvailability[$trainerId] = [
                'available' => empty($conflicts),
                'conflicts' => $conflicts,
            ];
        }

        // Check each active group
        $groups = TrainingGroup::whereIn('status', ['active', 'forming', 'full'])->pluck('id');

        $this->groupAvailability = [];
        foreach ($groups as $groupId) {
            $conflicts = $conflictService->checkGroupConflicts(
                $groupId,
                $this->selectedDay,
                $this->selectedSlotStart,
                $this->selectedSlotEnd
            );
            $this->groupAvailability[$groupId] = [
                'available' => empty($conflicts),
                'conflicts' => $conflicts,
            ];
        }
    }

    private function persistGroupAssignment(array $assignment, Facility $facility, ?SpaceLayout $layout, int $dayOfWeek): void
    {
        $group = TrainingGroup::find($assignment['group_id']);
        if (!$group) return;

        $schedule = TrainingSchedule::updateOrCreate(
            [
                'academy_id' => $group->academy_id,
                'training_group_id' => $group->id,
                'day_of_week' => $dayOfWeek,
                'start_time' => $this->selectedSlotStart,
            ],
            [
                'end_time' => $this->selectedSlotEnd,
                'facility_id' => $facility->id,
                'space_reservation_template' => $assignment['segment_ids'],
                'effective_from' => $this->selectedDay,
                'is_active' => true,
            ]
        );

        SpaceReservation::create([
            'academy_id' => $group->academy_id,
            'facility_id' => $facility->id,
            'space_layout_id' => $layout?->id,
            'status' => 'confirmed',
            'reservation_date' => $this->selectedDay,
            'start_time' => $this->selectedSlotStart,
            'end_time' => $this->selectedSlotEnd,
            'segment_ids' => $assignment['segment_ids'],
            'reservable_type' => TrainingSchedule::class,
            'reservable_id' => $schedule->id,
            'title' => $group->name_ar,
            'created_by' => auth()->id(),
        ]);
    }

    private function persistTrainerAssignment(array $assignment, Facility $facility, int $dayOfWeek): void
    {
        // Re-checked at the write, not only at the drop: trainer_id lands on a
        // TrainingSchedule row, and users carry no branch column for the scope
        // to catch.
        if (!$this->isBranchTrainer((int) $assignment['trainer_id'])) {
            return;
        }

        // Find schedule(s) in this slot and attach trainer
        $schedules = TrainingSchedule::where('facility_id', $facility->id)
            ->where('day_of_week', $dayOfWeek)
            ->where('start_time', $this->selectedSlotStart)
            ->where('end_time', $this->selectedSlotEnd)
            ->where('is_active', true)
            ->get();

        if ($schedules->isEmpty()) {
            // No group schedule at this time — create a standalone reservation for the trainer
            SpaceReservation::create([
                'academy_id' => app('current_academy')?->id,
                'facility_id' => $facility->id,
                'space_layout_id' => null,
                'status' => 'confirmed',
                'reservation_date' => $this->selectedDay,
                'start_time' => $this->selectedSlotStart,
                'end_time' => $this->selectedSlotEnd,
                'segment_ids' => $assignment['segment_ids'],
                'title' => ($assignment['trainer_name'] ?? __('مدرب')) . ' — ' . __('إشراف'),
                'created_by' => auth()->id(),
            ]);
            return;
        }

        // Attach to the relevant schedule(s) that overlap with the trainer's segments
        foreach ($schedules as $schedule) {
            $scheduleSegments = $schedule->space_reservation_template ?? [];
            $overlap = array_intersect($assignment['segment_ids'], $scheduleSegments);

            if (!empty($overlap) || empty($scheduleSegments)) {
                if (!$schedule->trainer_id) {
                    $schedule->update(['trainer_id' => $assignment['trainer_id']]);
                } elseif (!$schedule->assistant_trainer_id) {
                    $schedule->update(['assistant_trainer_id' => $assignment['trainer_id']]);
                }
            }
        }
    }

    private function resetGrid(): void
    {
        $this->selectedSlotStart = '';
        $this->selectedSlotEnd = '';
        $this->customStartTime = '';
        $this->customEndTime = '';
        $this->useCustomTime = false;
        $this->gridSegments = [];
        $this->assignments = [];
        $this->conflicts = [];
        $this->warnings = [];
    }

    private function loadGridForSlot(): void
    {
        $facility = $this->resolveFacility();
        if (!$facility) {
            $this->gridSegments = [];
            $this->assignments = [];
            return;
        }

        $layout = $this->resolveLayout($facility);
        if (!$layout) {
            $this->gridSegments = [];
            return;
        }

        $segments = $layout->segments()->orderBy('sort_order')->get();
        $this->gridSegments = $segments->map(fn ($seg) => [
            'id' => $seg->id,
            'code' => $seg->code,
            'name' => $seg->name_ar ?: $seg->name,
            'row' => $seg->row_index,
            'col' => $seg->col_index,
            'lane' => $seg->lane_number,
            'available' => $seg->is_available,
            'capacity' => $seg->capacity,
        ])->toArray();

        // Load existing reservations for this slot
        $this->assignments = [];
        $reservations = SpaceReservation::where('facility_id', $facility->id)
            ->where('reservation_date', $this->selectedDay)
            ->where('status', 'confirmed')
            ->where('start_time', '<', $this->selectedSlotEnd)
            ->where('end_time', '>', $this->selectedSlotStart)
            ->with('reservable')
            ->get();

        $reservedScheduleIds = [];

        foreach ($reservations as $res) {
            $trainerName = null;
            if ($res->reservable && $res->reservable instanceof TrainingSchedule) {
                $trainerName = $res->reservable->trainer?->name_ar ?? $res->reservable->trainer?->name;
                $reservedScheduleIds[] = $res->reservable_id;
            }

            $this->assignments[] = [
                'type' => 'existing',
                'reservation_id' => $res->id,
                'title' => $res->title,
                'segment_ids' => $res->segment_ids ?? [],
                'is_recurring' => $res->is_recurring,
                'reservable_type' => $res->reservable_type,
                'reservable_id' => $res->reservable_id,
                'trainer_name' => $trainerName,
                'time' => substr($res->start_time, 0, 5) . ' - ' . substr($res->end_time, 0, 5),
            ];
        }

        // Also show active training_schedules that match this day/time but have no reservation
        $dayOfWeek = Carbon::parse($this->selectedDay)->dayOfWeek;
        $orphanSchedules = TrainingSchedule::where('facility_id', $facility->id)
            ->where('day_of_week', $dayOfWeek)
            ->where('is_active', true)
            ->where('start_time', '<', $this->selectedSlotEnd)
            ->where('end_time', '>', $this->selectedSlotStart)
            ->when(!empty($reservedScheduleIds), fn ($q) => $q->whereNotIn('id', $reservedScheduleIds))
            ->with(['group', 'trainer'])
            ->get();

        foreach ($orphanSchedules as $schedule) {
            $this->assignments[] = [
                'type' => 'existing',
                'reservation_id' => null,
                'schedule_id' => $schedule->id,
                'title' => $schedule->group?->name_ar ?? __('جدول بدون مجموعة'),
                'segment_ids' => $schedule->space_reservation_template ?? [],
                'is_recurring' => true,
                'reservable_type' => TrainingSchedule::class,
                'reservable_id' => $schedule->id,
                'trainer_name' => $schedule->trainer?->name_ar ?? $schedule->trainer?->name,
                'time' => substr($schedule->start_time, 0, 5) . ' - ' . substr($schedule->end_time, 0, 5),
            ];
        }
    }

    private function generateTimeSlots(?Facility $facility): array
    {
        if (!$facility) return [];

        $start = $facility->operating_start ?? '06:00';
        $end = $facility->operating_end ?? '23:00';

        $slots = [];
        $current = Carbon::parse($this->selectedDay . ' ' . $start);
        $endTime = Carbon::parse($this->selectedDay . ' ' . $end);

        while ($current < $endTime) {
            $slotEnd = $current->copy()->addHour();
            if ($slotEnd > $endTime) $slotEnd = $endTime;

            $slots[] = [
                'start' => $current->format('H:i'),
                'end' => $slotEnd->format('H:i'),
                'label' => $current->format('H:i') . ' - ' . $slotEnd->format('H:i'),
            ];

            $current->addHour();
        }

        return $slots;
    }

    private function getExistingReservations(?Facility $facility): array
    {
        if (!$facility || !$this->selectedDay) return [];

        $reservations = SpaceReservation::where('facility_id', $facility->id)
            ->where('reservation_date', $this->selectedDay)
            ->where('status', 'confirmed')
            ->orderBy('start_time')
            ->get()
            ->map(fn ($r) => [
                'id' => $r->id,
                'title' => $r->title ?? __('حجز'),
                'start' => substr($r->start_time, 0, 5),
                'end' => substr($r->end_time, 0, 5),
                'segment_ids' => $r->segment_ids ?? [],
                'is_recurring' => $r->is_recurring,
                'source' => 'reservation',
            ])
            ->toArray();

        // Also include active schedules for this day that have no reservation
        $dayOfWeek = Carbon::parse($this->selectedDay)->dayOfWeek;
        $reservedScheduleIds = SpaceReservation::where('facility_id', $facility->id)
            ->where('reservation_date', $this->selectedDay)
            ->where('status', 'confirmed')
            ->where('reservable_type', TrainingSchedule::class)
            ->pluck('reservable_id')
            ->toArray();

        $orphanSchedules = TrainingSchedule::where('facility_id', $facility->id)
            ->where('day_of_week', $dayOfWeek)
            ->where('is_active', true)
            ->when(!empty($reservedScheduleIds), fn ($q) => $q->whereNotIn('id', $reservedScheduleIds))
            ->with('group')
            ->orderBy('start_time')
            ->get();

        foreach ($orphanSchedules as $schedule) {
            $reservations[] = [
                'id' => $schedule->id,
                'title' => $schedule->group?->name_ar ?? __('جدول'),
                'start' => substr($schedule->start_time, 0, 5),
                'end' => substr($schedule->end_time, 0, 5),
                'segment_ids' => $schedule->space_reservation_template ?? [],
                'is_recurring' => true,
                'source' => 'schedule',
            ];
        }

        return $reservations;
    }

    private function resolveLayout(?Facility $facility): ?SpaceLayout
    {
        if (!$facility) return null;

        $dayOfWeek = Carbon::parse($this->selectedDay)->dayOfWeek;

        // Date-specific override first
        $layout = SpaceLayout::where('facility_id', $facility->id)
            ->where('is_active', true)
            ->where('is_recurring', false)
            ->where('effective_date', $this->selectedDay)
            ->first();

        if ($layout) return $layout;

        // Recurring for this day
        $layout = SpaceLayout::where('facility_id', $facility->id)
            ->where('is_active', true)
            ->where('is_recurring', true)
            ->where('effective_day_of_week', $dayOfWeek)
            ->first();

        if ($layout) return $layout;

        // Any active layout as fallback
        return SpaceLayout::where('facility_id', $facility->id)
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->first();
    }

    /**
     * Groups offerable in the selected facility.
     *
     * Branch is BranchScope's job. What this method adds is the sport filter:
     * a football court must not list swimming groups. A facility that declares
     * no activities hosts anything, so nothing breaks for academies that have
     * not filled the link in yet.
     */
    private function getAvailableGroups(): array
    {
        $facility = $this->resolveFacility(['activities:id']);

        // Branch comes from BranchScope, not from the facility. Deriving it
        // from a browser-settable facility_id was both an authorisation bypass
        // and, once the scope landed, a silent emptier of this sidebar: the
        // derived clause ANDed with the scope instead of replacing it.
        $activityIds = $facility ? $facility->activities->pluck('id')->all() : [];

        return TrainingGroup::whereIn('status', ['active', 'forming', 'full'])
            ->when($activityIds, fn ($q) => $q->whereHas(
                'program',
                fn ($p) => $p->whereIn('activity_id', $activityIds)
            ))
            ->with(['program', 'headTrainer'])
            ->withCount(['schedules as active_schedules_count' => fn ($q) => $q->where('is_active', true)])
            ->orderBy('name_ar')
            ->get()
            ->map(fn ($g) => [
                'id' => $g->id,
                'name' => $g->name_ar,
                'code' => $g->code,
                'program' => $g->program?->name_ar,
                'count' => $g->current_count,
                'max' => $g->max_capacity,
                'trainer' => $g->headTrainer?->name_ar ?? $g->headTrainer?->name,
                'trainer_id' => $g->head_trainer_id,
                'color' => $this->groupColor($g->id),
                'available' => $this->groupAvailability[$g->id]['available'] ?? true,
                'conflict_reason' => $this->groupAvailability[$g->id]['conflicts'][0]['message'] ?? null,
                'scheduled_sessions' => $g->active_schedules_count,
                'sessions_per_week' => $g->program?->sessions_per_week ?? 2,
            ])
            ->toArray();
    }

    private function getAvailableTrainers(): array
    {
        $users = $this->branchTrainers()
            ->orderBy('name_ar')
            ->get();

        $userIds = $users->pluck('id')->toArray();
        $workloads = app(TrainerWorkloadService::class)->getWorkloadsForUsers($userIds);

        return $users->map(fn ($u) => [
            'id' => $u->id,
            'name' => $u->name_ar ?? $u->name,
            'available' => $this->trainerAvailability[$u->id]['available'] ?? true,
            'conflict_reason' => $this->trainerAvailability[$u->id]['conflicts'][0]['message'] ?? null,
            'workload' => $workloads[$u->id] ?? null,
        ])->toArray();
    }

    private function groupColor(int $id): string
    {
        // Shared with every printed sheet — screen and paper must agree.
        return \App\Domain\Training\Support\GroupColor::for($id);
    }
}
