<?php

declare(strict_types=1);

namespace App\Filament\Pages\Schedule;

use App\Models\ClassCourse;
use App\Models\ClassSchedule;
use App\Models\ClassScheduleException;
use App\Models\ClassSession;
use App\Models\ClassScheduleWeekPublication;
use App\Models\Coach;
use App\Models\User;
use App\Models\Venue;
use App\Support\Schedule\WeeklyClassSessionGenerator;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Auth;

class ClassSchedulesCalendar extends Page implements HasActions
{
    use InteractsWithActions;

    protected static ?string $slug = 'class-schedules';

    protected static string|\UnitEnum|null $navigationGroup = '課表管理';
    protected static ?string $navigationLabel = '本週課表';
    protected static ?string $title = '本週課表';
    protected static ?int $navigationSort = 10;

    protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-calendar-days';

    protected string $view = 'filament.pages.schedule.class-schedules-calendar';

    public int $venueId;

    /**
     * @var array<string, mixed>
     */
    public array $calendarActionDefaults = [];

    protected ?WeeklyClassSessionGenerator $weeklyGenerator = null;

    public function mount(): void
    {
        $venue = Venue::query()->where('is_active', true)->orderBy('sort_order')->first();

        abort_unless($venue instanceof Venue, 404);

        $this->venueId = (int) $venue->id;

        $this->weeklyGenerator = app(WeeklyClassSessionGenerator::class);
    }

    /**
     * Generate sessions for the given weekStart (YYYY-MM-DD, Monday).
     */
    public function generateWeekFromTemplates(string $weekStartDate): void
    {
        $start = Carbon::parse($weekStartDate)->startOfWeek(Carbon::MONDAY);

        $result = $this->weeklyGenerator->generate($this->venueId, $start, skipExisting: false);

        if (($result['created'] + $result['updated'] + $result['deleted']) > 0) {
            $this->markWeekDirty($start);
        }

        Notification::make()
            ->title('已生成目前週課表')
            ->body("created={$result['created']} updated={$result['updated']} deleted={$result['deleted']} skipped_closed={$result['skipped_closed']} skipped_canceled={$result['skipped_canceled']}")
            ->success()
            ->send();

        $this->dispatch('class-sessions-updated');
    }

    /**
     * Generate sessions for next week (weekStart + 7 days).
     */
    public function generateNextWeekFromTemplates(string $weekStartDate): void
    {
        $start = Carbon::parse($weekStartDate)->startOfWeek(Carbon::MONDAY)->addWeek();

        $result = $this->weeklyGenerator->generate($this->venueId, $start, skipExisting: false);

        if (($result['created'] + $result['updated'] + $result['deleted']) > 0) {
            $this->markWeekDirty($start);
        }

        Notification::make()
            ->title('已生成下一週課表')
            ->body("created={$result['created']} updated={$result['updated']} deleted={$result['deleted']} skipped_closed={$result['skipped_closed']} skipped_canceled={$result['skipped_canceled']}")
            ->success()
            ->send();

        $this->dispatch('class-sessions-updated');
    }

    /**
     * @param  array<string, mixed>  $defaults
     */
    public function setCalendarActionDefaults(array $defaults): void
    {
        $this->calendarActionDefaults = $defaults;
    }

    public static function canAccess(): bool
    {
        $user = Auth::user();

        return $user instanceof User
            && $user->hasAnyRole(['super_admin', 'admin']);
    }

    /**
     * Livewire endpoint for FullCalendar `events`.
     *
     * @return array<int, array<string, mixed>>
     */
    public function getCalendarEvents(string $startDate, string $endDate, string $classroom): array
    {
        $query = ClassSession::query()
            ->where('venue_id', $this->venueId)
            ->where('classroom', $classroom)
            ->whereDate('date', '>=', $startDate)
            ->whereDate('date', '<', $endDate)
            ->where('is_active', true)
            ->orderBy('date')
            ->orderBy('start_time');

        $events = $query
            ->get()
            ->map(static function (ClassSession $session): array {
                $title = $session->class_title;

                if (filled($session->coach_display_name)) {
                    $title .= ' / ' . $session->coach_display_name;
                }

                if ($session->is_exception) {
                    $title .= '（異動）';
                }

                if ($session->is_suspended ?? false) {
                    $title .= '（停課）';
                }

                return [
                    'id' => (string) $session->id,
                    'title' => $title,
                    'start' => $session->date->format('Y-m-d') . 'T' . $session->start_time,
                    'end' => $session->date->format('Y-m-d') . 'T' . $session->end_time,
                    'editable' => ! (bool) ($session->is_suspended ?? false),
                    'extendedProps' => [
                        'isSuspended' => (bool) ($session->is_suspended ?? false),
                    ],
                ];
            })
            ->values();

        $closeExceptions = ClassScheduleException::query()
            ->where('venue_id', $this->venueId)
            ->where('is_active', true)
            ->where('action', 'close')
            ->whereDate('date', '>=', $startDate)
            ->whereDate('date', '<', $endDate)
            ->get();

        foreach ($closeExceptions as $exception) {
            $dateKey = $exception->date?->toDateString();
            if (! filled($dateKey)) {
                continue;
            }

            $exceptionClassroom = $exception->classroom ?: '全館';
            if ($exceptionClassroom !== '全館' && $exceptionClassroom !== $classroom) {
                continue;
            }

            $startTime = $exception->start_time ?: '07:00:00';
            $endTime = $exception->end_time ?: '24:00:00';

            $title = '休館';
            if (filled($exception->note)) {
                $title .= '：' . $exception->note;
            }

            $events->push([
                'id' => 'close-' . $exception->id . '-' . $exceptionClassroom,
                'title' => $title,
                'start' => $dateKey . 'T' . $startTime,
                'end' => $dateKey . 'T' . $endTime,
                'editable' => false,
                'startEditable' => false,
                'durationEditable' => false,
                'backgroundColor' => '#e5e7eb',
                'borderColor' => '#d1d5db',
                'textColor' => '#4b5563',
                'classNames' => ['close-placeholder'],
                'extendedProps' => [
                    'isClose' => true,
                    'exceptionId' => $exception->id,
                    'exceptionClassroom' => $exceptionClassroom,
                ],
            ]);
        }

        return $events->values()->all();
    }

