<?php

declare(strict_types=1);

namespace App\Support\Schedule;

use App\Models\ClassSchedule;
use App\Models\ClassScheduleException;
use App\Models\ClassSession;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;

class WeeklyClassSessionGenerator
{
    /**
     * Generate sessions for a venue & a given week (Monday start).
     *
     * - Uses `class_schedules` as the weekly template.
     * - Applies `class_schedule_exceptions` (cancel/override/add/close).
     * - Marks `class_sessions.is_exception` automatically for override/add.
     *
    * @return array{created:int, updated:int, deleted:int, skipped_closed:int, skipped_canceled:int}
     */
    public function generate(int $venueId, Carbon $weekStart, bool $skipExisting = false): array
    {
        $weekStart = $weekStart->copy()->startOfWeek(Carbon::MONDAY);
        $weekEndExclusive = $weekStart->copy()->addWeek();

        $schedules = ClassSchedule::query()
            ->where('venue_id', $venueId)
            ->where('is_active', true)
            ->with([
                'course:id,name,level',
                'coach:id,name',
            ])
            ->orderBy('day_of_week')
            ->orderBy('start_time')
            ->orderBy('sort_order')
            ->get();

        $exceptions = ClassScheduleException::query()
            ->where('venue_id', $venueId)
            ->where('is_active', true)
            ->whereDate('date', '>=', $weekStart->toDateString())
            ->whereDate('date', '<', $weekEndExclusive->toDateString())
            ->with([
                'course:id,name,level',
                'coach:id,name',
                'schedule:id,course_id,day_of_week,start_time,end_time,classroom,class_title,coach_id,coach_display_name,location,level,note,is_active',
            ])
            ->orderBy('date')
            ->orderBy('sort_order')
            ->get();

        $exceptionsByDate = $exceptions->groupBy(fn (ClassScheduleException $e) => $e->date?->toDateString());

        /** @var array<string, array<int, array{start:?string, end:?string}>> $closeWindowsByDate */
        $closeWindowsByDate = [];
        foreach ($exceptions as $exception) {
            if ($exception->action !== 'close') {
                continue;
            }

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

            $closeWindowsByDate[$dateKey] ??= [];
            $closeWindowsByDate[$dateKey][] = [
                'start' => $exception->start_time,
                'end' => $exception->end_time,
            ];
        }

        $created = 0;
        $updated = 0;
        $deleted = 0;
        $skippedClosed = 0;
        $skippedCanceled = 0;

        foreach ($schedules as $schedule) {
            $date = $this->dateForDayOfWeek($weekStart, (int) $schedule->day_of_week);
            $dateKey = $date->toDateString();

            $classroom = $schedule->classroom;
            if (blank($classroom)) {
                $classroom = $this->mapLocationToClassroom($schedule->location);
            }

            if (blank($classroom)) {
                continue;
            }

            $exception = $this->findScheduleException($exceptionsByDate->get($dateKey, collect()), (int) $schedule->id);

            $originalKey = [
                'venue_id' => $venueId,
                'classroom' => $classroom,
                'date' => $dateKey,
                'start_time' => (string) $schedule->start_time,
            ];

            if ($exception instanceof ClassScheduleException && $exception->action === 'cancel') {
                $deleted += $this->deleteSessionByKey($originalKey);
                $skippedCanceled++;
                continue;
            }

            $courseName = $schedule->course?->name;
            $courseLevel = $schedule->course?->level;
            $coachName = $schedule->coach?->name;

            $payload = [
                'venue_id' => $venueId,
                'date' => $dateKey,
                'start_time' => $schedule->start_time,
                'end_time' => $schedule->end_time,
                'classroom' => $classroom,
                'course_id' => $schedule->course_id,
                'class_title' => filled($schedule->class_title) ? $schedule->class_title : (string) ($courseName ?? ''),
                'coach_id' => $schedule->coach_id,
                'coach_display_name' => filled($schedule->coach_display_name) ? $schedule->coach_display_name : (string) ($coachName ?? null),
                'level' => filled($schedule->level) ? $schedule->level : ($courseLevel ?? null),
                'note' => $schedule->note,
                'is_exception' => false,
                'is_suspended' => false,
                'is_active' => $schedule->is_active,
            ];

            if ($exception instanceof ClassScheduleException && in_array($exception->action, ['override', 'suspend'], true)) {
                $payload['start_time'] = $exception->start_time ?? $payload['start_time'];
                $payload['end_time'] = $exception->end_time ?? $payload['end_time'];

                $overrideClassroom = $exception->classroom;
                if (blank($overrideClassroom)) {
                    $overrideClassroom = $this->mapLocationToClassroom($exception->location);
                }
                $payload['classroom'] = filled($overrideClassroom) ? $overrideClassroom : $payload['classroom'];

                $payload['course_id'] = $exception->course_id ?? $payload['course_id'];
                $payload['class_title'] = $exception->class_title ?? $payload['class_title'];
                $payload['coach_id'] = $exception->coach_id ?? $payload['coach_id'];
                $payload['coach_display_name'] = $exception->coach_display_name ?? $payload['coach_display_name'];
                $payload['level'] = $exception->level ?? $payload['level'];
                $payload['note'] = $exception->note ?? $payload['note'];
                $payload['is_exception'] = true;
                $payload['is_suspended'] = $exception->action === 'suspend';

                // If override moves the session (time/classroom), delete the original slot.
                if (
                    $originalKey['classroom'] !== (string) $payload['classroom']
                    || $originalKey['start_time'] !== (string) $payload['start_time']
                ) {
                    $deleted += $this->deleteSessionByKey($originalKey);
                }

                // If override points to a course but leaves title/level blank, fill from course.
                if (empty($exception->class_title) && $exception->course?->name) {
                    $payload['class_title'] = $exception->course->name;
                }
                if (empty($exception->level) && $exception->course?->level) {
                    $payload['level'] = $exception->course->level;
                }
                if (empty($exception->coach_display_name) && $exception->coach?->name) {
                    $payload['coach_display_name'] = $exception->coach->name;
                }
            }

            if ($this->isWithinCloseWindow($dateKey, (string) $payload['start_time'], (string) $payload['end_time'], $closeWindowsByDate)) {
                $skippedClosed++;
                continue;
            }

            $result = $this->upsertSession($payload, $skipExisting);
            $created += $result['created'];
            $updated += $result['updated'];
        }

        // Add sessions (action=add)
        foreach ($exceptions as $exception) {
            if ($exception->action !== 'add') {
                continue;
            }

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

            $start = $exception->start_time;
            $end = $exception->end_time;
            if (blank($start) || blank($end)) {
                continue;
            }

            $classroom = $exception->classroom;
            if (blank($classroom)) {
                $classroom = $this->mapLocationToClassroom($exception->location);
            }
            if (blank($classroom)) {
                continue;
            }

            if ($this->isWithinCloseWindow($dateKey, (string) $start, (string) $end, $closeWindowsByDate)) {
                $skippedClosed++;
                continue;
            }

            if ($skipExisting && $this->sessionExists($venueId, (string) $classroom, $dateKey, (string) $start)) {
                continue;
            }

            $classTitle = $exception->class_title;
            if (blank($classTitle) && $exception->course?->name) {
                $classTitle = $exception->course->name;
            }

            $level = $exception->level;
            if (blank($level) && $exception->course?->level) {
                $level = $exception->course->level;
            }

            $coachDisplay = $exception->coach_display_name;
            if (blank($coachDisplay) && $exception->coach?->name) {
                $coachDisplay = $exception->coach->name;
            }

            $result = $this->upsertSession([
                'venue_id' => $venueId,
                'date' => $dateKey,
                'start_time' => $start,
                'end_time' => $end,
                'classroom' => $classroom,
                'course_id' => $exception->course_id,
                'class_title' => (string) ($classTitle ?? ''),
                'coach_id' => $exception->coach_id,
                'coach_display_name' => $coachDisplay,
                'level' => $level,
                'note' => $exception->note,
                'is_exception' => true,
                'is_suspended' => false,
                'is_active' => true,
            ], $skipExisting);

            $created += $result['created'];
            $updated += $result['updated'];
        }

        // Close = remove generated sessions (authoritative)
        foreach ($closeWindowsByDate as $dateKey => $windows) {
            foreach ($windows as $window) {
                $start = $window['start'];
                $end = $window['end'];

                $query = ClassSession::query()
                    ->where('venue_id', $venueId)
                    ->whereDate('date', $dateKey);

                if (blank($start) || blank($end)) {
                    // Full day close.
                    $count = (int) $query->count();
                    $skippedClosed += $count;
                    $deleted += $count;
                    $query->delete();
                    continue;
                }

                $query
                    ->where(function ($q) use ($start, $end) {
                        $q
                            ->where('start_time', '<', $end)
                            ->where('end_time', '>', $start);
                    });

                $count = (int) $query->count();
                $skippedClosed += $count;
                $deleted += $count;
                $query->delete();
            }
        }

        return [
            'created' => $created,
            'updated' => $updated,
            'deleted' => $deleted,
            'skipped_closed' => $skippedClosed,
            'skipped_canceled' => $skippedCanceled,
        ];
    }

