<?php
declare(strict_types=1);

use App\Support\Translator;

if (!function_exists('quaeriva_escape')) {
    function quaeriva_escape(mixed $value): string
    {
        return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }
}

if (!function_exists('quaeriva_t')) {
    function quaeriva_t(Translator $translator, string $key, array $replace = []): string
    {
        return quaeriva_escape($translator->get($key, $replace));
    }
}

if (!function_exists('quaeriva_url')) {
    function quaeriva_url(string $path = '', array $query = []): string
    {
        $base = app_base_path();
        $path = trim($path, "\\/");
        $url = $base === '' ? '' : $base;

        // A slug that kept its own script is stored decoded, so it has to be
        // percent-encoded on the way into an href, a canonical tag or the
        // sitemap. Segments that are pure ASCII come back unchanged, and one
        // that already carries a % is left alone rather than double-encoded.
        if ($path !== '' && preg_match('/[^\x00-\x7F]/', $path) === 1 && !str_contains($path, '%')) {
            $path = implode('/', array_map('rawurlencode', explode('/', $path)));
        }

        if ($path !== '') {
            $url .= '/' . $path;
        } else {
            $url .= '/';
        }

        if ($query !== []) {
            $url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
        }

        return $url === '' ? '/' : $url;
    }
}


if (!function_exists('quaeriva_default_avatar_url')) {
    function quaeriva_default_avatar_url(int $userId): string
    {
        $variant = (abs($userId) % 8) + 1;
        return quaeriva_url('assets/media/avatars/avatar-' . $variant . '.svg');
    }
}

if (!function_exists('quaeriva_render_template')) {
    /** @param array<string, mixed> $data */
    function quaeriva_render_template(string $template, array $data = []): string
    {
        $direction = 'ltr';
        if (($data['localeContext'] ?? null) instanceof \App\Support\LocaleContext) {
            $direction = ($data['localeContext'])->direction();
        }
        $activeTheme = trim((string) ($data['_theme'] ?? ($GLOBALS['quaeriva_active_theme'] ?? 'default')));
        $file = \App\Theme\ThemeManager::templatePath($template, $activeTheme, $direction)
            ?? app_root_path('app', 'views', $template . '.php');

        if (!is_file($file)) {
            throw new RuntimeException('View template not found.');
        }

        extract($data, EXTR_SKIP);
        ob_start();

        try {
            require $file;
            return (string) ob_get_clean();
        } catch (Throwable $throwable) {
            ob_end_clean();
            throw $throwable;
        }
    }
}

if (!function_exists('quaeriva_plain_text')) {
    function quaeriva_plain_text(mixed $value): string
    {
        return nl2br(quaeriva_escape($value), false);
    }
}

if (!function_exists('quaeriva_rich_text')) {
    function quaeriva_rich_text(mixed $value, string $mode = 'full'): string
    {
        static $formatter = null;
        if (!$formatter instanceof \App\Content\RichTextFormatter) {
            $formatter = new \App\Content\RichTextFormatter();
        }
        return $formatter->render((string) $value, $mode);
    }
}

if (!function_exists('quaeriva_rich_editor_attributes')) {
    function quaeriva_rich_editor_attributes(Translator $translator, string $mode = 'full', string $value = ''): string
    {
        $mode = $mode === 'comment' ? 'comment' : 'full';
        $labels = [
            'bold' => $translator->get('editor.bold'),
            'italic' => $translator->get('editor.italic'),
            'heading' => $translator->get('editor.heading'),
            'list' => $translator->get('editor.list'),
            'numbered-list' => $translator->get('editor.numbered_list'),
            'quote' => $translator->get('editor.quote'),
            'code' => $translator->get('editor.code'),
            'code-block' => $translator->get('editor.code_block'),
            'link' => $translator->get('editor.link'),
            'cancel' => $translator->get('common.cancel'),
        ];
        $attributes = ' data-rich-editor="' . quaeriva_escape($mode) . '"';
        foreach ($labels as $key => $label) {
            $attributes .= ' data-editor-' . $key . '="' . quaeriva_escape($label) . '"';
        }
        $attributes .= ' data-preview-url="' . quaeriva_escape(quaeriva_url('preview')) . '"';
        // The upload endpoint is advertised only to accounts allowed to use it,
        // so the editor never offers an action the server would refuse.
        if (quaeriva_user_has_role(\App\Web\Session::user(), 'admin', 'moderator')) {
            $attributes .= ' data-image-upload-url="' . quaeriva_escape(quaeriva_url('content-image')) . '"';
            $attributes .= ' data-editor-image-uploading="' . quaeriva_escape($translator->get('editor.image_uploading')) . '"';
            $attributes .= ' data-editor-image-failed="' . quaeriva_escape($translator->get('editor.image_failed')) . '"';
        }
        $attributes .= ' data-editor-markdown="' . quaeriva_escape($translator->get('editor.markdown')) . '"';
        $attributes .= ' data-editor-link-url-prompt="' . quaeriva_escape($translator->get('editor.link_url_prompt')) . '"';
        $attributes .= ' data-editor-invalid-link="' . quaeriva_escape($translator->get('editor.invalid_link')) . '"';
        $attributes .= ' data-initial-html="' . quaeriva_escape(quaeriva_rich_text($value, $mode)) . '"';
        return $attributes;
    }
}

