<?php

declare(strict_types=1);

/**
 * Application Configuration
 *
 * Core application constants, session setup, and helper functions
 * for the Gmind Institute computer education portal.
 */

// ─── Application Constants ───────────────────────────────────────────────────

const APP_NAME = 'Gmind Institute';
const APP_URL  = 'https://advaysingh.co.in/gmind';
const APP_ROOT = __DIR__ . '/..';
const PUBLIC_ROOT = APP_ROOT;  // Public root is the project root (not /public subdirectory)

// Base path for URLs (used for asset paths, redirects, etc.)
const BASE_PATH = '/gmind';

// ─── Debug Mode ──────────────────────────────────────────────────────────────

const APP_DEBUG = true;

// Route debug mode — set to true to see routing diagnostics
const ROUTE_DEBUG = false;

// ─── Session Configuration ───────────────────────────────────────────────────

const SESSION_LIFETIME = 3600;       // 1 hour in seconds
const SESSION_NAME     = 'gmind_session';

// ─── Language / i18n Configuration ──────────────────────────────────────────

const DEFAULT_LOCALE   = 'en';                    // Default language
const SUPPORTED_LOCALES = ['en', 'hi'];           // English + Hindi
const LOCALE_PATH      = APP_ROOT . '/lang';       // Translation files directory
const LOCALE_COOKIE    = 'gmind_lang';             // Cookie name for language preference
const LOCALE_COOKIE_TTL = 86400 * 365;            // 1 year cookie lifetime

// ─── Timezone ────────────────────────────────────────────────────────────────

date_default_timezone_set('Asia/Kolkata');

// ─── Security Functions ──────────────────────────────────────────────────────

/**
 * Generate a CSRF token and store it in the session.
 *
 * Generates a new token if one does not already exist for the current session,
 * ensuring the token persists across requests within the same session.
 *
 * @return string The CSRF token
 */
function generateCsrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }

    return $_SESSION['csrf_token'];
}

/**
 * Verify a CSRF token using constant-time comparison.
 *
 * Uses hash_equals() to prevent timing attacks when comparing tokens.
 *
 * @param string $token The token submitted with the form
 * @return bool True if the token is valid, false otherwise
 */
function verifyCsrfToken(string $token): bool
{
    if (empty($_SESSION['csrf_token']) || empty($token)) {
        return false;
    }

    return hash_equals($_SESSION['csrf_token'], $token);
}

// ─── Helper Functions ────────────────────────────────────────────────────────

/**
 * Sanitize input data for safe output in HTML.
 *
 * Trims whitespace, applies htmlspecialchars with UTF-8 encoding,
 * and optionally filters through filter_var for additional safety.
 *
 * @param string $input The raw input string
 * @return string The sanitized string
 */
