<?php
namespace App\Services;

use App\Core\Database;
use RuntimeException;
use PDO;
use Throwable;

final class PhotoVotingService
{
    public function registerPhoto(int $walkId, int $userId, string $title, string $caption, array $stored, array $locationMeta = [], ?int $replacePhotoId = null): int
    {
        $pdo = Database::connection();
        $oldFiles = [];

        $pdo->beginTransaction();
        try {
            $this->assertPhotoUploadAllowed($walkId, $userId);

            $settings = $this->contestSettings($walkId);
            $limit = $this->maxPhotosPerParticipant($settings);
            $existingPhotos = $this->userPhotosForWalk($pdo, $walkId, $userId, true);

            if (count($existingPhotos) >= $limit) {
                if ($replacePhotoId === null || $replacePhotoId <= 0) {
                    throw new RuntimeException('This walk allows ' . $limit . ' photo' . ($limit === 1 ? '' : 's') . ' per participant. Please confirm replacement before uploading another photo.');
                }

                $replacePhoto = null;
                foreach ($existingPhotos as $photo) {
                    if ((int)$photo['id'] === $replacePhotoId) {
                        $replacePhoto = $photo;
                        break;
                    }
                }

                if (!$replacePhoto) {
                    throw new RuntimeException('The photo selected for replacement was not found for your account. Refresh the page and try again.');
                }

                if (count($existingPhotos) > $limit) {
                    throw new RuntimeException('You already have more photos than the current walk limit. Ask an admin to delete old photos, then upload again.');
                }

                $oldFiles[] = [
                    'file_path' => (string)($replacePhoto['file_path'] ?? ''),
                    'thumbnail_path' => (string)($replacePhoto['thumbnail_path'] ?? ''),
                ];
                $this->deletePhotoRecordRows($pdo, (int)$replacePhoto['id']);
            }

            $requiresApproval = (bool)($GLOBALS['config']['features']['photo_admin_approval_required'] ?? true);
            $status = $requiresApproval ? 'pending' : 'approved';
            $location = $this->normalizePhotoLocation($locationMeta);

            $columns = ['walk_id', 'user_id', 'title', 'caption', 'file_path', 'thumbnail_path', 'status', 'votes_count'];
            $placeholders = ['?', '?', '?', '?', '?', '?', '?', '0'];
            $values = [$walkId, $userId, $title, $caption, $stored['file_path'], $stored['thumbnail_path'], $status];

            if ($location && $this->columnExists('walk_photos', 'photo_latitude') && $this->columnExists('walk_photos', 'photo_longitude')) {
                $columns[] = 'photo_latitude';
                $columns[] = 'photo_longitude';
                $placeholders[] = '?';
                $placeholders[] = '?';
                $values[] = $location['latitude'];
                $values[] = $location['longitude'];

                if ($this->columnExists('walk_photos', 'photo_location_accuracy')) {
                    $columns[] = 'photo_location_accuracy';
                    $placeholders[] = '?';
                    $values[] = $location['accuracy'];
                }
                if ($this->columnExists('walk_photos', 'photo_location_source')) {
                    $columns[] = 'photo_location_source';
                    $placeholders[] = '?';
                    $values[] = $location['source'];
                }
                if ($this->columnExists('walk_photos', 'photo_location_captured_at')) {
                    $columns[] = 'photo_location_captured_at';
                    $placeholders[] = '?';
                    $values[] = $location['captured_at'];
                }
            }

            $columns = array_merge($columns, ['uploaded_at', 'created_at', 'updated_at']);
            $placeholders = array_merge($placeholders, ['NOW()', 'NOW()', 'NOW()']);

            $sql = 'INSERT INTO walk_photos (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $placeholders) . ')';
            $stmt = $pdo->prepare($sql);
            $stmt->execute($values);
            $newPhotoId = (int)$pdo->lastInsertId();

            $pdo->commit();
            foreach ($oldFiles as $oldFile) {
                (new UploadService())->deleteStoredPhoto($oldFile);
            }
            return $newPhotoId;
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            throw $e;
        }
    }

    public function uploadPolicy(int $walkId, int $userId): array
    {
        $settings = $this->contestSettings($walkId);
        $limit = $this->maxPhotosPerParticipant($settings);
        $photos = $this->userPhotosForWalk(Database::connection(), $walkId, $userId, false);
        $count = count($photos);
        $remaining = max(0, $limit - $count);

        return [
            'max_photos_per_participant' => $limit,
            'current_count' => $count,
            'remaining' => $remaining,
            'limit_reached' => $count >= $limit,
            'can_upload_more' => $count < $limit,
            'existing_photos' => array_map(static function (array $photo): array {
                return [
                    'id' => (int)$photo['id'],
                    'title' => (string)($photo['title'] ?? 'Walk photo'),
                    'status' => (string)($photo['status'] ?? ''),
                    'thumbnail_path' => (string)($photo['thumbnail_path'] ?? ''),
                    'uploaded_at' => (string)($photo['uploaded_at'] ?? ''),
                ];
            }, $photos),
            'message' => $count >= $limit
                ? 'Photo limit reached for this walk. Uploading another photo will require replacing an existing one.'
                : 'You can upload ' . $remaining . ' more photo' . ($remaining === 1 ? '' : 's') . ' for this walk.',
        ];
    }

    public function deletePhoto(int $photoId): array
    {
        $pdo = Database::connection();
        $pdo->beginTransaction();
        try {
            $stmt = $pdo->prepare('SELECT id, walk_id, user_id, title, file_path, thumbnail_path FROM walk_photos WHERE id = ? LIMIT 1 FOR UPDATE');
            $stmt->execute([$photoId]);
            $photo = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$photo) {
                throw new RuntimeException('Photo not found.');
            }

            $this->deletePhotoRecordRows($pdo, $photoId);
            $pdo->commit();

            (new UploadService())->deleteStoredPhoto([
                'file_path' => (string)($photo['file_path'] ?? ''),
                'thumbnail_path' => (string)($photo['thumbnail_path'] ?? ''),
            ]);

            return $photo;
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            throw $e;
        }
    }

    public function assertPhotoUploadAllowed(int $walkId, int $userId): void
    {
        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM attendance WHERE walk_id = ? AND user_id = ? AND status IN ("checked_in", "started", "finished", "manual_verified", "partially_completed")');
        $stmt->execute([$walkId, $userId]);
        if ((int)$stmt->fetchColumn() === 0) {
            throw new RuntimeException('Only attendees who checked in can upload photos for this walk.');
        }
    }

    public function vote(int $photoId, int $userId): void
    {
        $pdo = Database::connection();
        $pdo->beginTransaction();
        try {
            $photoStmt = $pdo->prepare('SELECT p.*, w.voting_status AS legacy_voting_status, w.voting_deadline AS legacy_voting_deadline FROM walk_photos p JOIN walks w ON w.id = p.walk_id WHERE p.id = ? AND p.status IN ("approved", "winner") FOR UPDATE');
            $photoStmt->execute([$photoId]);
            $photo = $photoStmt->fetch(PDO::FETCH_ASSOC);
            if (!$photo) {
                throw new RuntimeException('Photo not found or not approved.');
            }

            $settings = $this->contestSettings((int)$photo['walk_id']);
            $this->assertCanVote($settings, $photo, $userId);

            // One clean rule for Pyrk: one vote per member per walk contest.
            // This avoids the MySQL index error and matches per-walk voting permissions.
            $pdo->prepare('DELETE FROM photo_votes WHERE walk_id = ? AND user_id = ?')->execute([(int)$photo['walk_id'], $userId]);

            $insert = $pdo->prepare('INSERT INTO photo_votes (photo_id, walk_id, user_id, created_at) VALUES (?, ?, ?, NOW())');
            $insert->execute([$photoId, (int)$photo['walk_id'], $userId]);
            $this->recountVotes((int)$photo['walk_id']);
            $pdo->commit();
        } catch (Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }
    }

    public function contestSettings(int $walkId): array
    {
        $pdo = Database::connection();
        $legacy = [
            'id' => null,
            'walk_id' => $walkId,
            'voting_status' => 'open',
            'voting_permission' => 'walk_registered',
            'vote_mode' => 'one_per_contest',
            'allow_self_vote' => 1,
            'show_photo_owner_name' => 0,
            'max_photos_per_participant' => 1,
            'voting_deadline' => null,
            'rules_note' => '',
            'groups' => [],
            'permission_label' => 'Walk registrants',
            'vote_mode_label' => 'One vote per contest',
            'can_change_rules' => $this->tableExists('photo_contests'),
        ];

        $walkStmt = $pdo->prepare('SELECT id, title, voting_status, voting_deadline FROM walks WHERE id = ? LIMIT 1');
        $walkStmt->execute([$walkId]);
        $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
        if ($walk) {
            $legacy['walk_title'] = $walk['title'] ?? '';
            $legacy['voting_status'] = $walk['voting_status'] ?: 'open';
            $legacy['voting_deadline'] = $walk['voting_deadline'] ?: null;
        }

        if (!$this->tableExists('photo_contests')) {
            return $this->withLabels($legacy);
        }

        $stmt = $pdo->prepare('SELECT * FROM photo_contests WHERE walk_id = ? LIMIT 1');
        $stmt->execute([$walkId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return $this->withLabels($legacy);
        }

        $settings = array_merge($legacy, $row);
        $settings['groups'] = [];
        if ($this->tableExists('photo_contest_groups') && !empty($row['id'])) {
            $groups = $pdo->prepare('SELECT g.id, g.group_name, g.description, g.status, (SELECT COUNT(*) FROM member_group_members mgm WHERE mgm.group_id = g.id) AS member_count FROM photo_contest_groups pcg JOIN member_groups g ON g.id = pcg.group_id WHERE pcg.contest_id = ? ORDER BY g.group_name ASC');
            $groups->execute([(int)$row['id']]);
            $settings['groups'] = $groups->fetchAll(PDO::FETCH_ASSOC);
        }

        return $this->withLabels($settings);
    }

    public function saveContestSettings(int $walkId, array $settings, int $adminId): array
    {
        $pdo = Database::connection();
        if (!$this->tableExists('photo_contests')) {
            throw new RuntimeException('Photo contest tables are missing. Run database/migrations/010_photo_voting_per_walk_rules_safe.sql first.');
        }

        $status = (string)($settings['voting_status'] ?? 'open');
        $permission = (string)($settings['voting_permission'] ?? 'walk_registered');
        $voteMode = 'one_per_contest'; // Safe mode: one vote per member per walk/contest.
        $deadline = trim((string)($settings['voting_deadline'] ?? '')) ?: null;
        $allowSelfVote = !empty($settings['allow_self_vote']) ? 1 : 0;
        $showOwnerName = !empty($settings['show_photo_owner_name']) ? 1 : 0;
        $maxPhotos = $this->maxPhotosPerParticipant(['max_photos_per_participant' => $settings['max_photos_per_participant'] ?? 1]);
        $rulesNote = trim((string)($settings['rules_note'] ?? ''));
        $groupIds = $this->normalizeIdList($settings['group_ids'] ?? []);

        if (!in_array($status, ['open', 'closed'], true)) $status = 'open';
        if (!in_array($permission, ['any_member', 'walk_registered', 'walk_checked_in', 'selected_groups', 'closed'], true)) $permission = 'walk_registered';
                if ($permission === 'closed') $status = 'closed';

        $pdo->beginTransaction();
        try {
            if ($this->columnExists('photo_contests', 'show_photo_owner_name')) {
                $stmt = $pdo->prepare('INSERT INTO photo_contests (walk_id, voting_status, voting_permission, vote_mode, allow_self_vote, show_photo_owner_name, voting_deadline, rules_note, created_by, updated_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE voting_status = VALUES(voting_status), voting_permission = VALUES(voting_permission), vote_mode = VALUES(vote_mode), allow_self_vote = VALUES(allow_self_vote), show_photo_owner_name = VALUES(show_photo_owner_name), voting_deadline = VALUES(voting_deadline), rules_note = VALUES(rules_note), updated_by = VALUES(updated_by), updated_at = NOW()');
                $stmt->execute([$walkId, $status, $permission, $voteMode, $allowSelfVote, $showOwnerName, $deadline, $rulesNote, $adminId, $adminId]);
            } else {
                $stmt = $pdo->prepare('INSERT INTO photo_contests (walk_id, voting_status, voting_permission, vote_mode, allow_self_vote, voting_deadline, rules_note, created_by, updated_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE voting_status = VALUES(voting_status), voting_permission = VALUES(voting_permission), vote_mode = VALUES(vote_mode), allow_self_vote = VALUES(allow_self_vote), voting_deadline = VALUES(voting_deadline), rules_note = VALUES(rules_note), updated_by = VALUES(updated_by), updated_at = NOW()');
                $stmt->execute([$walkId, $status, $permission, $voteMode, $allowSelfVote, $deadline, $rulesNote, $adminId, $adminId]);
            }

            if ($this->columnExists('photo_contests', 'max_photos_per_participant')) {
                $pdo->prepare('UPDATE photo_contests SET max_photos_per_participant = ?, updated_by = ?, updated_at = NOW() WHERE walk_id = ?')->execute([$maxPhotos, $adminId, $walkId]);
            }

            $contestId = (int)$pdo->query('SELECT id FROM photo_contests WHERE walk_id = ' . (int)$walkId . ' LIMIT 1')->fetchColumn();
            if ($contestId <= 0) {
                throw new RuntimeException('Could not save contest settings.');
            }

            if ($this->tableExists('photo_contest_groups')) {
                $pdo->prepare('DELETE FROM photo_contest_groups WHERE contest_id = ?')->execute([$contestId]);
                if ($permission === 'selected_groups') {
                    $insert = $pdo->prepare('INSERT INTO photo_contest_groups (contest_id, group_id, created_at) VALUES (?, ?, NOW())');
                    foreach ($groupIds as $groupId) {
                        $insert->execute([$contestId, $groupId]);
                    }
                }
            }

            // Keep legacy walk fields synced for older screens and cron jobs.
            $pdo->prepare('UPDATE walks SET voting_status = ?, voting_deadline = ?, updated_at = NOW() WHERE id = ?')->execute([$status, $deadline, $walkId]);
            $pdo->commit();
            return $this->contestSettings($walkId);
        } catch (Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }
    }

    public function setStatus(int $photoId, string $status, int $adminId): void
    {
        if (!in_array($status, ['approved', 'rejected'], true)) throw new RuntimeException('Invalid status.');
        $pdo = Database::connection();
        $stmt = $pdo->prepare('UPDATE walk_photos SET status = ?, approved_by = ?, approved_at = NOW(), updated_at = NOW() WHERE id = ?');
        $stmt->execute([$status, $adminId, $photoId]);
    }

    public function closeContest(int $walkId): ?array
    {
        $pdo = Database::connection();
        $pdo->beginTransaction();
        try {
            $this->recountVotes($walkId);
            $stmt = $pdo->prepare('SELECT * FROM walk_photos WHERE walk_id = ? AND status = "approved" ORDER BY votes_count DESC, uploaded_at ASC LIMIT 1');
            $stmt->execute([$walkId]);
            $winner = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
            $pdo->prepare('UPDATE walks SET voting_status = "closed", updated_at = NOW() WHERE id = ?')->execute([$walkId]);
            if ($this->tableExists('photo_contests')) {
                $pdo->prepare('INSERT INTO photo_contests (walk_id, voting_status, voting_permission, vote_mode, allow_self_vote, created_at, updated_at) VALUES (?, "closed", "closed", "one_per_contest", 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE voting_status = "closed", voting_permission = "closed", updated_at = NOW()')->execute([$walkId]);
            }
            if ($winner) {
                $pdo->prepare('UPDATE walk_photos SET status = "winner", updated_at = NOW() WHERE id = ?')->execute([(int)$winner['id']]);
                (new LoyaltyService())->awardPhotoWinner((int)$winner['user_id'], $walkId);
            }
            $pdo->commit();
            return $winner;
        } catch (Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }
    }

    public function reopenContest(int $walkId): void
    {
        $pdo = Database::connection();
        $pdo->prepare('UPDATE walks SET voting_status = "open", updated_at = NOW() WHERE id = ?')->execute([$walkId]);
        if ($this->tableExists('photo_contests')) {
            $pdo->prepare('INSERT INTO photo_contests (walk_id, voting_status, voting_permission, vote_mode, allow_self_vote, created_at, updated_at) VALUES (?, "open", "walk_registered", "one_per_contest", 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE voting_status = "open", voting_permission = IF(voting_permission = "closed", "walk_registered", voting_permission), updated_at = NOW()')->execute([$walkId]);
        }
    }

    public function userCanVoteMeta(int $walkId, int $userId): array
    {
        try {
            $settings = $this->contestSettings($walkId);
            $fakePhoto = ['walk_id' => $walkId, 'user_id' => -1];
            $this->assertCanVote($settings, $fakePhoto, $userId, false);
            return ['can_vote' => true, 'reason' => 'Voting is available.', 'settings' => $settings];
        } catch (Throwable $e) {
            return ['can_vote' => false, 'reason' => $e->getMessage(), 'settings' => $this->contestSettings($walkId)];
        }
    }

    private function assertCanVote(array $settings, array $photo, int $userId, bool $checkSelfVote = true): void
    {
        $pdo = Database::connection();
        $walkId = (int)$photo['walk_id'];

        if (($settings['voting_status'] ?? 'open') === 'closed' || ($settings['voting_permission'] ?? '') === 'closed') {
            throw new RuntimeException('Voting is closed for this photo contest.');
        }
        if (!empty($settings['voting_deadline']) && strtotime((string)$settings['voting_deadline']) < time()) {
            throw new RuntimeException('Voting deadline has passed.');
        }
        if ($checkSelfVote && empty($settings['allow_self_vote']) && (int)$photo['user_id'] === $userId) {
            throw new RuntimeException('Voting for your own photo is disabled for this contest.');
        }

        $permission = (string)($settings['voting_permission'] ?? 'walk_registered');
        if ($permission === 'any_member') {
            $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE id = ? AND status = "active"');
            $stmt->execute([$userId]);
            if ((int)$stmt->fetchColumn() === 0) throw new RuntimeException('Only active logged-in users can vote.');
            return;
        }

        if ($permission === 'walk_checked_in') {
            $stmt = $pdo->prepare('SELECT COUNT(*) FROM attendance WHERE walk_id = ? AND user_id = ? AND status IN ("checked_in", "started", "finished", "manual_verified", "partially_completed")');
            $stmt->execute([$walkId, $userId]);
            if ((int)$stmt->fetchColumn() === 0) throw new RuntimeException('Only checked-in attendees can vote in this contest.');
            return;
        }

        if ($permission === 'selected_groups') {
            if (!$this->tableExists('photo_contests') || !$this->tableExists('photo_contest_groups')) {
                throw new RuntimeException('Group voting is not configured.');
            }
            $stmt = $pdo->prepare('SELECT COUNT(*) FROM photo_contests pc JOIN photo_contest_groups pcg ON pcg.contest_id = pc.id JOIN member_group_members mgm ON mgm.group_id = pcg.group_id WHERE pc.walk_id = ? AND mgm.user_id = ?');
            $stmt->execute([$walkId, $userId]);
            if ((int)$stmt->fetchColumn() === 0) throw new RuntimeException('Only members of the selected voting groups can vote.');
            return;
        }

        $stmt = $pdo->prepare('SELECT COUNT(*) FROM walk_registrations WHERE walk_id = ? AND user_id = ? AND registration_status IN ("registered", "approved")');
        $stmt->execute([$walkId, $userId]);
        if ((int)$stmt->fetchColumn() === 0) {
            throw new RuntimeException('Only registered members can vote in this walk contest.');
        }
    }

    private function recountVotes(int $walkId): void
    {
        $pdo = Database::connection();
        $pdo->prepare('UPDATE walk_photos p SET votes_count = (SELECT COUNT(*) FROM photo_votes v WHERE v.photo_id = p.id) WHERE p.walk_id = ?')->execute([$walkId]);
    }

    private function normalizeIdList(mixed $value): array
    {
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) $value = $decoded;
            else $value = preg_split('/[,\s]+/', $value) ?: [];
        }
        if (!is_array($value)) return [];
        $ids = [];
        foreach ($value as $item) {
            $id = (int)$item;
            if ($id > 0) $ids[$id] = $id;
        }
        return array_values($ids);
    }


    private function maxPhotosPerParticipant(array $settings): int
    {
        $limit = (int)($settings['max_photos_per_participant'] ?? 1);
        if ($limit < 1) return 1;
        if ($limit > 50) return 50;
        return $limit;
    }

    private function userPhotosForWalk(PDO $pdo, int $walkId, int $userId, bool $forUpdate = false): array
    {
        $sql = 'SELECT id, title, status, file_path, thumbnail_path, uploaded_at FROM walk_photos WHERE walk_id = ? AND user_id = ? ORDER BY uploaded_at DESC, id DESC';
        if ($forUpdate) $sql .= ' FOR UPDATE';
        $stmt = $pdo->prepare($sql);
        $stmt->execute([$walkId, $userId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function deletePhotoRecordRows(PDO $pdo, int $photoId): void
    {
        $pdo->prepare('DELETE FROM photo_votes WHERE photo_id = ?')->execute([$photoId]);
        $pdo->prepare('DELETE FROM walk_photos WHERE id = ?')->execute([$photoId]);
    }

    private function normalizePhotoLocation(array $meta): ?array
    {
        $lat = $this->floatInRange($meta['latitude'] ?? $meta['lat'] ?? null, -90, 90);
        $lng = $this->floatInRange($meta['longitude'] ?? $meta['lng'] ?? $meta['lon'] ?? null, -180, 180);
        if ($lat === null || $lng === null) return null;

        $accuracy = $this->floatInRange($meta['accuracy'] ?? null, 0, 100000);
        $source = preg_replace('/[^a-z0-9_ -]/i', '', (string)($meta['source'] ?? 'browser_geolocation')) ?: 'browser_geolocation';
        $source = substr(str_replace(' ', '_', strtolower($source)), 0, 40);

        $capturedAt = null;
        $rawCapturedAt = trim((string)($meta['captured_at'] ?? ''));
        if ($rawCapturedAt !== '') {
            $ts = strtotime($rawCapturedAt);
            if ($ts !== false) $capturedAt = date('Y-m-d H:i:s', $ts);
        }

        return [
            'latitude' => $lat,
            'longitude' => $lng,
            'accuracy' => $accuracy,
            'source' => $source,
            'captured_at' => $capturedAt,
        ];
    }

    private function floatInRange(mixed $value, float $min, float $max): ?float
    {
        if ($value === null || $value === '') return null;
        $value = str_replace(',', '.', (string)$value);
        if (!is_numeric($value)) return null;
        $float = (float)$value;
        if ($float < $min || $float > $max) return null;
        return round($float, 7);
    }

    private function withLabels(array $settings): array
    {
        $permissionLabels = [
            'any_member' => 'Anyone logged in',
            'walk_registered' => 'Walk registrants',
            'walk_checked_in' => 'Checked-in attendees',
            'selected_groups' => 'Selected groups only',
            'closed' => 'Closed',
        ];
        $modeLabels = [
            'one_per_contest' => 'One vote per walk contest',
        ];
        $settings['permission_label'] = $permissionLabels[$settings['voting_permission'] ?? 'walk_registered'] ?? 'Walk registrants';
        $settings['vote_mode_label'] = $modeLabels[$settings['vote_mode'] ?? 'one_per_contest'] ?? 'One vote per contest';
        $settings['allow_self_vote'] = (int)($settings['allow_self_vote'] ?? 0);
        $settings['show_photo_owner_name'] = (int)($settings['show_photo_owner_name'] ?? 0);
        $settings['max_photos_per_participant'] = $this->maxPhotosPerParticipant($settings);
        $settings['photo_upload_limit_label'] = $settings['max_photos_per_participant'] . ' photo' . ($settings['max_photos_per_participant'] === 1 ? '' : 's') . ' per participant';
        $settings['owner_name_visibility_label'] = !empty($settings['show_photo_owner_name']) ? 'Owner names visible' : 'Owner names masked';
        return $settings;
    }


    private function columnExists(string $table, string $column): bool
    {
        static $cache = [];
        $key = $table . '.' . $column;
        if (array_key_exists($key, $cache)) return $cache[$key];
        try {
            $pdo = Database::connection();
            $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?');
            $stmt->execute([$table, $column]);
            return $cache[$key] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable) {
            return $cache[$key] = false;
        }
    }

    private function tableExists(string $table): bool
    {
        static $cache = [];
        if (array_key_exists($table, $cache)) return $cache[$table];
        try {
            $pdo = Database::connection();
            $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?');
            $stmt->execute([$table]);
            return $cache[$table] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable) {
            return $cache[$table] = false;
        }
    }
}
