<?php

declare(strict_types=1);



class Album
{
    public const VALID_TYPES = ['hetero', 'gay', 'hentai'];
    public const DEFAULT_TYPE = 'hetero';

     
    public static function featureEnabled(): bool
    {
        return Setting::get('albums_enabled', '1') === '1';
    }

     

    public static function userFavorites(int $userId, int $limit = 24, int $offset = 0): array
    {
        $rows = Database::fetchAll(
            "SELECT a.* FROM albums a JOIN album_favorites f ON a.id = f.album_id
             WHERE f.user_id = ? AND a.status = 'published'
             ORDER BY f.created_at DESC LIMIT ? OFFSET ?",
            [$userId, $limit, $offset]
        );
        return self::localizeResults($rows);
    }

    public static function userFavoritesCount(int $userId): int
    {
        return (int) (Database::fetchOne(
            "SELECT COUNT(*) c FROM album_favorites f JOIN albums a ON a.id = f.album_id
             WHERE f.user_id = ? AND a.status = 'published'",
            [$userId]
        )['c'] ?? 0);
    }

    public static function userLiked(int $userId, int $limit = 24, int $offset = 0): array
    {
        $rows = Database::fetchAll(
            "SELECT a.* FROM albums a JOIN album_likes l ON a.id = l.album_id
             WHERE l.user_id = ? AND l.type = 'like' AND a.status = 'published'
             ORDER BY l.created_at DESC LIMIT ? OFFSET ?",
            [$userId, $limit, $offset]
        );
        return self::localizeResults($rows);
    }

    public static function userLikedCount(int $userId): int
    {
        return (int) (Database::fetchOne(
            "SELECT COUNT(*) c FROM album_likes l JOIN albums a ON a.id = l.album_id
             WHERE l.user_id = ? AND l.type = 'like' AND a.status = 'published'",
            [$userId]
        )['c'] ?? 0);
    }

     

    public static function localizeResults(array $albums): array
    {
        if (empty($albums) || Translator::locale() === Translator::getDefaultCode()) {
            return $albums;
        }
        $ids = array_column($albums, 'id');
        if (empty($ids)) {
            return $albums;
        }
        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $params = array_merge([Translator::locale()], $ids);
        $rows = Database::fetchAll(
            "SELECT album_id, title, description, slug FROM album_translations WHERE lang_code = ? AND album_id IN ({$placeholders})",
            $params
        );
        $map = [];
        foreach ($rows as $row) {
            $map[(int) $row['album_id']] = $row;
        }
        foreach ($albums as &$album) {
            $aid = (int) $album['id'];
            if (isset($map[$aid])) {
                $tr = $map[$aid];
                if (!empty($tr['title']))       $album['title'] = $tr['title'];
                if (!empty($tr['description']))  $album['description'] = $tr['description'];
                if (!empty($tr['slug']))         $album['slug'] = $tr['slug'];
            }
        }
        unset($album);
        return $albums;
    }

    public static function localize(?array $album): ?array
    {
        if ($album === null || Translator::locale() === Translator::getDefaultCode()) {
            return $album;
        }
        $row = Database::fetchOne(
            'SELECT title, description, slug FROM album_translations WHERE album_id = ? AND lang_code = ?',
            [(int) $album['id'], Translator::locale()]
        );
        if ($row) {
            if (!empty($row['title']))       $album['title'] = $row['title'];
            if (!empty($row['description']))  $album['description'] = $row['description'];
            if (!empty($row['slug']))         $album['slug'] = $row['slug'];
        }
        return $album;
    }

     

    public static function defaultType(): string { return Video::defaultType(); }
    public static function typeLabel(string $code): string { return Video::typeLabel($code); }
    public static function typeLabels(): array { return Video::typeLabels(); }
    public static function typeSlug(string $code): string { return Video::typeSlug($code); }
    public static function typeFromSlug(string $slug): string { return Video::typeFromSlug($slug); }
    public static function typeFieldOrder(string $column = 'video_type'): string { return Video::typeFieldOrder($column); }

    private static ?array $_availableTypesCache = null;