if (!function_exists('quaeriva_bot_guard_fields')) {
    function quaeriva_bot_guard_fields(): string
    {
        $token = \App\Security\FormBotGuard::sessionGuard()->issue();
        return '<input type="hidden" name="_qb_started" value="' . quaeriva_escape($token['started']) . '">' .
            '<input type="hidden" name="_qb_sig" value="' . quaeriva_escape($token['signature']) . '">' .
            '<div class="form-bot-trap" aria-hidden="true"><label>Leave this field empty<input type="text" name="_qb_hp" value="" tabindex="-1" autocomplete="off"></label></div>';
    }
}

if (!function_exists('quaeriva_turnstile_widget')) {
    /** @param array<string,mixed>|null $site */
    function quaeriva_turnstile_widget(?array $site = null): string
    {
        if (!is_array($site) || ($site['turnstile_enabled'] ?? false) !== true) return '';
        $siteKey = trim((string) ($site['turnstile_site_key'] ?? ''));
        if ($siteKey === '') return '';
        return '<div class="cf-turnstile" data-sitekey="' . quaeriva_escape($siteKey) . '"></div>';
    }
}

if (!function_exists('quaeriva_datetime_utc')) {
    /** Database timestamps are stored in UTC; turn any accepted input into a UTC instant. */
    function quaeriva_datetime_utc(mixed $datetime): ?DateTimeImmutable
    {
        if ($datetime === null || $datetime === '' || $datetime === 0) return null;
        try {
            if ($datetime instanceof DateTimeInterface) {
                return DateTimeImmutable::createFromInterface($datetime)->setTimezone(new DateTimeZone('UTC'));
            }
            if (is_numeric($datetime)) {
                return (new DateTimeImmutable('@' . (int) $datetime))->setTimezone(new DateTimeZone('UTC'));
            }
            $raw = trim((string) $datetime);
            if ($raw === '' || str_starts_with($raw, '0000-00-00')) return null;
            return new DateTimeImmutable($raw, new DateTimeZone('UTC'));
        } catch (Throwable) {
            return null;
        }
    }
}

if (!function_exists('quaeriva_format_datetime')) {
    function quaeriva_format_datetime(mixed $datetime, string $format = 'Y-m-d H:i'): string
    {
        $moment = quaeriva_datetime_utc($datetime);
        if ($moment === null) return '';

        return $moment->setTimezone(new DateTimeZone(date_default_timezone_get()))->format($format);
    }
}

if (!function_exists('quaeriva_relative_time')) {
    function quaeriva_relative_time(mixed $datetime, ?Translator $translator = null): string
    {
        $moment = quaeriva_datetime_utc($datetime);
        if ($moment === null) return '';

        $diff = time() - $moment->getTimestamp();
        // A future timestamp means a clock or timezone problem, not "now": clamp it
        // so the bug is visible instead of being hidden behind "just now".
        if ($diff < 0) $diff = 0;

        if ($diff < 60) {
            return $translator ? $translator->get('common.time_just_now') : 'just now';
        }
        if ($diff < 3600) {
            $count = max(1, (int) floor($diff / 60));
            return $translator ? quaeriva_time_ago_text($translator, 'minutes', $count) : ($count . 'm ago');
        }
        if ($diff < 86400) {
            $count = max(1, (int) floor($diff / 3600));
            return $translator ? quaeriva_time_ago_text($translator, 'hours', $count) : ($count . 'h ago');
        }
        if ($diff < 2592000) {
            $count = max(1, (int) floor($diff / 86400));
            return $translator ? quaeriva_time_ago_text($translator, 'days', $count) : ($count . 'd ago');
        }

        return quaeriva_format_datetime($moment, 'Y-m-d');
    }
}

if (!function_exists('quaeriva_time_ago_text')) {
    /** Arabic needs singular / dual / small-plural forms; English collapses to one. */
    function quaeriva_time_ago_text(Translator $translator, string $unit, int $count): string
    {
        $suffix = match (true) {
            $count === 1 => '_one',
            $count === 2 => '_two',
            $count <= 10 => '_few',
            default => '',
        };
        $key = 'common.time_' . $unit . '_ago' . $suffix;
        $text = $translator->get($key, ['count' => $count]);

        return $text === $key ? $translator->get('common.time_' . $unit . '_ago', ['count' => $count]) : $text;
    }
}