function sanitize(string $input): string
{
    $trimmed = trim($input);
    $escaped = htmlspecialchars($trimmed, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $filtered = filter_var($escaped, FILTER_SANITIZE_SPECIAL_CHARS);

    return $filtered !== false ? $filtered : $escaped;
}

/**
 * Perform an HTTP redirect and terminate script execution.
 *
 * @param string $url The URL to redirect to (absolute or relative)
 * @return never
 */
function redirect(string $url): never
{
    // Prevent header injection by stripping newline characters
    $url = str_replace(["\r", "\n", '%0d', '%0a'], '', $url);

    header("Location: {$url}");
    exit;
}

/**
 * Set or retrieve a flash message from the session.
 *
 * When called with a message, it stores the message in the session
 * under the given key. When called without a message, it retrieves
 * and removes the stored message.
 *
 * @param string $key     The flash message key
 * @param string|null $message The message to store, or null to retrieve
 * @return string|null The stored message, or null if none exists
 */
function flash(string $key, ?string $message = null): ?string
{
    if ($message !== null) {
        $_SESSION['flash'][$key] = $message;
        return $message;
    }

    if (isset($_SESSION['flash'][$key])) {
        $msg = $_SESSION['flash'][$key];
        unset($_SESSION['flash'][$key]);
        return $msg;
    }

    return null;
}

/**
 * Convert text into a URL-safe slug.
 *
 * Transliterates non-ASCII characters, converts to lowercase,
 * replaces non-alphanumeric characters with hyphens, and
 * collapses multiple hyphens into one.
 *
 * @param string $text The text to slugify
 * @return string The generated slug
 */
function slugify(string $text): string
{
    // Transliterate non-ASCII characters
    $transliterated = transliterator_transliterate(
        'Any-Latin; Latin-ASCII; Lower()',
        $text
    );

    if ($transliterated === false) {
        $transliterated = $text;
    }

    // Replace non-alphanumeric characters with hyphens
    $slug = preg_replace('/[^a-z0-9]+/i', '-', $transliterated);

    if ($slug === null) {
        $slug = $text;
    }

    // Trim hyphens from ends and collapse multiple hyphens
    $slug = trim($slug, '-');
    $slug = preg_replace('/-+/', '-', $slug);

    return strtolower($slug);
}

/**
 * Generate a URL with the BASE_PATH prefix.
 *
 * @param string $path The path (without leading BASE_PATH)
 * @return string The full URL path
 */
function url(string $path = ''): string
{
    $path = '/' . ltrim($path, '/');
    return BASE_PATH . $path;
}

/**
 * Generate an asset URL with the BASE_PATH prefix.
 *
 * @param string $path The asset path relative to public root
 * @return string The full asset URL path
 */
function asset(string $path = ''): string
{
    $path = '/' . ltrim($path, '/');
    return BASE_PATH . $path;
}

/**
 * Generate a URL for uploaded files stored in the database.
 *
 * Database image paths are stored as 'uploads/...' (relative to project root).
 * Since the app is deployed in a subdirectory (/gmind), we must prepend
 * BASE_PATH so the browser resolves the URL correctly.
 *
 * Handles paths that may already contain BASE_PATH, are absolute URLs,
 * or are just relative paths from the database.
 *
 * @param string $path The upload path from the database (e.g. 'uploads/sliders/slider-1.jpg')
 * @return string The full URL path (e.g. '/gmind/uploads/sliders/slider-1.jpg')
 */
function upload_url(string $path): string
{
    // Already an absolute URL (http:// or https://)
    if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
        return $path;
    }

    // Already has BASE_PATH prefix — return as-is
    $bp = BASE_PATH;
    if ($bp !== '' && str_starts_with($path, $bp . '/')) {
        return $path;
    }

    // Starts with / — it's an absolute path from web root
    if (str_starts_with($path, '/')) {
        // If BASE_PATH is set and path doesn't start with it, prepend it
        if ($bp !== '' && !str_starts_with($path, $bp)) {
            return $bp . $path;
        }
        return $path;
    }

    // Relative path from database (e.g. 'uploads/sliders/slider-1.jpg')
    return $bp . '/' . $path;
}

// ─── Language / i18n Helper Functions ────────────────────────────────────────

/**
 * Get the current active locale.
 *
 * Priority: 1) URL param ?lang=xx  2) Cookie  3) Session  4) Browser Accept-Language  5) DEFAULT_LOCALE
 * Sets cookie and session for persistence.
 *
 * @return string Current locale code ('en' or 'hi')
 */
function getCurrentLocale(): string
{
    // 1. Check URL parameter (for toggle clicks)
    if (isset($_GET['lang']) && is_string($_GET['lang'])) {
        $lang = strtolower(trim($_GET['lang']));
        if (in_array($lang, SUPPORTED_LOCALES, true)) {
            // Persist the choice (only if headers not already sent)
            if (!headers_sent()) {
                setcookie(LOCALE_COOKIE, $lang, time() + LOCALE_COOKIE_TTL, '/', '', false, false);
            }
            $_SESSION['gmind_locale'] = $lang;
            return $lang;
        }
    }

    // 2. Check cookie
    if (isset($_COOKIE[LOCALE_COOKIE])) {
        $lang = strtolower(trim($_COOKIE[LOCALE_COOKIE]));
        if (in_array($lang, SUPPORTED_LOCALES, true)) {
            if (!headers_sent()) {
                $_SESSION['gmind_locale'] = $lang;
            }
            return $lang;
        }
    }

    // 3. Check session
    if (isset($_SESSION['gmind_locale'])) {
        $lang = $_SESSION['gmind_locale'];
        if (in_array($lang, SUPPORTED_LOCALES, true)) {
            return $lang;
        }
    }

    // 4. Browser Accept-Language header
    if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
        $browserLangs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
        foreach ($browserLangs as $bl) {
            $code = strtolower(substr(trim($bl), 0, 2));
            if ($code === 'hi' || $code === 'hn') {
                $_SESSION['gmind_locale'] = 'hi';
                return 'hi';
            }
        }
    }

    // 5. Default
    return DEFAULT_LOCALE;
}