    public static function resetAvailableTypesCache(): void { self::$_availableTypesCache = null; }

     
    public static function availableTypes(): array
    {
        if (self::$_availableTypesCache !== null) {
            return self::$_availableTypesCache;
        }
        $rows = Database::fetchAll("SELECT DISTINCT video_type FROM albums WHERE status = 'published'");
        $found = array_column($rows, 'video_type');
        $default = self::defaultType();
        $ordered = [];
        if (in_array($default, $found, true)) {
            $ordered[] = $default;
        }
        foreach (self::VALID_TYPES as $t) {
            if ($t !== $default && in_array($t, $found, true)) {
                $ordered[] = $t;
            }
        }
        self::$_availableTypesCache = $ordered;
        return $ordered;
    }

     
    public static function activeType(): string
    {
        $types = self::availableTypes();
        if (count($types) < 2) {
            return '';
        }
        $routerType = Router::typeCode();
        if ($routerType !== '' && in_array($routerType, $types, true)) {
            return $routerType;
        }
        $cookie = $_COOKIE['tp_video_type'] ?? '';
        if ($cookie !== '' && in_array($cookie, $types, true)) {
            return $cookie;
        }
        return $types[0] ?? '';
    }

     

    public static function find(int $id): ?array
    {
        $album = Database::fetchOne('SELECT * FROM albums WHERE id = ?', [$id]);
        if ($album) {
            self::loadRelations($album);
        }
        return $album;
    }

    public static function findBySlug(string $slug): ?array
    {
        if (Translator::locale() !== Translator::getDefaultCode()) {
            $trans = Database::fetchOne(
                'SELECT album_id FROM album_translations WHERE slug = ? AND lang_code = ?',
                [$slug, Translator::locale()]
            );
            if ($trans) {
                $album = self::find((int) $trans['album_id']);
                return $album ? self::localize($album) : null;
            }
        }
        $album = Database::fetchOne('SELECT * FROM albums WHERE slug = ?', [$slug]);
        if ($album) {
            self::loadRelations($album);
            $album = self::localize($album);
        }
        return $album;
    }

     
    private static function loadRelations(array &$album): void
    {
        $id = (int) $album['id'];
        $rows = Database::fetchAll(
            "SELECT 'category' as _type, c.id, c.name, c.slug, c.video_type, c.album_count, c.total_views, c.thumbnail, c.parent_id, c.sort_order, c.description, c.created_at FROM album_categories c JOIN album_category_map ac ON c.id = ac.category_id WHERE ac.album_id = ?
             UNION ALL
             SELECT 'tag', t.id, t.name, t.slug, t.video_type, t.album_count, NULL, NULL, NULL, NULL, NULL, t.created_at FROM album_tags t JOIN album_tag_map at2 ON t.id = at2.tag_id WHERE at2.album_id = ?
             UNION ALL
             SELECT 'performer', p.id, p.name, p.slug, p.video_type, p.album_count, p.total_views, p.photo, NULL, NULL, p.bio, p.created_at FROM album_performers p JOIN album_performer_map ap ON p.id = ap.performer_id WHERE ap.album_id = ?
             UNION ALL
             SELECT 'channel', s.id, s.name, s.slug, s.video_type, s.album_count, s.total_views, s.logo, NULL, NULL, s.description, s.created_at FROM album_channels s JOIN album_channel_map ach ON s.id = ach.channel_id WHERE ach.album_id = ?",
            [$id, $id, $id, $id]
        );

        $album['categories'] = [];
        $album['tags'] = [];
        $album['performers'] = [];
        $album['channels'] = [];

        foreach ($rows as $row) {
            $type = $row['_type'];
            unset($row['_type']);
            $album[$type === 'category' ? 'categories' : ($type === 'tag' ? 'tags' : ($type === 'performer' ? 'performers' : 'channels'))][] = $row;
        }

        $album['categories'] = AlbumCategory::localizeResults($album['categories']);
        $album['tags']       = AlbumTag::localizeResults($album['tags']);
        $album['performers'] = AlbumPerformer::localizeResults($album['performers']);
        $album['channels']   = AlbumChannel::localizeResults($album['channels']);
    }

     
    public static function images(array $album): array
    {
        if (empty($album['images'])) {
            return [];
        }
        $arr = json_decode($album['images'], true);
        return is_array($arr) ? $arr : [];
    }

     

    public static function all(array $filters = [], int $limit = 24, int $offset = 0): array
    {
        [$joinStr, $whereStr, $params] = self::buildQuery($filters);
        $sort = self::resolveSort($filters['sort'] ?? 'newest');

        $sql = "SELECT a.* FROM albums a {$joinStr} WHERE {$whereStr} ORDER BY {$sort} LIMIT ? OFFSET ?";
        $params = array_merge($params, [$limit, $offset]);

        $results = Database::fetchAll($sql, $params);
        return empty($filters['admin']) ? self::localizeResults($results) : $results;
    }