if (!function_exists('quaeriva_timezone_groups')) {
    /**
     * Every IANA zone, grouped by region and sorted alphabetically inside each group.
     * No country or region is promoted above another.
     *
     * @return array<string, array<string, string>>
     */
    function quaeriva_timezone_groups(): array
    {
        $groups = [];
        $now = new DateTimeImmutable('now');

        foreach (DateTimeZone::listIdentifiers() as $identifier) {
            $parts = explode('/', $identifier, 2);
            $group = count($parts) === 2 ? $parts[0] : 'UTC';
            $offset = (new DateTimeZone($identifier))->getOffset($now);
            $sign = $offset < 0 ? '-' : '+';
            $offset = abs($offset);
            $label = str_replace('_', ' ', $identifier)
                . sprintf(' — UTC%s%02d:%02d', $sign, intdiv($offset, 3600), intdiv($offset % 3600, 60));
            $groups[$group][$identifier] = $label;
        }

        ksort($groups);
        foreach ($groups as $group => $zones) {
            asort($zones);
            $groups[$group] = $zones;
        }

        return $groups;
    }
}

if (!function_exists('quaeriva_permalink_settings')) {
    /**
     * Question permalink shape, set once per request from the seo.* settings.
     *
     * The prefix and structure live here rather than being threaded through
     * every view because roughly thirty call sites build a question URL, and a
     * link that disagrees with the router is a 404 the operator cannot see.
     *
     * @return array{prefix: string, structure: string}
     */
    function quaeriva_permalink_settings(): array
    {
        $prefix = (string) ($GLOBALS['quaeriva_question_prefix'] ?? 'q');
        $prefix = quaeriva_permalink_prefix_is_valid($prefix) ? $prefix : 'q';
        $structure = (string) ($GLOBALS['quaeriva_question_permalink'] ?? 'slug');

        return [
            'prefix' => $prefix,
            'structure' => in_array($structure, ['slug', 'id-slug', 'id', 'id-dash-slug'], true) ? $structure : 'slug',
        ];
    }
}

if (!function_exists('quaeriva_icon')) {
    /** Render one of the fixed, dependency-free interface icons. */
    function quaeriva_icon(string $name): string
    {
        $paths = [
            'bold' => '<path d="M7 5h6.5a4.5 4.5 0 0 1 1.7 8.67A4.5 4.5 0 0 1 13.5 22H7V5Zm3 3v4h3.5a2 2 0 1 0 0-4H10Zm0 7v4h3.5a2 2 0 1 0 0-4H10Z"/>',
            'italic' => '<path d="M14 5h6v3h-2.08l-4.84 8H16v3H7v-3h2.08l4.84-8H11V5h3Z"/>',
            'heading' => '<path d="M5 5h3v6h8V5h3v14h-3v-5H8v5H5V5Z"/>',
            'bullet-list' => '<path d="M5 6.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM8 5h13v3H8V5Zm0 5.5h13v3H8v-3ZM8 16h13v3H8v-3Z"/>',
            'numbered-list' => '<path d="M3 5h2v5H3V7H2V5h1Zm-1 8h3v2H2v-1.5l1.5-1.2H2v-1.8h3v1.5L3.5 14H5v2H2v-3ZM2 18h3v2H2v-1.5l1.7-1.4H2V15h3v1.5L3.5 18H5v2H2v-2ZM8 5h13v3H8V5Zm0 5.5h13v3H8v-3ZM8 16h13v3H8v-3Z"/>',
            'quote' => '<path d="M6 6h6v6H8.5A3.5 3.5 0 0 0 12 15.5V18a6 6 0 0 1-6-6V6Zm10 0h6v6h-3.5a3.5 3.5 0 0 0 3.5 3.5V18a6 6 0 0 1-6-6V6Z"/>',
            'inline-code' => '<path d="m8.7 7.3-5 5 5 5 2.1-2.1-2.9-2.9 2.9-2.9-2.1-2.1Zm6.6 0-2.1 2.1 2.9 2.9-2.9 2.9 2.1 2.1 5-5-5-5Z"/>',
            'code-block' => '<path d="m7.7 6.3-6 5.7 6 5.7 2-2.1-3.8-3.6 3.8-3.6-2-2.1Zm8.6 0-2 2.1 3.8 3.6-3.8 3.6 2 2.1 6-5.7-6-5.7Z"/><path d="M12 4h2l-2 16h-2l2-16Z"/>',
            'link' => '<path d="M10.6 13.4a4 4 0 0 0 5.66 0l2-2a4 4 0 0 0-5.66-5.66l-1.15 1.15 2.12 2.12 1.15-1.15a1 1 0 1 1 1.42 1.42l-2 2a1 1 0 0 1-1.42 0l-2.12 2.12Zm2.8-2.8a4 4 0 0 0-5.66 0l-2 2a4 4 0 1 0 5.66 5.66l1.15-1.15-2.12-2.12-1.15 1.15a1 1 0 1 1-1.42-1.42l2-2a1 1 0 0 1 1.42 0l2.12-2.12Z"/>',
            'markdown-toggle' => '<path d="M3 5h18a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm2 3v8h2v-4l2 3 2-3v4h2V8h-2l-2 3-2-3H5Zm10 0v3h-2v2h2v3h2v-3h2v-2h-2V8h-2Z"/>',
            'thumbs-up' => '<path d="M10 10V5.5A2.5 2.5 0 0 1 12.5 3l.5.1V8h6a2 2 0 0 1 2 2l-1.1 8.2A2 2 0 0 1 18 20H10V10Zm-6 0h3v10H4V10Z"/>',
            'thumbs-down' => '<path d="M10 14v4.5A2.5 2.5 0 0 0 12.5 21l.5-.1V16h6a2 2 0 0 0 2-2l-1.1-8.2A2 2 0 0 0 18 4H10v10ZM4 4h3v10H4V4Z"/>',
            'bookmark' => '<path d="M6 3h12a2 2 0 0 1 2 2v16l-8-4-8 4V5a2 2 0 0 1 2-2Zm0 3v10.15l6-3 6 3V6H6Z"/>',
            'flag' => '<path d="M6 3v18H3V3h3Zm2 1h11l-2 4 2 4H8V4Z"/>',
            'share' => '<path d="m14 5 5 5-5 5v-3c-4.8 0-7.6 1.5-9.5 5.1C5 11.6 8 9 14 9V5Z"/>',
            'check' => '<path d="m9.2 16.2-4.4-4.4-2.1 2.1 6.5 6.5L23 6.6l-2.1-2.1-11.7 11.7Z"/>',
        ];
        $path = $paths[$name] ?? '';
        return '<svg class="icon icon-' . quaeriva_escape($name) . '" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">' . $path . '</svg>';
    }
}

