<?php

namespace App\Http\Middleware;

use App\Models\Gallery;
use App\Settings\GeneralSettings;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Symfony\Component\HttpFoundation\Response;

class EnsureGalleryAccess
{
    public const SESSION_GRANTED_BASE = 'gallery_access_granted';

    public const SESSION_GRANTED_AT_BASE = 'gallery_access_granted_at';

    public const COOKIE_GRANTED_AT_BASE = 'gallery_access_granted_at';

    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $settings = app(GeneralSettings::class);
        $gallery = $this->resolveGallery($request);

        if ($this->requestedGalleryMissing($request, $gallery)) {
            return $this->missingGalleryResponse($request);
        }

        if ($this->isGalleryExpired($gallery, $settings)) {
            return $this->deny($request, __('api.gallery.expired'), $gallery);
        }

        $hasValidAccess = $this->hasValidAccess($request, $gallery, $settings);
        $hasExistingGrant = (bool) ($this->sessionGrantedAt($request, $gallery) ?? $this->cookieGrantedAt($request, $gallery));

        if ($hasValidAccess) {
            return $next($request);
        }

        if ($this->accessDuration($gallery, $settings) && $hasExistingGrant) {
            return $this->deny($request, __('api.gallery.expired'), $gallery);
        }

        if (! $this->requiresPassword($gallery, $settings) || ! $this->passwordHash($gallery, $settings)) {
            if ($this->accessDuration($gallery, $settings) && ! $hasExistingGrant) {
                self::grantForGallery($request, $gallery, $settings);
            }

            return $next($request);
        }

