<?php

declare(strict_types=1);

namespace PpPhp\Modules\Help\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\Security;

class HelpController
{
    /**
     * Developer guides restricted to owners/administrators. They are hidden from
     * the sidebar and search for other roles and return 403 on direct access.
     */
    private const ADMIN_TOPICS = ['designer_components', 'field_plugins', 'performance', 'licensing', 'updates', 'advisories', 'visitor_engine'];

    /**
     * Guides restricted to the platform OWNER alone (admin-ppradhan). Stricter
     * than ADMIN_TOPICS — hidden from superadmin/developer too, matching the
     * owner-only Deliverables menu they document.
     */
    private const OWNER_TOPICS = ['owner_deliverables', 'community_updates', 'server_migration'];

    public function __construct(private Config $config) {}

    /** True when the current user may not see this topic (owner/admin gating). */
    private function topicBlocked(string $topic): bool
    {
        $developer = in_array($topic, self::ADMIN_TOPICS, true) || in_array($topic, self::OWNER_TOPICS, true);
        // On a distributed CLIENT edition, every developer/platform guide is hidden
        // outright — these describe the platform's internal structure, file layout
        // and the vendor's own licensing/update/provisioning operations, none of
        // which belong in a customer copy. (Belt-and-suspenders: the packager also
        // strips their help/ folders, so on a fresh client the files aren't even
        // present — this closes the door at runtime too, including any already-
        // deployed bundle.)
        if ($developer && \PpPhp\Core\Licensing\LicenseGate::isClientEdition()) return true;
        if (in_array($topic, self::ADMIN_TOPICS, true) && !Security::IsAdmin()) return true;
        if (in_array($topic, self::OWNER_TOPICS, true) && !Security::IsOwner()) return true;
        return false;
    }

    public function index(): string
    {
        Security::RequireAuth();
        // The home page lists the module guides, so it needs the topic metadata
        // and base URL in scope (loadContent() is generic and has neither).
        $baseUrl    = $this->config->get('base_url');
        $topicsMeta = $this->topicsMeta();
        $isAdmin    = Security::IsAdmin();
        $file = constant('BASE_PATH') . '/help/index/index.php';
        ob_start();
        if (is_file($file)) include $file;
        $content = (string) ob_get_clean();
        return $this->render($content, 'Welcome', 'index');
    }

    public function topic(string $topic): string
    {
        Security::RequireAuth();
        if ($topic === 'search') {
            http_response_code(404);
            return $this->render('<div class="alert alert-danger">Topic not found.</div>', 'Not found');
        }
        $topic = preg_replace('/[^a-zA-Z0-9_]/', '', $topic);
        // Developer guides are for owners/admins only; a few are owner-only.
        if ($this->topicBlocked($topic)) {
            http_response_code(403);
            $who = in_array($topic, self::OWNER_TOPICS, true) ? 'the platform owner' : 'owners and administrators';
            return $this->render('<div class="alert alert-danger"><i class="bi bi-shield-lock"></i> This guide is available to ' . $who . ' only.</div>', 'Restricted');
        }
        $content = $this->loadContent($topic);
        if ($content === null) {
            http_response_code(404);
            $content = '<div class="alert alert-danger">Topic not found.</div>';
        }
        return $this->render($content, ucfirst(str_replace('_', ' ', $topic)), $topic);
    }

    public function search(): string
    {
        Security::RequireAuth();
        $query = trim($_GET['q'] ?? '');
        $results = [];

        if (!empty($query)) {
            $results = $this->performSearch($query);
        }

        $content = $this->renderSearchResults($results, $query);
        return $this->render($content, 'Search Results');
    }