/**
 * Load translation strings for the given locale.
 *
 * @param string $locale Locale code ('en', 'hi')
 * @return array<string, string> Associative array of translation key => string
 */
function loadTranslations(string $locale): array
{
    $file = LOCALE_PATH . '/' . $locale . '.php';
    if (file_exists($file)) {
        return require $file;
    }

    // Fallback to English if requested locale file doesn't exist
    $fallback = LOCALE_PATH . '/en.php';
    if (file_exists($fallback)) {
        return require $fallback;
    }

    return [];
}

/**
 * Translate a key into the current locale's string.
 *
 * Uses a global cache to avoid re-loading translations on every call.
 * Falls back to English if the key is missing in the current locale,
 * and returns the key itself if not found in English either.
 *
 * @param string $key Translation key (e.g. 'nav.home', 'stats.students')
 * @param array<string, string> $params Optional parameters for string interpolation
 * @return string Translated string
 */
function __(string $key, array $params = []): string
{
    static $translations = null;
    static $fallbackTranslations = null;

    // Load current locale translations (cached per request)
    if ($translations === null) {
        $locale = getCurrentLocale();
        $translations = loadTranslations($locale);

        // Also load English fallback (only if current locale is not English)
        if ($locale !== 'en') {
            $fallbackTranslations = loadTranslations('en');
        } else {
            $fallbackTranslations = [];
        }
    }

    // Look up key in current locale
    $text = $translations[$key] ?? null;

    // Fallback to English if not found
    if ($text === null && !empty($fallbackTranslations)) {
        $text = $fallbackTranslations[$key] ?? null;
    }

    // Final fallback: return the key itself
    if ($text === null) {
        return $key;
    }

    // Interpolate parameters (e.g. __('msg', ['name' => 'Ravi']) → "Hello, Ravi!")
    if (!empty($params)) {
        foreach ($params as $placeholder => $value) {
            $text = str_replace(':' . $placeholder, (string) $value, $text);
        }
    }

    return $text;
}

/**
 * Get the alternate locale (for toggle button display).
 *
 * If current is 'en', returns 'hi'; if current is 'hi', returns 'en'.
 *
 * @return string The other supported locale
 */
function getAlternateLocale(): string
{
    $current = getCurrentLocale();
    return $current === 'en' ? 'hi' : 'en';
}

/**
 * Get the label for the language toggle button.
 *
 * Returns the label in the target language (e.g. "हिंदी" when current is English,
 * "English" when current is Hindi).
 *
 * @return string Toggle button label
 */
function getLangToggleLabel(): string
{
    $alternate = getAlternateLocale();
    return $alternate === 'hi' ? 'हिंदी' : 'English';
}

/**
 * Check if current locale is Hindi.
 *
 * @return bool True if Hindi, false otherwise
 */
function isHindi(): bool
{
    return getCurrentLocale() === 'hi';
}

/**
 * Get a bilingual field value from a database row.
 *
 * If current locale is Hindi and the _hi field is non-empty, returns it.
 * Otherwise returns the original field value (English fallback).
 *
 * @param array<string, mixed> $row Database row
 * @param string $field Field name (e.g. 'name', 'description')
 * @return string The field value in current locale, or English fallback
 */
function getBilingual(array $row, string $field): string
{
    if (getCurrentLocale() === 'hi') {
        $hiField = $field . '_hi';
        if (!empty($row[$hiField])) {
            return (string) $row[$hiField];
        }
    }
    return (string) ($row[$field] ?? '');
}