    public static function count(array $filters = []): int
    {
        [$joinStr, $whereStr, $params] = self::buildQuery($filters);
        $result = Database::fetchOne("SELECT COUNT(DISTINCT a.id) FROM albums a {$joinStr} WHERE {$whereStr}", $params);
        return (int) ($result ? reset($result) : 0);
    }

     
    private static function buildQuery(array $filters): array
    {
        $where = ['1=1'];
        $whereParams = [];
        $joins = [];
        $joinParams = [];

        if (!empty($filters['status'])) {
            $where[] = 'a.status = ?';
            $whereParams[] = $filters['status'];
        } elseif (empty($filters['admin'])) {
            $where[] = 'a.status = ?';
            $whereParams[] = 'published';
        }

        if (!empty($filters['category_id'])) {
            $joins[] = 'INNER JOIN album_category_map ac_filter ON a.id = ac_filter.album_id AND ac_filter.category_id = ?';
            $joinParams[] = $filters['category_id'];
        }
        if (!empty($filters['tag_id'])) {
            $joins[] = 'INNER JOIN album_tag_map at_filter ON a.id = at_filter.album_id AND at_filter.tag_id = ?';
            $joinParams[] = $filters['tag_id'];
        }
        if (!empty($filters['performer_id'])) {
            $joins[] = 'INNER JOIN album_performer_map ap_filter ON a.id = ap_filter.album_id AND ap_filter.performer_id = ?';
            $joinParams[] = $filters['performer_id'];
        }
        if (!empty($filters['channel_id'])) {
            $joins[] = 'INNER JOIN album_channel_map ach_filter ON a.id = ach_filter.album_id AND ach_filter.channel_id = ?';
            $joinParams[] = $filters['channel_id'];
        }

        if (!empty($filters['search'])) {
            $where[] = 'MATCH(a.title, a.description) AGAINST(? IN BOOLEAN MODE)';
            $whereParams[] = $filters['search'];
        }
        if (!empty($filters['is_featured'])) {
            $where[] = 'a.is_featured = 1';
        }
        if (isset($filters['has_cover'])) {
            if ($filters['has_cover'] === 'yes') {
                $where[] = "(a.cover_file IS NOT NULL AND a.cover_file != '')";
            } elseif ($filters['has_cover'] === 'no') {
                $where[] = "(a.cover_file IS NULL OR a.cover_file = '')";
            }
        }
        if (!empty($filters['video_type'])) {
            $where[] = 'a.video_type = ?';
            $whereParams[] = $filters['video_type'];
        }

        return [implode("\n", $joins), implode(' AND ', $where), array_merge($joinParams, $whereParams)];
    }

    private static function resolveSort(string $sortKey): string
    {
        $allowed = [
            'newest'         => 'a.published_at DESC, a.id DESC',
            'popular'        => 'a.views DESC',
            'trending'       => 'a.views DESC',
            'most_liked'     => 'a.likes DESC',
            'top_rated'      => 'a.likes DESC',
            'oldest'         => 'a.created_at ASC',
            'most_favorited' => 'a.favorites_count DESC',
            'most_images'    => 'a.image_count DESC',
        ];
        $allowed = HookSystem::applyFilter('album.sort_options', $allowed);
        return $allowed[$sortKey] ?? $allowed['newest'];
    }

     

    public static function create(array $data): int
    {
        return Database::insert('albums', $data);
    }

    public static function update(int $id, array $data): void
    {
        Database::update('albums', $data, 'id = ?', [$id]);
    }

    /**
     * Tables holding an album_id with no foreign key. Nothing cascades them, so deleting an album
     * used to abandon its comments, its reports, its likes, its view log and its CTR rows for ever.
     */
    private const ALBUM_ORPHAN_TABLES = [
        'album_likes', 'album_view_log', 'ctr_album_context', 'ctr_album_hourly', 'ctr_album_image',
        'comments', 'reports',
    ];

    /** taxonomy table, map table, map FK column */
    private const ALBUM_TAXONOMIES = [
        ['album_categories', 'album_category_map',  'category_id'],
        ['album_tags',       'album_tag_map',       'tag_id'],
        ['album_performers', 'album_performer_map', 'performer_id'],
        ['album_channels',   'album_channel_map',   'channel_id'],
    ];