    protected function dateForDayOfWeek(Carbon $weekStart, int $dayOfWeek): Carbon
    {
        // day_of_week: 0=Sun, 1=Mon ... 6=Sat
        // weekStart is Monday.
        $offset = ($dayOfWeek + 6) % 7;

        return $weekStart->copy()->addDays($offset);
    }

    protected function mapLocationToClassroom(?string $location): ?string
    {
        $value = (string) ($location ?? '');

        if ($value === '') {
            return null;
        }

        if (str_contains($value, '飛輪')) {
            return '飛輪教室';
        }

        if (str_contains($value, '有氧') || str_contains($value, '團體')) {
            return '有氧團體教室';
        }

        return null;
    }

    /**
     * @param  Collection<int, ClassScheduleException>  $exceptions
     */
    protected function findScheduleException(Collection $exceptions, int $scheduleId): ?ClassScheduleException
    {
        foreach ($exceptions as $exception) {
            if ((int) $exception->schedule_id !== $scheduleId) {
                continue;
            }

            return $exception;
        }

        return null;
    }

    /**
     * @param  array<string, array<int, array{start:?string, end:?string}>>  $closeWindowsByDate
     */
    protected function isWithinCloseWindow(string $dateKey, string $start, string $end, array $closeWindowsByDate): bool
    {
        $windows = Arr::get($closeWindowsByDate, $dateKey, []);
        if ($windows === []) {
            return false;
        }

        foreach ($windows as $window) {
            $ws = $window['start'];
            $we = $window['end'];

            if (blank($ws) || blank($we)) {
                return true; // full-day close
            }

            // overlap check: [start,end) overlaps [ws,we)
            if ($start < $we && $end > $ws) {
                return true;
            }
        }

        return false;
    }

