<?php
declare(strict_types=1);

/**
 * Main Entry Point
 * Handles routing and page rendering
 */

// Start output buffering
ob_start();

// Load bootstrap
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/serial-scene-handlers.php';
require_once __DIR__ . '/../includes/auth-helpers.php';

use App\Auth;
use App\Database;
use App\AgeVerification;
use App\LoginThrottle;
use App\LoginIpThrottle;
use App\UserTracking;
use App\CmsService;
use App\SiteManager;
use App\CmsDatabase;
use App\UserAgeVerifyLog;
use App\YotiAgeVerification;
use App\PasswordResetService;
use App\VerotelFlexPay;
// Get global Smarty instance
global $smarty;

// Get the request URI and parse it
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$requestUri = parse_url($requestUri, PHP_URL_PATH);

// Remove query string if present
$path = explode('?', $requestUri)[0];
$path = trim($path, '/');

// Simple routing
$routes = [
    '' => 'home-atp', // Default to home page
    'entry' => 'entry',
    'age-verification' => 'age-verification',
    'age-verification-status' => 'age-verification-status',
    'home' => 'home-atp',
    'home-atp' => 'home-atp',
    'models' => 'models',
    'updates' => 'updates',
    'login' => 'login',
    'signup' => 'signup',
    'register' => 'signup',
    'forgot-password' => 'forgot-password',
    'reset-password' => 'reset-password',
    'join' => 'join',
    'logout' => 'logout',
    'contact' => 'contact',
    '2257' => '2257',
    'terms' => 'terms',
    'privacy' => 'privacy',
    'affiliates' => 'affiliates',
    'video' => 'video-legacy',
    // Admin static paths (resolver in includes/admin-routes.php handles dynamic /admin/users/{id}, etc.)
    'admin/security' => 'admin-security',
    'admin/downloads' => 'admin-downloads',
];

// Handle dynamic routes (model pages and video pages)
$route = $routes[$path] ?? null;

// Check if it's a model page (e.g., /model/1 or /model/sarah-johnson)
if ($route === null && strpos($path, 'model/') === 0) {
    $route = 'model';
} elseif ($route === null && strpos($path, 'trailer/') === 0) {
    $route = 'trailer';
} elseif ($route === null && strpos($path, 'members/tour') === 0) {
    $route = 'members-tour';
} elseif ($route === null && strpos($path, 'members/updates') === 0) {
    $route = 'members-updates';
} elseif ($route === null && strpos($path, 'members/models') === 0) {
    $route = 'members-models';
} elseif ($route === null && strpos($path, 'members/model/') === 0) {
    $route = 'members-model';
} elseif ($route === null && strpos($path, 'members/video/') === 0) {
    $route = 'members-video';
} elseif ($route === null && strpos($path, 'members/profile') === 0) {
    $route = 'members-profile';
} elseif ($route === null && strpos($path, 'members/account') === 0) {
    $route = 'members-profile';
} elseif ($route === null && strpos($path, 'members/likes') === 0) {
    $route = 'members-likes';
} elseif ($route === null && $path === 'members') {
    $route = 'members-root';
} elseif ($path === 'admin' || str_starts_with($path, 'admin/')) {
    require_once __DIR__ . '/../includes/admin-routes.php';
    $adminResolved = admin_resolve_route($path, $_SERVER['REQUEST_METHOD'] ?? 'GET');
    if ($adminResolved !== null) {
        $route = $adminResolved['route'] ?? '404';
        $GLOBALS['admin_route_resolved'] = $adminResolved;
    } elseif (is_string($route) && str_starts_with($route, 'admin-')) {
        // Static $routes map (e.g. admin/security) when resolver returns null
        $GLOBALS['admin_route_resolved'] = ['route' => $route];
    } else {
        $route = '404';
        $GLOBALS['admin_route_resolved'] = null;
    }
} elseif ($route === null) {
    $route = '404';
}

// Handle authentication routes
$auth = new Auth();

// Routes that should skip age verification check
$skipAgeVerificationRoutes = [
    'entry',
    'age-verification',
    'age-verification-status',
    'login',
    'signup',
    'forgot-password',
    'reset-password',
    'join',
    'logout',
    '2257',
    'terms',
    'privacy',
    'affiliates',
    'contact',
    // Member routes (users must be logged in, so age verification is handled at login)
    'members-root',
    'members-tour',
    'members-updates',
    'members-models',
    'members-model',
    'members-video',
    'members-profile',
    'members-likes',
];

// Age verification check (skip for certain routes)
if (APP_DEBUG) {
    error_log('Age verification check: route = ' . ($route ?? 'null') . ', path = ' . ($path ?? 'null'));
}

$isAdminRoute = is_string($route) && str_starts_with($route, 'admin-');
$ageGatePending = false;
$ageGateConfig = [];

if (!$isAdminRoute && !in_array($route, $skipAgeVerificationRoutes, true) && !$auth->isLoggedIn()) {
    if (AgeVerification::isSiteAgeVerificationEnabled()) {
        try {
            $ageVerification = new AgeVerification();
            $clientIp = getClientIp();
            $requiredPage = $ageVerification->getRequiredPage($clientIp);

            if ($requiredPage === 'entry') {
                header('Location: /entry');
                exit;
            }
            if ($requiredPage === 'age-verification') {
                header('Location: /age-verification');
                exit;
            }

            if ($ageVerification->shouldShowModalGate($clientIp)) {
                $ageGatePending = true;
                $ageGateConfig = SiteManager::getCurrentSite()['age_gate'] ?? [];
            }
        } catch (\Exception $e) {
            error_log('Age verification check error: ' . $e->getMessage());
            if (AgeVerification::getGateStyle() === 'modal') {
                $ageGatePending = true;
                $ageGateConfig = SiteManager::getCurrentSite()['age_gate'] ?? [];
            } else {
                header('Location: /entry');
                exit;
            }
        }
    } elseif (APP_DEBUG) {
        error_log('Age verification check: Disabled for site ' . (SiteManager::getCurrentSite()['id'] ?? 'unknown'));
    }
} elseif (in_array($route, $skipAgeVerificationRoutes, true) && APP_DEBUG) {
    error_log('Age verification check: Skipping for route = ' . ($route ?? 'null'));
}

$smarty->assign('age_gate_pending', $ageGatePending);
$smarty->assign('age_gate_config', $ageGateConfig);

// Sanitized search term for header input (strip ?page= or & so input never shows "term?page=2")
$headerSearchValue = trim($_GET['search'] ?? '');
$headerSearchValue = trim(explode('?', $headerSearchValue)[0]);
$headerSearchValue = trim(explode('&', $headerSearchValue)[0]);
$smarty->assign('header_search_value', $headerSearchValue);

if ($isAdminRoute) {
    require_once __DIR__ . '/../includes/admin-routes.php';
}

