<?php
declare(strict_types=1);

class Gallery
{
    private string $file;
    private string $uploadDir;
    private string $thumbDir;
    private int    $maxFileSize  = 20971520; // 20 MB
    private int    $thumbSize    = 400;
    private array  $allowedMime  = ['image/jpeg', 'image/png', 'image/webp'];

    public function __construct(string $dataDir, string $rootDir)
    {
        $this->file      = $dataDir . '/gallery.json';
        $this->uploadDir = $rootDir . '/uploads/gallery';
        $this->thumbDir  = $rootDir . '/uploads/gallery/thumbs';
    }

    // ── Albums ────────────────────────────────────────────────────────────────

    public function getAlbums(): array
    {
        return $this->load()['albums'] ?? [];
    }

    public function createAlbum(array $input): array
    {
        $data   = $this->load();
        $album  = [
            'id'          => $this->uuid(),
            'name'        => htmlspecialchars(strip_tags($input['name'] ?? ''), ENT_QUOTES, 'UTF-8'),
            'description' => htmlspecialchars(strip_tags($input['description'] ?? ''), ENT_QUOTES, 'UTF-8'),
            'order'       => count($data['albums']),
        ];
        $data['albums'][] = $album;
        $this->saveData($data);
        return $album;
    }

    public function updateAlbum(string $id, array $input): array
    {
        $data = $this->load();
        foreach ($data['albums'] as &$album) {
            if ($album['id'] === $id) {
                if (isset($input['name']))        $album['name']        = htmlspecialchars(strip_tags($input['name']), ENT_QUOTES, 'UTF-8');
                if (isset($input['description'])) $album['description'] = htmlspecialchars(strip_tags($input['description']), ENT_QUOTES, 'UTF-8');
                $this->saveData($data);
                return $album;
            }
        }
        throw new RuntimeException("Album not found: $id");
    }

    public function deleteAlbum(string $id): void
    {
        $data = $this->load();
        // Unassign images from this album
        foreach ($data['images'] as &$img) {
            if (($img['album_id'] ?? '') === $id) $img['album_id'] = null;
        }
        $data['albums'] = array_values(array_filter($data['albums'], fn($a) => $a['id'] !== $id));
        $this->saveData($data);
    }

    // ── Images ────────────────────────────────────────────────────────────────

    public function getAll(): array
    {
        $data = $this->load();
        usort($data['images'], fn($a, $b) => ($a['order'] ?? 0) <=> ($b['order'] ?? 0));
        return $data;
    }

    public function upload(array $files): array
    {
        $this->ensureDirs();
        $data    = $this->load();
        $maxOrder = empty($data['images']) ? 0 : max(array_column($data['images'], 'order')) + 1;
        $uploaded = [];

        // Handle both single file and multiple files
        $fileList = $this->normalizeFiles($files);

        foreach ($fileList as $file) {
            if ($file['error'] !== UPLOAD_ERR_OK) continue;
            if ($file['size'] > $this->maxFileSize) continue;

            $mime = mime_content_type($file['tmp_name']);
            if (!in_array($mime, $this->allowedMime, true)) continue;

            $baseName = $this->uuid();

            // Save upload to a temp path so we can process it
            $ext      = $this->mimeToExt($mime);
            $tmpPath  = $this->uploadDir . '/tmp_' . $baseName . '.' . $ext;
            if (!move_uploaded_file($file['tmp_name'], $tmpPath)) continue;

            try {
                // Generate 3 responsive AVIF/WebP sizes
                $mPath  = $this->convertToResponsive($tmpPath, $this->uploadDir . '/' . $baseName . '--size-m',  920,  80);
                $lPath  = $this->convertToResponsive($tmpPath, $this->uploadDir . '/' . $baseName . '--size-l',  1440, 80);
                $xlPath = $this->convertToResponsive($tmpPath, $this->uploadDir . '/' . $baseName . '--size-xl', 2700, 80);

                // Derive the extension actually used (avif or webp)
                $imgExt = pathinfo($mPath, PATHINFO_EXTENSION);

                // If any size failed, skip and clean up
                if (!file_exists($mPath) || !file_exists($lPath) || !file_exists($xlPath)) {
                    @unlink($tmpPath);
                    continue;
                }

                // Generate 400×400 WebP thumbnail for editor
                $thumbPath = $this->thumbDir . '/' . $baseName . '.webp';
                $this->generateThumb($tmpPath, $thumbPath, $this->thumbSize);

                // Delete the temporary original
                @unlink($tmpPath);
            } catch (Throwable) {
                @unlink($tmpPath);
                continue;
            }

            $image = [
                'id'         => $baseName,
                'file_m'     => 'uploads/gallery/' . $baseName . '--size-m.'  . $imgExt,
                'file_l'     => 'uploads/gallery/' . $baseName . '--size-l.'  . $imgExt,
                'file_xl'    => 'uploads/gallery/' . $baseName . '--size-xl.' . $imgExt,
                'file_thumb' => 'uploads/gallery/thumbs/' . $baseName . '.webp',
                'album_id'   => null,
                'order'      => $maxOrder++,
                'uploaded_at'=> date('c'),
            ];

            $data['images'][] = $image;
            $uploaded[]       = $image;
        }

        $this->saveData($data);
        return $uploaded;
    }

