<?php

namespace Components\Ui;

use Components\Base;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;

class MarkdownGuide extends Base
{
    /**
     * Render a Markdown file as a collapsible FAQ/Guide.
     *
     * Expected Markdown structure:
     * - Use headings (default: `##`) to define sections
     * - Content under each heading becomes the collapsible body
     *
     * Options:
     * - baseDir: (string) base directory for relative paths. Default: BEEZUI_ROOT
     * - headingLevel: (int) heading level used for section splits. Default: 2
     * - openFirst: (bool) open first section by default. Default: true
     * - idPrefix: (string) prefix for generated DOM ids. Default: 'mdg'
     * - title: (string|null) optional title for the whole guide
     * - showControls: (bool) show expand/collapse controls when multiple sections exist. Default: true
     */
    public static function renderFromFile(string $markdownFile, array $options = []): string
    {
        $baseDir = isset($options['baseDir']) ? (string)$options['baseDir'] : (defined('BEEZUI_ROOT') ? (string)BEEZUI_ROOT : getcwd());
        $realBase = realpath($baseDir);
        if ($realBase === false) {
            return self::renderError('MarkdownGuide: invalid base directory.');
        }

        $path = $markdownFile;
        if (!self::isAbsolutePath($path)) {
            $path = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
        }

        $real = realpath($path);
        if ($real === false || !is_file($real) || !is_readable($real)) {
            return self::renderError('MarkdownGuide: markdown file not found or not readable.');
        }

        $basePrefix = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
        if (strpos($real, $basePrefix) !== 0) {
            return self::renderError('MarkdownGuide: markdown file must be inside baseDir.');
        }

        $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION));
        if ($ext !== 'md' && $ext !== 'markdown') {
            return self::renderError('MarkdownGuide: only .md/.markdown files are supported.');
        }

        $markdown = (string)file_get_contents($real);
        if ($markdown === '') {
            return self::renderError('MarkdownGuide: markdown file is empty.');
        }

        return self::renderFromString($markdown, $options);
    }

    /**
     * Render a Markdown guide with language fallback.
     *
     * Resolution rules:
     * - If an active language code is available (e.g. `de`) and a sibling file
     *   like `guide.de.md` exists, it will be rendered.
     * - Otherwise it falls back to the original `$markdownFile`.
     *
     * You can force a language via options:
     * - lang: string (e.g. `de` or `de-DE`)
     *
     * Automatic detection (best effort):
     * - `mh_session('language_code')` / `$_SESSION['language_code']`
     * - request `lng_id` / session `languages_id` mapped via `mh_get_language_code()`
     */
    public static function renderFromFileLocalized(string $markdownFile, array $options = []): string
    {
        $lang = self::detectLanguageCode($options);
        if ($lang !== '') {
            foreach (self::buildLocalizedCandidates($markdownFile, $lang) as $candidate) {
                if (self::isReadableMarkdownFile($candidate, $options)) {
                    return self::renderFromFile($candidate, $options);
                }
            }
        }

        return self::renderFromFile($markdownFile, $options);
    }

    public static function renderFromString(string $markdown, array $options = []): string
    {
        $headingLevel = isset($options['headingLevel']) ? (int)$options['headingLevel'] : 2;
        if ($headingLevel < 1 || $headingLevel > 6) {
            $headingLevel = 2;
        }

        $openFirst = isset($options['openFirst']) ? (bool)$options['openFirst'] : true;
        $idPrefix = isset($options['idPrefix']) ? (string)$options['idPrefix'] : 'mdg';
        $title = isset($options['title']) ? (string)$options['title'] : '';
        $showControls = isset($options['showControls']) ? (bool)$options['showControls'] : false;

        $sections = self::parseSections($markdown, $headingLevel);
        if (count($sections) === 0) {
            $sections = [
                [
                    'title' => 'Guide',
                    'bodyMarkdown' => $markdown,
                ],
            ];
        }

        $openMap = [];
        foreach ($sections as $i => $_section) {
            $openMap[(string)$i] = $openFirst && $i === 0;
        }

        // Ensure we serialize as an object (not a JSON array) for consistent key access.
        $openMapJson = json_encode((object)$openMap);
        if (!is_string($openMapJson) || $openMapJson === '') {
            $openMapJson = '{}';
        }

        $hasMultiple = count($sections) > 1;
        $showControls = $showControls && $hasMultiple;

        $openMapJsonAttr = htmlspecialchars($openMapJson, ENT_QUOTES, 'UTF-8');

        ob_start();
        ?>
        <div class="markdown-guide" x-data='{
            openMap: <?= $openMapJsonAttr ?>,
            toggle(i) { this.openMap[i] = !this.openMap[i]; },
            expandAll() { Object.keys(this.openMap).forEach(k => this.openMap[k] = true); },
            collapseAll() { Object.keys(this.openMap).forEach(k => this.openMap[k] = false); },
            allOpen() { return Object.values(this.openMap).every(v => v === true); },
            allClosed() { return Object.values(this.openMap).every(v => v === false); }
        }' x-cloak>

            <?php if ($title !== ''): ?>
                <div class="mb-4">
                    <h2 class="text-base font-semibold text-gray-900 dark:text-white"><?= self::h($title) ?></h2>
                </div>
            <?php endif; ?>

            <?php if ($showControls): ?>
                <div class="mb-3 flex items-center gap-2">
                    <button type="button"
                            class="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-white/10 dark:text-gray-200 dark:hover:bg-white/15"
                            @click="expandAll()"
                            :disabled="allOpen()"
                            :class="allOpen() ? 'opacity-40 cursor-not-allowed' : ''">
                        Expand all
                    </button>
                    <button type="button"
                            class="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-white/10 dark:text-gray-200 dark:hover:bg-white/15"
                            @click="collapseAll()"
                            :disabled="allClosed()"
                            :class="allClosed() ? 'opacity-40 cursor-not-allowed' : ''">
                        Collapse all
                    </button>
                </div>
            <?php endif; ?>

            <div class="divide-y divide-gray-200 dark:divide-white/10 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
                <?php foreach ($sections as $i => $section):
                    $sectionTitle = (string)($section['title'] ?? '');
                    $bodyMd = (string)($section['bodyMarkdown'] ?? '');
                    $bodyHtml = self::markdownToHtml($bodyMd);
                    $safeIndex = (string)$i;
                    $domId = self::h($idPrefix . '-' . self::slug($sectionTitle) . '-' . $safeIndex);
                ?>
                    <div class="bg-white dark:bg-gray-900">
                        <button
                            type="button"
                            class="w-full flex items-center justify-between gap-3 px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-white/5"
                            @click="toggle('<?= self::h($safeIndex) ?>')"
                            :aria-expanded="openMap['<?= self::h($safeIndex) ?>'] ? 'true' : 'false'"
                            aria-controls="<?= $domId ?>"
                        >
                            <span class="text-sm font-medium text-gray-900 dark:text-white">
                                <?= self::h($sectionTitle !== '' ? $sectionTitle : ('Section ' . ($i + 1))) ?>
                            </span>
                            <span class="shrink-0 text-gray-500 dark:text-gray-400" :class="openMap['<?= self::h($safeIndex) ?>'] ? 'rotate-180' : ''" style="transition: transform 150ms ease;">
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
                                    <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd" />
                                </svg>
                            </span>
                        </button>

                        <div
                            id="<?= $domId ?>"
                            x-show="openMap['<?= self::h($safeIndex) ?>']"
                            x-collapse.duration.200ms
                            x-cloak
                            class="px-4 pb-4"
                        >
                            <div class="text-sm text-gray-700 dark:text-gray-200 leading-6">
                                <?= $bodyHtml ?>
                            </div>
                        </div>
                    </div>
                <?php endforeach; ?>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Parse Markdown into titled sections by heading level.
     *
     * @return array<int, array{title: string, bodyMarkdown: string}>
     */
    public static function parseSections(string $markdown, int $headingLevel = 2): array
    {
        $markdown = str_replace(["\r\n", "\r"], "\n", $markdown);
        $lines = explode("\n", $markdown);

        $prefix = str_repeat('#', max(1, min(6, $headingLevel))) . ' ';

        $sections = [];
        $currentTitle = '';
        $currentBody = [];
        $started = false;

        foreach ($lines as $line) {
            if (strpos($line, $prefix) === 0) {
                if ($started) {
                    $sections[] = [
                        'title' => trim($currentTitle),
                        'bodyMarkdown' => trim(implode("\n", $currentBody)),
                    ];
                }

                $started = true;
                $currentTitle = trim(substr($line, strlen($prefix)));
                // Strip common trailing anchors like "{#id}" if present.
                $currentTitle = preg_replace('/\s*\{#.+\}\s*$/', '', $currentTitle) ?? $currentTitle;
                $currentBody = [];
                continue;
            }

            if ($started) {
                $currentBody[] = $line;
            }
        }

        if ($started) {
            $sections[] = [
                'title' => trim($currentTitle),
                'bodyMarkdown' => trim(implode("\n", $currentBody)),
            ];
        }

        // If there is leading content before the first heading, keep it as an intro section.
        if (!$started) {
            return [];
        }

        /*

        $leading = self::leadingContentBeforeFirstHeading($markdown, $headingLevel);
        if ($leading !== '') {
            array_unshift($sections, [
                'title' => 'Introduction',
                'bodyMarkdown' => $leading,
            ]);
        }
*/
        return $sections;
    }

    private static function leadingContentBeforeFirstHeading(string $markdown, int $headingLevel): string
    {
        $markdown = str_replace(["\r\n", "\r"], "\n", $markdown);
        $prefix = str_repeat('#', max(1, min(6, $headingLevel))) . ' ';
        $pos = strpos($markdown, "\n" . $prefix);
        if ($pos === false) {
            // Also consider heading at the very start of the document
            if (strpos($markdown, $prefix) === 0) {
                return '';
            }
            return '';
        }
        $leading = substr($markdown, 0, $pos);
        return trim($leading);
    }

    private static function markdownToHtml(string $markdown): string
    {
        static $converter = null;
        if ($converter === null) {
            $environment = new Environment([
                // Basic hardening: no raw HTML passthrough, no unsafe links
                'html_input' => 'strip',
                'allow_unsafe_links' => false,
            ]);
            $converter = new CommonMarkConverter([], $environment);
        }

        try {
            $html = $converter->convert($markdown)->getContent();
            return is_string($html) ? $html : '';
        } catch (\Throwable $e) {
            return '';
        }
    }

    private static function renderError(string $message): string
    {
        ob_start();
        ?>
        <div class="rounded-md bg-danger-50 dark:bg-danger-500/10 p-4 outline outline-danger-500 dark:outline-danger-500/20">
            <div class="text-sm text-danger-700 dark:text-danger-200"><?= self::h($message) ?></div>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function slug(string $value): string
    {
        $value = strtolower(trim($value));
        $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
        $value = trim($value, '-');
        return $value !== '' ? $value : 'section';
    }

    private static function isAbsolutePath(string $path): bool
    {
        if ($path === '') {
            return false;
        }
        if ($path[0] === '/' || $path[0] === '\\') {
            return true;
        }
        return (bool)preg_match('/^[A-Za-z]:\\\\/', $path);
    }

    private static function detectLanguageCode(array $options): string
    {
        $raw = '';
        if (isset($options['lang'])) {
            $raw = (string)$options['lang'];
        } elseif (isset($options['language'])) {
            $raw = (string)$options['language'];
        }

        if ($raw === '') {
            try {
                if (function_exists('mh_session')) {
                    $raw = (string)\mh_session('language_code', '');
                }
            } catch (\Throwable $e) {
                // ignore
            }
        }
        if ($raw === '' && isset($_SESSION['language_code'])) {
            $raw = (string)($_SESSION['language_code'] ?? '');
        }

        if ($raw === '') {
            $lngId = 0;
            try {
                if (function_exists('mh_get')) {
                    $lngId = (int)\mh_get('lng_id', 0, 'int');
                } elseif (function_exists('mh_request')) {
                    $lngId = (int)\mh_request('lng_id', 0, 'int');
                }
            } catch (\Throwable $e) {
                // ignore
            }

            if ($lngId <= 0) {
                try {
                    if (function_exists('mh_session')) {
                        $lngId = (int)\mh_session('languages_id', 0);
                    }
                } catch (\Throwable $e) {
                    // ignore
                }
            }

            if ($lngId > 0) {
                try {
                    if (function_exists('mh_get_language_code')) {
                        $code = \mh_get_language_code($lngId);
                        if (is_string($code) && $code !== '') {
                            $raw = $code;
                        }
                    } elseif (function_exists('mh_get_language_list')) {
                        $list = (array)\mh_get_language_list();
                        foreach ($list as $li) {
                            if (!is_array($li)) {
                                continue;
                            }
                            $id = isset($li['id']) ? (int)$li['id'] : (int)($li['languages_id'] ?? 0);
                            if ($id !== $lngId) {
                                continue;
                            }
                            $code = isset($li['code']) ? (string)$li['code'] : (string)($li['languages_code'] ?? '');
                            if ($code !== '') {
                                $raw = $code;
                                break;
                            }
                        }
                    }
                } catch (\Throwable $e) {
                    // ignore
                }
            }
        }

        $raw = strtolower(trim($raw));
        $raw = str_replace('_', '-', $raw);
        $raw = preg_replace('/[^a-z0-9\-]/', '', $raw) ?? '';
        $raw = trim($raw, '-');

        return $raw;
    }

    /**
     * @return string[]
     */
    private static function buildLocalizedCandidates(string $markdownFile, string $lang): array
    {
        if ($markdownFile === '' || $lang === '') {
            return [];
        }

        $ext = pathinfo($markdownFile, PATHINFO_EXTENSION);
        if ($ext === '') {
            return [];
        }

        $base = substr($markdownFile, 0, -strlen($ext) - 1);
        if ($base === '') {
            return [];
        }

        $candidates = [];
        $candidates[] = $base . '.' . $lang . '.' . $ext;

        // Also try primary language if a region code is present (e.g. de-de -> de)
        $dashPos = strpos($lang, '-');
        if ($dashPos !== false) {
            $primary = substr($lang, 0, $dashPos);
            if ($primary !== '' && $primary !== $lang) {
                $candidates[] = $base . '.' . $primary . '.' . $ext;
            }
        }

        // De-duplicate while preserving order
        $out = [];
        foreach ($candidates as $c) {
            if (!in_array($c, $out, true)) {
                $out[] = $c;
            }
        }
        return $out;
    }

    private static function isReadableMarkdownFile(string $markdownFile, array $options = []): bool
    {
        $baseDir = isset($options['baseDir']) ? (string)$options['baseDir'] : (defined('BEEZUI_ROOT') ? (string)BEEZUI_ROOT : getcwd());
        $realBase = realpath($baseDir);
        if ($realBase === false) {
            return false;
        }

        $path = $markdownFile;
        if (!self::isAbsolutePath($path)) {
            $path = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
        }

        $real = realpath($path);
        if ($real === false || !is_file($real) || !is_readable($real)) {
            return false;
        }

        $basePrefix = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
        if (strpos($real, $basePrefix) !== 0) {
            return false;
        }

        $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION));
        if ($ext !== 'md' && $ext !== 'markdown') {
            return false;
        }

        return true;
    }
}