    /**
     * @param  array<string, mixed>  $payload
     */
    public function updateSessionTiming(array $payload): void
    {
        $sessionId = (int) ($payload['id'] ?? 0);
        if ($sessionId <= 0) {
            return;
        }

        $session = ClassSession::query()
            ->where('venue_id', $this->venueId)
            ->findOrFail($sessionId);

        if ($session->is_suspended ?? false) {
            return;
        }

        $newDate = $payload['date'] ?? null;
        $newStart = $payload['start_time'] ?? null;
        $newEnd = $payload['end_time'] ?? null;

        if (blank($newDate) || blank($newStart) || blank($newEnd)) {
            $startIso = $payload['start'] ?? null;
            $endIso = $payload['end'] ?? null;
            if (blank($startIso) || blank($endIso)) {
                return;
            }

            $timezone = config('app.timezone') ?: 'UTC';
            $start = Carbon::parse((string) $startIso)->setTimezone($timezone);
            $end = Carbon::parse((string) $endIso)->setTimezone($timezone);

            $newDate = $start->toDateString();
            $newStart = $start->format('H:i');
            $newEnd = $end->format('H:i');
        }
        $newClassroom = (string) ($payload['classroom'] ?? $session->classroom);
        $markException = (bool) ($payload['mark_exception'] ?? false);

        $exception = $this->findExceptionForSession($session);
        $schedule = $this->findScheduleForSession($session);

        $oldDate = $session->date->toDateString();
        $oldKey = [
            'venue_id' => $this->venueId,
            'classroom' => (string) $session->classroom,
            'date' => $oldDate,
            'start_time' => (string) $session->start_time,
        ];

        if (! $markException) {
            if ($schedule instanceof ClassSchedule) {
                $oldSchedule = $schedule->replicate();
                $oldScheduleId = $schedule->id;
                $schedule->update([
                    'day_of_week' => Carbon::parse($newDate)->dayOfWeek,
                    'start_time' => $newStart,
                    'end_time' => $newEnd,
                    'classroom' => $newClassroom,
                    'course_id' => $session->course_id,
                    'class_title' => $session->class_title,
                    'coach_id' => $session->coach_id,
                    'coach_display_name' => $session->coach_display_name,
                    'level' => $session->level,
                    'note' => $session->note,
                ]);

                // Remove exceptions tied to the old slot (both template-linked and add-based).
                ClassScheduleException::query()
                    ->where('venue_id', $this->venueId)
                    ->whereDate('date', $oldDate)
                    ->whereIn('action', ['add', 'override', 'suspend'])
                    ->where(function ($query) use ($oldScheduleId, $oldSchedule): void {
                        $query
                            ->where('schedule_id', $oldScheduleId)
                            ->orWhere(function ($inner) use ($oldSchedule): void {
                                $inner
                                    ->whereNull('schedule_id')
                                    ->where('start_time', $oldSchedule->start_time)
                                    ->where('classroom', $oldSchedule->classroom);
                            });
                    })
                    ->delete();

                // Remove any exceptions on the new slot to keep it clean.
                ClassScheduleException::query()
                    ->where('venue_id', $this->venueId)
                    ->whereIn('action', ['add', 'override', 'suspend'])
                    ->whereDate('date', $newDate)
                    ->where('start_time', $newStart)
                    ->where('classroom', $newClassroom)
                    ->delete();

                if ($exception instanceof ClassScheduleException) {
                    $exception->delete();
                }

                // Update current session to the new slot.
                $session->update([
                    'date' => $newDate,
                    'start_time' => $newStart,
                    'end_time' => $newEnd,
                    'classroom' => $newClassroom,
                    'is_exception' => false,
                    'is_suspended' => false,
                ]);

                // Remove any other sessions on old or new slot to avoid duplicates.
                ClassSession::query()
                    ->where('venue_id', $this->venueId)
                    ->whereDate('date', $oldDate)
                    ->where('start_time', $oldSchedule->start_time)
                    ->where('classroom', $oldSchedule->classroom)
                    ->where('id', '!=', $session->id)
                    ->delete();

                ClassSession::query()
                    ->where('venue_id', $this->venueId)
                    ->whereDate('date', $newDate)
                    ->where('start_time', $newStart)
                    ->where('classroom', $newClassroom)
                    ->where('id', '!=', $session->id)
                    ->delete();

                $this->markWeekDirty(Carbon::parse($newDate));
                $this->dispatch('class-sessions-updated');
                return;
            }

            // No template exists; create one so the change persists without exceptions.
            ClassSchedule::create([
                'venue_id' => $this->venueId,
                'course_id' => $session->course_id,
                'day_of_week' => Carbon::parse($newDate)->dayOfWeek,
                'start_time' => $newStart,
                'end_time' => $newEnd,
                'classroom' => $newClassroom,
                'class_title' => $session->class_title,
                'coach_id' => $session->coach_id,
                'coach_display_name' => $session->coach_display_name,
                'level' => $session->level,
                'note' => $session->note,
                'sort_order' => 0,
                'is_active' => true,
            ]);

            if ($exception instanceof ClassScheduleException) {
                $exception->delete();
            }

            $session->update([
                'date' => $newDate,
                'start_time' => $newStart,
                'end_time' => $newEnd,
                'classroom' => $newClassroom,
                'is_exception' => false,
                'is_suspended' => false,
            ]);

            $this->markWeekDirty(Carbon::parse($newDate));
            $this->dispatch('class-sessions-updated');
            return;
        }

        if ($oldDate !== $newDate) {
            // Moving across days: cancel original template slot (if any) and add a new session.
            if ($schedule instanceof ClassSchedule) {
                $cancelPayload = [
                    'venue_id' => $this->venueId,
                    'schedule_id' => $schedule->id,
                    'date' => $oldDate,
                    'action' => 'cancel',
                    'course_id' => null,
                    'start_time' => null,
                    'end_time' => null,
                    'classroom' => null,
                    'class_title' => null,
                    'coach_id' => null,
                    'coach_display_name' => null,
                    'level' => null,
                    'note' => null,
                    'sort_order' => 0,
                    'is_active' => true,
                ];

                if ($exception instanceof ClassScheduleException && in_array($exception->action, ['override', 'suspend'], true)) {
                    $exception->update($cancelPayload);
                } else {
                    ClassScheduleException::create($cancelPayload);
                }
            } else {
                if ($exception instanceof ClassScheduleException) {
                    $exception->update([
                        'action' => 'add',
                        'schedule_id' => null,
                    ]);
                }
            }

            ClassScheduleException::create([
                'venue_id' => $this->venueId,
                'schedule_id' => null,
                'date' => $newDate,
                'action' => 'add',
                'course_id' => $session->course_id,
                'start_time' => $newStart,
                'end_time' => $newEnd,
                'classroom' => $newClassroom,
                'class_title' => $session->class_title,
                'coach_id' => $session->coach_id,
                'coach_display_name' => $session->coach_display_name,
                'level' => $session->level,
                'note' => $session->note,
                'sort_order' => 0,
                'is_active' => true,
            ]);

            $this->deleteSessionByKey($oldKey);

            $this->regenerateWeekForDate(Carbon::parse($oldDate));
            $this->regenerateWeekForDate(Carbon::parse($newDate));
            $this->markWeekDirty(Carbon::parse($oldDate));
            if ($oldDate !== $newDate) {
                $this->markWeekDirty(Carbon::parse($newDate));
            }
        } else {
            $oldStartCompare = substr((string) $oldKey['start_time'], 0, 5);
            $newStartCompare = substr((string) $newStart, 0, 5);
            if ($oldStartCompare !== $newStartCompare || (string) $oldKey['classroom'] !== (string) $newClassroom) {
                $this->deleteSessionByKey($oldKey);
            }

            $actionType = $schedule instanceof ClassSchedule ? 'override' : 'add';

            $exceptionData = [
                'venue_id' => $this->venueId,
                'schedule_id' => $actionType === 'override' ? $schedule?->id : null,
                'date' => $newDate,
                'action' => $actionType,
                'course_id' => $session->course_id,
                'start_time' => $newStart,
                'end_time' => $newEnd,
                'classroom' => $newClassroom,
                'class_title' => $session->class_title,
                'coach_id' => $session->coach_id,
                'coach_display_name' => $session->coach_display_name,
                'level' => $session->level,
                'note' => $session->note,
                'sort_order' => 0,
                'is_active' => true,
            ];

            if ($exception instanceof ClassScheduleException) {
                $exception->update($exceptionData);
            } else {
                ClassScheduleException::create($exceptionData);
            }

            $this->regenerateWeekForDate(Carbon::parse($oldDate));
            $this->markWeekDirty(Carbon::parse($oldDate));
        }

        $this->dispatch('class-sessions-updated');
    }