    private function performSearch(string $query): array
    {
        $helpDir = constant('BASE_PATH') . '/help/';
        $results = [];
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($helpDir, \RecursiveDirectoryIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if ($file->getExtension() !== 'php') continue;

            $relativePath = str_replace($helpDir, '', $file->getPathname());
            $parts = explode('/', $relativePath);
            $topic = $parts[0];

            // Never surface owner/admin-only guides in search for other roles.
            if ($this->topicBlocked($topic)) continue;

            $content = file_get_contents($file->getPathname());
            $text = strip_tags($content);

            $count = mb_substr_count(mb_strtolower($text), mb_strtolower($query));
            if ($count > 0 || stripos($topic, $query) !== false) {
                // Find the nearest heading anchor that contains the query
                $anchor = $this->findRelevantAnchor($content, $query);
                $snippet = $this->extractSnippet($text, $query);
                $results[] = [
                    'topic'   => $topic,
                    'title'   => ucfirst(str_replace('_', ' ', $topic)),
                    'snippet' => $snippet,
                    'url'     => '/help/' . $topic . ($anchor ? '#' . $anchor : ''),
                    'score'   => $count + (stripos($topic, $query) !== false ? 10 : 0),
                ];
            }
        }

        usort($results, fn($a, $b) => $b['score'] <=> $a['score']);
        return $results;
    }

    /**
     * Scan HTML content for a heading (h2, h3) whose id or text contains the search query.
     * Returns the anchor id, or null if none found.
     */
    private function findRelevantAnchor(string $html, string $query): ?string
    {
        // Match <h2 id="..."> or <h3 id="..."> and capture the id
        preg_match_all('/<h[23]\s+[^>]*\bid="([^"]*)"[^>]*>(.*?)<\/h[23]>/si', $html, $matches, PREG_SET_ORDER);
        foreach ($matches as $match) {
            $id   = $match[1];
            $text = strip_tags($match[2]);
            if (stripos($id, $query) !== false || stripos($text, $query) !== false) {
                return $id;
            }
        }
        return null;
    }

    private function extractSnippet(string $text, string $query): string
    {
        $pos = stripos($text, $query);
        if ($pos === false) {
            return substr($text, 0, 200) . '...';
        }
        $start = max(0, $pos - 60);
        $length = 120 + strlen($query);
        $snippet = substr($text, $start, $length);
        if ($start > 0) $snippet = '...' . $snippet;
        if ($start + $length < strlen($text)) $snippet .= '...';
        return htmlspecialchars($snippet);
    }

    private function renderSearchResults(array $results, string $query): string
    {
        if (empty($query)) {
            return '<div class="alert alert-info">Enter a keyword to search the documentation.</div>';
        }
        if (empty($results)) {
            return '<div class="alert alert-warning">No results found for <strong>' . htmlspecialchars($query) . '</strong>.</div>';
        }

        $html = '<h4>Search results for "' . htmlspecialchars($query) . '"</h4>';
        $html .= '<p class="text-muted">' . count($results) . ' topic(s) found.</p>';
        $html .= '<div class="list-group">';
        foreach ($results as $r) {
            $html .= '<a href="' . $this->config->get('base_url') . $r['url'] . '" class="list-group-item list-group-item-action">';
            $html .= '<strong>' . htmlspecialchars($r['title']) . '</strong>';
            $html .= '<br><small class="text-muted">' . $r['snippet'] . '</small>';
            $html .= '</a>';
        }
        $html .= '</div>';
        return $html;
    }

    private function loadContent(string $name): ?string
    {
        $file = constant('BASE_PATH') . '/help/' . $name . '/index.php';
        if (file_exists($file)) {
            // Expose the app base URL to the content file so cross-links between
            // guides resolve correctly under any subpath (they build their hrefs
            // from $baseUrl, e.g. "$baseUrl/help/PPjs").
            $baseUrl = (string) $this->config->get('base_url');
            ob_start();
            include $file;
            return ob_get_clean();
        }
        return null;
    }

    private function render(string $content, string $title, string $active = ''): string
    {
        $baseUrl = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $topics = $this->getTopics();
        $topicsMeta = $this->topicsMeta();
        $grouped = $this->groupedTopics();          // sidebar sections, ordered
        $groups  = self::groups();
        $activeTopic = $active;

        // Prev / Next within the ordered topic list (skip on home / search).
        $prev = $next = null;
        $flat = $this->orderedTopics();
        foreach ($flat as $i => $m) {
            if ($m['name'] === $active) {
                $prev = $flat[$i - 1] ?? null;
                $next = $flat[$i + 1] ?? null;
                break;
            }
        }

        ob_start();
        include __DIR__ . '/../Views/layout.php';
        return ob_get_clean();
    }