    public function reorderImages(array $ids): void
    {
        $data = $this->load();
        $map  = [];
        foreach ($data['images'] as $img) $map[$img['id']] = $img;

        $ordered = [];
        foreach ($ids as $i => $id) {
            if (isset($map[$id])) {
                $map[$id]['order'] = $i;
                $ordered[] = $map[$id];
                unset($map[$id]);
            }
        }
        foreach ($map as $img) $ordered[] = $img;

        $data['images'] = $ordered;
        $this->saveData($data);
    }

    public function assignAlbum(string $id, ?string $albumId): array
    {
        $data = $this->load();
        foreach ($data['images'] as &$img) {
            if ($img['id'] === $id) {
                $img['album_id'] = $albumId;
                $this->saveData($data);
                return $img;
            }
        }
        throw new RuntimeException("Image not found: $id");
    }

    public function delete(string $id): void
    {
        $data = $this->load();
        $img  = null;
        foreach ($data['images'] as $i) {
            if ($i['id'] === $id) { $img = $i; break; }
        }
        if (!$img) throw new RuntimeException("Image not found: $id");

        // Delete files — new format (file_m/l/xl/thumb) + backward-compat (file_original/file_webp)
        $root = dirname(dirname($this->uploadDir));
        foreach (['file_m', 'file_l', 'file_xl', 'file_thumb', 'file_original', 'file_webp'] as $key) {
            if (!empty($img[$key])) {
                $path = $root . '/' . $img[$key];
                if (file_exists($path)) @unlink($path);
            }
        }

        $data['images'] = array_values(array_filter($data['images'], fn($i) => $i['id'] !== $id));
        $this->saveData($data);
    }

    // ── ZIP Download ─────────────────────────────────────────────────────────