    public function createSessionAction(): Action
    {
        // Backwards compatibility: old UI used to create ClassSession directly.
        // We now create an "add" exception and regenerate the week.
        return Action::make('createSession')
            ->label('建立課程')
            ->modalHeading('新增課程')
            ->form($this->exceptionFormSchema())
            ->mountUsing(function (Action $action, ?Schema $schema = null): void {
                $payload = array_merge($this->calendarActionDefaults, $action->getArguments());
                $this->calendarActionDefaults = [];

                $schema?->fill([
                    'exception_id' => null,
                    'schedule_id' => null,
                    'action' => 'add',
                    'action_choice' => 'add',
                    'close_scope' => 'all_day',
                    'is_edit_mode' => false,
                    'date' => $payload['date'] ?? null,
                    'start_time' => $this->normalizeTime($payload['start_time'] ?? null),
                    'end_time' => $this->normalizeTime($payload['end_time'] ?? null),
                    'classroom' => $payload['classroom'] ?? '有氧團體教室',
                    'is_active' => true,
                ]);
            })
            ->action(function (array $data): void {
                $actionType = (string) ($data['action_choice'] ?? $data['action'] ?? 'add');
                $closeScope = (string) ($data['close_scope'] ?? 'all_day');

                $exception = ClassScheduleException::create([
                    'venue_id' => $this->venueId,
                    'schedule_id' => filled($data['schedule_id'] ?? null) ? (int) $data['schedule_id'] : null,
                    'date' => $data['date'],
                    'action' => $actionType,
                    'course_id' => $data['course_id'] ?? null,
                    'start_time' => ($actionType === 'close' && $closeScope === 'all_day') ? null : ($data['start_time'] ?? null),
                    'end_time' => ($actionType === 'close' && $closeScope === 'all_day') ? null : ($data['end_time'] ?? null),
                    'classroom' => $data['classroom'] ?? null,
                    'class_title' => $data['class_title'] ?? null,
                    'coach_id' => $data['coach_id'] ?? null,
                    'coach_display_name' => $data['coach_display_name'] ?? null,
                    'level' => $data['level'] ?? null,
                    'note' => $data['note'] ?? null,
                    'sort_order' => 0,
                    'is_active' => (bool) ($data['is_active'] ?? true),
                ]);

                $this->regenerateWeekForDate($exception->date);
                $this->markWeekDirty($exception->date);

                Notification::make()
                    ->title('已建立異動')
                    ->success()
                    ->send();

                $this->dispatch('class-sessions-updated');
            });
    }