        return $this->deny($request, __('api.gallery.password_required'), $gallery);
    }

    public static function grantForGallery(Request $request, ?Gallery $gallery, GeneralSettings $settings, ?Carbon $grantedAt = null): void
    {
        $grantedAt = $grantedAt ?: Carbon::now();
        $suffix = self::keySuffix($gallery);

        if ($request->hasSession()) {
            $request->session()->put(self::SESSION_GRANTED_BASE.'_'.$suffix, true);
            $request->session()->put(self::SESSION_GRANTED_AT_BASE.'_'.$suffix, $grantedAt->toIso8601String());
        }

        $cookieMinutes = self::calculateCookieLifetime($gallery, $settings, $grantedAt);

        cookie()->queue(
            cookie(
                self::COOKIE_GRANTED_AT_BASE.'_'.$suffix,
                $grantedAt->toIso8601String(),
                $cookieMinutes,
                path: '/',
                secure: config('session.secure', false),
                httpOnly: true,
                raw: false,
                sameSite: config('session.same_site', 'lax')
            )
        );
    }

    private function hasValidAccess(Request $request, ?Gallery $gallery, GeneralSettings $settings): bool
    {
        if ($this->sessionAccessValid($request, $gallery, $settings)) {
            return true;
        }

        $grantedAt = $this->cookieGrantedAt($request, $gallery);

        if (! $grantedAt) {
            return false;
        }

        $duration = $this->accessDuration($gallery, $settings);

        if ($duration) {
            return ! $grantedAt->copy()->addMinutes($duration)->isPast();
        }

        return (bool) $grantedAt;
    }

    private function sessionAccessValid(Request $request, ?Gallery $gallery, GeneralSettings $settings): bool
    {
        if (! $request->hasSession()) {
            return false;
        }

        $suffix = self::keySuffix($gallery);

        if (! $request->session()->get(self::SESSION_GRANTED_BASE.'_'.$suffix, false)) {
            return false;
        }

        $grantedAt = $this->sessionGrantedAt($request, $gallery);

        if (! $grantedAt) {
            return true;
        }

        $duration = $this->accessDuration($gallery, $settings);

        if ($duration) {
            $expiresAt = $grantedAt->copy()->addMinutes($duration);

            if ($expiresAt->isPast()) {
                $request->session()->forget([
                    self::SESSION_GRANTED_BASE.'_'.$suffix,
                    self::SESSION_GRANTED_AT_BASE.'_'.$suffix,
                ]);

                return false;
            }
        }

        return true;
    }

    private function sessionGrantedAt(Request $request, ?Gallery $gallery): ?Carbon
    {
        if (! $request->hasSession()) {
            return null;
        }

        $value = $request->session()->get(self::SESSION_GRANTED_AT_BASE.'_'.self::keySuffix($gallery));

        if (! $value) {
            return null;
        }

        try {
            return Carbon::parse($value);
        } catch (\Exception) {
            return null;
        }
    }

    private function cookieGrantedAt(Request $request, ?Gallery $gallery): ?Carbon
    {
        $value = $request->cookie(self::COOKIE_GRANTED_AT_BASE.'_'.self::keySuffix($gallery));

        if (! $value) {
            return null;
        }

        try {
            $value = Crypt::decryptString($value);
        } catch (\Exception) {
            // Cookie might already be decrypted by web middleware.
        }

        try {
            return Carbon::parse($value);
        } catch (\Exception) {
            return null;
        }
    }

    private function isGalleryExpired(?Gallery $gallery, GeneralSettings $settings): bool
    {
        $expiresAt = $this->expiresAt($gallery, $settings);

        return $expiresAt !== null && Carbon::now()->greaterThanOrEqualTo($expiresAt);
    }

    private function deny(Request $request, string $message, ?Gallery $gallery = null): Response
    {
        if ($request->expectsJson()) {
            return response()->json(['message' => $message], Response::HTTP_FORBIDDEN);
        }

        $routeName = $gallery ? 'gallery.access.show' : 'gallery.access.default';

        return redirect()
            ->route($routeName, $gallery)
            ->with('gallery_access_message', $message);
    }

    private static function calculateCookieLifetime(?Gallery $gallery, GeneralSettings $settings, Carbon $grantedAt): int
    {
        $expiresAt = $gallery?->expires_at ?? $settings->gallery_expires_at;

        if ($expiresAt) {
            $expiresAt = Carbon::parse($expiresAt);

            return max(1, $grantedAt->diffInMinutes($expiresAt));
        }

        $duration = $gallery?->access_duration_minutes ?? $settings->gallery_access_duration_minutes;

        if ($duration) {
            return max(1, $duration);
        }

        return 24 * 60;
    }

    private function resolveGallery(Request $request): ?Gallery
    {
        $routeGallery = $request->route('gallery');

        if ($routeGallery instanceof Gallery) {
            return $routeGallery;
        }

        $slug = is_string($routeGallery) ? $routeGallery : $request->query('gallery');

        if (! $slug) {
            return Gallery::first();
        }

        return Gallery::where('slug', $slug)->first();
    }

    private function requestedGalleryMissing(Request $request, ?Gallery $gallery): bool
    {
        if ($gallery) {
            return false;
        }

        return (bool) ($request->route('gallery') || $request->query('gallery'));
    }

    private function missingGalleryResponse(Request $request): Response
    {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Gallery not found.'], Response::HTTP_NOT_FOUND);
        }

        abort(404, 'Gallery not found');
    }

    private static function keySuffix(?Gallery $gallery): string
    {
        return $gallery ? 'gallery_'.$gallery->id : 'global';
    }

    private function expiresAt(?Gallery $gallery, GeneralSettings $settings): ?Carbon
    {
        $value = $gallery?->expires_at ?? $settings->gallery_expires_at;

        if (! $value) {
            return null;
        }

        return Carbon::parse($value);
    }

    private function accessDuration(?Gallery $gallery, GeneralSettings $settings): ?int
    {
        return $gallery?->access_duration_minutes ?? $settings->gallery_access_duration_minutes;
    }

    private function requiresPassword(?Gallery $gallery, GeneralSettings $settings): bool
    {
        return (bool) ($gallery?->require_password ?? $settings->require_gallery_password);
    }

    private function passwordHash(?Gallery $gallery, GeneralSettings $settings): ?string
    {
        return $gallery?->password_hash ?? $settings->gallery_password_hash;
    }
}
