<?php

declare(strict_types=1);

class AdminCatalogueController
{
    public static function routes(): void
    {
        Router::get('/admin/catalogue/browse', [self::class, 'browse']);
        Router::get('/admin/catalogue/filters', [self::class, 'filters']);
        Router::get('/admin/catalogue/balance', [self::class, 'balance']);
        Router::post('/admin/catalogue/purchase', [self::class, 'purchase']);
        Router::get('/admin/catalogue/downloads', [self::class, 'downloads']);
        Router::post('/admin/catalogue/cancel-download', [self::class, 'cancelDownload']);
        Router::post('/admin/catalogue/retry-download', [self::class, 'retryDownload']);
        Router::post('/admin/catalogue/cancel-all', [self::class, 'cancelAll']);
        Router::post('/admin/catalogue/cancel-batch', [self::class, 'cancelBatch']);
        Router::post('/admin/catalogue/retry-batch', [self::class, 'retryBatch']);
        Router::post('/admin/catalogue/retry-all', [self::class, 'retryAllFailed']);
        Router::post('/admin/catalogue/delete-download', [self::class, 'deleteDownload']);
        Router::post('/admin/catalogue/delete-batch', [self::class, 'deleteBatch']);
        Router::post('/admin/catalogue/clear-downloads', [self::class, 'clearDownloads']);
        Router::post('/admin/catalogue/save-config', [self::class, 'saveConfig']);
        Router::get('/admin/catalogue/packs', [self::class, 'packs']);
        Router::post('/admin/catalogue/create-checkout', [self::class, 'createCheckout']);
        Router::get('/admin/catalogue/session-status', [self::class, 'sessionStatus']);
        Router::get('/admin/catalogue/billing-status', [self::class, 'billingStatus']);
        Router::post('/admin/catalogue/save-billing', [self::class, 'saveBilling']);
        Router::post('/admin/catalogue/marketplace-purchase', [self::class, 'marketplacePurchase']);
        Router::get('/admin/catalogue/marketplace-item-info', [self::class, 'marketplaceItemInfo']);
        Router::post('/admin/catalogue/save-ai-prompts', [self::class, 'saveAiPrompts']);
        Router::post('/admin/catalogue/pause-service', [self::class, 'pauseService']);
        Router::post('/admin/catalogue/resume-service', [self::class, 'resumeService']);
        Router::post('/admin/catalogue/set-workers', [self::class, 'setWorkers']);
         
        Router::post('/admin/catalogue/ai-completion', [self::class, 'aiCompletion']);
        Router::post('/admin/catalogue/test-ai', [self::class, 'testAi']);
         
        Router::get('/admin/catalogue/media/{type}/{token}', [self::class, 'mediaProxy']);
        Router::get('/admin/catalogue/cdn/{ref}', [self::class, 'mediaCdn']);

         
        Router::get('/admin/catalogue/gallery-lookup', [self::class, 'galleryLookup']);
        Router::get('/admin/catalogue/gallery-item', [self::class, 'galleryItem']);
        Router::post('/admin/catalogue/gallery-import', [self::class, 'galleryImport']);
    }

    public static function browse(): void
    {
        Auth::requireAdmin();
        session_write_close();  

         
        if (($_GET['media'] ?? '') === 'galleries') { self::browseGalleries(); return; }

        $filters = [
            'cursor'       => trim($_GET['cursor'] ?? ''),
            'page'         => max(1, (int) ($_GET['page'] ?? 1)),
            'per_page'     => max(1, min(500, (int) ($_GET['per_page'] ?? 24))),
            'search'       => trim($_GET['search'] ?? ''),
            'source'       => trim($_GET['source'] ?? ''),
            'category'     => trim($_GET['category'] ?? ''),
            'quality'      => trim($_GET['quality'] ?? ''),
            'video_type'   => trim($_GET['video_type'] ?? ''),
            'has_480p'     => trim($_GET['has_480p'] ?? ''),
            'views'        => trim($_GET['views'] ?? ''),
            'durations'    => trim($_GET['durations'] ?? ''),
        ];

         
         
         
         
        $importedIds = CatalogueProcessor::getAllImportedRemoteIds();
        if ($importedIds) {
            $filters['demote_ids'] = implode(',', $importedIds);
        }

        $result = CatalogueClient::browse($filters);

         
        $videos = $result['videos'] ?? [];
        if ($videos) {
            $remoteIds = array_column($videos, 'remote_video_id');
            $existing = CatalogueProcessor::getExistingRemoteIds($remoteIds);
            foreach ($videos as &$video) {
                $video['already_imported'] = in_array($video['remote_video_id'], $existing, true);
            }
            unset($video);
            $result['videos'] = $videos;
        }

        Router::json($result);
    }

    public static function filters(): void
    {
        Auth::requireAdmin();
        session_write_close();
        if (($_GET['media'] ?? '') === 'galleries') {
            Router::json(CatalogueClient::getGalleryFilters([]));
            return;
        }
        $videoType = trim($_GET['video_type'] ?? '');
        $params = [];
        if ($videoType !== '' && in_array($videoType, Video::VALID_TYPES, true)) {
            $params['video_type'] = $videoType;
        }
        Router::json(CatalogueClient::getFilters($params));
    }

     
    public static function browseGalleries(): void
    {
        $filters = [
            'page'      => max(1, (int) ($_GET['page'] ?? 1)),
            'per_page'  => max(1, min(200, (int) ($_GET['per_page'] ?? 24))),
            'search'    => trim($_GET['search'] ?? ''),
            'source'    => trim($_GET['source'] ?? ''),
            'category'  => trim($_GET['category'] ?? ''),
            'performer' => trim($_GET['performer'] ?? ''),
            'tag'       => trim($_GET['tag'] ?? ''),
            'channel'   => trim($_GET['channel'] ?? ''),
            'images'    => trim($_GET['images'] ?? ''),
            'sort'      => trim($_GET['sort'] ?? 'newest'),
        ];
        Router::json(CatalogueClient::browseGalleries($filters));
    }

     
    public static function galleryLookup(): void
    {
        Auth::requireAdmin();
        session_write_close();
        Router::json(CatalogueClient::galleryLookup(trim($_GET['facet'] ?? ''), trim($_GET['q'] ?? '')));
    }

     
    public static function galleryItem(): void
    {
        Auth::requireAdmin();
        session_write_close();
        $r = CatalogueClient::galleryItem((int) ($_GET['id'] ?? 0));
        Router::json($r ?? ['error' => 'not_found']);
    }

    