    public function editSessionAction(): Action
    {
        // Backwards compatibility: old UI used to edit ClassSession directly.
        // We now upsert an exception based on the clicked session.
        return Action::make('editSession')
            ->label('編輯課程')
            ->modalHeading('課程更新異動')
            ->form($this->exceptionFormSchema())
            ->mountUsing(function (Action $action, ?Schema $schema = null): void {
                $payload = array_merge($this->calendarActionDefaults, $action->getArguments());
                $this->calendarActionDefaults = [];

                $sessionId = (int) ($payload['session_id'] ?? $payload['id'] ?? 0);

                $session = ClassSession::query()
                    ->where('venue_id', $this->venueId)
                    ->findOrFail($sessionId);

                $exception = $this->findExceptionForSession($session);

                if ($exception instanceof ClassScheduleException) {
                    $schema?->fill([
                        'session_id' => $session->id,
                        'exception_id' => $exception->id,
                        'schedule_id' => $exception->schedule_id,
                        'action' => $exception->action,
                        'action_choice' => $exception->action,
                        'close_scope' => filled($exception->start_time) || filled($exception->end_time) ? 'time_slot' : 'all_day',
                        'is_edit_mode' => true,
                        'date' => $exception->date->toDateString(),
                        'start_time' => $this->normalizeTime($exception->start_time ?? $session->start_time),
                        'end_time' => $this->normalizeTime($exception->end_time ?? $session->end_time),
                        'classroom' => $exception->classroom ?? $session->classroom,
                        'course_id' => $exception->course_id ?? $session->course_id,
                        'class_title' => $exception->class_title ?? $session->class_title,
                        'coach_id' => $exception->coach_id ?? $session->coach_id,
                        'coach_display_name' => $exception->coach_display_name ?? $session->coach_display_name,
                        'level' => $exception->level ?? $session->level,
                        'note' => $exception->note ?? $session->note,
                        'is_active' => $exception->is_active,
                    ]);

                    return;
                }

                $schedule = $this->findScheduleForSession($session);
                $defaultAction = $schedule instanceof ClassSchedule ? 'override' : 'add';

                $schema?->fill([
                    'session_id' => $session->id,
                    'exception_id' => null,
                    'schedule_id' => $schedule?->id,
                    'action' => $defaultAction,
                    'action_choice' => $defaultAction,
                    'is_edit_mode' => true,
                    'date' => $session->date->toDateString(),
                    'start_time' => $this->normalizeTime($session->start_time),
                    'end_time' => $this->normalizeTime($session->end_time),
                    'classroom' => $session->classroom,
                    'course_id' => $session->course_id,
                    'class_title' => $session->class_title,
                    'coach_id' => $session->coach_id,
                    'coach_display_name' => $session->coach_display_name,
                    'level' => $session->level,
                    'note' => $session->note,
                    'is_active' => true,
                ]);
            })
            ->action(function (array $data): void {
                $exceptionId = $data['exception_id'] ?? null;
                $exception = null;

                if (filled($exceptionId)) {
                    $exception = ClassScheduleException::query()
                        ->where('venue_id', $this->venueId)
                        ->findOrFail((int) $exceptionId);
                }

                $oldKey = $exception instanceof ClassScheduleException
                    ? $this->effectiveExceptionKey($exception)
                    : null;

                $actionType = (string) ($data['action_choice'] ?? $data['action'] ?? 'override');
                $closeScope = (string) ($data['close_scope'] ?? 'all_day');
                $scheduleId = filled($data['schedule_id'] ?? null) ? (int) $data['schedule_id'] : null;
                $sessionId = filled($data['session_id'] ?? null) ? (int) $data['session_id'] : null;

                if ($actionType === 'cancel' && ! $scheduleId && $sessionId) {
                    $session = ClassSession::query()
                        ->where('venue_id', $this->venueId)
                        ->find($sessionId);

                    if ($session instanceof ClassSession) {
                        $schedule = $this->findScheduleForSession($session);
                        if ($schedule instanceof ClassSchedule) {
                            $scheduleId = $schedule->id;
                        }
                    }
                }

                $payload = [
                    'schedule_id' => $scheduleId,
                    'date' => $data['date'],
                    'action' => $actionType,
                    'course_id' => $data['course_id'] ?? null,
                    'start_time' => ($actionType === 'close' && $closeScope === 'all_day') ? null : ($data['start_time'] ?? null),
                    'end_time' => ($actionType === 'close' && $closeScope === 'all_day') ? null : ($data['end_time'] ?? null),
                    'classroom' => $data['classroom'] ?? null,
                    'class_title' => $data['class_title'] ?? null,
                    'coach_id' => $data['coach_id'] ?? null,
                    'coach_display_name' => $data['coach_display_name'] ?? null,
                    'level' => $data['level'] ?? null,
                    'note' => $data['note'] ?? null,
                    'is_active' => (bool) ($data['is_active'] ?? true),
                ];

                if (! ($exception instanceof ClassScheduleException)) {
                    $exception = ClassScheduleException::create([
                        'venue_id' => $this->venueId,
                        'sort_order' => 0,
                        ...$payload,
                    ]);
                } else {
                    $exception->update($payload);
                }

                if ($exception->action === 'cancel' && $sessionId) {
                    ClassSession::query()
                        ->where('venue_id', $this->venueId)
                        ->whereKey($sessionId)
                        ->delete();

                    $session = ClassSession::withTrashed()
                        ->where('venue_id', $this->venueId)
                        ->find($sessionId);

                    if ($session instanceof ClassSession) {
                        ClassSession::query()
                            ->where('venue_id', $this->venueId)
                            ->whereDate('date', $session->date?->toDateString())
                            ->where('classroom', $session->classroom)
                            ->where('start_time', $session->start_time)
                            ->delete();
                    }
                }

                $newKey = $this->effectiveExceptionKey($exception);

                if ($oldKey && $newKey && ($oldKey !== $newKey)) {
                    $this->deleteSessionByKey($oldKey);
                }

                if ($exception->action === 'cancel') {
                    if ($newKey) {
                        $this->deleteSessionByKey($newKey);
                    }
                }

                if (! $exception->is_active) {
                    if ($newKey) {
                        $this->deleteSessionByKey($newKey);
                    }
                }

                $this->regenerateWeekForDate(Carbon::parse($exception->date));
                $this->markWeekDirty($exception->date);

                Notification::make()
                    ->title('已更新異動')
                    ->success()
                    ->send();

                $this->dispatch('class-sessions-updated');
            });
    }