    /**
     * Delete a batch of albums with set-based SQL, the album counterpart of Video::deleteMany().
     *
     * Returns ['deleted' => int, 'assets' => [...]] so the caller purges storage after the
     * transaction commits - a gallery can hold hundreds of images and those deletes must never be
     * held open inside a database transaction.
     */
    public static function deleteMany(array $ids): array
    {
        $ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn($i) => $i > 0)));
        if (!$ids) return ['deleted' => 0, 'assets' => []];

        $ph = implode(',', array_fill(0, count($ids), '?'));
        $assets = Database::fetchAll(
            "SELECT id, album_folder, storage_id, cover_file, images FROM albums WHERE id IN ({$ph})",
            $ids
        );
        if (!$assets) return ['deleted' => 0, 'assets' => []];

        $ids = array_map('intval', array_column($assets, 'id'));
        $ph  = implode(',', array_fill(0, count($ids), '?'));

        Database::transaction(static function () use ($ids, $ph) {
            $touched = [];
            foreach (self::ALBUM_TAXONOMIES as [$table, $map, $fk]) {
                $touched[$table] = array_map('intval', array_column(
                    Database::fetchAll("SELECT DISTINCT {$fk} AS id FROM {$map} WHERE album_id IN ({$ph})", $ids),
                    'id'
                ));
            }

            foreach (self::ALBUM_ORPHAN_TABLES as $table) {
                try {
                    Database::query("DELETE FROM {$table} WHERE album_id IN ({$ph})", $ids);
                } catch (\Throwable $e) {
                    error_log("Album::deleteMany: could not clean {$table}: " . $e->getMessage());
                }
            }

            Database::query("DELETE FROM albums WHERE id IN ({$ph})", $ids);

            foreach (self::ALBUM_TAXONOMIES as [$table, $map, $fk]) {
                $tids = $touched[$table];
                if (!$tids) continue;
                $tph = implode(',', array_fill(0, count($tids), '?'));

                Database::query(
                    "DELETE FROM {$table} WHERE id IN ({$tph})
                       AND NOT EXISTS (SELECT 1 FROM {$map} WHERE {$map}.{$fk} = {$table}.id)",
                    $tids
                );
                Database::query(
                    "UPDATE {$table} x SET x.album_count = (
                         SELECT COUNT(*) FROM {$map} m JOIN albums a ON a.id = m.album_id
                          WHERE m.{$fk} = x.id AND a.status = 'published')
                      WHERE x.id IN ({$tph})",
                    $tids
                );
            }
        });

        return ['deleted' => count($ids), 'assets' => $assets];
    }

    /**
     * Remote paths of every file a batch of albums owns, grouped by storage id, so the whole batch
     * is purged in as few requests as possible instead of one request per image.
     *
     * @return array<int, string[]> storage id => remote paths
     */
    public static function assetPaths(array $assets): array
    {
        $byStorage = [];
        foreach ($assets as $album) {
            $folder = (string) ($album['album_folder'] ?? '');
            if ($folder === '' || !preg_match('/^[a-f0-9]+$/i', $folder)) continue;
            $sid = (int) ($album['storage_id'] ?? 0);

            $files = [];
            $cover = (string) ($album['cover_file'] ?? '');
            if ($cover !== '' && !str_starts_with($cover, 'http')) $files[] = $cover;
            foreach (self::images($album) as $im) {
                $f = (string) ($im['f'] ?? '');
                if ($f !== '' && !str_starts_with($f, 'http')) $files[] = $f;
            }
            foreach (array_unique($files) as $f) {
                if (preg_match('/^[A-Za-z0-9_.-]+$/', $f) && !str_contains($f, '..')) {
                    $byStorage[$sid][] = 'uploads/albums/' . $folder . '/' . $f;
                }
            }
        }
        return $byStorage;
    }

    public static function delete(int $id): void
    {
        $result = self::deleteMany([$id]);
        foreach (self::assetPaths($result['assets']) as $storageId => $paths) {
            MediaStorage::deleteRemoteKeys($paths, $storageId > 0 ? $storageId : null);
        }
        foreach ($result['assets'] as $album) {
            $folder = (string) ($album['album_folder'] ?? '');
            if ($folder !== '' && class_exists('AlbumAsset')) AlbumAsset::purgeLocalFolder($folder);
        }
    }

    private static function cleanupOrphansAndUpdateCounts(string $entityTable, string $mapTable, string $fkColumn, array $ids, string $modelClass): void
    {
        if (empty($ids)) {
            return;
        }
        $ids = array_map('intval', $ids);
        $ph = implode(',', array_fill(0, count($ids), '?'));

        Database::query(
            "DELETE FROM {$entityTable} WHERE id IN ({$ph})
               AND id NOT IN (SELECT DISTINCT {$fkColumn} FROM {$mapTable})",
            $ids
        );

        $survivors = Database::fetchAll("SELECT id FROM {$entityTable} WHERE id IN ({$ph})", $ids);
        foreach ($survivors as $row) {
            $modelClass::updateCounts((int) $row['id']);
        }
    }

    public static function syncCategories(int $id, array $categoryIds): void { self::syncRelation($id, $categoryIds, 'album_category_map', 'category_id', AlbumCategory::class); }
    public static function syncTags(int $id, array $tagIds): void { self::syncRelation($id, $tagIds, 'album_tag_map', 'tag_id', AlbumTag::class); }
    public static function syncChannels(int $id, array $channelIds): void { self::syncRelation($id, $channelIds, 'album_channel_map', 'channel_id', AlbumChannel::class); }

    public static function syncPerformers(int $id, array $performerIds): void
    {
        self::syncRelation($id, $performerIds, 'album_performer_map', 'performer_id', AlbumPerformer::class);
        self::refreshPerformerName($id);
    }

    private static function syncRelation(int $id, array $entityIds, string $pivot, string $fk, string $modelClass): void
    {
        $entityIds = array_values(array_unique(array_map('intval', $entityIds)));
        Database::transaction(function () use ($id, $entityIds, $pivot, $fk, $modelClass) {
            $oldIds = array_column(Database::fetchAll("SELECT {$fk} FROM {$pivot} WHERE album_id = ?", [$id]), $fk);
            Database::delete($pivot, 'album_id = ?', [$id]);
            foreach ($entityIds as $eid) {
                Database::insert($pivot, ['album_id' => $id, $fk => (int) $eid]);
            }
            $affected = array_unique(array_merge(array_map('intval', $oldIds), $entityIds));
            foreach ($affected as $eid) {
                $modelClass::updateCounts($eid);
            }
        });
    }

    public static function refreshPerformerName(int $id): void
    {
        $row = Database::fetchOne(
            'SELECT MIN(p.name) as pname FROM album_performer_map ap JOIN album_performers p ON ap.performer_id = p.id WHERE ap.album_id = ?',
            [$id]
        );
        Database::query('UPDATE albums SET performer_name = ? WHERE id = ?', [$row['pname'] ?? null, $id]);
    }

    public static function refreshFavoritesCount(int $id): void
    {
        $cnt = Database::count('album_favorites', 'album_id = ?', [$id]);
        Database::query('UPDATE albums SET favorites_count = ? WHERE id = ?', [$cnt, $id]);
    }

    public static function incrementViews(int $id): void
    {
        $hash = md5(RateLimiter::ip() . RateLimiter::fingerprint());
        $window = 30;
        $stmt = Database::query(
            'INSERT INTO album_view_log (album_id, viewer_hash, viewed_at)
             VALUES (?, UNHEX(?), NOW())
             ON DUPLICATE KEY UPDATE
                viewed_at = IF(viewed_at <= DATE_SUB(NOW(), INTERVAL ? MINUTE), NOW(), viewed_at)',
            [$id, $hash, $window]
        );
        if ($stmt->rowCount() > 0) {
            Database::query('UPDATE albums SET views = views + 1, recent_views = recent_views + 1 WHERE id = ?', [$id]);
        }
        if (random_int(1, 100) === 1) {
            Database::query('DELETE FROM album_view_log WHERE viewed_at < DATE_SUB(NOW(), INTERVAL 2 HOUR) LIMIT 10000');
        }
    }

    public static function getCategories(int $id): array { return Database::fetchAll('SELECT c.* FROM album_categories c JOIN album_category_map ac ON c.id = ac.category_id WHERE ac.album_id = ?', [$id]); }
    public static function getTags(int $id): array { return Database::fetchAll('SELECT t.* FROM album_tags t JOIN album_tag_map at2 ON t.id = at2.tag_id WHERE at2.album_id = ?', [$id]); }
    public static function getPerformers(int $id): array { return Database::fetchAll('SELECT p.* FROM album_performers p JOIN album_performer_map ap ON p.id = ap.performer_id WHERE ap.album_id = ?', [$id]); }
    public static function getChannels(int $id): array { return Database::fetchAll('SELECT s.* FROM album_channels s JOIN album_channel_map ach ON s.id = ach.channel_id WHERE ach.album_id = ?', [$id]); }

     

    public static function similar(int $id, int $limit = 12): array
    {
        $override = HookSystem::applyFilter('album.similar', null, $id, $limit);
        if ($override !== null) {
            return self::localizeResults($override);
        }
        $catIds = array_column(Database::fetchAll('SELECT category_id FROM album_category_map WHERE album_id = ?', [$id]), 'category_id');
        $tagIds = array_column(Database::fetchAll('SELECT tag_id FROM album_tag_map WHERE album_id = ?', [$id]), 'tag_id');
        if (empty($catIds) && empty($tagIds)) {
            return [];
        }
        $scoreParts = [];
        $scoreParams = [];
        if (!empty($catIds)) {
            $ph = implode(',', array_fill(0, count($catIds), '?'));
            $scoreParts[] = "SELECT album_id, COUNT(DISTINCT category_id) AS score
                         FROM album_category_map WHERE category_id IN ({$ph}) GROUP BY album_id";
            $scoreParams = array_merge($scoreParams, $catIds);
        }
        if (!empty($tagIds)) {
            $ph = implode(',', array_fill(0, count($tagIds), '?'));
            $scoreParts[] = "SELECT album_id, COUNT(DISTINCT tag_id) AS score
                         FROM album_tag_map WHERE tag_id IN ({$ph}) GROUP BY album_id";
            $scoreParams = array_merge($scoreParams, $tagIds);
        }
        $scoreUnion = implode("\n                         UNION ALL\n                         ", $scoreParts);
        $params = array_merge($scoreParams, ['published', $id], [$limit]);
        return self::localizeResults(Database::fetchAll(
            "SELECT a.*, k._relevance
             FROM (
                 SELECT m.album_id, m._relevance, asrc.views
                 FROM (
                     SELECT album_id, SUM(score) AS _relevance
                     FROM (
                         {$scoreUnion}
                     ) s
                     GROUP BY album_id
                 ) m
                 JOIN albums asrc ON asrc.id = m.album_id
                 WHERE asrc.status = ? AND asrc.id != ?
                 ORDER BY m._relevance DESC, asrc.views DESC
                 LIMIT ?
             ) k
             JOIN albums a ON a.id = k.album_id
             ORDER BY k._relevance DESC, k.views DESC",
            $params
        ));
    }

    public static function recommended(int $excludeId, array $excludeIds = [], int $limit = 12): array
    {
        $override = HookSystem::applyFilter('album.recommended', null, $excludeId, $excludeIds, $limit);
        if ($override !== null) {
            return self::localizeResults($override);
        }
        $allExclude = array_merge([$excludeId], $excludeIds);
        $placeholders = implode(',', array_fill(0, count($allExclude), '?'));
        $range = Database::fetchOne("SELECT MIN(id) as min_id, MAX(id) as max_id FROM albums WHERE status = ?", ['published']);
        if (!$range || !$range['min_id']) {
            return [];
        }
        $randId = random_int((int) $range['min_id'], (int) $range['max_id']);
        $params = array_merge($allExclude, ['published', $randId, $limit]);
        $results = Database::fetchAll(
            "SELECT a.* FROM albums a WHERE a.id NOT IN ({$placeholders}) AND a.status = ? AND a.id >= ? ORDER BY a.id LIMIT ?",
            $params
        );
        if (count($results) < $limit) {
            $gotIds = array_column($results, 'id');
            $allExclude2 = array_merge($allExclude, $gotIds);
            $ph2 = implode(',', array_fill(0, count($allExclude2), '?'));
            $remaining = $limit - count($results);
            $more = Database::fetchAll(
                "SELECT a.* FROM albums a WHERE a.id NOT IN ({$ph2}) AND a.status = ? ORDER BY a.id LIMIT ?",
                array_merge($allExclude2, ['published', $remaining])
            );
            $results = array_merge($results, $more);
        }
        return self::localizeResults($results);
    }

    public static function trending(int $limit = 12): array
    {
        $override = HookSystem::applyFilter('album.trending', null, $limit);
        if ($override !== null) {
            return self::localizeResults($override);
        }
        return self::localizeResults(Database::fetchAll(
            "SELECT a.* FROM albums a WHERE a.status = ? AND a.published_at > DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY a.views DESC LIMIT ?",
            ['published', $limit]
        ));
    }

    public static function translations(int $albumId): array
    {
        $rows = Database::fetchAll('SELECT lang_code, title, slug FROM album_translations WHERE album_id = ? ORDER BY lang_code', [$albumId]);
        $result = [];
        foreach ($rows as $row) {
            $result[$row['lang_code']] = $row;
        }
        return $result;
    }
}