    public static function galleryImport(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired. Please refresh the page.'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        if (!is_array($input)) {
            Router::json(['error' => 'Invalid request.'], 400);
        }

         
        $rawIds = $input['gallery_ids'] ?? [];
        $ids = [];
        if (is_array($rawIds)) {
            foreach ($rawIds as $gid) {
                $gid = (int) $gid;
                if ($gid > 0 && !in_array($gid, $ids, true)) {
                    $ids[] = $gid;
                }
            }
        }
        if (empty($ids)) {
            Router::json(['error' => 'No galleries selected.'], 400);
        }

         
         
         
        $rawType = (string) ($input['video_type'] ?? '');
        $defaultType = in_array($rawType, Album::VALID_TYPES, true) ? $rawType : Album::DEFAULT_TYPE;

         
         
        $aiSettings = null;
        $aiIn = $input['ai_settings'] ?? null;
        if (is_array($aiIn) && !empty($aiIn['enabled'])) {
            $clean = ['enabled' => true];
            if (!empty($aiIn['rewrite_title'])) {
                $clean['rewrite_title'] = true;
                $clean['title_prompt'] = mb_substr(trim((string) ($aiIn['title_prompt'] ?? '')), 0, 500);
            }
            if (!empty($aiIn['rewrite_description'])) {
                $clean['rewrite_description'] = true;
                $clean['desc_prompt'] = mb_substr(trim((string) ($aiIn['desc_prompt'] ?? '')), 0, 500);
            }
            $langs = [];
            if (!empty($aiIn['translate_languages']) && is_array($aiIn['translate_languages'])) {
                foreach (array_slice($aiIn['translate_languages'], 0, CatalogueClient::TRANSLATE_MAX_LANGS) as $l) {
                    $code = preg_replace('/[^a-zA-Z-]/', '', is_array($l) ? (string) ($l['code'] ?? '') : (string) $l);
                    if ($code !== '') {
                        $clean_name = is_array($l) ? (string) ($l['name'] ?? $code) : (string) $l;
                        $langs[] = ['code' => $code, 'name' => mb_substr($clean_name, 0, 60)];
                    }
                }
            }
            if (!empty($aiIn['translate']) && $langs) {
                $clean['translate'] = true;
                $clean['translate_prompt'] = mb_substr(trim((string) ($aiIn['translate_prompt'] ?? '')), 0, 500);
            }
            if (!empty($aiIn['translate_description']) && $langs) {
                $clean['translate_description'] = true;
                $clean['translate_desc_prompt'] = mb_substr(trim((string) ($aiIn['translate_desc_prompt'] ?? '')), 0, 500);
            }
            if ((!empty($clean['translate']) || !empty($clean['translate_description'])) && $langs) {
                $clean['translate_languages'] = $langs;
                $clean['translate_format'] = mb_substr(trim((string) ($aiIn['translate_format'] ?? '')), 0, 2000);
            }
            $aiSettings = (count($clean) > 1) ? $clean : null;  
        }

         
        $maxBatch = 25;
        $maxImages = 300;
        $remaining = 0;
        if (count($ids) > $maxBatch) {
            $remaining = count($ids) - $maxBatch;
            $ids = array_slice($ids, 0, $maxBatch);
        }

         
         
         
        $newIds = [];
        foreach ($ids as $gid) {
            if (!Database::fetchOne('SELECT id FROM albums WHERE external_id = ? LIMIT 1', ['catgal-' . $gid])) {
                $newIds[] = $gid;
            }
        }
        $newBalance = null;
        $charged = 0.0;
        if (!empty($newIds)) {
            $charge = CatalogueClient::galleryPurchase($newIds, $aiSettings);
            if (!is_array($charge) || isset($charge['error'])) {
                $err = is_array($charge) ? ($charge['error'] ?? 'Purchase failed.') : 'The catalogue is unreachable. Please try again.';
                $code = (is_array($charge) && stripos((string) ($charge['error'] ?? ''), 'insufficient') !== false) ? 402 : 502;
                $payload = ['error' => $err];
                if (is_array($charge)) {
                    if (isset($charge['required'])) $payload['required'] = $charge['required'];
                    if (isset($charge['available'])) $payload['available'] = $charge['available'];
                }
                Router::json($payload, $code);
            }
            $newBalance = $charge['new_balance'] ?? null;
            $charged = (float) ($charge['charged'] ?? 0);
        }

         
        $incIn = is_array($input['import_settings'] ?? null) ? $input['import_settings'] : [];
        $inc = [
            'description' => !array_key_exists('description', $incIn) || !empty($incIn['description']),
            'categories'  => !array_key_exists('categories', $incIn)  || !empty($incIn['categories']),
            'tags'        => !array_key_exists('tags', $incIn)        || !empty($incIn['tags']),
            'performers'  => !array_key_exists('performers', $incIn)  || !empty($incIn['performers']),
            'channels'    => !array_key_exists('channels', $incIn)    || !empty($incIn['channels']),
            'views'       => !empty($incIn['views']),
        ];

        $sid = MediaStorage::activeDefaultId() ?: null;
        $imported = [];
        $skipped = [];
        $errors = [];