    public function createExceptionAction(): Action
    {
        return Action::make('createException')
            ->label('建立課程')
            ->modalHeading('新增課程')
            ->form($this->exceptionFormSchema())
            ->mountUsing(function (Action $action, ?Schema $schema = null): void {
                $payload = array_merge($this->calendarActionDefaults, $action->getArguments());
                $this->calendarActionDefaults = [];

                $schema?->fill([
                    'exception_id' => null,
                    'schedule_id' => null,
                    'action' => 'add',
                    'action_choice' => 'add',
                    'close_scope' => 'all_day',
                    'close_scope' => 'all_day',
                    'is_edit_mode' => false,
                    'date' => $payload['date'] ?? null,
                    'start_time' => $this->normalizeTime($payload['start_time'] ?? null),
                    'end_time' => $this->normalizeTime($payload['end_time'] ?? null),
                    'classroom' => $payload['classroom'] ?? '有氧團體教室',
                    'is_active' => true,
                ]);
            })
            ->action(function (array $data): void {
                $exception = ClassScheduleException::create([
                    'venue_id' => $this->venueId,
                    'schedule_id' => filled($data['schedule_id'] ?? null) ? (int) $data['schedule_id'] : null,
                    'date' => $data['date'],
                    'action' => (string) ($data['action_choice'] ?? $data['action'] ?? 'add'),
                    'course_id' => $data['course_id'] ?? null,
                    'start_time' => $data['start_time'] ?? null,
                    'end_time' => $data['end_time'] ?? null,
                    'classroom' => $data['classroom'] ?? null,
                    'class_title' => $data['class_title'] ?? null,
                    'coach_id' => $data['coach_id'] ?? null,
                    'coach_display_name' => $data['coach_display_name'] ?? null,
                    'level' => $data['level'] ?? null,
                    'note' => $data['note'] ?? null,
                    'sort_order' => 0,
                    'is_active' => (bool) ($data['is_active'] ?? true),
                ]);

                $this->regenerateWeekForDate($exception->date);
                $this->markWeekDirty($exception->date);

                Notification::make()
                    ->title('已建立異動')
                    ->success()
                    ->send();

                $this->dispatch('class-sessions-updated');
            });
    }