if (!function_exists('quaeriva_permalink_prefix_is_valid')) {
    /** Reserved segments would shadow a real route and take the section offline. */
    function quaeriva_permalink_prefix_is_valid(string $prefix): bool
    {
        $reserved = [
            'admin', 'api', 'ask', 'assets', 'c', 'contact', 'forgot-password', 'install',
            'lang', 'login', 'logout', 'notifications', 'preview', 'public', 'register',
            'resend-verification', 'reset-password', 'robots.txt', 'search', 'settings',
            'sitemap.xml', 't', 'tags', 'u', 'users',
        ];

        return preg_match('/^[a-z0-9][a-z0-9_-]{0,31}$/', $prefix) === 1
            && !in_array($prefix, $reserved, true);
    }
}

if (!function_exists('quaeriva_question_path')) {
    /** The canonical path for a question, without the base path or encoding. */
    function quaeriva_question_path(string $slug, int $id = 0): string
    {
        ['prefix' => $prefix, 'structure' => $structure] = quaeriva_permalink_settings();
        $slug = trim($slug);

        // Falling back to the slug form whenever the id is unknown keeps every
        // caller that only has a slug -- notifications, the API, older code --
        // pointing at a URL the router still resolves.
        if ($id <= 0 || $structure === 'slug') {
            return $prefix . '/' . ($slug !== '' ? $slug : (string) $id);
        }

        return match ($structure) {
            'id' => $prefix . '/' . $id,
            'id-slug' => $prefix . '/' . $id . '/' . $slug,
            'id-dash-slug' => $prefix . '/' . $id . '-' . $slug,
            default => $prefix . '/' . $slug,
        };
    }
}

if (!function_exists('quaeriva_question_url')) {
    /** @param array<string, mixed> $query */
    function quaeriva_question_url(string $slug, int $id = 0, array $query = []): string
    {
        return quaeriva_url(quaeriva_question_path($slug, $id), $query);
    }
}

if (!function_exists('quaeriva_question_action_url')) {
    /**
     * Vote, follow, report and answer endpoints keep the slug form whatever the
     * permalink structure is: they are POST targets, never indexed, and pinning
     * them means the structure setting cannot break a form.
     *
     * @param array<string, mixed> $query
     */
    function quaeriva_question_action_url(string $slug, string $action = '', array $query = []): string
    {
        $path = quaeriva_permalink_settings()['prefix'] . '/' . trim($slug);
        $action = trim($action, '/');

        return quaeriva_url($action === '' ? $path : $path . '/' . $action, $query);
    }
}