        foreach ($ids as $gid) {
            $folder = null;
            $saved = [];
            $albumId = 0;
            try {
                $extId = 'catgal-' . $gid;

                 
                $exists = Database::fetchOne('SELECT id FROM albums WHERE external_id = ? LIMIT 1', [$extId]);
                if ($exists) {
                    $skipped[] = ['gallery_id' => $gid, 'album_id' => (int) $exists['id'], 'reason' => 'already_imported'];
                    continue;
                }

                $g = CatalogueClient::galleryItemRaw($gid);
                if (!is_array($g) || isset($g['error']) || !empty($g['_error'])) {
                    $msg = is_array($g) ? (string) ($g['message'] ?? $g['error'] ?? 'fetch_failed') : 'fetch_failed';
                    $errors[] = ['gallery_id' => $gid, 'error' => $msg];
                    continue;
                }

                $images = (isset($g['images']) && is_array($g['images'])) ? $g['images'] : [];
                if (empty($images)) {
                    $errors[] = ['gallery_id' => $gid, 'error' => 'no_images'];
                    continue;
                }
                if (count($images) > $maxImages) {
                    $images = array_slice($images, 0, $maxImages);
                }

                 
                $rawVt = (string) ($g['video_type'] ?? $g['orientation'] ?? '');
                $vt = in_array($rawVt, Album::VALID_TYPES, true) ? $rawVt : $defaultType;

                 
                $folder = AlbumAsset::createFolder();
                $coverFile = '';
                $coverW = null;
                $coverH = null;
                $seq = 0;
                foreach ($images as $im) {
                    $url = is_array($im) ? (string) ($im['url'] ?? '') : (is_string($im) ? $im : '');
                    if ($url === '') {
                        continue;
                    }
                    $bytes = self::fetchCatalogueImage($url);
                    if ($bytes === '') {
                        continue;
                    }
                    $seq++;
                    $filename = sprintf('img-%03d.%s', $seq, self::galleryImageExt($url, $bytes));
                    if (!AlbumAsset::saveRaw($folder, $filename, $bytes, $sid)) {
                        throw new \RuntimeException('storage write failed');
                    }
                    $w = null;
                    $h = null;
                    $info = @getimagesizefromstring($bytes);
                    if (is_array($info)) {
                        $w = (int) $info[0];
                        $h = (int) $info[1];
                    } elseif (is_array($im)) {
                        $w = isset($im['w']) ? (int) $im['w'] : null;
                        $h = isset($im['h']) ? (int) $im['h'] : null;
                    }
                    if ($coverFile === '') {
                        $coverFile = $filename;
                        $coverW = $w;
                        $coverH = $h;
                    }
                    $saved[] = ['f' => $filename, 'w' => $w, 'h' => $h];
                }

                if (empty($saved)) {
                    AlbumAsset::deleteFolder($folder, $sid, []);
                    $folder = null;
                    $errors[] = ['gallery_id' => $gid, 'error' => 'all_image_downloads_failed'];
                    continue;
                }

                $title = trim((string) ($g['title'] ?? ''));
                if ($title === '') {
                    $title = 'Untitled';
                }
                $desc = isset($g['description']) ? trim((string) $g['description']) : '';

                try {
                    $albumId = Album::create([
                        'external_id'  => $extId,
                        'title'        => mb_substr($title, 0, 255),
                        'slug'         => Slug::unique('albums', $title),
                        'description'  => ($inc['description'] && $desc !== '') ? $desc : null,
                        'album_folder' => $folder,
                        'storage_id'   => $sid,
                        'cover_file'   => $coverFile,
                        'cover_width'  => $coverW,
                        'cover_height' => $coverH,
                        'image_count'  => count($saved),
                        'views'        => $inc['views'] ? (int) ($g['views'] ?? 0) : 0,
                        'images'       => json_encode($saved),
                        'status'       => 'published',
                        'video_type'   => $vt,
                        'user_id'      => Auth::id(),
                        'published_at' => date('Y-m-d H:i:s'),
                    ]);
                } catch (\PDOException $e) {
                     
                    if ($e->getCode() === '23000') {
                        AlbumAsset::deleteFolder($folder, $sid, array_column($saved, 'f'));
                        $folder = null;
                        $dup = Database::fetchOne('SELECT id FROM albums WHERE external_id = ? LIMIT 1', [$extId]);
                        $skipped[] = ['gallery_id' => $gid, 'album_id' => $dup ? (int) $dup['id'] : 0, 'reason' => 'already_imported'];
                        continue;
                    }
                    throw $e;
                }

                 
                self::mapGalleryTaxonomy((int) $albumId, $g, $vt, $inc);

                $imported[] = ['gallery_id' => $gid, 'album_id' => (int) $albumId, 'title' => $title, 'images' => count($saved)];
            } catch (\Throwable $e) {
                error_log('AdminCatalogueController::galleryImport: gallery ' . $gid . ' failed: ' . $e->getMessage());
                if ($albumId === 0 && $folder !== null) {
                    AlbumAsset::deleteFolder($folder, $sid, array_column($saved, 'f'));
                }
                $errors[] = ['gallery_id' => $gid, 'error' => $e->getMessage()];
            }
        }

         
         
         
        $aiQueued = false;
        if ($aiSettings !== null && !empty($imported)) {
            $aiQueued = self::spawnGalleryAiWorker(array_map(static fn($r) => (int) $r['album_id'], $imported), $aiSettings);
        }