    public function editExceptionAction(): Action
    {
        return Action::make('editException')
            ->label('編輯課程')
            ->modalHeading('課程更新異動')
            ->form($this->exceptionFormSchema())
            ->mountUsing(function (Action $action, ?Schema $schema = null): void {
                $payload = array_merge($this->calendarActionDefaults, $action->getArguments());
                $this->calendarActionDefaults = [];

                $exceptionId = $payload['exception_id'] ?? null;
                if (filled($exceptionId)) {
                    $exception = ClassScheduleException::query()
                        ->where('venue_id', $this->venueId)
                        ->findOrFail((int) $exceptionId);

                    $schema?->fill([
                        'session_id' => null,
                        'exception_id' => $exception->id,
                        'schedule_id' => $exception->schedule_id,
                        'action' => $exception->action,
                        'action_choice' => $exception->action,
                        'close_scope' => filled($exception->start_time) || filled($exception->end_time) ? 'time_slot' : 'all_day',
                        'is_edit_mode' => true,
                        'date' => $exception->date->toDateString(),
                        'start_time' => $this->normalizeTime($exception->start_time),
                        'end_time' => $this->normalizeTime($exception->end_time),
                        'classroom' => $exception->classroom ?? '全館',
                        'course_id' => $exception->course_id,
                        'class_title' => $exception->class_title,
                        'coach_id' => $exception->coach_id,
                        'coach_display_name' => $exception->coach_display_name,
                        'level' => $exception->level,
                        'note' => $exception->note,
                        'is_active' => $exception->is_active,
                    ]);

                    return;
                }

                $sessionId = (int) ($payload['session_id'] ?? $payload['id'] ?? 0);

                $session = ClassSession::query()
                    ->where('venue_id', $this->venueId)
                    ->findOrFail($sessionId);

                $exception = $this->findExceptionForSession($session);

                if ($exception instanceof ClassScheduleException) {
                    $schema?->fill([
                        'session_id' => $session->id,
                        'exception_id' => $exception->id,
                        'schedule_id' => $exception->schedule_id,
                        'action' => $exception->action,
                        'action_choice' => $exception->action,
                        'close_scope' => filled($exception->start_time) || filled($exception->end_time) ? 'time_slot' : 'all_day',
                        'is_edit_mode' => true,
                        'date' => $exception->date->toDateString(),
                        'start_time' => $this->normalizeTime($exception->start_time ?? $session->start_time),
                        'end_time' => $this->normalizeTime($exception->end_time ?? $session->end_time),
                        'classroom' => $exception->classroom ?? $session->classroom,
                        'course_id' => $exception->course_id ?? $session->course_id,
                        'class_title' => $exception->class_title ?? $session->class_title,
                        'coach_id' => $exception->coach_id ?? $session->coach_id,
                        'coach_display_name' => $exception->coach_display_name ?? $session->coach_display_name,
                        'level' => $exception->level ?? $session->level,
                        'note' => $exception->note ?? $session->note,
                        'is_active' => $exception->is_active,
                    ]);

                    return;
                }

                $schedule = $this->findScheduleForSession($session);

                $defaultAction = $schedule instanceof ClassSchedule ? 'override' : 'add';

                $schema?->fill([
                    'session_id' => $session->id,
                    'exception_id' => null,
                    'schedule_id' => $schedule?->id,
                    'action' => $defaultAction,
                    'action_choice' => $defaultAction,
                    'is_edit_mode' => true,
                    'date' => $session->date->toDateString(),
                    'start_time' => $this->normalizeTime($session->start_time),
                    'end_time' => $this->normalizeTime($session->end_time),
                    'classroom' => $session->classroom,
                    'course_id' => $session->course_id,
                    'class_title' => $session->class_title,
                    'coach_id' => $session->coach_id,
                    'coach_display_name' => $session->coach_display_name,
                    'level' => $session->level,
                    'note' => $session->note,
                    'is_active' => true,
                ]);
            })
            ->action(function (array $data): void {
                $exceptionId = $data['exception_id'] ?? null;
                $exception = null;

                if (filled($exceptionId)) {
                    $exception = ClassScheduleException::query()
                        ->where('venue_id', $this->venueId)
                        ->findOrFail((int) $exceptionId);
                }

                $oldKey = $exception instanceof ClassScheduleException
                    ? $this->effectiveExceptionKey($exception)
                    : null;

                $actionType = (string) ($data['action_choice'] ?? $data['action'] ?? 'override');
                $closeScope = (string) ($data['close_scope'] ?? 'all_day');
                $scheduleId = filled($data['schedule_id'] ?? null) ? (int) $data['schedule_id'] : null;
                $sessionId = filled($data['session_id'] ?? null) ? (int) $data['session_id'] : null;

                if ($actionType === 'cancel' && ! $scheduleId && $sessionId) {
                    $session = ClassSession::query()
                        ->where('venue_id', $this->venueId)
                        ->find($sessionId);

                    if ($session instanceof ClassSession) {
                        $schedule = $this->findScheduleForSession($session);
                        if ($schedule instanceof ClassSchedule) {
                            $scheduleId = $schedule->id;
                        }
                    }
                }

                $payload = [
                    'schedule_id' => $scheduleId,
                    'date' => $data['date'],
                    'action' => $actionType,
                    'course_id' => $data['course_id'] ?? null,
                    'start_time' => ($actionType === 'close' && $closeScope === 'all_day') ? null : ($data['start_time'] ?? null),
                    'end_time' => ($actionType === 'close' && $closeScope === 'all_day') ? null : ($data['end_time'] ?? null),
                    'classroom' => $data['classroom'] ?? null,
                    'class_title' => $data['class_title'] ?? null,
                    'coach_id' => $data['coach_id'] ?? null,
                    'coach_display_name' => $data['coach_display_name'] ?? null,
                    'level' => $data['level'] ?? null,
                    'note' => $data['note'] ?? null,
                    'is_active' => (bool) ($data['is_active'] ?? true),
                ];

                if (! ($exception instanceof ClassScheduleException)) {
                    $exception = ClassScheduleException::create([
                        'venue_id' => $this->venueId,
                        'sort_order' => 0,
                        ...$payload,
                    ]);
                } else {
                    $exception->update($payload);
                }

                if ($exception->action === 'cancel' && $sessionId) {
                    ClassSession::query()
                        ->where('venue_id', $this->venueId)
                        ->whereKey($sessionId)
                        ->delete();

                    $session = ClassSession::withTrashed()
                        ->where('venue_id', $this->venueId)
                        ->find($sessionId);

                    if ($session instanceof ClassSession) {
                        ClassSession::query()
                            ->where('venue_id', $this->venueId)
                            ->whereDate('date', $session->date?->toDateString())
                            ->where('classroom', $session->classroom)
                            ->where('start_time', $session->start_time)
                            ->delete();
                    }
                }

                $newKey = $this->effectiveExceptionKey($exception);

                if ($oldKey && $newKey && ($oldKey !== $newKey)) {
                    $this->deleteSessionByKey($oldKey);
                }

                if (! $exception->is_active) {
                    // If the exception is turned off, delete the corresponding session too,
                    // otherwise a previously-generated moved slot may linger.
                    if ($newKey) {
                        $this->deleteSessionByKey($newKey);
                    }
                }

                $this->regenerateWeekForDate(Carbon::parse($exception->date));
                $this->markWeekDirty($exception->date);

                Notification::make()
                    ->title('已更新異動')
                    ->success()
                    ->send();

                $this->dispatch('class-sessions-updated');
            });
    }

    /**
     * Copy the visible week to next week.
     */
    public function copyWeek(string $weekStartDate, bool $includeExceptions = true): void
    {
        $start = Carbon::parse($weekStartDate)->startOfWeek(Carbon::MONDAY);
        $end = $start->copy()->addWeek();

        $query = ClassSession::query()
            ->where('venue_id', $this->venueId)
            ->whereDate('date', '>=', $start->toDateString())
            ->whereDate('date', '<', $end->toDateString());

        if (! $includeExceptions) {
            $query->where('is_exception', false);
        }

        $sessions = $query->get();

        $copied = 0;
        $skipped = 0;

        foreach ($sessions as $session) {
            $targetDate = Carbon::parse($session->date)->addWeek()->toDateString();

            $exists = ClassSession::query()
                ->where('venue_id', $this->venueId)
                ->where('classroom', $session->classroom)
                ->whereDate('date', $targetDate)
                ->where('start_time', $session->start_time)
                ->whereNull('deleted_at')
                ->exists();

            if ($exists) {
                $skipped++;
                continue;
            }

            ClassSession::create([
                'venue_id' => $this->venueId,
                'date' => $targetDate,
                'start_time' => $session->start_time,
                'end_time' => $session->end_time,
                'classroom' => $session->classroom,
                'course_id' => $session->course_id,
                'class_title' => $session->class_title,
                'coach_id' => $session->coach_id,
                'coach_display_name' => $session->coach_display_name,
                'level' => $session->level,
                'note' => $session->note,
                'is_exception' => $session->is_exception,
                'is_suspended' => $session->is_suspended,
                'is_active' => $session->is_active,
            ]);

            $copied++;
        }

        Notification::make()
            ->title('已複製到下週')
            ->body("複製 {$copied} 筆，略過 {$skipped} 筆（目標週同時段已存在）。")
            ->success()
            ->send();

        $this->dispatch('class-sessions-updated');

        $this->markWeekDirty($start->copy()->addWeek());
    }