switch ($route) {
    case 'model':
        $modelId = (int)substr($path, 6); // Remove 'model/' prefix

        if ($modelId <= 0) {
            render_model_not_found($smarty);
        }

        $modelVideos = [];
        $page = 1;
        $totalPages = 1;
        $totalVideos = 0;
        $modelData = [
            'id' => $modelId,
            'name' => 'Model ' . $modelId,
            'image' => resolveModelImage(null),
            'likes' => '0',
            'views' => '0',
        ];

        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $modelModel = $cmsService->getModelModel();
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                $modelCmsSiteId = $siteId;
                if (cms_multi_source_hub_enabled()) {
                    $resolved = hub_resolve_model_cms_site_id($modelModel, $modelId);
                    if ($resolved === null) {
                        render_model_not_found($smarty);
                    }
                    $modelCmsSiteId = $resolved;
                }

                $cmsModel = cms_fetch_model_data($modelModel, $modelId, $modelCmsSiteId);
                if ($cmsModel === null || !isset($cmsModel[0])) {
                    render_model_not_found($smarty);
                }

                $modelData = [
                    'id' => $cmsModel[0]['id'] ?? $modelId,
                    'name' => $cmsModel[0]['name'] ?? 'Model ' . $modelId,
                    'image' => resolveModelImage($cmsModel[0]['image'] ?? null),
                    'likes' => '0',
                    'views' => '0',
                ];

                $page = max(1, (int)($_GET['page'] ?? 1));
                $perPage = 12;

                $updates = cms_fetch_updates_for_model($updateModel, $modelId, $modelCmsSiteId);
                $updates = array_values(array_filter($updates, function ($update) use ($siteId) {
                    $uid = (int)($update['id'] ?? 0);

                    return cms_update_passes_site_visibility($siteId, $uid);
                }));

                if (serial_scene_updates_enabled()) {
                    $sceneList = serial_scene_model_video_list($updates, false, $page, $perPage);
                    $modelVideos = $sceneList['videos'];
                    $totalVideos = $sceneList['total'];
                    $totalPages = $sceneList['total_pages'];
                    $page = $sceneList['page'];
                    $smarty->assign('serial_scene_updates', true);
                } elseif (photos_only_site_enabled()) {
                    $modelVideos = photos_only_filter_model_updates($updateModel, $modelCmsSiteId, $updates);
                    $totalVideos = count($modelVideos);
                    $totalPages = max(1, (int) ceil($totalVideos / $perPage));
                    $page = min($page, $totalPages);
                    $skip = ($page - 1) * $perPage;
                    $modelVideos = array_slice($modelVideos, $skip, $perPage);
                } else {
                    $totalVideos = count($updates);
                    $totalPages = max(1, (int)ceil($totalVideos / $perPage));
                    $page = min($page, $totalPages);
                    $skip = ($page - 1) * $perPage;
                    $updatesPage = array_slice($updates, $skip, $perPage);

                    $modelVideos = array_map(function ($update) {
                        $thumbnail = null;
                        if (!empty($update['image'])) {
                            $resolved = resolveSceneImage($update['image']);
                            if ($resolved !== '/assets/images/placeholders/video-placeholder.jpg') {
                                $thumbnail = $resolved;
                            }
                        }

                        return [
                            'id' => $update['id'] ?? null,
                            'title' => $update['scene']['title'] ?? null,
                            'thumbnail' => $thumbnail,
                            'duration' => isset($update['videoLength']) ? formatVideoDuration($update['videoLength']) : null,
                            'release_date' => isset($update['liveDate']) ? date('M j, Y', strtotime($update['liveDate'])) : null,
                            'views' => null,
                            ...cms_hub_video_labels_for_update($update),
                        ];
                    }, $updatesPage);
                }
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for model page: ' . $e->getMessage());
            $page = 1;
            $totalPages = 1;
        }

        try {
            $modelCounts = cms_fetch_model_view_counts([$modelId]);
            $modelData['views'] = number_format($modelCounts[$modelId] ?? 0);
            cms_apply_video_view_counts_to_cards($modelVideos, getCmsSiteId());
        } catch (\Exception $e) {
            $modelData['views'] = '0';
        }

        try {
            $userId = $auth->isLoggedIn() ? (int)($auth->getUser()['id'] ?? 0) : null;
            $tracking = new UserTracking();
            $tracking->trackModelView($modelId, $userId ?: null);
        } catch (\Exception $e) {
            error_log('Error tracking model view: ' . $e->getMessage());
        }

        $baseUrl = '/model/' . $modelId;
        $sep = (strpos($baseUrl, '?') !== false ? '&' : '?');
        $prevPageUrl = $page > 1 ? $baseUrl . $sep . 'page=' . ($page - 1) : null;
        $nextPageUrl = $page < $totalPages ? $baseUrl . $sep . 'page=' . ($page + 1) : null;

        $smarty->assign('page_title', $modelData['name']);
        $smarty->assign('current_page', 'models');
        $smarty->assign('model', $modelData);
        $smarty->assign('model_videos', video_likes_enrich_cards_for_request($modelVideos));
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => $sep,
            'prev_page_url' => $prevPageUrl,
            'next_page_url' => $nextPageUrl,
        ]);
        $smarty->display('pages/public/model.tpl');
        break;
    
    case 'trailer':
        // Extract video ID from path
        $videoId = (int)substr($path, 8); // Remove 'trailer/' prefix

        if (serial_scene_updates_enabled()) {
            serial_scene_handle_trailer($smarty, $videoId);
            break;
        }

        if ($videoId <= 0) {
            render_scene_not_found($smarty);
        }

        $videoData = null;
        $relatedVideos = [];

        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                $contentSiteId = $siteId;
                if (cms_multi_source_hub_enabled()) {
                    $update = cms_fetch_update_with_files($updateModel, $videoId, $siteId);
                    $origin = cms_origin_site_id_for_update_id($videoId);
                    if ($origin !== null) {
                        $contentSiteId = $origin;
                    }
                } else {
                    $update = cms_fetch_update_with_files($updateModel, $videoId, $siteId);
                }
                if (!empty($update) && cms_update_passes_site_visibility($siteId, $videoId)) {
                    if (photos_only_site_enabled()) {
                        $videoData = photos_only_build_detail_payload($update, $videoId, $contentSiteId, false);
                        if ($videoData !== null) {
                            $videoData = photos_only_shape_detail_gallery_for_user(
                                $videoData,
                                $auth->getUser()
                            );
                            if (
                                !photos_only_free_preview_enabled($auth->getUser())
                                && site_related_content_enabled()
                            ) {
                                $relatedVideos = photos_only_build_related_cards(
                                    $updateModel,
                                    $videoId,
                                    $contentSiteId,
                                    8
                                );
                            }
                        }
                    } else {
                        $trailer = cms_fetch_video_trailer($updateModel, $videoId, $contentSiteId);
                        $trailerVideoUrl = null;
                        $trailerDuration = null;
                        if (!empty($trailer['link'])) {
                            $trailerVideoUrl = resolveVideoPath($trailer['link']);
                        }

                        $thumbnail = null;
                        if (!empty($update['image'])) {
                            $resolvedThumbnail = resolveSceneImage($update['image']);
                            if ($resolvedThumbnail !== '/assets/images/placeholders/video-placeholder.jpg') {
                                $thumbnail = $resolvedThumbnail;
                            }
                        }

                        $releaseDate = null;
                        if (isset($update['liveDate'])) {
                            $releaseDate = date('M j, Y', strtotime($update['liveDate']));
                        }

                        $videoData = [
                            'id' => $update['id'] ?? $videoId,
                            'title' => $update['scene']['title'] ?? null,
                            'description' => $update['scene']['description'] ?? null,
                            'duration' => $trailerDuration ? formatVideoDuration($trailerDuration) : null,
                            'release_date' => $releaseDate,
                            'likes' => null,
                            'thumbnail' => $thumbnail,
                            'video_url' => $trailerVideoUrl,
                            ...cms_hub_video_labels_for_update($update),
                            'models' => array_map(function ($model) {
                                $modelImage = null;
                                if (!empty($model['image'])) {
                                    $resolvedImage = resolveModelImage($model['image']);
                                    if ($resolvedImage !== '/assets/images/placeholders/model-placeholder.jpg') {
                                        $modelImage = $resolvedImage;
                                    }
                                }

                                return [
                                    'id' => $model['id'] ?? null,
                                    'name' => $model['name'] ?? null,
                                    'image' => $modelImage,
                                ];
                            }, $update['models'] ?? []),
                            'tags' => [],
                        ];
                    }
                }
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for trailer: ' . $e->getMessage());
            error_log('Stack trace: ' . $e->getTraceAsString());
        }

        if ($videoData === null) {
            render_scene_not_found($smarty);
        }

        $smarty->assign('page_title', $videoData['title']);
        $smarty->assign('current_page', '');
        $smarty->assign('video', $videoData);
        $smarty->assign('related_videos', video_likes_enrich_cards_for_request($relatedVideos));
        $smarty->display('pages/public/trailer.tpl');
        break;
    
    case 'entry':
        // Already age-verified (e.g. after Yoti or login): go home, avoid entry + index redirect churn
        if (AgeVerification::isSessionAgeVerified()) {
            header('Location: /');
            exit;
        }
        // Check if entry_cookie is already set - if so, redirect to home page
        if (isset($_COOKIE['entry_cookie']) && $_COOKIE['entry_cookie'] === '1') {
            header('Location: /');
            exit;
        }
        
        // Safety check: If user is from banned location, redirect to age-verification page
        try {
            $ageVerification = new AgeVerification();
            $clientIp = getClientIp();
            $bannedCheck = $ageVerification->isBannedLocation($clientIp);
            if ($bannedCheck['banned']) {
                error_log("Entry page: User from banned location ({$bannedCheck['reason']}) tried to access entry page, redirecting to age-verification");
                header('Location: /age-verification');
                exit;
            }
        } catch (\Exception $e) {
            error_log('Error checking banned location on entry page: ' . $e->getMessage());
        }
        
        // Handle POST - user clicked "Enter"
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            try {
                $ageVerification = new AgeVerification();
                $clientIp = getClientIp();
                
                // Double-check: Don't set cookies if user is from banned location
                $bannedCheck = $ageVerification->isBannedLocation($clientIp);
                if ($bannedCheck['banned']) {
                    error_log("Entry page POST: User from banned location ({$bannedCheck['reason']}) tried to set cookies, redirecting to age-verification");
                    header('Location: /age-verification');
                    exit;
                }
                
                // Set entry cookie so entry page won't be shown again (age gate uses PHP session after Yoti)
                $ageVerification->setEntryCookie();
                $_COOKIE['entry_cookie'] = '1';
                error_log('Entry page: Entry cookie set');
            } catch (\Exception $e) {
                error_log('Error setting age verification cookie: ' . $e->getMessage());
                error_log('Stack trace: ' . $e->getTraceAsString());
            }
            
            // Redirect to home page (index)
            header('Location: /');
            exit;
        }
        
        $smarty->assign('page_title', 'Age Verification');
        $smarty->display('pages/public/entry.tpl');
        break;

    case 'age-verification':
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');

        // Yoti redirects with sessionId (query). Trust the result API for that UUID; PHP session may be missing after cross-site return.
        $yotiSessionIdReturn = $_GET['sessionId'] ?? $_POST['sessionId'] ?? null;
        if ($yotiSessionIdReturn === null || $yotiSessionIdReturn === '') {
            $yotiSessionIdReturn = $_GET['SessionId'] ?? $_POST['SessionId'] ?? null;
        }
        if (!is_string($yotiSessionIdReturn) || $yotiSessionIdReturn === '') {
            $yotiSessionIdReturn = null;
        }

        $pendingUi = isset($_GET['pending']) && (string) $_GET['pending'] === '1';

        if ($yotiSessionIdReturn !== null && $pendingUi) {
            if (!YotiAgeVerification::isAllowedSessionIdForPolling($yotiSessionIdReturn)) {
                YotiAgeVerification::clearFlowSessionKeys();
                header('Location: /age-verification?error=invalid_session');
                exit;
            }
            $smarty->assign('page_title', 'Verifying your age');
            $smarty->assign('yoti_session_id', $yotiSessionIdReturn);
            $smarty->assign('yoti_session_id_json', json_encode($yotiSessionIdReturn, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT));
            $smarty->display('pages/public/age-verification-pending.tpl');
            break;
        }

        if ($yotiSessionIdReturn !== null) {
            require_once __DIR__ . '/idverify/verify.php';

            $storedReferenceId = $_SESSION['yoti_reference_id'] ?? null;

            $resultPayload = fetchYotiSessionResult($yotiSessionIdReturn);
            if ($resultPayload === null) {
                YotiAgeVerification::clearFlowSessionKeys();
                header('Location: /age-verification?error=result_unavailable');
                exit;
            }

            $eval = YotiAgeVerification::evaluateResultPayload(
                $resultPayload,
                $yotiSessionIdReturn,
                is_string($storedReferenceId) ? $storedReferenceId : null,
                $auth
            );

            switch ($eval['outcome']) {
                case 'complete':
                    $_SESSION['flash_age_verify_success'] = 1;
                    header('Location: /');
                    exit;
                case 'pending':
                    header(
                        'Location: /age-verification?sessionId=' . rawurlencode($yotiSessionIdReturn) . '&pending=1'
                    );
                    exit;
                case 'invalid':
                    YotiAgeVerification::clearFlowSessionKeys();
                    header('Location: /age-verification?error=invalid_callback');
                    exit;
                case 'reference_mismatch':
                    YotiAgeVerification::clearFlowSessionKeys();
                    header('Location: /age-verification?error=invalid_callback');
                    exit;
                case 'fail':
                    YotiAgeVerification::clearFlowSessionKeys();
                    header('Location: /age-verification?error=verification_failed');
                    exit;
                case 'expired':
                    YotiAgeVerification::clearFlowSessionKeys();
                    header('Location: /age-verification?error=session_expired');
                    exit;
                case 'terminal_error':
                    YotiAgeVerification::clearFlowSessionKeys();
                    header('Location: /age-verification?error=verification_failed');
                    exit;
                default:
                    YotiAgeVerification::clearFlowSessionKeys();
                    header('Location: /age-verification?error=verification_failed');
                    exit;
            }
        }

        // Legacy: some flows may POST/GET state, result, reference_id to the callback URL.
        $yotiCallback = $_POST['state'] ?? $_GET['state'] ?? null;
        $yotiResult = $_POST['result'] ?? $_GET['result'] ?? null;
        $yotiReferenceId = $_POST['reference_id'] ?? $_GET['reference_id'] ?? null;

        if ($yotiCallback !== null) {
            // This is a Yoti callback - handle verification result
            error_log('Yoti callback received: state=' . $yotiCallback . ', result=' . $yotiResult . ', reference_id=' . $yotiReferenceId);

            // Verify reference_id matches stored session reference_id
            $storedReferenceId = $_SESSION['yoti_reference_id'] ?? null;
            if ($storedReferenceId && $yotiReferenceId && $yotiReferenceId === $storedReferenceId) {
                $legacySessionId = (string) ($_SESSION['yoti_session_id'] ?? '');
                if ($legacySessionId === '') {
                    $legacySessionId = 'unknown';
                }
                $legacyPayload = [
                    'state' => $yotiCallback,
                    'result' => $yotiResult,
                    'reference_id' => $yotiReferenceId,
                    'source' => 'legacy_callback',
                ];
                UserAgeVerifyLog::recordYotiApiResult(
                    $legacyPayload,
                    $legacySessionId,
                    (string) $storedReferenceId,
                    $auth->isLoggedIn() ? $auth->getUserId() : null
                );

                if ($yotiCallback === 'COMPLETE' && isset($yotiResult) && $yotiResult == 1) {
                    AgeVerification::markSessionAgeVerified();
                    YotiAgeVerification::clearFlowSessionKeys();
                    error_log('Age verify session set after legacy Yoti callback COMPLETE');
                    if (session_status() === PHP_SESSION_ACTIVE) {
                        session_write_close();
                    }
                    header('Location: /');
                    exit;
                }

                error_log('Yoti verification failed: state=' . $yotiCallback . ', result=' . $yotiResult);
                YotiAgeVerification::clearFlowSessionKeys();
                header('Location: /age-verification?error=verification_failed');
                exit;
            } else {
                // Reference ID mismatch - potential security issue
                error_log('Yoti callback reference_id mismatch. Stored: ' . $storedReferenceId . ', Received: ' . $yotiReferenceId);
                YotiAgeVerification::clearFlowSessionKeys();
                header('Location: /age-verification?error=invalid_callback');
                exit;
            }
        }

        // Not a callback - create Yoti session and display age verification page
        $yotiSession = null;
        $yotiError = null;

        try {
            // Include verify.php to get createYotiSession function
            require_once __DIR__ . '/idverify/verify.php';

            // Generate unique reference ID (token)
            $referenceId = session_id() . '_' . time() . '_' . bin2hex(random_bytes(4));

            // Store reference_id in session for callback verification
            // Session is already started in bootstrap.php
            $_SESSION['yoti_reference_id'] = $referenceId;

            // Create Yoti session
            $yotiSession = createYotiSession($referenceId);

            if (!$yotiSession || !isset($yotiSession['id'])) {
                throw new \Exception('Failed to create Yoti session - no session ID returned');
            }

            $_SESSION['yoti_session_id'] = (string) $yotiSession['id'];

            UserAgeVerifyLog::recordPendingSessionStart(
                [
                    'phase' => 'pending',
                    'created_at' => gmdate('c'),
                    'source' => 'session_create',
                ],
                (string) $yotiSession['id'],
                $referenceId,
                $auth->isLoggedIn() ? $auth->getUserId() : null
            );

            error_log('Yoti session created successfully: ' . $yotiSession['id']);
        } catch (\Exception $e) {
            error_log('Error creating Yoti session: ' . $e->getMessage());
            $yotiError = 'Unable to initialize age verification. Please try again later.';
        }

        $smarty->assign('page_title', 'Age Verification Required');

        $siteForAv = SiteManager::getCurrentSite();
        $appNameForAv = $siteForAv['settings']['app_name'] ?? $siteForAv['name'] ?? 'Site';
        $avPageCfg = $siteForAv['age_verification_page'] ?? [];
        $avConfigPath = CONFIG_PATH . '/age-verification.php';
        $avGlobalCfg = is_file($avConfigPath) ? (require $avConfigPath) : [];
        $avDefaultMessage = (string) ($avGlobalCfg['page_message'] ?? '');
        if ($avDefaultMessage === '') {
            $avDefaultMessage = 'You are seeing this message because your state or country has mandated age verification before you can access our content. Please Verify your age below with Yoti.com. Yoti is the top app used by most adult sites to verify your age. If you already have a membership you do not have to verify your age again, just click the login button.';
        }
        $avMessage = array_key_exists('message', $avPageCfg) && trim((string) $avPageCfg['message']) !== ''
            ? trim((string) $avPageCfg['message'])
            : $avDefaultMessage;
        $avHtmlTitle = array_key_exists('html_title', $avPageCfg) && (string) $avPageCfg['html_title'] !== ''
            ? (string) $avPageCfg['html_title']
            : 'Age Verification - ' . $appNameForAv;
        $smarty->assign('age_verification_message', $avMessage);
        $smarty->assign('age_verification_html_title', $avHtmlTitle);

        $smarty->assign('yotiSession', $yotiSession);
        $smarty->assign('yotiError', $yotiError);
        $smarty->display('pages/public/age-verification.tpl');
        break;

    case 'age-verification-status':
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');

        $statusSessionId = $_GET['sessionId'] ?? '';
        if (!is_string($statusSessionId) || $statusSessionId === '') {
            http_response_code(400);
            echo json_encode(['ok' => false, 'error' => 'bad_request'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        if (!YotiAgeVerification::isAllowedSessionIdForPolling($statusSessionId)) {
            http_response_code(403);
            echo json_encode(['ok' => false, 'error' => 'forbidden'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        require_once __DIR__ . '/idverify/verify.php';

        $storedReferenceId = $_SESSION['yoti_reference_id'] ?? null;
        $resultPayload = fetchYotiSessionResult($statusSessionId);
        if ($resultPayload === null) {
            echo json_encode(
                ['ok' => true, 'state' => 'error', 'error' => 'result_unavailable'],
                JSON_UNESCAPED_UNICODE
            );
            exit;
        }

        $eval = YotiAgeVerification::evaluateResultPayload(
            $resultPayload,
            $statusSessionId,
            is_string($storedReferenceId) ? $storedReferenceId : null,
            $auth
        );

        switch ($eval['outcome']) {
            case 'complete':
                // Match the non-polling COMPLETE flow: show a one-time toast on the home page.
                $_SESSION['flash_age_verify_success'] = 1;
                echo json_encode(
                    ['ok' => true, 'state' => 'complete', 'redirect' => '/'],
                    JSON_UNESCAPED_UNICODE
                );
                exit;
            case 'pending':
                echo json_encode(['ok' => true, 'state' => 'pending'], JSON_UNESCAPED_UNICODE);
                exit;
            case 'fail':
                YotiAgeVerification::clearFlowSessionKeys();
                echo json_encode(
                    [
                        'ok' => true,
                        'state' => 'failed',
                        'redirect' => '/age-verification?error=verification_failed',
                    ],
                    JSON_UNESCAPED_UNICODE
                );
                exit;
            case 'expired':
                YotiAgeVerification::clearFlowSessionKeys();
                echo json_encode(
                    [
                        'ok' => true,
                        'state' => 'failed',
                        'redirect' => '/age-verification?error=session_expired',
                    ],
                    JSON_UNESCAPED_UNICODE
                );
                exit;
            case 'terminal_error':
                YotiAgeVerification::clearFlowSessionKeys();
                echo json_encode(
                    [
                        'ok' => true,
                        'state' => 'failed',
                        'redirect' => '/age-verification?error=verification_failed',
                    ],
                    JSON_UNESCAPED_UNICODE
                );
                exit;
            case 'invalid':
            case 'reference_mismatch':
                YotiAgeVerification::clearFlowSessionKeys();
                echo json_encode(
                    [
                        'ok' => true,
                        'state' => 'failed',
                        'redirect' => '/age-verification?error=invalid_callback',
                    ],
                    JSON_UNESCAPED_UNICODE
                );
                exit;
            default:
                YotiAgeVerification::clearFlowSessionKeys();
                echo json_encode(
                    [
                        'ok' => true,
                        'state' => 'failed',
                        'redirect' => '/age-verification?error=verification_failed',
                    ],
                    JSON_UNESCAPED_UNICODE
                );
                exit;
        }
        break;

    case 'home-atp':
        if ($auth->isLoggedIn()) {
            header('Location: ' . member_default_logged_in_path($auth->getUser()));
            exit;
        }

        $ageVerifySuccess = !empty($_SESSION['flash_age_verify_success']);
        unset($_SESSION['flash_age_verify_success']);
        $smarty->assign('age_verify_success', $ageVerifySuccess);

        // Load latest videos from CMS
        $latestVideos = [];
        
        try {
            if (serial_scene_updates_enabled()) {
                $latestVideos = serial_scene_fetch_latest_cards(24, false);
            } elseif (CmsService::getInstance()->isAvailable()) {
                $cmsService = CmsService::getInstance();
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                
                if (photos_only_site_enabled()) {
                    $latestVideos = photos_only_latest_cards($updateModel, $siteId, 24);
                } elseif (cms_multi_source_hub_enabled()) {
                    $latestVideos = hub_latest_cards($updateModel, 24, false);
                } else {
                    $updates = cms_fetch_updates_list($updateModel, $siteId, 24, 0, false, 'live_date DESC');
                    $latestVideos = array_map(function($update) {
                        return [
                            'id' => $update['id'] ?? null,
                            'title' => $update['scene']['title'] ?? 'Video Title',
                            'thumbnail' => resolveSceneImage($update['image'] ?? null),
                            'duration' => formatVideoDuration($update['videoLength'] ?? null, '15:23'),
                            'release_date' => isset($update['liveDate']) ? date('M j, Y', strtotime($update['liveDate'])) : null,
                            'views' => null,
                            ...cms_hub_video_labels_for_update($update),
                        ];
                    }, $updates);
                }
                cms_apply_video_view_counts_to_cards($latestVideos, $siteId);
            }
        } catch (\Exception $e) {
            error_log('Error loading latest videos for home page: ' . $e->getMessage());
            // Continue with empty array if CMS fails
        }
        
        $smarty->assign('page_title', 'Home');
        $smarty->assign('current_page', 'home');
        // Ensure current_page is always set for header
        if (!isset($current_page)) {
            $smarty->assign('current_page', '');
        }
        $smarty->assign('latest_videos', video_likes_enrich_cards_for_request($latestVideos));
        $smarty->display('pages/public/home-atp.tpl');
        break;

    case 'models':
        
        // Get sort parameters
        $sortBy = $_GET['sort'] ?? 'popular';
        $sortDir = $_GET['sort_dir'] ?? 'desc';
        $sortDir = strtoupper(trim($sortDir));
        $sortDir = preg_replace('/[^A-Z]/', '', $sortDir);
        if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
            $sortDir = 'DESC';
        }
        
        // Get current page from query string
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = 24; // Models per page
        
        // Map sort to CMS whitelist: popular => weight (model_weight), alphabetical => name
        $allowedModelSort = ['popular', 'alphabetical'];
        $sortBy = in_array($sortBy, $allowedModelSort, true) ? $sortBy : 'popular';
        $cmsModelSortField = $sortBy === 'alphabetical' ? 'name' : 'weight';
        
        // Load models from CMS
        $models = [];
        $totalModels = 0;
        
        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $modelModel = $cmsService->getModelModel();
                $siteId = getCmsSiteId();
                
                // Calculate skip for pagination
                $skip = ($page - 1) * $perPage;
                
                // Get models with pagination and sort
                if (cms_multi_source_hub_enabled()) {
                    $models = hub_merged_models($modelModel, $perPage, $skip, $cmsModelSortField, $sortDir);
                    $totalModels = count($models) < $perPage ? count($models) + $skip : ($skip + $perPage + 1);
                } else {
                    $cmsModels = cms_fetch_models_list($modelModel, $siteId, $perPage, $skip, $cmsModelSortField, $sortDir);

                    $models = array_map(function($model) {
                        return [
                            'id' => $model['id'] ?? null,
                            'name' => $model['name'] ?? 'Model Name',
                            'image' => resolveModelImage($model['image'] ?? null),
                            'views' => null,
                            'latest_update' => $model['latestUpdateDate'] ?? null,
                        ];
                    }, $cmsModels);
                    $totalModels = count($cmsModels) < $perPage ? count($cmsModels) + $skip : ($skip + $perPage + 1);
                }
                cms_apply_model_view_counts_to_cards($models, getCmsSiteId());
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for models page: ' . $e->getMessage());
            $totalModels = 0;
        }
        
        $totalPages = max(1, (int)ceil($totalModels / $perPage));
        $page = min($page, $totalPages);
        
        // Build query string for pagination (preserve sort)
        $queryParams = [];
        if ($sortBy !== 'popular') $queryParams['sort'] = $sortBy;
        if ($sortDir !== 'desc') $queryParams['sort_dir'] = $sortDir;
        $baseUrl = '/models' . (!empty($queryParams) ? '?' . http_build_query($queryParams) : '');
        
        $smarty->assign('page_title', 'Models');
        $smarty->assign('current_page', 'models');
        $smarty->assign('sort_by', $sortBy);
        $smarty->assign('sort_dir', $sortDir);
        $smarty->assign('models', $models);
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => (strpos($baseUrl, '?') !== false ? '&' : '?'),
        ]);
        $smarty->display('pages/public/models.tpl');
        break;

    case 'updates':
        if (serial_scene_updates_enabled()) {
            serial_scene_handle_updates($smarty, false);
            break;
        }

        // Get sort and search parameters (strip any ?page= or & that could be in search param from bad URLs)
        $sortBy = $_GET['sort'] ?? 'release_date';
        $sortDir = $_GET['sort_dir'] ?? 'desc'; // Default to descending (most recent first)
        $searchTerm = trim($_GET['search'] ?? '');
        $searchTerm = trim(explode('?', $searchTerm)[0]);
        $searchTerm = trim(explode('&', $searchTerm)[0]);
        
        // Get current page from query string
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = 12; // Videos per page
        
        // Load videos from CMS
        $videos = [];
        $totalVideos = 0;
        
        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                
                // Calculate skip for pagination
                $skip = ($page - 1) * $perPage;
                
                // Build order by clause - sanitize to prevent SQL injection and query string contamination
                $sortDir = strtoupper(trim($sortDir));
                // Remove any query string characters that might have been accidentally included
                $sortDir = preg_replace('/[^A-Z]/', '', $sortDir);
                if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
                    $sortDir = 'DESC'; // Default to DESC
                }
                
                // Map sort to CMS whitelist: release_date, video_time, alphabetical (title)
                $allowedSort = ['release_date', 'video_time', 'alphabetical'];
                $sortBy = in_array($sortBy, $allowedSort, true) ? $sortBy : 'release_date';
                $cmsSortField = $sortBy === 'alphabetical' ? 'title' : ($sortBy === 'video_time' ? 'video_time' : 'live_date');
                
                $cmsSiteId = getCmsSiteId();
                if (photos_only_site_enabled()) {
                    $photoResult = photos_only_paginated_cards(
                        $updateModel,
                        $cmsSiteId,
                        $page,
                        $perPage,
                        false,
                        'live_date DESC',
                        $searchTerm !== '' ? $searchTerm : null,
                        $cmsSortField,
                        $sortDir
                    );
                    $videos = $photoResult['videos'];
                    $totalVideos = $photoResult['total'];
                } elseif (cms_multi_source_hub_enabled()) {
                    $hubResult = hub_paginated_list(
                        $updateModel,
                        $page,
                        $perPage,
                        false,
                        $searchTerm !== '' ? $searchTerm : null,
                        $cmsSortField,
                        $sortDir
                    );
                    $videos = $hubResult['videos'];
                    $totalVideos = $hubResult['total'];
                } else {
                    try {
                        $totalVideos = cms_fetch_updates_count($updateModel, $siteId, false, $searchTerm !== '' ? $searchTerm : null);
                    } catch (\Exception $e) {
                        error_log('Error getting total count for updates: ' . $e->getMessage());
                        $totalVideos = 0;
                    }

                    $updates = cms_fetch_updates_list($updateModel, $siteId, $perPage, $skip, false, 'live_date DESC', $searchTerm !== '' ? $searchTerm : null, $cmsSortField, $sortDir);

                    $videos = array_map(function($update) {
                        $releaseDate = null;
                        if (isset($update['liveDate'])) {
                            $releaseDate = date('M j, Y', strtotime($update['liveDate']));
                        }

                        $thumbnail = null;
                        if (!empty($update['image'])) {
                            $resolvedThumbnail = resolveSceneImage($update['image']);
                            if ($resolvedThumbnail !== '/assets/images/placeholders/video-placeholder.jpg') {
                                $thumbnail = $resolvedThumbnail;
                            }
                        }

                        return [
                            'id' => $update['id'] ?? null,
                            'title' => $update['scene']['title'] ?? null,
                            'thumbnail' => $thumbnail,
                            'duration' => isset($update['videoLength']) ? formatVideoDuration($update['videoLength']) : null,
                            'release_date' => $releaseDate,
                            'description' => $update['scene']['description'] ?? null,
                            'views' => null,
                            ...cms_hub_video_labels_for_update($update),
                        ];
                    }, $updates);
                }
                cms_apply_video_view_counts_to_cards($videos, $cmsSiteId);
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for updates page: ' . $e->getMessage());
            $totalVideos = 0;
        }
        
        $totalPages = max(1, (int)ceil($totalVideos / $perPage));
        $page = min($page, $totalPages);
        $showFrom = $totalVideos === 0 ? 0 : (($page - 1) * $perPage) + 1;
        $showTo = $totalVideos === 0 ? 0 : min($page * $perPage, $totalVideos);
        
        // Build query string for pagination (preserve sort and search, but exclude default sort_dir)
        $queryParams = [];
        if ($sortBy !== 'release_date') $queryParams['sort'] = $sortBy;
        // Only include sort_dir if it's not the default (desc/DESC)
        if (strtolower($sortDir) !== 'desc') $queryParams['sort_dir'] = strtolower($sortDir);
        if (!empty($searchTerm)) $queryParams['search'] = $searchTerm;
        $baseUrl = '/updates' . (!empty($queryParams) ? '?' . http_build_query($queryParams) : '');
        $sep = (strpos($baseUrl, '?') !== false ? '&' : '?');
        $prevPageUrl = $page > 1 ? $baseUrl . $sep . 'page=' . ($page - 1) : null;
        $nextPageUrl = $page < $totalPages ? $baseUrl . $sep . 'page=' . ($page + 1) : null;
        
        $smarty->assign('page_title', !empty($searchTerm) ? 'Search Results - Updates' : 'Updates');
        $smarty->assign('current_page', 'updates');
        $smarty->assign('sort_by', $sortBy);
        $smarty->assign('sort_dir', $sortDir);
        $smarty->assign('search_term', $searchTerm);
        $smarty->assign('videos', video_likes_enrich_cards_for_request($videos));
        $smarty->assign('video_count', $totalVideos);
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => $sep,
            'prev_page_url' => $prevPageUrl,
            'next_page_url' => $nextPageUrl,
            'show_from' => $showFrom,
            'show_to' => $showTo,
        ]);
        $smarty->display('pages/public/updates.tpl');
        break;

    case 'home':
        $smarty->assign('page_title', 'Home');
        $smarty->display('pages/public/home.tpl');
        break;

    case 'login':
        if ($auth->isLoggedIn()) {
            $return = auth_safe_return_path((string) ($_GET['return'] ?? ''), member_default_logged_in_path($auth->getUser()));
            header('Location: ' . $return);
            exit;
        }

        $return = auth_safe_return_path((string) ($_GET['return'] ?? $_POST['return'] ?? ''), member_default_logged_in_path(null));
        $error = null;
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $login = trim($_POST['login'] ?? $_POST['username'] ?? '');
            $password = $_POST['password'] ?? '';
            $csrfToken = $_POST['csrf_token'] ?? '';
            $loginDb = Database::getInstance();
            $loginSiteId = SiteManager::getCurrentSiteId();
            $loginClientIp = getClientIp();

            if (LoginIpThrottle::isThrottled($loginDb, $loginSiteId, $loginClientIp)) {
                $error = LoginIpThrottle::USER_MESSAGE;
            } elseif ($login === '' || $password === '') {
                $error = 'Please enter your username or email and password';
                try {
                    $tracking = new UserTracking();
                    $tracking->trackLoginAttempt(null, $login !== '' ? $login : null, false, 'Missing credentials');
                } catch (\Exception $e) {
                    error_log('Error tracking login attempt: ' . $e->getMessage());
                }
            } elseif (!$auth->validateCsrfToken($csrfToken)) {
                $error = 'Invalid security token. Please refresh the page and try again.';
                try {
                    $tracking = new UserTracking();
                    $tracking->trackLoginAttempt(null, $login !== '' ? $login : null, false, 'Invalid CSRF token');
                } catch (\Exception $e) {
                    error_log('Error tracking login attempt: ' . $e->getMessage());
                }
            } else {
                if (LoginThrottle::isThrottled($loginDb, $loginSiteId, $login)) {
                    $error = LoginIpThrottle::USER_MESSAGE;
                } else {
                    $result = $auth->login($login, $password);
                    if ($result['success']) {
                        $user = $auth->getUser();
                        $dest = $return;
                        if (
                            !user_has_active_membership(is_array($user) ? $user : null)
                            && preg_match('#^/members/video/(\d+)#', $return, $videoMatch)
                        ) {
                            $dest = '/trailer/' . $videoMatch[1];
                        }
                        header('Location: ' . $dest . (str_contains($dest, '?') ? '&' : '?') . 'login=success');
                        exit;
                    } else {
                        $error = $result['errors']['credentials'] ?? 'Invalid username or password';
                    }
                }
            }
        }

        $smarty->assign('page_title', 'Login');
        $smarty->assign('error', $error);
        $smarty->assign('return', $return);
        $smarty->assign('verotel_notice', VerotelFlexPay::checkoutFlashMessage((string) ($_GET['verotel'] ?? '')));
        $smarty->display('pages/public/login.tpl');
        break;

    case 'signup':
        if ($auth->isLoggedIn()) {
            $return = auth_safe_return_path((string) ($_GET['return'] ?? ''), '/join');
            header('Location: ' . $return);
            exit;
        }

        $return = auth_safe_return_path((string) ($_GET['return'] ?? $_POST['return'] ?? ''), '/join');
        $error = null;
        $emailPrefill = '';

        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $emailPrefill = strtolower(trim((string) ($_POST['email'] ?? '')));
            $password = (string) ($_POST['password'] ?? '');
            $confirm = (string) ($_POST['password_confirm'] ?? '');
            $csrfToken = $_POST['csrf_token'] ?? '';

            if (!$auth->validateCsrfToken($csrfToken)) {
                $error = 'Invalid security token. Please refresh the page and try again.';
            } else {
                $result = $auth->registerWithEmail($emailPrefill, $password, $confirm);
                if ($result['success']) {
                    header('Location: ' . $return);
                    exit;
                }
                $error = $result['errors']['general']
                    ?? $result['errors']['email']
                    ?? $result['errors']['username']
                    ?? $result['errors']['password']
                    ?? $result['errors']['password_confirm']
                    ?? 'Registration failed. Please try again.';
            }
        }

        $smarty->assign('page_title', 'Sign Up');
        $smarty->assign('error', $error);
        $smarty->assign('email_prefill', $emailPrefill);
        $smarty->assign('return', $return);
        $smarty->display('pages/public/signup.tpl');
        break;

    case 'forgot-password':
        if ($auth->isLoggedIn()) {
            header('Location: /members/profile');
            exit;
        }

        $sent = false;
        $error = null;
        $emailPrefill = '';

        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $emailPrefill = strtolower(trim((string) ($_POST['email'] ?? '')));
            $csrfToken = $_POST['csrf_token'] ?? '';

            if (!$auth->validateCsrfToken($csrfToken)) {
                $error = 'Invalid security token. Please refresh the page and try again.';
            } else {
                (new PasswordResetService())->requestReset($emailPrefill);
                $sent = true;
            }
        }

        $smarty->assign('page_title', 'Forgot Password');
        $smarty->assign('error', $error);
        $smarty->assign('sent', $sent);
        $smarty->assign('email_prefill', $emailPrefill);
        $smarty->display('pages/public/forgot-password.tpl');
        break;

    case 'reset-password':
        if ($auth->isLoggedIn()) {
            header('Location: /members/profile');
            exit;
        }

        $token = trim((string) ($_GET['token'] ?? $_POST['token'] ?? ''));
        $resetService = new PasswordResetService();
        $error = null;

        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $csrfToken = $_POST['csrf_token'] ?? '';
            $password = (string) ($_POST['password'] ?? '');
            $confirm = (string) ($_POST['password_confirm'] ?? '');

            if (!$auth->validateCsrfToken($csrfToken)) {
                $error = 'Invalid security token. Please refresh the page and try again.';
            } else {
                $result = $resetService->completeReset($token, $password, $confirm, $auth);
                if ($result['success']) {
                    if (!empty($result['login_failed'])) {
                        header('Location: /login?password_reset=success');
                        exit;
                    }
                    header('Location: /members/tour?password_reset=success');
                    exit;
                }
                $error = $result['errors']['token']
                    ?? $result['errors']['password']
                    ?? $result['errors']['password_confirm']
                    ?? 'Unable to reset password.';
                if (isset($result['errors']['token'])) {
                    header('Location: /forgot-password');
                    exit;
                }
            }
        } elseif ($resetService->findUserForToken($token) === null) {
            header('Location: /forgot-password');
            exit;
        }

        $smarty->assign('page_title', 'Reset Password');
        $smarty->assign('error', $error);
        $smarty->assign('token', $token);
        $smarty->display('pages/public/reset-password.tpl');
        break;

    case 'join':
        if (!$auth->isLoggedIn()) {
            header('Location: /login?return=' . rawurlencode('/join'));
            exit;
        }

        $userId = (int) ($auth->getUserId() ?? 0);
        $member = $auth->getUser();
        $siteConfig = SiteManager::getCurrentSite();
        $joinConfig = is_array($siteConfig['join'] ?? null) ? $siteConfig['join'] : [];
        if ($userId > 0 && $joinConfig !== []) {
            $memberEmail = is_array($member) && is_string($member['email'] ?? null)
                ? $member['email']
                : null;
            $joinConfig = join_config_with_member_checkout(
                $joinConfig,
                $userId,
                SiteManager::getCurrentSiteId(),
                $memberEmail
            );
        }

        $smarty->assign('page_title', 'Join');
        $smarty->assign('current_page', '');
        $smarty->assign('join_config', $joinConfig);
        $smarty->assign('verotel_notice', VerotelFlexPay::checkoutFlashMessage((string) ($_GET['verotel'] ?? '')));
        $smarty->display('pages/public/join.tpl');
        break;

    case 'logout':
        $auth->logout();
        header('Location: /?logout=success');
        exit;

    case 'members-root':
        if (!$auth->isLoggedIn()) {
            header('Location: /login');
            exit;
        }
        header('Location: ' . member_default_logged_in_path($auth->getUser()));
        exit;

    case 'members-tour':
        member_require_login($auth);

        // Load videos from CMS
        $comingSoonVideos = [];
        $latestVideos = [];
        
        try {
            if (serial_scene_updates_enabled()) {
                $latestVideos = serial_scene_fetch_latest_cards(12, true);
            }
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                
                if (!serial_scene_updates_enabled()) {
                if (photos_only_site_enabled()) {
                    $comingSoonVideos = photos_only_latest_cards($updateModel, $siteId, 3, true);
                    $latestVideos = photos_only_latest_cards($updateModel, $siteId, 12, false);
                } elseif (cms_multi_source_hub_enabled()) {
                    $comingSoonVideos = hub_latest_cards($updateModel, 3, true);
                    $latestVideos = hub_latest_cards($updateModel, 12, false);
                } else {
                $comingSoonUpdates = cms_fetch_updates_list($updateModel, $siteId, 3, 0, true, 'live_date ASC');
                $comingSoonVideos = array_map(function($update) {
                    return [
                        'id' => $update['id'] ?? null,
                        'title' => $update['scene']['title'] ?? 'Coming Soon',
                        'thumbnail' => resolveSceneImage($update['image'] ?? null),
                        'release_date' => $update['liveDate'] ?? 'Coming Soon',
                        ...cms_hub_video_labels_for_update($update),
                    ];
                }, $comingSoonUpdates);

                $updates = cms_fetch_updates_list($updateModel, $siteId, 12, 0, false, 'live_date DESC');
                $latestVideos = array_map(function($update) {
                    return [
                        'id' => $update['id'] ?? null,
                        'title' => $update['scene']['title'] ?? 'Video Title',
                        'thumbnail' => resolveSceneImage($update['image'] ?? null),
                        'duration' => formatVideoDuration($update['videoLength'] ?? null, '15:23'),
                        'views' => null,
                        'release_date' => isset($update['liveDate']) ? date('M j, Y', strtotime($update['liveDate'])) : null,
                        ...cms_hub_video_labels_for_update($update),
                    ];
                }, $updates);
                }
                cms_apply_video_view_counts_to_cards($latestVideos, $siteId);
                cms_apply_video_view_counts_to_cards($comingSoonVideos, $siteId);
                }
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for members-tour: ' . $e->getMessage());
            // Continue with empty arrays - fallback to placeholders
        }
        
        $smarty->assign('page_title', 'Members Tour');
        $smarty->assign('current_page', 'home');
        $smarty->assign('latest_videos', video_likes_enrich_cards_for_request($latestVideos));
        $smarty->assign('coming_soon_videos', $comingSoonVideos);
        $smarty->assign('verotel_notice', VerotelFlexPay::checkoutFlashMessage((string) ($_GET['verotel'] ?? '')));
        $smarty->display('pages/members/tour.tpl');
        break;

    case 'members-updates':
        member_require_login($auth);

        if (serial_scene_updates_enabled()) {
            serial_scene_handle_updates($smarty, true);
            break;
        }

        // Get sort and search parameters (strip any ?page= or & that could be in search param from bad URLs)
        $sortBy = $_GET['sort'] ?? 'release_date';
        $sortDir = $_GET['sort_dir'] ?? 'desc'; // Default to descending (most recent first)
        $searchTerm = trim($_GET['search'] ?? '');
        $searchTerm = trim(explode('?', $searchTerm)[0]);
        $searchTerm = trim(explode('&', $searchTerm)[0]);
        
        // Get current page from query string
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = 12; // Videos per page
        
        // Load videos from CMS
        $videos = [];
        $totalVideos = 0;
        
        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                
                // Calculate skip for pagination
                $skip = ($page - 1) * $perPage;
                
                // Build order by clause - sanitize to prevent SQL injection and query string contamination
                $sortDir = strtoupper(trim($sortDir));
                // Remove any query string characters that might have been accidentally included
                $sortDir = preg_replace('/[^A-Z]/', '', $sortDir);
                if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
                    $sortDir = 'DESC'; // Default to DESC
                }
                
                // Map sort to CMS whitelist: release_date, video_time, alphabetical (title)
                $allowedSort = ['release_date', 'video_time', 'alphabetical'];
                $sortBy = in_array($sortBy, $allowedSort, true) ? $sortBy : 'release_date';
                $cmsSortField = $sortBy === 'alphabetical' ? 'title' : ($sortBy === 'video_time' ? 'video_time' : 'live_date');

                if (photos_only_site_enabled()) {
                    $photoResult = photos_only_paginated_cards(
                        $updateModel,
                        $siteId,
                        $page,
                        $perPage,
                        false,
                        'live_date DESC',
                        $searchTerm !== '' ? $searchTerm : null,
                        $cmsSortField,
                        $sortDir
                    );
                    $videos = $photoResult['videos'];
                    $totalVideos = $photoResult['total'];
                } elseif (cms_multi_source_hub_enabled()) {
                    $hubResult = hub_paginated_list(
                        $updateModel,
                        $page,
                        $perPage,
                        false,
                        $searchTerm !== '' ? $searchTerm : null,
                        $cmsSortField,
                        $sortDir
                    );
                    $videos = $hubResult['videos'];
                    $totalVideos = $hubResult['total'];
                } else {
                    try {
                        $totalVideos = cms_fetch_updates_count($updateModel, $siteId, false, $searchTerm !== '' ? $searchTerm : null);
                    } catch (\Exception $e) {
                        error_log('Error getting total count for members-updates: ' . $e->getMessage());
                        $totalVideos = 0;
                    }

                    $updates = cms_fetch_updates_list($updateModel, $siteId, $perPage, $skip, false, 'live_date DESC', $searchTerm !== '' ? $searchTerm : null, $cmsSortField, $sortDir);

                    $videos = array_map(function($update) {
                        $releaseDate = null;
                        if (isset($update['liveDate'])) {
                            $releaseDate = date('M j, Y', strtotime($update['liveDate']));
                        }

                        $thumbnail = null;
                        if (!empty($update['image'])) {
                            $resolvedThumbnail = resolveSceneImage($update['image']);
                            if ($resolvedThumbnail !== '/assets/images/placeholders/video-placeholder.jpg') {
                                $thumbnail = $resolvedThumbnail;
                            }
                        }

                        return [
                            'id' => $update['id'] ?? null,
                            'title' => $update['scene']['title'] ?? null,
                            'thumbnail' => $thumbnail,
                            'duration' => isset($update['videoLength']) ? formatVideoDuration($update['videoLength']) : null,
                            'release_date' => $releaseDate,
                            'description' => $update['scene']['description'] ?? null,
                            'views' => null,
                            ...cms_hub_video_labels_for_update($update),
                        ];
                    }, $updates);
                }
                cms_apply_video_view_counts_to_cards($videos, $siteId);
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for members-updates: ' . $e->getMessage());
            $totalVideos = 0;
        }
        
        $totalPages = max(1, (int)ceil($totalVideos / $perPage));
        $page = min($page, $totalPages);
        $showFrom = $totalVideos === 0 ? 0 : (($page - 1) * $perPage) + 1;
        $showTo = $totalVideos === 0 ? 0 : min($page * $perPage, $totalVideos);
        
        // Build query string for pagination (preserve sort and search)
        $queryParams = [];
        if ($sortBy !== 'release_date') $queryParams['sort'] = $sortBy;
        if ($sortDir !== 'asc') $queryParams['sort_dir'] = $sortDir;
        if (!empty($searchTerm)) $queryParams['search'] = $searchTerm;
        $baseUrl = '/members/updates' . (!empty($queryParams) ? '?' . http_build_query($queryParams) : '');
        $sep = (strpos($baseUrl, '?') !== false ? '&' : '?');
        $prevPageUrl = $page > 1 ? $baseUrl . $sep . 'page=' . ($page - 1) : null;
        $nextPageUrl = $page < $totalPages ? $baseUrl . $sep . 'page=' . ($page + 1) : null;

        $smarty->assign('page_title', !empty($searchTerm) ? 'Search Results - Updates' : 'Updates');
        $smarty->assign('current_page', 'updates');
        $smarty->assign('sort_by', $sortBy);
        $smarty->assign('sort_dir', $sortDir);
        $smarty->assign('search_term', $searchTerm);
        $smarty->assign('videos', video_likes_enrich_cards_for_request($videos));
        $smarty->assign('video_count', $totalVideos);
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => $sep,
            'prev_page_url' => $prevPageUrl,
            'next_page_url' => $nextPageUrl,
            'show_from' => $showFrom,
            'show_to' => $showTo,
        ]);
        $smarty->display('pages/members/updates.tpl');
        break;

    case 'members-models':
        member_require_login($auth);

        // Get sort parameters
        $sortBy = $_GET['sort'] ?? 'popular';
        $sortDir = $_GET['sort_dir'] ?? 'desc';
        $sortDir = strtoupper(trim($sortDir));
        $sortDir = preg_replace('/[^A-Z]/', '', $sortDir);
        if (!in_array($sortDir, ['ASC', 'DESC'], true)) {
            $sortDir = 'DESC';
        }
        
        // Get current page from query string
        $page = max(1, (int)($_GET['page'] ?? 1));
        $perPage = 24; // Models per page
        
        // Map sort to CMS whitelist: popular => weight (model_weight), alphabetical => name
        $allowedModelSort = ['popular', 'alphabetical'];
        $sortBy = in_array($sortBy, $allowedModelSort, true) ? $sortBy : 'popular';
        $cmsModelSortField = $sortBy === 'alphabetical' ? 'name' : 'weight';
        
        // Load models from CMS
        $models = [];
        $totalModels = 0;
        
        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $modelModel = $cmsService->getModelModel();
                $siteId = getCmsSiteId();
                
                // Calculate skip for pagination
                $skip = ($page - 1) * $perPage;
                
                // Get models with pagination and sort
                if (cms_multi_source_hub_enabled()) {
                    $models = hub_merged_models($modelModel, $perPage, $skip, $cmsModelSortField, $sortDir);
                    $totalModels = count($models) < $perPage ? count($models) + $skip : ($skip + $perPage + 1);
                } else {
                    $cmsModels = cms_fetch_models_list($modelModel, $siteId, $perPage, $skip, $cmsModelSortField, $sortDir);

                    $models = array_map(function($model) {
                        $image = null;
                        if (!empty($model['image'])) {
                            $resolvedImage = resolveModelImage($model['image']);
                            if ($resolvedImage !== '/assets/images/placeholders/model-placeholder.jpg') {
                                $image = $resolvedImage;
                            }
                        }

                        return [
                            'id' => $model['id'] ?? null,
                            'name' => $model['name'] ?? null,
                            'image' => $image,
                            'views' => null,
                            'latest_update' => $model['latestUpdateDate'] ?? null,
                        ];
                    }, $cmsModels);
                    $totalModels = count($cmsModels) < $perPage ? count($cmsModels) + $skip : ($skip + $perPage + 1);
                }
                cms_apply_model_view_counts_to_cards($models, $siteId);
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for members-models: ' . $e->getMessage());
            $totalModels = 0;
        }
        
        $totalPages = max(1, (int)ceil($totalModels / $perPage));
        $page = min($page, $totalPages);
        
        // Build query string for pagination (preserve sort)
        $queryParams = [];
        if ($sortBy !== 'popular') $queryParams['sort'] = $sortBy;
        if ($sortDir !== 'desc') $queryParams['sort_dir'] = $sortDir;
        $baseUrl = '/members/models' . (!empty($queryParams) ? '?' . http_build_query($queryParams) : '');
        
        $smarty->assign('page_title', 'Models');
        $smarty->assign('current_page', 'models');
        $smarty->assign('sort_by', $sortBy);
        $smarty->assign('sort_dir', $sortDir);
        $smarty->assign('models', $models);
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => (strpos($baseUrl, '?') !== false ? '&' : '?'),
        ]);
        $smarty->display('pages/members/models.tpl');
        break;

    case 'members-model':
        member_require_login($auth);

        $modelId = (int)substr($path, 14); // Remove 'members/model/' prefix

        if ($modelId <= 0) {
            render_model_not_found($smarty);
        }

        $modelVideos = [];
        $page = 1;
        $totalPages = 1;
        $totalVideos = 0;
        $modelData = [
            'id' => $modelId,
            'name' => 'Model ' . $modelId,
            'image' => resolveModelImage(null),
            'views' => '0',
        ];

        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $modelModel = $cmsService->getModelModel();
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                $modelCmsSiteId = $siteId;
                if (cms_multi_source_hub_enabled()) {
                    $resolved = hub_resolve_model_cms_site_id($modelModel, $modelId);
                    if ($resolved === null) {
                        render_model_not_found($smarty);
                    }
                    $modelCmsSiteId = $resolved;
                }

                $cmsModel = cms_fetch_model_data($modelModel, $modelId, $modelCmsSiteId);
                if ($cmsModel === null || !isset($cmsModel[0])) {
                    render_model_not_found($smarty);
                }

                $modelData = [
                    'id' => $cmsModel[0]['id'] ?? $modelId,
                    'name' => $cmsModel[0]['name'] ?? 'Model ' . $modelId,
                    'image' => resolveModelImage($cmsModel[0]['image'] ?? null),
                    'views' => '0',
                ];

                $page = max(1, (int)($_GET['page'] ?? 1));
                $perPage = 12;

                $updates = cms_fetch_updates_for_model($updateModel, $modelId, $modelCmsSiteId);
                $updates = array_values(array_filter($updates, function ($update) use ($siteId) {
                    $uid = (int)($update['id'] ?? 0);

                    return cms_update_passes_site_visibility($siteId, $uid);
                }));

                if (serial_scene_updates_enabled()) {
                    $sceneList = serial_scene_model_video_list($updates, true, $page, $perPage);
                    $modelVideos = $sceneList['videos'];
                    $totalVideos = $sceneList['total'];
                    $totalPages = $sceneList['total_pages'];
                    $page = $sceneList['page'];
                    $smarty->assign('serial_scene_updates', true);
                } else {
                    $totalVideos = count($updates);
                    $totalPages = max(1, (int)ceil($totalVideos / $perPage));
                    $page = min($page, $totalPages);
                    $skip = ($page - 1) * $perPage;
                    $updatesPage = array_slice($updates, $skip, $perPage);

                    $modelVideos = array_map(function ($update) {
                        $releaseDate = null;
                        if (isset($update['liveDate'])) {
                            $releaseDate = date('M j, Y', strtotime($update['liveDate']));
                        }

                        $thumbnail = null;
                        if (!empty($update['image'])) {
                            $resolvedThumbnail = resolveSceneImage($update['image']);
                            if ($resolvedThumbnail !== '/assets/images/placeholders/video-placeholder.jpg') {
                                $thumbnail = $resolvedThumbnail;
                            }
                        }

                        return [
                            'id' => $update['id'] ?? null,
                            'title' => $update['scene']['title'] ?? null,
                            'thumbnail' => $thumbnail,
                            'duration' => isset($update['videoLength']) ? formatVideoDuration($update['videoLength']) : null,
                            'release_date' => $releaseDate,
                            'views' => null,
                            ...cms_hub_video_labels_for_update($update),
                        ];
                    }, $updatesPage);
                }
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for members-model: ' . $e->getMessage());
        }

        try {
            $modelCounts = cms_fetch_model_view_counts([$modelId]);
            $modelData['views'] = number_format($modelCounts[$modelId] ?? 0);
            cms_apply_video_view_counts_to_cards($modelVideos, getCmsSiteId());
        } catch (\Exception $e) {
            $modelData['views'] = '0';
        }

        try {
            $user = $auth->getUser();
            $userId = $user ? (int)$user['id'] : null;
            $tracking = new UserTracking();
            $tracking->trackModelView($modelId, $userId);
        } catch (\Exception $e) {
            error_log('Error tracking model view: ' . $e->getMessage());
        }

        $baseUrl = '/members/model/' . $modelId;
        $sep = (strpos($baseUrl, '?') !== false ? '&' : '?');
        $prevPageUrl = $page > 1 ? $baseUrl . $sep . 'page=' . ($page - 1) : null;
        $nextPageUrl = $page < $totalPages ? $baseUrl . $sep . 'page=' . ($page + 1) : null;

        $smarty->assign('page_title', $modelData['name']);
        $smarty->assign('current_page', 'models');
        $smarty->assign('model', $modelData);
        $smarty->assign('model_videos', video_likes_enrich_cards_for_request($modelVideos));
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => $sep,
            'prev_page_url' => $prevPageUrl,
            'next_page_url' => $nextPageUrl,
        ]);
        $smarty->display('pages/members/model.tpl');
        break;

    case 'members-video':
        // Extract video ID from path
        $videoId = substr($path, 14); // Remove 'members/video/' prefix
        $videoId = (int)$videoId; // Ensure it's an integer
        member_require_login_for_video($auth, $videoId);

        if (serial_scene_updates_enabled()) {
            serial_scene_handle_members_video($smarty, $videoId, $auth);
            break;
        }

        if ($videoId <= 0) {
            render_scene_not_found($smarty);
        }

        // Get current user
        $user = $auth->getUser();
        $userId = $user ? (int)$user['id'] : null;

        // Check if user has liked this video
        $likeStorageId = video_likes_storage_id($videoId);
        $likeState = $userId && video_likes_feature_enabled()
            ? video_likes_get_state($userId, $likeStorageId)
            : ['is_liked' => false, 'like_count' => 0, 'formatted_count' => '0'];
        $isLiked = $likeState['is_liked'];
        $likeCount = $likeState['like_count'];
        $formattedLikes = $likeState['formatted_count'];

        $videoData = null;
        $relatedVideos = [];

        try {
            $cmsService = CmsService::getInstance();
            if ($cmsService->isAvailable()) {
                $updateModel = $cmsService->getUpdateModel();
                $siteId = getCmsSiteId();
                $contentSiteId = $siteId;
                if (cms_multi_source_hub_enabled()) {
                    $update = cms_fetch_update_with_files($updateModel, $videoId, $siteId);
                    $origin = cms_origin_site_id_for_update_id($videoId);
                    if ($origin !== null) {
                        $contentSiteId = $origin;
                    }
                } else {
                    $update = cms_fetch_update_with_files($updateModel, $videoId, $siteId);
                }
                if (!empty($update) && is_array($update) && cms_update_passes_site_visibility($siteId, $videoId)) {
                    defer_video_view_track($videoId, $userId);

                    if (photos_only_site_enabled()) {
                        $videoData = photos_only_build_detail_payload($update, $videoId, $contentSiteId, false);
                        if ($videoData !== null) {
                            $videoData['likes'] = $formattedLikes;
                            $videoData['like_count'] = $likeCount;
                            $videoData['is_liked'] = $isLiked;
                            if (site_related_content_enabled()) {
                                $relatedVideos = photos_only_build_related_cards(
                                    $updateModel,
                                    $videoId,
                                    $contentSiteId,
                                    8
                                );
                            }
                        }
                    } else {

                    $sceneImages = [];

                    if (!empty($update['files']) && is_array($update['files'])) {
                        foreach ($update['files'] as $file) {
                            if (!is_array($file)) {
                                continue;
                            }
                            $filePath = $file['path'] ?? $file['link'] ?? $file['url'] ?? $file['file'] ?? null;
                            $fileName = $file['name'] ?? $file['filename'] ?? $filePath ?? '';
                            if (empty($filePath) || !preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', (string) $fileName)) {
                                continue;
                            }
                            if (preg_match('/\/thumbs?\//i', (string) $filePath)) {
                                continue;
                            }
                            $resolvedPath = resolveSceneImage($filePath, null);
                            if ($resolvedPath && $resolvedPath !== '/assets/images/placeholders/video-placeholder.jpg') {
                                $sceneImages[] = [
                                    'url' => $resolvedPath,
                                    'thumbnail' => $resolvedPath,
                                ];
                            }
                        }
                    }

                    $sources = buildMemberVideoSources($update);
                    $videoFiles = $sources['full'];
                    if ($sources['trailer'] !== null && $sources['trailer'] !== '') {
                        $videoFiles['trailer'] = $sources['trailer'];
                    }

                    $updateSceneId = (int)($update['scene']['id'] ?? $update['scene_id'] ?? 0);

                    // Also check for scene images in other possible locations
                    if (empty($sceneImages)) {
                        // Check for pics array
                        if (isset($update['scene']['pics']) && is_array($update['scene']['pics'])) {
                            foreach ($update['scene']['pics'] as $index => $pic) {
                                $picPath = is_string($pic) ? $pic : ($pic['path'] ?? $pic['url'] ?? null);
                                if ($picPath) {
                                    $resolvedPath = resolveSceneImage($picPath, null);
                                    // Only add if it's not a placeholder
                                    if ($resolvedPath && $resolvedPath !== '/assets/images/placeholders/video-placeholder.jpg') {
                                        $sceneImages[] = [
                                            'url' => $resolvedPath,
                                            'thumbnail' => $resolvedPath,
                                        ];
                                    }
                                }
                            }
                        }
                        // Check for images array
                        if (empty($sceneImages) && isset($update['images']) && is_array($update['images'])) {
                            foreach ($update['images'] as $index => $img) {
                                $imgPath = null;
                                $thumbRaw = null;

                                if (is_string($img)) {
                                    $imgPath = $img;
                                } elseif (is_array($img)) {
                                    // CMS createImageFileModel: downloadLink + thumbnailLink; also accept path/url/link
                                    $imgPath = $img['path'] ?? $img['downloadLink'] ?? $img['url'] ?? $img['link'] ?? null;
                                    $thumbRaw = $img['thumbnailLink'] ?? null;
                                    if (($imgPath === null || $imgPath === '') && $thumbRaw !== null && $thumbRaw !== '') {
                                        $imgPath = $thumbRaw;
                                    }
                                }

                                if ($imgPath === null || $imgPath === '') {
                                    continue;
                                }

                                $resolvedPath = resolveSceneImage($imgPath, null);
                                $ph = '/assets/images/placeholders/video-placeholder.jpg';
                                if ($resolvedPath === null || $resolvedPath === '' || $resolvedPath === $ph) {
                                    continue;
                                }

                                $resolvedThumb = $resolvedPath;
                                if ($thumbRaw !== null && $thumbRaw !== '' && $thumbRaw !== $imgPath) {
                                    $t = resolveSceneImage($thumbRaw, null);
                                    if ($t !== null && $t !== '' && $t !== $ph) {
                                        $resolvedThumb = $t;
                                    }
                                }

                                $sceneImages[] = [
                                    'url' => $resolvedPath,
                                    'thumbnail' => $resolvedThumb,
                                ];
                            }
                        }
                    }
                    
                    
                    // Primary: best full tier first, then trailer
                    $primaryVideoUrl = $videoFiles['1080p'] ?? $videoFiles['720p'] ?? $videoFiles['480p'] ?? $videoFiles['trailer'] ?? null;
                    
                    // Resolve thumbnail image (remove fallback to placeholder)
                    $thumbnail = null;
                    if (!empty($update['image'])) {
                        $thumbnail = resolveSceneImage($update['image'], null);
                        // If resolveSceneImage returns placeholder, set to null
                        if ($thumbnail === '/assets/images/placeholders/video-placeholder.jpg') {
                            $thumbnail = null;
                        }
                    }
                    
                    // getUpdateWithFiles returns a single update object, not an array
                    // Debug: Check videoLength value
                    if (isset($update['videoLength'])) {
                        // Test if it's milliseconds (if > 3600000, likely milliseconds)
                        if ($update['videoLength'] > 3600000) {
                        }
                    }
                    
                    // Format duration using helper function
                    $duration = formatVideoDuration($update['videoLength'] ?? null, '00:00');
                    
                    $videoData = [
                        'id' => $update['up_date_id'] ?? $videoId,
                        'title' => $update['scene']['title'] ?? 'Video ' . $videoId,
                        'description' => $update['scene']['description'] ?? '',
                        'duration' => $duration,
                        'release_date' => isset($update['liveDate']) ? date('M j, Y', strtotime($update['liveDate'])) : null,
                        'likes' => $formattedLikes,
                        'like_count' => $likeCount,
                        'is_liked' => $isLiked,
                        'thumbnail' => $thumbnail,
                        'video_url' => $primaryVideoUrl,
                        ...cms_hub_video_labels_for_update($update),
                        'video_files' => $videoFiles,
                        'photos' => $sceneImages,
                        'models' => array_map(function($model) {
                            return [
                                'id' => $model['id'] ?? null,
                                'name' => $model['name'] ?? 'Model',
                                'image' => resolveModelImage($model['image'] ?? null),
                            ];
                        }, $update['models'] ?? []),
                        'tags' => [],
                    ];

                    $relatedVideos = [];
                    if (site_related_content_enabled() && !site_related_videos_lazy_enabled()) {
                        $relatedVideos = related_videos_build_cards($videoId, 8);
                    }
                    }
                }
            }
        } catch (\Exception $e) {
            error_log('Error loading CMS data for members-video: ' . $e->getMessage());
        }

        if ($videoData === null) {
            if ($userId && video_likes_feature_enabled() && CmsService::getInstance()->isAvailable()) {
                if (!video_likes_content_still_available($likeStorageId)) {
                    video_likes_delete_for_user($userId, $likeStorageId);
                }
            }
            render_scene_not_found($smarty);
        }

        $smarty->assign('page_title', $videoData['title']);
        $smarty->assign('current_page', '');
        $smarty->assign('video', $videoData);
        $smarty->assign('related_videos', video_likes_enrich_cards_for_request($relatedVideos));
        $smarty->assign('related_videos_lazy', site_related_videos_lazy_enabled());
        $smarty->assign('like_video_id', video_likes_storage_id($videoId));
        $smarty->display('pages/members/video.tpl');
        break;

    case 'members-likes':
        member_require_login($auth);

        if (!video_likes_feature_enabled()) {
            header('Location: /members/profile');
            exit;
        }

        $user = $auth->getUser();
        $userId = (int) ($user['id'] ?? 0);
        $page = max(1, (int) ($_GET['page'] ?? 1));
        $perPage = 12;
        $totalLiked = $userId > 0 ? video_likes_count_for_user($userId, true) : 0;
        $totalPages = max(1, (int) ceil($totalLiked / $perPage));
        $page = min($page, $totalPages);
        $likedVideos = $userId > 0 ? video_likes_build_member_cards($userId, $page, $perPage) : [];
        $showFrom = $totalLiked === 0 ? 0 : (($page - 1) * $perPage) + 1;
        $showTo = $totalLiked === 0 ? 0 : min($page * $perPage, $totalLiked);
        $baseUrl = '/members/likes';
        $prevPageUrl = $page > 1 ? $baseUrl . '?page=' . ($page - 1) : null;
        $nextPageUrl = $page < $totalPages ? $baseUrl . '?page=' . ($page + 1) : null;

        $smarty->assign('page_title', 'Liked Videos');
        $smarty->assign('current_page', 'likes');
        $smarty->assign('liked_videos', $likedVideos);
        $smarty->assign('liked_video_count', $totalLiked);
        $smarty->assign('pagination', [
            'current_page' => $page,
            'total_pages' => $totalPages,
            'base_url' => $baseUrl,
            'url_separator' => '?',
            'prev_page_url' => $prevPageUrl,
            'next_page_url' => $nextPageUrl,
            'show_from' => $showFrom,
            'show_to' => $showTo,
        ]);
        $smarty->display('pages/members/likes.tpl');
        break;

    case 'members-profile':
        if (!$auth->isLoggedIn()) {
            header('Location: /login');
            exit;
        }

        $user = $auth->getUser();
        if (is_array($user)) {
            $user = ccbill_datalink_sync_user_membership($user) ?? $user;
        }
        $membership = member_account_build_summary($user ?? []);
        $memberSince = $user['created_at'] ?? date('Y-m-d');
        $memberSinceFormatted = date('F j, Y', strtotime((string) $memberSince));

        $likedVideoCount = 0;
        if (video_likes_feature_enabled() && !empty($user['id'])) {
            $likedVideoCount = video_likes_count_for_user((int) $user['id'], false);
        }

        $smarty->assign('page_title', 'My Account');
        $smarty->assign('current_page', 'profile');
        $smarty->assign('user', $user);
        $smarty->assign('membership', $membership);
        $smarty->assign('member_since', $memberSinceFormatted);
        $smarty->assign('liked_video_count', $likedVideoCount);
        $smarty->display('pages/members/profile.tpl');
        break;

    case 'contact':
        $sent = false;
        $error = null;
        $namePrefill = '';
        $emailPrefill = '';
        $messagePrefill = '';
        $contactUser = $auth->isLoggedIn() ? $auth->getUser() : null;
        $contactService = \App\Mail\ContactFormService::forCurrentSite();
        $turnstile = \App\Security\TurnstileService::forCurrentSite();
        $turnstileConfigured = $turnstile !== null && $turnstile->isConfigured();
        $turnstileRequired = $turnstile !== null && $turnstile->isRequiredForContact();
        $contactFormAvailable = $contactService->isConfigured()
            && (!$turnstileRequired || $turnstileConfigured);
        if ($contactUser !== null) {
            $namePrefill = trim((string) ($contactUser['username'] ?? ''));
            $emailPrefill = trim((string) ($contactUser['email'] ?? ''));
        }

        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $namePrefill = trim((string) ($_POST['name'] ?? ''));
            $emailPrefill = strtolower(trim((string) ($_POST['email'] ?? '')));
            $messagePrefill = trim((string) ($_POST['message'] ?? ''));
            $csrfToken = $_POST['csrf_token'] ?? '';

            if (!$auth->validateCsrfToken($csrfToken)) {
                $error = 'Invalid security token. Please refresh the page and try again.';
            } elseif ($turnstileRequired && $turnstileConfigured) {
                $turnstileResult = $turnstile->verify(
                    trim((string) ($_POST['cf-turnstile-response'] ?? '')),
                    getClientIp()
                );
                if (!$turnstileResult['ok']) {
                    $error = $turnstileResult['error'] ?? 'Security check failed. Please try again.';
                }
            } elseif ($turnstileRequired && !$turnstileConfigured) {
                $error = 'Contact form is temporarily unavailable. Please try again later.';
            }

            if ($error === null) {
                $result = $contactService->send(
                    $contactUser,
                    $namePrefill,
                    $emailPrefill,
                    $messagePrefill,
                    getClientIp()
                );
                if ($result['ok']) {
                    $sent = true;
                    $messagePrefill = '';
                } else {
                    $error = $result['error'] ?? 'Could not send your message. Please try again later.';
                }
            }
        }

        $smarty->assign('page_title', 'Contact Us');
        $smarty->assign('current_page', '');
        $smarty->assign('is_logged_in', $auth->isLoggedIn());
        $smarty->assign('contact_sent', $sent);
        $smarty->assign('contact_error', $error);
        $smarty->assign('contact_name_prefill', $namePrefill);
        $smarty->assign('contact_email_prefill', $emailPrefill);
        $smarty->assign('contact_message_prefill', $messagePrefill);
        $smarty->assign('contact_member_id', $contactUser !== null ? (int) ($contactUser['id'] ?? 0) : 0);
        $smarty->assign('contact_username', $contactUser !== null ? (string) ($contactUser['username'] ?? '') : '');
        $smarty->assign('contact_form_available', $contactFormAvailable);
        $smarty->assign('contact_turnstile_site_key', $turnstileConfigured ? $turnstile->siteKey() : '');
        $smarty->display('pages/public/static/contact.tpl');
        break;

    case '2257':
        $smarty->assign('page_title', '18 U.S.C. 2257 Statement');
        $smarty->assign('current_page', '');
        $smarty->assign('is_logged_in', $auth->isLoggedIn());
        $smarty->display('pages/public/static/2257.tpl');
        break;

    case 'terms':
        $smarty->assign('page_title', 'Legal Notices and Terms of Use');
        $smarty->assign('current_page', '');
        $smarty->assign('is_logged_in', $auth->isLoggedIn());
        $smarty->display('pages/public/static/terms.tpl');
        break;

    case 'privacy':
        $smarty->assign('page_title', 'Privacy Policy');
        $smarty->assign('current_page', '');
        $smarty->assign('is_logged_in', $auth->isLoggedIn());
        $smarty->display('pages/public/static/privacy.tpl');
        break;

    case 'affiliates':
        require_once __DIR__ . '/../includes/affiliates-helpers.php';
        $smarty->assign('page_title', 'Affiliates');
        $smarty->assign('current_page', '');
        $smarty->assign('is_logged_in', $auth->isLoggedIn());
        assign_affiliates_page_vars($smarty, SiteManager::getCurrentSite());
        $affiliatesTpl = SiteManager::getTemplatePath('pages/public/static/affiliates.tpl');
        $smarty->display($affiliatesTpl);
        break;

    case 'admin-root':
    case 'admin-dashboard':
    case 'admin-users-index':
    case 'admin-users-new':
    case 'admin-users-create':
    case 'admin-users-show':
    case 'admin-users-edit':
    case 'admin-users-update':
    case 'admin-users-delete':
    case 'admin-users-grant-access':
    case 'admin-users-send-network-notice':
    case 'admin-user-import':
    case 'admin-announcements':
    case 'admin-announcements-send':
    case 'admin-failed-logins':
    case 'admin-security':
    case 'admin-downloads':
        admin_dispatch_resolved_route($auth, $path);
        break;

    case 'video-legacy':
        if (serial_scene_updates_enabled()) {
            serial_scene_handle_legacy_video_redirect($auth);
        }
        header('Location: /updates', true, 302);
        exit;

    case '404':
    default:
        if (is_string($route) && str_starts_with($route, 'admin-')) {
            admin_dispatch_resolved_route($auth, $path);
            break;
        }
        http_response_code(404);
        $smarty->assign('page_title', 'Page Not Found');
        $smarty->assign('current_page', '404');
        $smarty->display('pages/public/404.tpl');
        break;
}

ob_end_flush();