    protected function sessionExists(int $venueId, string $classroom, string $dateKey, string $startTime): bool
    {
        return ClassSession::query()
            ->where('venue_id', $venueId)
            ->where('classroom', $classroom)
            ->whereDate('date', $dateKey)
            ->where('start_time', $startTime)
            ->whereNull('deleted_at')
            ->exists();
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array{created:int, updated:int}
     */
    protected function upsertSession(array $payload, bool $skipExisting): array
    {
        $key = [
            'venue_id' => (int) $payload['venue_id'],
            'classroom' => (string) $payload['classroom'],
            'date' => (string) $payload['date'],
            'start_time' => (string) $payload['start_time'],
        ];

        $existing = ClassSession::withTrashed()
            ->where('venue_id', $key['venue_id'])
            ->where('classroom', $key['classroom'])
            ->whereDate('date', $key['date'])
            ->where('start_time', $key['start_time'])
            ->first();

        if ($existing && $skipExisting) {
            return ['created' => 0, 'updated' => 0];
        }

        if (! $existing) {
            ClassSession::create($payload);
            return ['created' => 1, 'updated' => 0];
        }

        if ($existing->trashed()) {
            $existing->restore();
        }

        $existing->update($payload);
        return ['created' => 0, 'updated' => 1];
    }

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

        $count = (int) $query->count();
        if ($count > 0) {
            $query->delete();
        }

        return $count;
    }
}