    /**
     * @return array{is_published:bool,publish_count:int,dirty_count:int,can_publish:bool,has_sessions:bool}
     */
    public function getPublishState(string $weekStartDate): array
    {
        $weekStart = Carbon::parse($weekStartDate)->startOfWeek(Carbon::MONDAY);
        $weekEnd = $weekStart->copy()->addWeek();

        $publication = $this->getPublicationForWeek($weekStart);

        $hasSessions = ClassSession::query()
            ->where('venue_id', $this->venueId)
            ->whereDate('date', '>=', $weekStart->toDateString())
            ->whereDate('date', '<', $weekEnd->toDateString())
            ->where('is_active', true)
            ->exists();

        $canPublish = $hasSessions && (! $publication->is_published || $publication->dirty_count > 0);

        return [
            'is_published' => (bool) $publication->is_published,
            'publish_count' => (int) $publication->publish_count,
            'dirty_count' => (int) $publication->dirty_count,
            'can_publish' => $canPublish,
            'has_sessions' => $hasSessions,
        ];
    }

    public function publishWeek(string $weekStartDate): void
    {
        $weekStart = Carbon::parse($weekStartDate)->startOfWeek(Carbon::MONDAY);
        $weekEnd = $weekStart->copy()->addWeek();

        $hasSessions = ClassSession::query()
            ->where('venue_id', $this->venueId)
            ->whereDate('date', '>=', $weekStart->toDateString())
            ->whereDate('date', '<', $weekEnd->toDateString())
            ->where('is_active', true)
            ->exists();

        if (! $hasSessions) {
            Notification::make()
                ->title('本週尚無課程，無法發佈')
                ->warning()
                ->send();

            return;
        }

        $publication = $this->getPublicationForWeek($weekStart);
        $publication->is_published = true;
        $publication->published_at = now();
        $publication->publish_count = (int) $publication->publish_count + 1;
        $publication->dirty_count = 0;
        $publication->save();

        Notification::make()
            ->title('已發佈本週課表')
            ->body('publish_count=' . $publication->publish_count)
            ->success()
            ->send();

        $this->dispatch('class-sessions-updated');
    }

    /**
     * @return array<int, \Filament\Forms\Components\Component>
     */
    protected function sessionFormSchema(): array
    {
        // Kept to avoid breaking old references.
        return $this->exceptionFormSchema();
    }

    /**
     * @return array<int, \Filament\Forms\Components\Component>
     */
    protected function exceptionFormSchema(): array
    {
        return [
            Hidden::make('session_id')->default(null),
            Hidden::make('exception_id')->default(null),
            Hidden::make('schedule_id')->default(null),
            Hidden::make('action')->default('add'),
            Hidden::make('is_edit_mode')->default(false)->dehydrated(false),
            Hidden::make('close_scope')->default('all_day'),

            Radio::make('action_choice')
                ->label('動作')
                ->options(fn (Get $get): array => $this->actionChoiceOptions($get))
                ->required()
                ->live(),

            Radio::make('close_scope')
                ->label('休館類型')
                ->options([
                    'all_day' => '全日休館',
                    'time_slot' => '時段休館',
                ])
                ->default('all_day')
                ->visible(fn (Get $get): bool => $this->isCloseMode($get))
                ->live(),

            DatePicker::make('date')
                ->label('日期')
                ->required(),

            TextInput::make('start_time')
                ->label('開始')
                ->type('time')
                ->required(fn (Get $get): bool => $this->requiresTimeFields($get))
                ->visible(fn (Get $get): bool => $this->showsTimeFields($get)),

            TextInput::make('end_time')
                ->label('結束')
                ->type('time')
                ->required(fn (Get $get): bool => $this->requiresTimeFields($get))
                ->visible(fn (Get $get): bool => $this->showsTimeFields($get)),

            Select::make('classroom')
                ->label('上課教室')
                ->options([
                    '有氧團體教室' => '有氧團體教室',
                    '飛輪教室' => '飛輪教室',
                    '全館' => '全館',
                ])
                ->required(fn (Get $get): bool => ! $this->isCancelMode($get))
                ->visible(fn (Get $get): bool => ! $this->isCancelMode($get)),

            Select::make('course_id')
                ->label('課程')
                ->options(fn (): array => ClassCourse::query()->orderBy('sort_order')->pluck('name', 'id')->all())
                ->searchable()
                ->preload()
                ->live()
                ->afterStateUpdated(function ($state, Set $set): void {
                    if (blank($state)) {
                        $set('class_title', null);
                        return;
                    }

                    $name = ClassCourse::query()->whereKey($state)->value('name');
                    $set('class_title', filled($name) ? $name : null);
                })
                ->nullable()
                ->visible(fn (Get $get): bool => $this->showsCourseFields($get)),

            TextInput::make('class_title')
                ->label('課程顯示名稱')
                ->maxLength(150)
                ->required(fn (Get $get): bool => $this->showsCourseFields($get))
                ->visible(fn (Get $get): bool => $this->showsCourseFields($get)),

            Select::make('coach_id')
                ->label('教練')
                ->options(fn (): array => Coach::query()->orderBy('sort_order')->pluck('name', 'id')->all())
                ->searchable()
                ->preload()
                ->live()
                ->afterStateUpdated(function ($state, Set $set, Get $get): void {
                    if (filled($get('coach_display_name'))) {
                        return;
                    }

                    if (blank($state)) {
                        return;
                    }

                    $name = Coach::query()->whereKey($state)->value('name');
                    if (filled($name)) {
                        $set('coach_display_name', $name);
                    }
                })
                ->nullable()
                ->visible(fn (Get $get): bool => $this->showsCourseFields($get)),

            TextInput::make('coach_display_name')
                ->label('教練顯示名稱')
                ->maxLength(100)
                ->nullable()
                ->visible(fn (Get $get): bool => $this->showsCourseFields($get)),

            TextInput::make('level')
                ->label('程度')
                ->maxLength(50)
                ->nullable()
                ->visible(fn (Get $get): bool => $this->showsCourseFields($get)),

            TextInput::make('note')
                ->label(fn (Get $get): string => $this->isCloseMode($get) ? '休館原因' : '備註')
                ->maxLength(255)
                ->nullable()
                ->visible(fn (Get $get): bool => $this->showsCourseFields($get) || $this->isCloseMode($get)),

            Toggle::make('is_active')
                ->label('啟用')
                ->default(true),
        ];
    }