    /**
     * Rich metadata for every VISIBLE topic — used by the sidebar and the home
     * page. Each entry: name, label, icon, blurb, category (general|developer),
     * admin flag. Unknown topics fall back to a humanised label + generic icon.
     *
     * @return array<int,array{name:string,label:string,icon:string,blurb:string,cat:string,admin:bool}>
     */
    private function topicsMeta(): array
    {
        $out = [];
        foreach ($this->getTopics() as $name) {
            $out[] = $this->topicInfo($name);
        }
        return $out;
    }

    /** @return array{name:string,label:string,icon:string,blurb:string,cat:string,admin:bool} */
    private function topicInfo(string $name): array
    {
        // group = sidebar section (see self::groups() for labels + order);
        // order = position within the section. cat is kept for access-gating.
        static $map = [
            // ── Pages & events (where does my code go?) ──
            'events'        => ['label' => 'Events & the page lifecycle', 'icon' => 'bi-diagram-3-fill', 'cat' => 'general', 'group' => 'guides', 'order' => 1, 'blurb' => 'Every event, when it runs, and exactly where to paste your code — page, field and button events.'],
            'page_list'     => ['label' => 'The List page',   'icon' => 'bi-table',            'cat' => 'general', 'group' => 'guides', 'order' => 2, 'blurb' => 'Filter, sort, colour rows, row clicks, bulk actions — the List page and its events.'],
            'page_add_edit' => ['label' => 'Add & Edit pages', 'icon' => 'bi-pencil-square',   'cat' => 'general', 'group' => 'guides', 'order' => 3, 'blurb' => 'Defaults, validation, calculated fields, dependent fields — the data-entry forms and their events.'],
            'page_view'     => ['label' => 'The View page',   'icon' => 'bi-eye-fill',         'cat' => 'general', 'group' => 'guides', 'order' => 4, 'blurb' => 'Read-only record pages — highlight values, conditional formatting, action buttons.'],
            'page_print'    => ['label' => 'Print & PDF pages', 'icon' => 'bi-printer-fill',    'cat' => 'general', 'group' => 'guides', 'order' => 5, 'blurb' => 'Printable + PDF record pages — branding, computed values, auto-print, server-side PDF.'],
            'page_export_import' => ['label' => 'Export & Import', 'icon' => 'bi-arrow-down-up', 'cat' => 'general', 'group' => 'guides', 'order' => 6, 'blurb' => 'CSV/Excel/JSON export and import — shape the rows, validate, and react.'],
            'page_dashboard' => ['label' => 'Dashboard pages', 'icon' => 'bi-speedometer2',      'cat' => 'general', 'group' => 'guides', 'order' => 7, 'blurb' => 'KPI tiles, charts and widgets — bind live data, refresh, and script the dashboard.'],
            'page_master_detail' => ['label' => 'Master / Detail', 'icon' => 'bi-diagram-2-fill', 'cat' => 'general', 'group' => 'guides', 'order' => 8, 'blurb' => 'Parent + child grids — read and write children two-way, total onto the master, lock buttons.'],
            'events_field'  => ['label' => 'Field events',    'icon' => 'bi-input-cursor-text', 'cat' => 'general', 'group' => 'guides', 'order' => 9,  'blurb' => 'Per-field hooks — Format, Calculate, Validate on the server; On Change / Focus / Blur in the browser.'],
            'events_global' => ['label' => 'Global events',   'icon' => 'bi-globe2',           'cat' => 'general', 'group' => 'guides', 'order' => 10, 'blurb' => 'App-wide hooks — Authentication, Email, Application start-up, File, API.'],

            // ── Components (for everyone who builds pages in the Designer) ──
            'component_custom_button' => ['label' => 'Custom Button', 'icon' => 'bi-play-btn-fill', 'cat' => 'components', 'group' => 'components', 'order' => 1, 'blurb' => 'A button with a 3-stage event flow — Client Before (JS) → Server (PHP) → Client After (JS).'],

            // ── Client-side (JavaScript) ──
            'PPjs'                => ['label' => 'PP.js — Frontend API', 'icon' => 'bi-braces',        'cat' => 'general', 'group' => 'client', 'order' => 1, 'blurb' => 'The client-side JavaScript API — Runner controls, grid, Master/Detail, buttons, dialogs.'],

            // ── Server-side (PHP) ──
            'pp_server_api'       => ['label' => 'pp() — Server API',    'icon' => 'bi-hdd-network-fill', 'cat' => 'general', 'group' => 'server', 'order' => 0, 'blurb' => 'The PHP companion to pp.js — drive the browser live from the server (toast, set fields, reload) plus context and permission-aware data.'],
            'sql_variables'       => ['label' => 'SQL Variables',       'icon' => 'bi-braces-asterisk',  'cat' => 'general', 'group' => 'server', 'order' => 3, 'blurb' => 'Dynamic queries with :session.x, :user.field, :userid — used in WHERE tabs, Lookup Wizard, charts and your own SQL.'],
            'DB'                  => ['label' => 'Database Helpers',     'icon' => 'bi-database-fill',  'cat' => 'general', 'group' => 'server', 'order' => 1, 'blurb' => 'Query and update the database from your own code.'],
            'Helpers'             => ['label' => 'Helpers',              'icon' => 'bi-tools',          'cat' => 'general', 'group' => 'server', 'order' => 2, 'blurb' => 'Global helper functions available everywhere.'],
            'Email'               => ['label' => 'Email',               'icon' => 'bi-envelope-fill',  'cat' => 'general', 'group' => 'server', 'order' => 3, 'blurb' => 'Send transactional and templated email.'],
            'SMS'                 => ['label' => 'SMS',                  'icon' => 'bi-chat-dots-fill', 'cat' => 'general', 'group' => 'server', 'order' => 4, 'blurb' => 'Send text messages from your apps.'],
            'PDF'                 => ['label' => 'PDF',                  'icon' => 'bi-file-earmark-pdf', 'cat' => 'general', 'group' => 'server', 'order' => 5, 'blurb' => 'Generate PDF documents from your data.'],
            'Search'              => ['label' => 'Search',               'icon' => 'bi-search',         'cat' => 'general', 'group' => 'server', 'order' => 6, 'blurb' => 'Add fast search across your records.'],
            'Notification'        => ['label' => 'Notifications',        'icon' => 'bi-bell-fill',      'cat' => 'general', 'group' => 'server', 'order' => 7, 'blurb' => 'Raise in-app notifications for your users.'],
            'Calendar'            => ['label' => 'Calendar & Events',    'icon' => 'bi-calendar-event', 'cat' => 'general', 'group' => 'server', 'order' => 8, 'blurb' => 'Enable, customize and script the standalone calendar app.'],
            'Security'            => ['label' => 'Security',             'icon' => 'bi-lock-fill',      'cat' => 'general', 'group' => 'server', 'order' => 9, 'blurb' => 'Authentication, permissions and app protections.'],

            // ── Developer & platform (owner / administrators) ──
            'designer_components' => ['label' => 'Designer Components', 'icon' => 'bi-grid-1x2-fill',   'cat' => 'developer', 'group' => 'developer', 'order' => 1, 'blurb' => 'Build custom drag-and-drop components for the visual designer.'],
            'field_plugins'       => ['label' => 'Field Plugins',       'icon' => 'bi-plugin',          'cat' => 'developer', 'group' => 'developer', 'order' => 2, 'blurb' => 'Create your own view, edit and filter field controls.'],
            'performance'         => ['label' => 'Performance',          'icon' => 'bi-speedometer2',    'cat' => 'developer', 'group' => 'developer', 'order' => 3, 'blurb' => 'Keep the console and your generated apps fast.'],
            'licensing'           => ['label' => 'Licensing',           'icon' => 'bi-shield-check',    'cat' => 'developer', 'group' => 'developer', 'order' => 5, 'blurb' => 'The licence server, signed leases and activation.'],
            'updates'             => ['label' => 'Online Updates',       'icon' => 'bi-cloud-arrow-down', 'cat' => 'developer', 'group' => 'developer', 'order' => 6, 'blurb' => 'Publish and deliver over-the-air platform updates.'],
            'advisories'          => ['label' => 'Push to Clients',      'icon' => 'bi-broadcast',        'cat' => 'developer', 'group' => 'developer', 'order' => 6.5, 'blurb' => 'Send notices and force critical security patches to your licensed installs — Apple-style, signed, targeted.'],
            'owner_deliverables'  => ['label' => 'Owner: Files & Deliverables', 'icon' => 'bi-send-fill', 'cat' => 'developer', 'group' => 'developer', 'order' => 7, 'blurb' => 'Where every file lives, and how to update & download the marketing site and community installer.'],
            'community_updates'   => ['label' => 'Owner: Community Updates (OTA)', 'icon' => 'bi-arrow-repeat', 'cat' => 'developer', 'group' => 'developer', 'order' => 7.5, 'blurb' => 'Push signed, in-place updates to deployed community forums — no reinstall. How to publish a release and how the forum applies it safely.'],
            'server_migration'    => ['label' => 'Owner: Server Migration', 'icon' => 'bi-hdd-network-fill', 'cat' => 'developer', 'group' => 'developer', 'order' => 7.7, 'blurb' => 'Move the whole platform — database, projects, user data and files — to a new server (cPanel/DirectAdmin/VPS). Rehearse, commit, then flip. Fully reversible.'],
            'visitor_engine'      => ['label' => 'Visitor Engine (Analytics)', 'icon' => 'bi-graph-up-arrow', 'cat' => 'developer', 'group' => 'developer', 'order' => 8, 'blurb' => 'The unified analytics reporter — accurate geo/device, channels, API clients, multi-property collector, and the self-updating IP databases.'],
        ];
        $info = $map[$name] ?? [
            'label' => ucfirst(str_replace('_', ' ', $name)),
            'icon'  => 'bi-file-earmark-text',
            'cat'   => 'general',
            'group' => 'server',
            'order' => 99,
            'blurb' => '',
        ];
        $info['name']  = $name;
        $info['group'] = $info['group'] ?? 'server';
        $info['order'] = $info['order'] ?? 99;
        $info['admin'] = in_array($name, self::ADMIN_TOPICS, true) || in_array($name, self::OWNER_TOPICS, true);
        $info['owner'] = in_array($name, self::OWNER_TOPICS, true);
        return $info;
    }

