<?php

namespace App\Http\Controllers\Api;

use App\Enums\PhotoLiveStatus;
use App\Models\Event;
use App\Models\Photo;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\StreamedEvent;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Arr;
use Symfony\Component\HttpFoundation\Response;

class LiveShowController extends BaseController
{
    private const DEFAULT_RETENTION_HOURS = 12;

    private const DEFAULT_LIMIT = 50;

    private const MAX_LIMIT = 200;

    private const STREAM_TICK_SECONDS = 2;

    private const STREAM_MAX_SECONDS = 60;

    private const STREAM_PING_SECONDS = 10;

    public function state(Request $request, string $token): JsonResponse
    {
        $event = $this->resolveEvent($token);

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

        $settings = $this->liveShowSettings($event);
        $settingsVersion = $this->settingsVersion($settings);
        $limit = $this->resolveLimit($request);

        $photos = $this->baseLiveShowQuery($event, $settings)
            ->orderByDesc('live_approved_at')
            ->orderByDesc('id')
            ->limit($limit)
            ->get()
            ->reverse()
            ->values();

        $cursor = $this->buildCursor($photos->last());

        return response()->json([
            'event' => [
                'id' => $event->id,
                'slug' => $event->slug,
                'name' => $event->name,
                'default_locale' => $event->default_locale,
            ],
            'settings' => $settings,
            'settings_version' => $settingsVersion,
            'photos' => $photos->map(fn (Photo $photo) => $this->serializePhoto($photo)),
            'cursor' => $cursor,
        ]);
    }