    protected function regenerateWeekForDate(Carbon $date): void
    {
        if (! ($this->weeklyGenerator instanceof WeeklyClassSessionGenerator)) {
            $this->weeklyGenerator = app(WeeklyClassSessionGenerator::class);
        }

        $weekStart = $date->copy()->startOfWeek(Carbon::MONDAY);
        $this->weeklyGenerator->generate($this->venueId, $weekStart, skipExisting: false);
    }

    protected function getPublicationForWeek(Carbon $weekStart): ClassScheduleWeekPublication
    {
        return ClassScheduleWeekPublication::query()->firstOrCreate(
            [
                'venue_id' => $this->venueId,
                'week_start' => $weekStart->toDateString(),
            ],
            [
                'is_published' => false,
                'publish_count' => 0,
                'dirty_count' => 0,
            ],
        );
    }

    protected function markWeekDirty(Carbon $date): void
    {
        $weekStart = $date->copy()->startOfWeek(Carbon::MONDAY);
        $publication = $this->getPublicationForWeek($weekStart);
        $publication->dirty_count = (int) $publication->dirty_count + 1;
        $publication->save();
    }

    protected function findScheduleForSession(ClassSession $session): ?ClassSchedule
    {
        $dayOfWeek = Carbon::parse($session->date)->dayOfWeek; // 0=Sun..6=Sat

        return ClassSchedule::query()
            ->where('venue_id', $this->venueId)
            ->where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where('start_time', $session->start_time)
            ->where('classroom', $session->classroom)
            ->first();
    }

    protected function findExceptionForSession(ClassSession $session): ?ClassScheduleException
    {
        $dateKey = Carbon::parse($session->date)->toDateString();

        $exceptions = ClassScheduleException::query()
            ->where('venue_id', $this->venueId)
            ->where('is_active', true)
            ->whereDate('date', $dateKey)
            ->whereIn('action', ['add', 'override', 'suspend'])
            ->with('schedule')
            ->get();

        $sessionKey = $this->makeKey($dateKey, $session->classroom, $session->start_time);

        foreach ($exceptions as $exception) {
            $effectiveStart = $exception->start_time ?? $exception->schedule?->start_time;
            $effectiveClassroom = $exception->classroom ?? $exception->schedule?->classroom;

            if (blank($effectiveStart) || blank($effectiveClassroom)) {
                continue;
            }

            if ($this->makeKey($dateKey, (string) $effectiveClassroom, (string) $effectiveStart) === $sessionKey) {
                return $exception;
            }
        }

        return null;
    }

    /**
     * @return array{venue_id:int, classroom:string, date:string, start_time:string}|null
     */
    protected function effectiveExceptionKey(ClassScheduleException $exception): ?array
    {
        $exception->loadMissing('schedule');

        $dateKey = Carbon::parse($exception->date)->toDateString();
        $start = $exception->start_time ?? $exception->schedule?->start_time;
        $classroom = $exception->classroom ?? $exception->schedule?->classroom;

        if (blank($start) || blank($classroom)) {
            return null;
        }

        return [
            'venue_id' => $this->venueId,
            'classroom' => (string) $classroom,
            'date' => $dateKey,
            'start_time' => (string) $start,
        ];
    }

    /**
     * @param  array{venue_id:int, classroom:string, date:string, start_time:string}  $key
     */
    protected function deleteSessionByKey(array $key): void
    {
        ClassSession::query()
            ->where('venue_id', $key['venue_id'])
            ->where('classroom', $key['classroom'])
            ->whereDate('date', $key['date'])
            ->where('start_time', $key['start_time'])
            ->delete();
    }

    protected function makeKey(string $date, string $classroom, string $startTime): string
    {
        return $date . '|' . $classroom . '|' . $startTime;
    }

    protected function normalizeTime(?string $time): ?string
    {
        if (blank($time)) {
            return null;
        }

        return substr($time, 0, 5);
    }

    protected function isCancelMode(Get $get): bool
    {
        return $this->getActionChoice($get) === 'cancel';
    }

    protected function isCloseMode(Get $get): bool
    {
        return $this->getActionChoice($get) === 'close';
    }

    protected function showsTimeFields(Get $get): bool
    {
        if ($this->isCancelMode($get)) {
            return false;
        }

        if ($this->isCloseMode($get)) {
            return ($get('close_scope') ?? 'all_day') === 'time_slot';
        }

        return true;
    }

    protected function requiresTimeFields(Get $get): bool
    {
        if ($this->isCancelMode($get)) {
            return false;
        }

        if ($this->isCloseMode($get)) {
            return ($get('close_scope') ?? 'all_day') === 'time_slot';
        }

        return true;
    }

    /**
     * @return array<string, string>
     */
    protected function actionChoiceOptions(Get $get): array
    {
        if ((bool) $get('is_edit_mode')) {
            return [
                'override' => '異動',
                'suspend' => '停課',
                'cancel' => '取消',
            ];
        }

        return [
            'add' => '新增',
            'close' => '休館',
        ];
    }

    protected function showsCourseFields(Get $get): bool
    {
        $choice = $this->getActionChoice($get);

        return in_array($choice, ['add', 'override', 'suspend'], true);
    }

    protected function getActionChoice(Get $get): string
    {
        return (string) ($get('action_choice') ?? $get('action') ?? 'add');
    }
}