    /** Sidebar sections in display order: key => [label, icon, order]. */
    public static function groups(): array
    {
        return [
            'guides'     => ['label' => 'Pages & Events',        'icon' => 'bi-signpost-split', 'order' => 1],
            'components' => ['label' => 'Components',            'icon' => 'bi-grid-1x2-fill', 'order' => 2],
            'client'     => ['label' => 'Client API · JavaScript', 'icon' => 'bi-braces',      'order' => 3],
            'server'     => ['label' => 'Server API · PHP',       'icon' => 'bi-server',        'order' => 4],
            'developer'  => ['label' => 'Developer & platform',   'icon' => 'bi-shield-lock',   'order' => 9],
        ];
    }

    /** topicsMeta grouped + ordered for the sidebar/home: [groupKey => [meta,…]]. */
    private function groupedTopics(): array
    {
        $meta = $this->topicsMeta();
        usort($meta, static fn ($a, $b) => ($a['order'] ?? 99) <=> ($b['order'] ?? 99));
        $out = [];
        foreach (array_keys(self::groups()) as $g) $out[$g] = [];
        foreach ($meta as $m) { $out[$m['group']][] = $m; }
        return array_filter($out);   // drop empty sections
    }

    /** Flat, ordered topic list (group order, then in-group order) for prev/next. */
    private function orderedTopics(): array
    {
        $flat = [];
        foreach ($this->groupedTopics() as $items) foreach ($items as $m) $flat[] = $m;
        return $flat;
    }

    private function getTopics(): array
    {
        $helpDir = constant('BASE_PATH') . '/help/';
        $topics = [];
        if (is_dir($helpDir)) {
            foreach (scandir($helpDir) as $entry) {
                if ($entry === '.' || $entry === '..') continue;
                // Skip the "index" folder — it's the welcome page
                if (strtolower($entry) === 'index') continue;
                // Hide owner/admin-only developer guides from other roles.
                if ($this->topicBlocked($entry)) continue;
                if (is_dir($helpDir . $entry) && file_exists($helpDir . $entry . '/index.php')) {
                    $topics[] = $entry;
                }
            }
        }
        return $topics;
    }


}