        Router::json([
            'success'   => true,
            'imported'  => $imported,
            'skipped'   => $skipped,
            'errors'    => $errors,
            'remaining' => $remaining,
            'charged'   => $charged,
            'new_balance' => $newBalance,
            'ai_queued' => $aiQueued,
        ]);
    }

     
    private static function fetchCatalogueImage(string $url): string
    {
        $tmp = APP_ROOT . '/storage/tmp/galimg-' . bin2hex(random_bytes(8));
        $res = CatalogueClient::downloadFile($url, $tmp);
        $bytes = (!empty($res['success']) && is_file($tmp)) ? (string) @file_get_contents($tmp) : '';
        if (is_file($tmp)) {
            @unlink($tmp);
        }
        return $bytes;
    }

     
    private static function galleryImageExt(string $url, string $bytes): string
    {
        $info = @getimagesizefromstring($bytes);
        if (is_array($info) && !empty($info['mime'])) {
            $byMime = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
            if (isset($byMime[$info['mime']])) {
                return $byMime[$info['mime']];
            }
        }
        $ext = strtolower((string) pathinfo((string) parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
        if ($ext === 'jpeg') {
            return 'jpg';
        }
        if (in_array($ext, ['jpg', 'png', 'webp', 'gif'], true)) {
            return $ext;
        }
        return 'jpg';
    }

     
    private static function mapGalleryTaxonomy(int $albumId, array $g, string $videoType, array $inc = []): void
    {
        $inc += ['categories' => true, 'tags' => true, 'performers' => true, 'channels' => true];
         
        $catIds = [];
        foreach (($inc['categories'] ? (array) ($g['categories'] ?? []) : []) as $c) {
            $name = is_array($c) ? (string) ($c['name'] ?? '') : (string) $c;
            $id = self::findOrCreateAlbumTaxonomy('album_categories', 'AlbumCategory', $name, $videoType);
            if ($id) {
                $catIds[] = $id;
            }
        }
        if ($catIds) {
            Album::syncCategories($albumId, $catIds);
        }

         
        $tagIds = [];
        foreach (($inc['tags'] ? (array) ($g['tags'] ?? []) : []) as $t) {
            $name = is_array($t) ? (string) ($t['name'] ?? '') : (string) $t;
            $id = self::findOrCreateAlbumTaxonomy('album_tags', 'AlbumTag', $name, $videoType);
            if ($id) {
                $tagIds[] = $id;
            }
        }
        if ($tagIds) {
            Album::syncTags($albumId, $tagIds);
        }

         
        $perfIds = [];
        foreach (($inc['performers'] ? (array) ($g['performers'] ?? []) : []) as $p) {
            $name = is_array($p) ? (string) ($p['name'] ?? '') : (string) $p;
            $id = self::findOrCreateAlbumTaxonomy('album_performers', 'AlbumPerformer', $name, $videoType);
            if ($id) {
                $perfIds[] = $id;
            }
        }
        if ($perfIds) {
            Album::syncPerformers($albumId, $perfIds);
        }

         
        $channel = $inc['channels'] ? trim((string) ($g['channel'] ?? '')) : '';
        if ($channel === '' && $inc['channels']) {
            $channel = trim((string) ($g['source_name'] ?? ''));
        }
        if ($channel !== '') {
            $chId = self::findOrCreateAlbumTaxonomy('album_channels', 'AlbumChannel', $channel, $videoType);
            if ($chId) {
                Album::syncChannels($albumId, [$chId]);
            }
        }
    }

    

    private static function findOrCreateAlbumTaxonomy(string $table, string $model, string $name, string $videoType): int
    {
        $name = trim($name);
        if ($name === '') {
            return 0;
        }
        $slug = Slug::generate($name);
        if ($slug === '') {
            return 0;
        }
        $existing = Database::fetchOne("SELECT id FROM {$table} WHERE slug = ? AND video_type = ?", [$slug, $videoType]);
        if ($existing) {
            return (int) $existing['id'];
        }
        try {
            return (int) $model::create(['name' => $name, 'slug' => $slug, 'video_type' => $videoType]);
        } catch (\PDOException $e) {
            if ($e->getCode() === '23000') {
                $existing = Database::fetchOne("SELECT id FROM {$table} WHERE slug = ? AND video_type = ?", [$slug, $videoType]);
                if ($existing) {
                    return (int) $existing['id'];
                }
            }
            throw $e;
        }
    }

    public static function balance(): void
    {
        Auth::requireAdmin();
        session_write_close();

        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured', 'credits' => 0, 'credit_rate' => 0.01], 200);
        }

        $balance = CatalogueClient::getBalance();
        if ($balance === null) {
            Router::json(['error' => 'Could not fetch balance', 'credits' => 0, 'credit_rate' => 0.01], 200);
        }

         
        if (!isset($balance['credit_rate'])) {
            $cachedRate = Setting::get('catalogue_credit_rate', '');
            $balance['credit_rate'] = $cachedRate !== '' ? (float) $cachedRate : 0.01;
        }

        Router::json($balance);
    }

    public static function packs(): void
    {
        Auth::requireAdmin();
        session_write_close();
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured', 'packs' => []], 200);
            return;
        }
        $result = CatalogueClient::getPacks();
        if ($result === null) {
            Router::json(['error' => 'Could not fetch packs', 'packs' => []], 200);
            return;
        }
        Router::json($result);
    }

    public static function purchase(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired. Please refresh the page.'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        if (!$input) {
            Router::json(['error' => 'Invalid request.'], 400);
        }

        $videoIds = $input['video_ids'] ?? [];
         
         
         

        if (empty($videoIds)) {
            Router::json(['error' => 'No videos selected.'], 400);
        }

         
         
         
         
        $aiInput = $input['ai_settings'] ?? [];
        $aiCreditsPerVideo = 0.0;
        if (!empty($aiInput['enabled'])) {
            $pricing = CatalogueClient::cataloguePricing();
            $flat    = is_array($pricing['ai'] ?? null) ? $pricing['ai'] : [];
            $perLang = is_array($pricing['ai_per_lang'] ?? null) ? $pricing['ai_per_lang'] : [];
            $nLangs  = (!empty($aiInput['translate_languages']) && is_array($aiInput['translate_languages']))
                ? min(count($aiInput['translate_languages']), CatalogueClient::TRANSLATE_MAX_LANGS) : 0;
            $translateCost = static function (string $op) use ($flat, $perLang, $nLangs): float {
                if ($nLangs > 0 && isset($perLang[$op])) {
                    return (float) $perLang[$op] * $nLangs;
                }
                return (float) ($flat[$op] ?? 0.40);
            };
            if (!empty($aiInput['rewrite_title']))          $aiCreditsPerVideo += (float) ($flat['rewrite_title'] ?? 0.30);
            if (!empty($aiInput['translate']))              $aiCreditsPerVideo += $translateCost('translate');
            if (!empty($aiInput['rewrite_description']))    $aiCreditsPerVideo += (float) ($flat['rewrite_description'] ?? 0.30);
            if (!empty($aiInput['translate_description']))  $aiCreditsPerVideo += $translateCost('translate_description');
            $aiCreditsPerVideo = round($aiCreditsPerVideo, 2);
        }

         
        $aiSettings = $input['ai_settings'] ?? null;
        if (is_array($aiSettings) && !empty($aiSettings['enabled'])) {
            $cleanAi = ['enabled' => true];
            if (!empty($aiSettings['rewrite_title'])) {
                $cleanAi['rewrite_title'] = true;
                $cleanAi['title_prompt'] = mb_substr(trim($aiSettings['title_prompt'] ?? ''), 0, 500);
            }
            if (!empty($aiSettings['translate']) && !empty($aiSettings['translate_languages'])) {
                $cleanAi['translate'] = true;
                $cleanAi['translate_prompt'] = mb_substr(trim($aiSettings['translate_prompt'] ?? ''), 0, 500);
            }
            if (!empty($aiSettings['translate_description']) && !empty($aiSettings['translate_languages'])) {
                $cleanAi['translate_description'] = true;
                $cleanAi['translate_desc_prompt'] = mb_substr(trim($aiSettings['translate_desc_prompt'] ?? ''), 0, 500);
            }
             
             
             
            if ((!empty($aiSettings['translate']) || !empty($aiSettings['translate_description'])) && !empty($aiSettings['translate_languages']) && is_array($aiSettings['translate_languages'])) {
                $langs = [];
                foreach (array_slice($aiSettings['translate_languages'], 0, CatalogueClient::TRANSLATE_MAX_LANGS) as $l) {
                    $code = preg_replace('/[^a-zA-Z-]/', '', is_array($l) ? (string) ($l['code'] ?? '') : (string) $l);
                    if ($code !== '') {
                        $name = is_array($l) ? (string) ($l['name'] ?? $code) : (string) $l;
                        $langs[] = ['code' => $code, 'name' => mb_substr($name, 0, 60)];
                    }
                }
                if ($langs) {
                    $cleanAi['translate_languages'] = $langs;
                    $cleanAi['translate_format'] = substr(trim($aiSettings['translate_format'] ?? ''), 0, 2000);
                } else {
                    unset($cleanAi['translate'], $cleanAi['translate_description']);
                }
            }
            if (!empty($aiSettings['rewrite_description'])) {
                $cleanAi['rewrite_description'] = true;
                $cleanAi['desc_prompt'] = mb_substr(trim($aiSettings['desc_prompt'] ?? ''), 0, 500);
            }
            $aiSettings = $cleanAi;
        } else {
            $aiSettings = null;
        }

        $importSettings = $input['import_settings'] ?? [];
        $rawFormats = $importSettings['transcode_formats'] ?? null;
         
        if (!is_array($rawFormats)) {
            $selectedFormats = array_keys(TranscodeManager::getFormats());
        } else {
             
            $knownFormats = array_keys(TranscodeManager::getFormats());
            $selectedFormats = array_values(array_intersect($rawFormats, $knownFormats));
        }
        $formatsJson = json_encode($selectedFormats);
        $inc = [
            'views'       => !empty($importSettings['views']),
            'channel'     => !empty($importSettings['channels']),
            'categories'  => !empty($importSettings['categories']),
            'tags'        => !empty($importSettings['tags']),
            'performers'  => !empty($importSettings['performers']),
            'description' => !empty($importSettings['description']),
            'preview'     => !empty($importSettings['preview']),
        ];

        $batchId = bin2hex(random_bytes(16));
        $count = count($videoIds);

         
        if ($count > 500) {
            $tmpDir = APP_ROOT . '/storage/tmp';
            if (!is_dir($tmpDir)) {
                @mkdir($tmpDir, 0755, true);
            }

            $jobFile = $tmpDir . '/purchase-job-' . $batchId . '.json';
            file_put_contents($jobFile, json_encode([
                'video_ids'            => $videoIds,
                'ai_settings'          => $aiSettings,
                'ai_credits_per_video' => $aiCreditsPerVideo,
                'user_id'              => Auth::id(),
                'batch_id'             => $batchId,
                'selected_formats'     => $formatsJson,
                'includes'             => $inc,
            ], JSON_UNESCAPED_UNICODE));

            $phpBin = self::findPhpCli();
            $script = APP_ROOT . '/bin/catalogue-purchase.php';
            if (is_file($script) && function_exists('exec')) {
                @exec(escapeshellarg($phpBin) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($batchId) . ' > /dev/null 2>&1 &');
            }

            Router::json([
                'success'     => true,
                'queued'      => $count,
                'batch_id'    => $batchId,
                'async'       => true,
            ]);
            return;
        }

         
        $result = CatalogueClient::purchase($videoIds, $aiCreditsPerVideo, null, $aiSettings);
        if ($result === null) {
            Router::json(['error' => 'Could not connect to the catalogue API. Please check your configuration.'], 500);
        }

        if (!empty($result['_error'])) {
            $msg = $result['message'] ?? $result['error'] ?? 'Purchase failed';
            Router::json(['error' => $msg], $result['_http_code'] ?? 400);
        }

        $purchased = $result['purchased'] ?? [];
        if (empty($purchased)) {
            Router::json(['error' => 'No videos were purchased. They may already be owned.'], 400);
        }

         
        $metaLookup = [];
        foreach (CatalogueClient::getByIds($videoIds) as $vm) {
            $rid = (string) ($vm['remote_video_id'] ?? '');
            if ($rid !== '') {
                $vm = CatalogueProcessor::filterMetaByIncludes($vm, $inc);
                $metaLookup[$rid] = $vm;
            }
        }

        $queued = CatalogueProcessor::queueDownloads($purchased, $metaLookup, Auth::id(), $batchId, $aiSettings, $formatsJson);

        if (isset($result['new_balance'])) {
            Setting::set('catalogue_balance_cache', (string) $result['new_balance']);
            Setting::set('catalogue_balance_updated', date('Y-m-d H:i:s'));
        }

        self::triggerTickAsync();

        Router::json([
            'success'     => true,
            'queued'      => $queued,
            'total_cost'  => $result['total_cost'] ?? 0,
            'new_balance' => $result['new_balance'] ?? null,
            'batch_id'    => $batchId,
        ]);
    }

    public static function downloads(): void
    {
        Auth::requireAdmin();
        session_write_close();  

        $status = trim($_GET['status'] ?? '');
        $search = trim($_GET['search'] ?? '');
        $batch  = trim($_GET['batch'] ?? '');
        $limit  = max(1, min(200, (int) ($_GET['limit'] ?? 100)));

        $downloads = CatalogueProcessor::getDownloads($status, $search, $batch, $limit);
        $stats = CatalogueProcessor::getQueueStats();
        $batches = CatalogueProcessor::getBatches();

         
         
         

         
        $servicePaused = Setting::get('catalogue_service_paused', '0') === '1';
        $pauseReason = $servicePaused ? Setting::get('catalogue_pause_reason', '') : null;
        $netFailCount = (int) Setting::get('catalogue_net_fail_count', '0');
        $maxWorkers = max(1, min(3, (int) Setting::get('catalogue_max_workers', '3')));

         
         
        if (!$servicePaused && ((int) ($stats['pending'] ?? 0) + (int) ($stats['downloading'] ?? 0)) > 0) {
            CatalogueProcessor::ensureWorkersRunning();
        }

        Router::json([
            'downloads'      => $downloads,
            'max_workers'    => $maxWorkers,
            'stats'          => $stats,
            'batches'        => $batches,
            'service_paused' => $servicePaused,
            'pause_reason'   => $pauseReason,
            'net_fail_count' => $netFailCount,
        ]);
    }

    public static function cancelDownload(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int) ($input['id'] ?? 0);

        if ($id <= 0) {
            Router::json(['error' => 'Invalid ID.'], 400);
        }

        Router::json(['success' => CatalogueProcessor::cancelDownload($id)]);
    }

    public static function retryDownload(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int) ($input['id'] ?? 0);

        if ($id <= 0) {
            Router::json(['error' => 'Invalid ID.'], 400);
        }

        $retried = CatalogueProcessor::retryDownload($id);

        if ($retried) {
            self::triggerTickAsync();
        }

        Router::json(['success' => $retried]);
    }

    public static function cancelAll(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $cancelled = CatalogueProcessor::cancelAll();
        Router::json(['success' => true, 'cancelled' => $cancelled]);
    }

    public static function cancelBatch(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $batchId = trim($input['batch_id'] ?? '');

        if ($batchId === '') {
            Router::json(['error' => 'Invalid batch ID.'], 400);
        }

        $cancelled = CatalogueProcessor::cancelBatch($batchId);
        Router::json(['success' => true, 'cancelled' => $cancelled]);
    }

    public static function retryBatch(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $batchId = trim($input['batch_id'] ?? '');

        if ($batchId === '') {
            Router::json(['error' => 'Invalid batch ID.'], 400);
        }

        $retried = CatalogueProcessor::retryBatch($batchId);

        if ($retried > 0) {
            self::triggerTickAsync();
        }

        Router::json(['success' => true, 'retried' => $retried]);
    }

    public static function retryAllFailed(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $retried = CatalogueProcessor::retryAllFailed();

        if ($retried > 0) {
            self::triggerTickAsync();
        }

        Router::json(['success' => true, 'retried' => $retried]);
    }

    public static function deleteDownload(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int) ($input['id'] ?? 0);

        if ($id <= 0) {
            Router::json(['error' => 'Invalid ID.'], 400);
        }

        $deleted = CatalogueProcessor::deleteDownload($id);
        Router::json(['success' => $deleted > 0, 'deleted' => $deleted]);
    }

    public static function deleteBatch(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $batchId = trim($input['batch_id'] ?? '');

        if ($batchId === '') {
            Router::json(['error' => 'Invalid batch ID.'], 400);
        }

        $deleted = CatalogueProcessor::deleteBatch($batchId);
        Router::json(['success' => true, 'deleted' => $deleted]);
    }

    public static function clearDownloads(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $status = trim($input['status'] ?? '');

        $cleared = CatalogueProcessor::clearDownloads($status);
        Router::json(['success' => true, 'cleared' => $cleared]);
    }

    public static function saveConfig(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        if (!$input) {
            Router::json(['error' => 'Invalid request.'], 400);
        }

        $allowedKeys = [
            'catalogue_api_key',
            'catalogue_api_secret',
            'catalogue_api_url',
            'catalogue_auto_publish',
            'catalogue_download_thumbnails',
        ];

        foreach ($allowedKeys as $key) {
            if (array_key_exists($key, $input)) {
                Setting::set($key, trim((string) $input[$key]));
            }
        }

         
        Setting::set('catalogue_balance_updated', '');

         
        $balance = null;
        if (CatalogueClient::isConfigured()) {
            $balance = CatalogueClient::getBalance();
        }

        Router::json([
            'success' => true,
            'configured' => CatalogueClient::isConfigured(),
            'balance' => $balance,
        ]);
    }

    public static function createCheckout(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired.'], 403);
        }
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured'], 400);
        }

        $input = json_decode(file_get_contents('php://input') ?: '{}', true);
        $packId = (int) ($input['pack_id'] ?? 0);
        if ($packId <= 0) {
            Router::json(['error' => 'pack_id required'], 400);
        }

        $result = CatalogueClient::createCheckout($packId);
        if (!$result || isset($result['error'])) {
            Router::json(['error' => $result['error'] ?? 'Checkout creation failed'], 500);
        }

        Router::json($result);
    }

    public static function sessionStatus(): void
    {
        Auth::requireAdmin();
        session_write_close();
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured'], 400);
        }

        $sessionId = trim($_GET['session_id'] ?? '');
        if ($sessionId === '') {
            Router::json(['error' => 'session_id required'], 400);
        }

        $result = CatalogueClient::sessionStatus($sessionId);
        if (!$result) {
            Router::json(['error' => 'Could not fetch session status'], 500);
        }

         
        if (isset($result['balance'])) {
            Setting::set('catalogue_balance_cache', (string) $result['balance']);
            Setting::set('catalogue_balance_updated', date('Y-m-d H:i:s'));
        }

        Router::json($result);
    }

    public static function billingStatus(): void
    {
        Auth::requireAdmin();
        session_write_close();
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured'], 400);
        }

        $result = CatalogueClient::billingStatus();
        if (!$result) {
            Router::json(['error' => 'Could not fetch billing status'], 500);
        }

        Router::json($result);
    }

    public static function saveBilling(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired.'], 403);
        }
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured'], 400);
        }

        $input = json_decode(file_get_contents('php://input') ?: '{}', true);
        if (!$input) {
            Router::json(['error' => 'Invalid request'], 400);
        }

        $result = CatalogueClient::saveBilling($input);
        if (!$result || isset($result['error'])) {
            Router::json(['error' => $result['error'] ?? 'Save failed'], 500);
        }

        Router::json($result);
    }

    public static function marketplacePurchase(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired.'], 403);
        }
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured'], 400);
        }

        $input = json_decode(file_get_contents('php://input') ?: '{}', true);
        $itemType = trim($input['item_type'] ?? '');
        $itemSlug = trim($input['item_slug'] ?? '');
        $tierId = isset($input['tier_id']) ? (int) $input['tier_id'] : null;

        if (!in_array($itemType, ['plugin', 'theme']) || $itemSlug === '') {
            Router::json(['error' => 'item_type and item_slug required'], 400);
        }

        $result = CatalogueClient::purchaseItem($itemType, $itemSlug, $tierId);
        if (!$result || isset($result['error'])) {
            $code = $result['_http_code'] ?? 400;
            Router::json(['error' => $result['error'] ?? 'Purchase failed', 'required' => $result['required'] ?? null, 'available' => $result['available'] ?? null], $code);
        }

         
        if (isset($result['new_balance'])) {
            Setting::set('catalogue_balance_cache', (string) $result['new_balance']);
            Setting::set('catalogue_balance_updated', date('Y-m-d H:i:s'));
        }

        Router::json($result);
    }

    

    public static function mediaProxy(): void
    {
        Auth::requireAdmin();
        session_write_close();

        $type = Router::param('type');
        $token = Router::param('token');

        if (!in_array($type, ['thumb', 'thumb_small', 'preview'], true) || $token === '') {
            http_response_code(404);
            exit;
        }

        $media = CatalogueClient::fetchMedia($type, $token);
        if ($media === null) {
            http_response_code(404);
            exit;
        }

        $body = $media['body'];
        $totalSize = strlen($body);
        $maxAge = $type === 'thumb' ? 3600 : 1800;

        header('Content-Type: ' . $media['content_type']);
        header('Cache-Control: private, max-age=' . $maxAge);
        header('Accept-Ranges: bytes');

         
        $range = $_SERVER['HTTP_RANGE'] ?? '';
        if ($range !== '' && preg_match('/bytes=(\d+)-(\d*)/', $range, $m)) {
            $start = (int) $m[1];
            $end = $m[2] !== '' ? (int) $m[2] : $totalSize - 1;
            $end = min($end, $totalSize - 1);

            if ($start > $end || $start >= $totalSize) {
                http_response_code(416);
                header('Content-Range: bytes */' . $totalSize);
                exit;
            }

            http_response_code(206);
            header('Content-Range: bytes ' . $start . '-' . $end . '/' . $totalSize);
            header('Content-Length: ' . ($end - $start + 1));
            echo substr($body, $start, $end - $start + 1);
        } else {
            header('Content-Length: ' . $totalSize);
            echo $body;
        }
        exit;
    }

    

    public static function mediaCdn(): void
    {
        Auth::requireAdmin();
        session_write_close();

        $path = CatalogueClient::unpackCdnRef((string) Router::param('ref'));
        if ($path === null) {
            http_response_code(404);
            exit;
        }

        $reqHeaders = [];
        if (!empty($_SERVER['HTTP_RANGE'])) {
            $reqHeaders[] = 'Range: ' . $_SERVER['HTTP_RANGE'];
        }

        $respHeaders = [];
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => 'https://cdn.tubepress.io' . $path,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 5,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT        => 60,
            CURLOPT_HTTPHEADER     => $reqHeaders,
            CURLOPT_USERAGENT      => 'TubePress/1.0',
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HEADERFUNCTION => function ($ch, $h) use (&$respHeaders) {
                $p = explode(':', $h, 2);
                if (count($p) === 2) {
                    $respHeaders[strtolower(trim($p[0]))] = trim($p[1]);
                }
                return strlen($h);
            },
        ]);

        $body = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($body === false || $error !== '' || $code >= 400) {
            http_response_code($code >= 400 ? $code : 502);
            exit;
        }

        http_response_code($code === 206 ? 206 : 200);
        header('Content-Type: ' . ($respHeaders['content-type'] ?? 'application/octet-stream'));
        header('Content-Length: ' . strlen((string) $body));
        if (isset($respHeaders['content-range'])) {
            header('Content-Range: ' . $respHeaders['content-range']);
        }
        header('Accept-Ranges: bytes');
        header('Cache-Control: private, max-age=3600');
        echo $body;
        exit;
    }

    public static function marketplaceItemInfo(): void
    {
        Auth::requireAdmin();
        session_write_close();
        if (!CatalogueClient::isConfigured()) {
            Router::json(['error' => 'API not configured'], 400);
        }

        $type = trim($_GET['type'] ?? '');
        $slug = trim($_GET['slug'] ?? '');

        if (!in_array($type, ['plugin', 'theme']) || $slug === '') {
            Router::json(['error' => 'type and slug required'], 400);
        }

         
        $mpData = $type === 'plugin'
            ? CatalogueClient::getMarketplacePlugins()
            : CatalogueClient::getMarketplaceThemes();

        if (!$mpData || empty($mpData['items'])) {
            Router::json(['error' => 'Could not fetch marketplace data'], 500);
        }

        $item = null;
        foreach ($mpData['items'] as $mpItem) {
            if (($mpItem['slug'] ?? '') === $slug) {
                $item = $mpItem;
                break;
            }
        }

        if (!$item) {
            Router::json(['error' => 'Item not found'], 404);
        }

         
        $balance = CatalogueClient::getBalance();

        Router::json([
            'item' => $item,
            'balance' => $balance['credits'] ?? 0,
        ]);
    }

    

    public static function aiCompletion(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired.'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        if (!$input) {
            Router::json(['error' => 'Invalid request.'], 400);
        }

        $messages = $input['messages'] ?? [];
        if (empty($messages)) {
            Router::json(['error' => 'messages required'], 400);
        }

        $model       = $input['model'] ?? 'auto';
        $temperature = isset($input['temperature']) ? (float) $input['temperature'] : 0.7;
        $maxTokens   = isset($input['max_tokens']) ? (int) $input['max_tokens'] : null;

        $result = CatalogueClient::aiCompletion($messages, $model, $temperature, $maxTokens);

        if ($result === null) {
            Router::json(['error' => 'AI service unavailable. Check your API configuration.'], 502);
        }

        if (!empty($result['_error'])) {
            $msg = $result['error'] ?? $result['message'] ?? 'AI request failed';
            Router::json(['error' => $msg], $result['_http_code'] ?? 502);
        }

        Router::json($result);
    }

    

    public static function testAi(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Session expired.'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        if (!$input) {
            Router::json(['error' => 'Invalid request.'], 400);
        }

        $type = $input['type'] ?? '';
        if (!in_array($type, ['title', 'translate', 'desc', 'transdesc'], true)) {
            Router::json(['error' => 'Invalid type.'], 400);
        }

        $prompt = trim($input['prompt'] ?? '');
        if ($prompt === '') {
            Router::json(['error' => 'Prompt is empty.'], 400);
        }

        $title = $input['title'] ?? '';
        $description = $input['description'] ?? '';

         
        $cfg = CatalogueClient::getAiConfig() ?? [
            'ai_title_begin'     => 'Title: {title}',
            'ai_title_end'       => 'DO NOT keep series numbers! Output ONLY the new title.',
            'ai_translate_begin' => 'Title: {title}',
            'ai_translate_end'   => '',
            'ai_translate_desc_begin' => 'Description: {description}',
            'ai_translate_desc_end'   => '',
            'ai_desc_begin'      => 'Description to be rewritten: {description}',
            'ai_desc_end'        => 'Rewritten description:',
        ];

         
        if ($type === 'title') {
            $begin = str_replace('{title}', $title, $cfg['ai_title_begin'] ?? '');
            $end = $cfg['ai_title_end'] ?? '';
            $fullPrompt = trim($begin . "\n\n" . $prompt . ($end !== '' ? "\n\n" . $end : ''));
        } elseif ($type === 'translate') {
            $formatLines = trim($input['translate_format'] ?? '');
            if ($formatLines === '') {
                Router::json(['error' => 'Translation format is missing.'], 400);
            }
            $begin = str_replace('{title}', $title, $cfg['ai_translate_begin'] ?? '');
            $end = $cfg['ai_translate_end'] ?? '';
            $fullPrompt = trim($begin . "\n\n" . $prompt . "\n\n" . "Output format (XML tags required):\n" . $formatLines . ($end !== '' ? "\n\n" . $end : ''));
        } elseif ($type === 'transdesc') {
            $formatLines = trim($input['translate_format'] ?? '');
            if ($formatLines === '') {
                Router::json(['error' => 'Translation format is missing.'], 400);
            }
            if ($description === '') {
                Router::json(['error' => 'This video has no description to translate.'], 400);
            }
            $begin = str_replace('{description}', $description, $cfg['ai_translate_desc_begin'] ?? 'Description: {description}');
            $end = $cfg['ai_translate_desc_end'] ?? '';
            $fullPrompt = trim($begin . "\n\n" . $prompt . "\n\n" . "Output format (XML tags required):\n" . $formatLines . ($end !== '' ? "\n\n" . $end : ''));
        } else {
             
            if ($description === '') {
                Router::json(['error' => 'This video has no description to rewrite.'], 400);
            }
            $begin = str_replace('{description}', $description, $cfg['ai_desc_begin'] ?? '');
            $end = $cfg['ai_desc_end'] ?? '';
            $fullPrompt = trim($begin . "\n\n" . $prompt . ($end !== '' ? "\n\n" . $end : ''));
        }

        $temperature = ($type === 'translate' || $type === 'transdesc') ? 0.5 : 0.7;

         
         
         
        $expect = [];
        if ($type === 'title') {
            $expect = ['title'];
        } elseif ($type === 'desc') {
            $expect = ['desc'];
        } elseif (preg_match_all('/<([A-Za-z]{2,5})>/', $formatLines, $mm)) {
            $expect = array_map('strtoupper', $mm[1]);
        }

        $content = '';
        for ($attempt = 1; $attempt <= 4; $attempt++) {
            $result = CatalogueClient::aiCompletion(
                [['role' => 'user', 'content' => $fullPrompt]],
                'auto',
                $temperature
            );
             
            if (is_array($result) && !empty($result['_error'])) {
                $msg = $result['error'] ?? $result['message'] ?? 'AI request failed';
                Router::json(['error' => $msg], $result['_http_code'] ?? 502);
            }
            $c = is_array($result) ? ($result['choices'][0]['message']['content'] ?? '') : '';
            if ($c === '') {
                continue;
            }
            $content = $c;
            $ok = true;
            foreach ($expect as $mk) {
                if (!preg_match('/<' . preg_quote($mk, '/') . '>(.*?)<\/' . preg_quote($mk, '/') . '>/si', $c)) {
                    $ok = false;
                    break;
                }
            }
            if ($ok) {
                break;
            }
        }

        if ($content === '') {
            Router::json(['error' => 'AI service did not return a usable response. Please try again.'], 502);
        }

        Router::json(['success' => true, 'content' => $content, 'type' => $type]);
    }

    public static function saveAiPrompts(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'Invalid CSRF token'], 403);
        }
        $input = json_decode(file_get_contents('php://input'), true) ?: [];

        $keys = ['import_ai_title_prompt', 'import_ai_translate_prompt', 'import_ai_desc_prompt', 'import_ai_translate_desc_prompt'];
        foreach ($keys as $key) {
            $field = str_replace('import_ai_', '', $key);  
            if (isset($input[$field])) {
                Setting::set($key, mb_substr(trim($input[$field]), 0, 500));
            }
        }

        Router::json(['success' => true]);
    }

    public static function pauseService(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        Setting::set('catalogue_service_paused', '1');
        Setting::set('catalogue_pause_reason', 'manual');

        Router::json(['success' => true]);
    }

    public static function resumeService(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        Setting::set('catalogue_service_paused', '0');
        Setting::set('catalogue_pause_reason', '');
        Setting::set('catalogue_pause_until', '');
        Setting::set('catalogue_net_fail_count', '0');

        self::triggerTickAsync();

        Router::json(['success' => true]);
    }

    

    public static function setWorkers(): void
    {
        Auth::requireAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['error' => 'CSRF'], 403);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $n = max(1, min(3, (int) ($input['workers'] ?? 0)));
        Setting::set('catalogue_max_workers', (string) $n);

         
        CatalogueProcessor::ensureWorkersRunning();

        Router::json(['success' => true, 'workers' => $n]);
    }

    

    private static function spawnGalleryAiWorker(array $albumIds, array $aiSettings): bool
    {
        $albumIds = array_values(array_filter(array_map('intval', $albumIds), static fn($i) => $i > 0));
        if (empty($albumIds) || empty($aiSettings['enabled'])) {
            return false;
        }
        $phpBin = self::findPhpCli();
        $script = APP_ROOT . '/bin/gallery-ai-worker.php';
        if (!is_file($script) || !function_exists('exec')) {
            return false;
        }
        $tmpDir = APP_ROOT . '/storage/tmp';
        if (!is_dir($tmpDir)) {
            @mkdir($tmpDir, 0775, true);
        }
        $batchId = bin2hex(random_bytes(8));
        $jobFile = $tmpDir . '/gallery-ai-job-' . $batchId . '.json';
        $ok = @file_put_contents($jobFile, json_encode([
            'album_ids'   => $albumIds,
            'ai_settings' => $aiSettings,
            'user_id'     => Auth::id(),
        ], JSON_UNESCAPED_UNICODE));
        if ($ok === false) {
            return false;
        }
        @exec(escapeshellarg($phpBin) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($batchId) . ' > /dev/null 2>&1 &');
        return true;
    }

    private static function findPhpCli(): string
    {
        return Env::phpCli();
    }

    

    private static function triggerTickAsync(): void
    {
         
         
         
        CatalogueProcessor::kickQueue();
    }

}