    public function updates(Request $request, string $token): JsonResponse
    {
        $event = $this->resolveEvent($token);

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

        $cursor = $this->parseCursor($request);

        if ($cursor === null) {
            return response()->json([
                'error' => 'invalid_cursor',
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $settings = $this->liveShowSettings($event);
        $settingsVersion = $this->settingsVersion($settings);
        $limit = $this->resolveLimit($request);

        $query = $this->baseLiveShowQuery($event, $settings);
        $this->applyCursor($query, $cursor);

        $photos = $query
            ->orderBy('live_approved_at')
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $nextCursor = $this->buildCursor($photos->last());
        $requestedSettingsVersion = (string) $request->query('settings_version', '');
        $includeSettings = $requestedSettingsVersion === '' || $requestedSettingsVersion !== $settingsVersion;

        return response()->json([
            'settings' => $includeSettings ? $settings : null,
            'settings_version' => $settingsVersion,
            'photos' => $photos->map(fn (Photo $photo) => $this->serializePhoto($photo)),
            'cursor' => $nextCursor ?? $cursor,
        ]);
    }

    public function stream(Request $request, string $token): Response
    {
        $event = $this->resolveEvent($token);

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

        $cursor = $this->parseCursor($request);

        if ($cursor === null) {
            return response()->json([
                'error' => 'invalid_cursor',
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $settings = $this->liveShowSettings($event);
        $settingsVersion = $this->settingsVersion($settings);
        $requestedSettingsVersion = (string) $request->query('settings_version', '');
        $lastSettingsCheck = CarbonImmutable::now();
        $lastPingAt = CarbonImmutable::now();
        $startedAt = CarbonImmutable::now();

        return response()->eventStream(function () use ($event, $cursor, $settings, $settingsVersion, $requestedSettingsVersion, $startedAt, $lastPingAt, $lastSettingsCheck) {
            $lastApprovedAt = $cursor['approved_at'];
            $lastId = $cursor['id'];
            $currentSettingsVersion = $requestedSettingsVersion !== '' ? $requestedSettingsVersion : $settingsVersion;
            $currentSettings = $settings;

            while (CarbonImmutable::now()->diffInSeconds($startedAt) < self::STREAM_MAX_SECONDS) {
                if (connection_aborted()) {
                    break;
                }

                $now = CarbonImmutable::now();

                if ($now->diffInSeconds($lastSettingsCheck) >= self::STREAM_PING_SECONDS) {
                    $event->refresh();
                    $currentSettings = $this->liveShowSettings($event);
                    $newSettingsVersion = $this->settingsVersion($currentSettings);

                    if ($newSettingsVersion !== $currentSettingsVersion) {
                        $currentSettingsVersion = $newSettingsVersion;
                        yield new StreamedEvent(
                            event: 'settings.updated',
                            data: [
                                'settings' => $currentSettings,
                                'settings_version' => $currentSettingsVersion,
                            ]
                        );
                    }

                    $lastSettingsCheck = $now;
                }

                $query = $this->baseLiveShowQuery($event, $currentSettings);
                $this->applyCursor($query, [
                    'approved_at' => $lastApprovedAt,
                    'id' => $lastId,
                ]);

                $updates = $query
                    ->orderBy('live_approved_at')
                    ->orderBy('id')
                    ->limit(self::MAX_LIMIT)
                    ->get();

                foreach ($updates as $photo) {
                    $payload = $this->serializePhoto($photo);
                    $lastApprovedAt = $photo->live_approved_at;
                    $lastId = $photo->id;

                    yield new StreamedEvent(
                        event: 'photo.approved',
                        data: [
                            'photo' => $payload,
                            'cursor' => $this->buildCursor($photo),
                        ]
                    );
                }

                if ($now->diffInSeconds($lastPingAt) >= self::STREAM_PING_SECONDS) {
                    $lastPingAt = $now;
                    yield new StreamedEvent(
                        event: 'ping',
                        data: [
                            'time' => $now->toAtomString(),
                        ]
                    );
                }

                sleep(self::STREAM_TICK_SECONDS);
            }
        }, Response::HTTP_OK, [
            'Cache-Control' => 'no-cache',
            'X-Accel-Buffering' => 'no',
        ]);
    }

    private function resolveEvent(string $token): ?Event
    {
        if ($token === '') {
            return null;
        }

        return Event::query()
            ->where('live_show_token', $token)
            ->first();
    }

    private function liveShowSettings(Event $event): array
    {
        $settings = is_array($event->settings) ? $event->settings : [];
        $liveShow = Arr::get($settings, 'live_show', []);

        return array_merge([
            'retention_window_hours' => self::DEFAULT_RETENTION_HOURS,
            'moderation_mode' => 'manual',
            'playback_mode' => 'newest_first',
            'pace_mode' => 'auto',
            'fixed_interval_seconds' => 8,
            'layout_mode' => 'single',
            'effect_preset' => 'film_cut',
            'effect_intensity' => 70,
            'background_mode' => 'blur_last',
        ], is_array($liveShow) ? $liveShow : []);
    }

    private function settingsVersion(array $settings): string
    {
        return sha1(json_encode($settings));
    }

    private function resolveLimit(Request $request): int
    {
        $limit = (int) $request->query('limit', self::DEFAULT_LIMIT);

        if ($limit <= 0) {
            return self::DEFAULT_LIMIT;
        }

        return min($limit, self::MAX_LIMIT);
    }

    private function baseLiveShowQuery(Event $event, array $settings): Builder
    {
        $query = Photo::query()
            ->where('event_id', $event->id)
            ->where('live_status', PhotoLiveStatus::APPROVED->value)
            ->where('status', 'approved')
            ->whereNotNull('live_approved_at');

        $retention = (int) ($settings['retention_window_hours'] ?? self::DEFAULT_RETENTION_HOURS);
        if ($retention > 0) {
            $query->where('live_approved_at', '>=', now()->subHours($retention));
        }

        return $query;
    }

    private function applyCursor(Builder $query, array $cursor): void
    {
        $afterAt = $cursor['approved_at'];
        $afterId = $cursor['id'];

        if (! $afterAt) {
            return;
        }

        if ($afterId <= 0) {
            $query->where('live_approved_at', '>', $afterAt);

            return;
        }

        $query->where(function (Builder $inner) use ($afterAt, $afterId) {
            $inner->where('live_approved_at', '>', $afterAt)
                ->orWhere(function (Builder $tie) use ($afterAt, $afterId) {
                    $tie->where('live_approved_at', $afterAt)
                        ->where('id', '>', $afterId);
                });
        });
    }

    private function serializePhoto(Photo $photo): array
    {
        return [
            'id' => $photo->id,
            'full_url' => $photo->file_path,
            'thumb_url' => $photo->thumbnail_path,
            'approved_at' => $photo->live_approved_at?->toAtomString(),
            'width' => $photo->width,
            'height' => $photo->height,
            'is_featured' => (bool) $photo->is_featured,
            'live_priority' => (int) ($photo->live_priority ?? 0),
        ];
    }

    private function buildCursor(?Photo $photo): ?array
    {
        if (! $photo || ! $photo->live_approved_at) {
            return null;
        }

        return [
            'approved_at' => $photo->live_approved_at,
            'id' => $photo->id,
        ];
    }

    private function parseCursor(Request $request): ?array
    {
        $afterAt = trim((string) $request->query('after_approved_at', ''));
        $afterId = (int) $request->query('after_id', 0);

        if ($afterAt === '' && $afterId === 0) {
            return [
                'approved_at' => null,
                'id' => 0,
            ];
        }

        try {
            $approvedAt = CarbonImmutable::parse($afterAt);
        } catch (\Throwable) {
            return null;
        }

        return [
            'approved_at' => $approvedAt,
            'id' => max(0, $afterId),
        ];
    }

    private function notFound(): JsonResponse
    {
        return response()->json([
            'error' => 'live_show_not_found',
        ], Response::HTTP_NOT_FOUND);
    }
}
