<?php

declare(strict_types=1);

namespace App\Http\Controllers\Settings;

use App\Http\Controllers\Controller;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;
use RuntimeException;

final class AcademyLessonMediaApiController extends Controller
{
    private const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];

    private const MAX_FILE_SIZE_KB = 6144;

    private const ASSET_CACHE_TTL_MINUTES = 15;

    private const RESPONSIVE_VARIANT_WIDTHS = [
        'thumb' => 480,
        'md' => 960,
    ];

    private ?ImageManager $manager = null;

    public function __construct()
    {
        try {
            $this->manager = extension_loaded('gd')
                ? new ImageManager(new GdDriver())
                : new ImageManager(new ImagickDriver());
        } catch (\Throwable) {
            $this->manager = null;
        }
    }

    public function store(Request $request): JsonResponse
    {
        $this->authorizeStaff($request);

        $validated = $request->validate([
            'slot' => ['nullable', 'string', 'in:cover,body'],
            'image' => [
                'required',
                'file',
                'image',
                'max:' . self::MAX_FILE_SIZE_KB,
                'mimes:jpg,jpeg,png,webp',
                'mimetypes:image/jpeg,image/png,image/webp',
            ],
        ]);

        /** @var UploadedFile $file */
        $file = $validated['image'];
        $slot = $this->normalizeSlot($validated['slot'] ?? null);

        try {
            $stored = $this->storeMediaFile($file, $slot);
            $this->forgetAssetCache();

            return response()->json([
                'success' => true,
                'slot' => $slot,
                'path' => $stored['path'],
                'url' => $this->publicUrlForPath($stored['path']),
                'thumb_path' => $stored['thumb_path'],
                'thumb_url' => $this->publicUrlForPath($stored['thumb_path']),
                'thumb_width' => $stored['thumb_width'],
                'thumb_height' => $stored['thumb_height'],
                'medium_path' => $stored['medium_path'],
                'medium_url' => $stored['medium_path'] !== '' ? $this->publicUrlForPath($stored['medium_path']) : null,
                'medium_width' => $stored['medium_width'],
                'medium_height' => $stored['medium_height'],
                'srcset' => $this->buildResponsiveSrcset([
                    ['path' => $stored['thumb_path'], 'width' => $stored['thumb_width']],
                    ['path' => $stored['medium_path'], 'width' => $stored['medium_width']],
                ]),
                'width' => $stored['width'],
                'height' => $stored['height'],
                'mime_type' => 'image/webp',
                'size_bytes' => $stored['size_bytes'],
            ]);
        } catch (RuntimeException $e) {
            return response()->json([
                'error' => 'Validation failed',
                'message' => $e->getMessage(),
            ], 422);
        } catch (\Throwable $e) {
            logger()->error('Academy lesson media upload failed', [
                'user_id' => (int) ($request->user()?->id ?? 0),
                'message' => $e->getMessage(),
            ]);

            return response()->json([
                'error' => 'Upload failed',
                'message' => 'Could not upload lesson media right now.',
            ], 500);
        }
    }

    public function destroy(Request $request): JsonResponse
    {
        $this->authorizeStaff($request);

        $validated = $request->validate([
            'path' => ['required', 'string', 'max:2048'],
        ]);

        $this->deleteMediaFile((string) $validated['path']);
        $this->forgetAssetCache();

        return response()->json([
            'success' => true,
        ]);
    }

    public function assets(Request $request): JsonResponse
    {
        $this->authorizeStaff($request);

        $validated = $request->validate([
            'limit' => ['nullable', 'integer', 'min:1', 'max:48'],
            'page' => ['nullable', 'integer', 'min:1'],
            'q' => ['nullable', 'string', 'max:100'],
        ]);

        $limit = (int) ($validated['limit'] ?? 24);
        $page = (int) ($validated['page'] ?? 1);
        $query = Str::lower(trim((string) ($validated['q'] ?? '')));

        $manifest = $this->academyAssetManifest();

        if ($query !== '') {
            $manifest = $manifest->filter(function (array $item) use ($query): bool {
                return Str::contains($item['search_text'], $query);
            })->values();
        }

        $total = $manifest->count();
        $lastPage = max(1, (int) ceil(max($total, 1) / max($limit, 1)));
        $page = min(max($page, 1), $lastPage);

        $items = $manifest
            ->forPage($page, $limit)
            ->values()
            ->map(function (array $item): array {
                return [
                    'path' => $item['path'],
                    'url' => $item['url'],
                    'name' => $item['name'],
                    'slot' => $item['slot'],
                    'modified_at' => $item['modified_at'] ? now()->setTimestamp($item['modified_at'])->toIso8601String() : null,
                ];
            })
            ->all();

        return response()->json([
            'success' => true,
            'items' => $items,
            'pagination' => [
                'page' => $page,
                'per_page' => $limit,
                'total' => $total,
                'last_page' => $lastPage,
                'has_more' => $page < $lastPage,
            ],
        ]);
    }

    /**
     * @return array{path:string,thumb_path:string,thumb_width:int,thumb_height:int,medium_path:string,medium_width:int|null,medium_height:int|null,width:int,height:int,size_bytes:int}
     */
    private function storeMediaFile(UploadedFile $file, string $slot): array
    {
        $this->assertImageManager();
        $this->assertStorageIsAllowed();
        $constraints = $this->mediaConstraints($slot);

        $uploadPath = (string) ($file->getRealPath() ?: $file->getPathname());

        if ($uploadPath === '' || ! is_readable($uploadPath)) {
            throw new RuntimeException('Unable to resolve uploaded image path.');
        }

        $raw = file_get_contents($uploadPath);
        if ($raw === false || $raw === '') {
            throw new RuntimeException('Unable to read uploaded image.');
        }

        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $mime = strtolower((string) $finfo->buffer($raw));
        if (! in_array($mime, self::ALLOWED_MIME_TYPES, true)) {
            throw new RuntimeException('Unsupported image mime type.');
        }

        $size = @getimagesizefromstring($raw);
        if (! is_array($size) || ($size[0] ?? 0) < 1 || ($size[1] ?? 0) < 1) {
            throw new RuntimeException('Uploaded file is not a valid image.');
        }

        $width = (int) ($size[0] ?? 0);
        $height = (int) ($size[1] ?? 0);

        if ($width < $constraints['min_width'] || $height < $constraints['min_height']) {
            throw new RuntimeException(sprintf(
                'Image is too small. Minimum required size is %dx%d.',
                $constraints['min_width'],
                $constraints['min_height'],
            ));
        }

        $encodedImage = $this->encodeScaledMedia($raw, $constraints['max_width'], $constraints['max_height']);
        $encoded = $encodedImage['binary'];

        $hash = hash('sha256', $encoded);
        $path = $this->mediaPath($hash, $slot);
        $disk = Storage::disk($this->mediaDiskName());

        $this->writeMediaBinary($disk, $path, $encoded);

        $thumbVariant = $this->storeResponsiveVariant(
            $disk,
            $raw,
            $constraints,
            $path,
            'thumb',
            self::RESPONSIVE_VARIANT_WIDTHS['thumb'],
            $encodedImage['width'],
            $encodedImage['height'],
        );

        $mediumVariant = $this->storeResponsiveVariant(
            $disk,
            $raw,
            $constraints,
            $path,
            'md',
            self::RESPONSIVE_VARIANT_WIDTHS['md'],
            $encodedImage['width'],
            $encodedImage['height'],
        );

        return [
            'path' => $path,
            'thumb_path' => $thumbVariant['path'] ?? $path,
            'thumb_width' => $thumbVariant['width'] ?? $encodedImage['width'],
            'thumb_height' => $thumbVariant['height'] ?? $encodedImage['height'],
            'medium_path' => $mediumVariant['path'] ?? '',
            'medium_width' => $mediumVariant['width'] ?? null,
            'medium_height' => $mediumVariant['height'] ?? null,
            'width' => $encodedImage['width'],
            'height' => $encodedImage['height'],
            'size_bytes' => strlen($encoded),
        ];
    }

    /**
     * @return array{binary:string,width:int,height:int}
     */
    private function encodeScaledMedia(string $raw, int $maxWidth, int $maxHeight): array
    {
        $image = $this->manager->read($raw)->scaleDown(width: $maxWidth, height: $maxHeight);
        $encoded = (string) $image->encode(new WebpEncoder(85));

        if ($encoded === '') {
            throw new RuntimeException('Unable to encode image to WebP.');
        }

        return [
            'binary' => $encoded,
            'width' => (int) $image->width(),
            'height' => (int) $image->height(),
        ];
    }

    /**
     * @param  array{max_width:int,max_height:int}  $constraints
     * @return array{path:string,width:int,height:int}|null
     */
    private function storeResponsiveVariant($disk, string $raw, array $constraints, string $path, string $variant, int $targetWidth, int $sourceWidth, int $sourceHeight): ?array
    {
        if ($sourceWidth <= $targetWidth && $sourceHeight <= $constraints['max_height']) {
            return null;
        }

        $encodedVariant = $this->encodeScaledMedia($raw, $targetWidth, $constraints['max_height']);

        if ($encodedVariant['width'] >= $sourceWidth && $encodedVariant['height'] >= $sourceHeight) {
            return null;
        }

        $variantPath = $this->responsiveVariantPath($path, $variant);
        $this->writeMediaBinary($disk, $variantPath, $encodedVariant['binary']);

        return [
            'path' => $variantPath,
            'width' => $encodedVariant['width'],
            'height' => $encodedVariant['height'],
        ];
    }

    private function writeMediaBinary($disk, string $path, string $binary): void
    {
        $written = $disk->put($path, $binary, [
            'visibility' => 'public',
            'CacheControl' => 'public, max-age=31536000, immutable',
            'ContentType' => 'image/webp',
        ]);

        if ($written !== true) {
            throw new RuntimeException('Unable to store image in object storage.');
        }
    }

    private function authorizeStaff(Request $request): void
    {
        abort_unless((bool) $request->user()?->hasStaffAccess(), 403);
    }

    private function mediaDiskName(): string
    {
        return (string) config('uploads.object_storage.disk', 's3');
    }

    private function mediaPath(string $hash, string $slot): string
    {
        $folder = $slot === 'body' ? 'body' : 'covers';

        return sprintf(
            'academy/lessons/%s/%s/%s/%s.webp',
            $folder,
            substr($hash, 0, 2),
            substr($hash, 2, 2),
            $hash,
        );
    }

    private function publicUrlForPath(string $path): string
    {
        return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
    }

    /**
     * @param  array<int, array{path:string,width:int|null}>  $variants
     */
    private function buildResponsiveSrcset(array $variants): ?string
    {
        $entries = collect($variants)
            ->filter(function (array $variant): bool {
                return trim((string) ($variant['path'] ?? '')) !== '' && (int) ($variant['width'] ?? 0) > 0;
            })
            ->unique(fn (array $variant): string => trim((string) ($variant['path'] ?? '')))
            ->map(fn (array $variant): string => sprintf('%s %dw', $this->publicUrlForPath((string) $variant['path']), (int) $variant['width']))
            ->values()
            ->all();

        return $entries !== [] ? implode(', ', $entries) : null;
    }

    private function responsiveVariantPath(string $path, string $variant): string
    {
        $directory = pathinfo($path, PATHINFO_DIRNAME);
        $filename = pathinfo($path, PATHINFO_FILENAME);

        return sprintf(
            '%s/%s-%s.webp',
            $directory === '.' ? '' : $directory,
            preg_replace('/-(thumb|md)$/', '', $filename) ?? $filename,
            $variant,
        );
    }

    private function canonicalMediaPath(string $path): string
    {
        $directory = pathinfo($path, PATHINFO_DIRNAME);
        $filename = pathinfo($path, PATHINFO_FILENAME);
        $baseFilename = preg_replace('/-(thumb|md)$/', '', $filename) ?? $filename;

        return sprintf(
            '%s/%s.webp',
            $directory === '.' ? '' : $directory,
            $baseFilename,
        );
    }

    private function isResponsiveVariantPath(string $path): bool
    {
        return preg_match('/-(thumb|md)\.webp$/i', $path) === 1;
    }

    private function academyAssetManifest(): Collection
    {
        return Cache::remember($this->academyAssetCacheKey(), now()->addMinutes(self::ASSET_CACHE_TTL_MINUTES), function (): Collection {
            $disk = Storage::disk($this->mediaDiskName());

            return collect($disk->allFiles('academy/lessons'))
                ->filter(fn (string $path): bool => Str::endsWith(Str::lower($path), ['.webp', '.jpg', '.jpeg', '.png']))
                ->reject(fn (string $path): bool => $this->isResponsiveVariantPath($path))
                ->map(function (string $path) use ($disk): array {
                    $modifiedAt = null;

                    try {
                        $modifiedAt = $disk->lastModified($path);
                    } catch (\Throwable) {
                        $modifiedAt = null;
                    }

                    $folder = Str::contains($path, '/body/') ? 'body' : (Str::contains($path, '/covers/') ? 'cover' : 'asset');

                    return [
                        'path' => $path,
                        'url' => $this->publicUrlForPath($path),
                        'name' => $this->humanAssetName($path),
                        'slot' => $folder,
                        'modified_at' => $modifiedAt ? (int) $modifiedAt : null,
                        'search_text' => Str::lower(implode(' ', [$path, $folder, $this->humanAssetName($path)])),
                    ];
                })
                ->sortByDesc(fn (array $item): int => (int) ($item['modified_at'] ?? 0))
                ->values();
        });
    }

    private function academyAssetCacheKey(): string
    {
        return 'academy.lesson.assets.' . md5($this->mediaDiskName());
    }

    private function forgetAssetCache(): void
    {
        Cache::forget($this->academyAssetCacheKey());
    }

    private function humanAssetName(string $path): string
    {
        $filename = pathinfo($path, PATHINFO_FILENAME);
        $clean = trim(str_replace(['-', '_'], ' ', $filename));

        return $clean !== '' ? Str::headline($clean) : 'Academy image';
    }

    private function safeFileSize($disk, string $path): ?int
    {
        try {
            $size = $disk->size($path);
            return is_int($size) ? $size : null;
        } catch (\Throwable) {
            return null;
        }
    }

    private function deleteMediaFile(string $path): void
    {
        $trimmed = ltrim(trim($path), '/');

        if ($trimmed === '' || ! Str::startsWith($trimmed, ['academy/lessons/covers/', 'academy/lessons/body/'])) {
            return;
        }

        $basePath = $this->canonicalMediaPath($trimmed);
        $paths = [
            $basePath,
            $this->responsiveVariantPath($basePath, 'thumb'),
            $this->responsiveVariantPath($basePath, 'md'),
        ];

        Storage::disk($this->mediaDiskName())->delete(array_values(array_unique($paths)));
    }

    private function normalizeSlot(mixed $slot): string
    {
        return Str::lower(trim((string) $slot)) === 'body' ? 'body' : 'cover';
    }

    /**
     * @return array{min_width:int,min_height:int,max_width:int,max_height:int}
     */
    private function mediaConstraints(string $slot): array
    {
        if ($slot === 'body') {
            return [
                'min_width' => 64,
                'min_height' => 64,
                'max_width' => 2400,
                'max_height' => 2400,
            ];
        }

        return [
            'min_width' => 600,
            'min_height' => 315,
            'max_width' => 2200,
            'max_height' => 1400,
        ];
    }

    private function assertImageManager(): void
    {
        if ($this->manager !== null) {
            return;
        }

        throw new RuntimeException('Image processing is not available on this environment.');
    }

    private function assertStorageIsAllowed(): void
    {
        $disk = Storage::disk($this->mediaDiskName());

        if (! method_exists($disk, 'put')) {
            throw new RuntimeException('Object storage is not configured for academy lesson uploads.');
        }
    }
}