    public function downloadZip(?string $albumId): never
    {
        if (!class_exists('ZipArchive')) {
            http_response_code(500);
            echo 'ZIP not available'; exit;
        }

        $data   = $this->load();
        $images = $data['images'] ?? [];

        if ($albumId && $albumId !== 'all') {
            $images = array_filter($images, fn($img) => ($img['album_id'] ?? null) === $albumId);
        }

        $root    = dirname(dirname($this->uploadDir));
        $tmpFile = sys_get_temp_dir() . '/hw_gallery_' . uniqid() . '.zip';

        $zip = new ZipArchive();
        if ($zip->open($tmpFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            http_response_code(500);
            echo 'Could not create ZIP'; exit;
        }

        foreach ($images as $img) {
            $xlPath = $root . '/' . ($img['file_xl'] ?? '');
            if (!file_exists($xlPath)) continue;
            $zip->addFile($xlPath, basename($xlPath));
        }
        $zip->close();

        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="galerie.zip"');
        header('Content-Length: ' . filesize($tmpFile));
        readfile($tmpFile);
        @unlink($tmpFile);
        exit;
    }

    // ── Private ───────────────────────────────────────────────────────────────

    public function reorderAlbums(array $ids): void
    {
        $data = $this->load();
        $map  = array_column($data['albums'], null, 'id');
        $ordered = [];
        foreach ($ids as $i => $id) {
            if (isset($map[$id])) {
                $map[$id]['order'] = $i;
                $ordered[] = $map[$id];
                unset($map[$id]);
            }
        }
        foreach ($map as $album) $ordered[] = $album;
        $data['albums'] = array_values($ordered);
        $this->saveData($data);
    }

    public function bulkAssignAlbum(array $ids, ?string $albumId): void
    {
        if (empty($ids)) return;
        $data  = $this->load();
        // Validate albumId exists (null = unassign)
        if ($albumId !== null) {
            $albumIds = array_column($data['albums'], 'id');
            if (!in_array($albumId, $albumIds, true)) {
                throw new RuntimeException("Album not found: $albumId");
            }
        }
        $idSet = array_flip($ids);
        foreach ($data['images'] as &$img) {
            if (isset($idSet[$img['id']])) $img['album_id'] = $albumId;
        }
        $this->saveData($data);
    }

    private function load(): array
    {
        if (!file_exists($this->file)) return ['albums' => [], 'images' => []];
        return json_decode(file_get_contents($this->file), true) ?? ['albums' => [], 'images' => []];
    }

    private function saveData(array $data): void
    {
        $tmp = $this->file . '.tmp';
        file_put_contents($tmp, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX);
        if (!rename($tmp, $this->file)) {
            @unlink($tmp);
            throw new RuntimeException('Failed to save gallery data');
        }
    }

    private function ensureDirs(): void
    {
        foreach ([$this->uploadDir, $this->thumbDir] as $dir) {
            if (!is_dir($dir)) mkdir($dir, 0755, true);
        }
    }

    private function normalizeFiles(array $files): array
    {
        $result = [];
        if (!isset($files['file'])) return $result;

        $f = $files['file'];
        if (is_array($f['name'])) {
            for ($i = 0; $i < count($f['name']); $i++) {
                $result[] = [
                    'name'     => $f['name'][$i],
                    'tmp_name' => $f['tmp_name'][$i],
                    'error'    => $f['error'][$i],
                    'size'     => $f['size'][$i],
                    'type'     => $f['type'][$i],
                ];
            }
        } else {
            $result[] = $f;
        }
        return $result;
    }

    /**
     * Scales $src so the longest edge ≤ $maxEdge and saves as AVIF (or WebP fallback).
     * Returns the actual destination path.
     */
    private function convertToResponsive(string $src, string $destBase, int $maxEdge, int $quality = 80): string
    {
        $img = $this->loadImage($src);
        if (!$img) throw new RuntimeException('Could not load image: ' . basename($src));

        [$w, $h] = getimagesize($src);

        if ($w > $maxEdge || $h > $maxEdge) {
            $scale = min($maxEdge / $w, $maxEdge / $h);
            $newW  = (int)round($w * $scale);
            $newH  = (int)round($h * $scale);
            $out   = imagecreatetruecolor($newW, $newH);
            $this->preserveAlpha($out);
            imagecopyresampled($out, $img, 0, 0, 0, 0, $newW, $newH, $w, $h);
            imagedestroy($img);
            $img = $out;
        }

        if (function_exists('imageavif')) {
            $dest = $destBase . '.avif';
            imageavif($img, $dest, $quality);
        } else {
            $dest = $destBase . '.webp';
            imagewebp($img, $dest, $quality);
        }
        imagedestroy($img);
        return $dest;
    }

    private function generateThumb(string $src, string $dest, int $size): void
    {
        $img = $this->loadImage($src);
        if (!$img) return;

        [$w, $h] = getimagesize($src);
        $ratio    = max($size / $w, $size / $h);
        $newW     = (int)round($w * $ratio);
        $newH     = (int)round($h * $ratio);

        $resized = imagecreatetruecolor($newW, $newH);
        $this->preserveAlpha($resized);
        imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h);
        imagedestroy($img);

        // Center crop
        $thumb = imagecreatetruecolor($size, $size);
        $this->preserveAlpha($thumb);
        $x = (int)(($newW - $size) / 2);
        $y = (int)(($newH - $size) / 2);
        imagecopy($thumb, $resized, 0, 0, $x, $y, $size, $size);
        imagedestroy($resized);

        imagewebp($thumb, $dest, 80);
        imagedestroy($thumb);
    }

    private function loadImage(string $path): \GdImage|false
    {
        $mime = mime_content_type($path);
        return match($mime) {
            'image/jpeg' => imagecreatefromjpeg($path),
            'image/png'  => imagecreatefrompng($path),
            'image/webp' => imagecreatefromwebp($path),
            default      => false,
        };
    }

    private function preserveAlpha(\GdImage $img): void
    {
        imagealphablending($img, false);
        imagesavealpha($img, true);
        $transparent = imagecolorallocatealpha($img, 255, 255, 255, 127);
        imagefilledrectangle($img, 0, 0, imagesx($img), imagesy($img), $transparent);
        imagealphablending($img, true);
    }

    private function mimeToExt(string $mime): string
    {
        return match($mime) {
            'image/jpeg' => 'jpg',
            'image/png'  => 'png',
            'image/gif'  => 'gif',
            'image/webp' => 'webp',
            default      => 'jpg',
        };
    }

    private function uuid(): string
    {
        $b = random_bytes(16);
        $b[6] = chr(ord($b[6]) & 0x0f | 0x40);
        $b[8] = chr(ord($b[8]) & 0x3f | 0x80);
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
    }
}
