<?php

declare(strict_types=1);

namespace PpPhp\Modules\Style\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\DB;
use PpPhp\Core\ModuleManager;
use PpPhp\Core\ModuleState;
use PpPhp\Core\ProjectNav;
use PpPhp\Core\Security;
use PpPhp\Core\Theme\ThemeRegistry;
use PpPhp\Core\Theme\ThemeCompiler;
use PpPhp\Core\Theme\NavbarRegistry;
use PpPhp\Core\Theme\MenuModel;
use PpPhp\Core\Assets\SpaAssetManager;

/**
 * Style module — theme designer.
 *
 * Themes are pure data (design tokens + optional dark overrides). Built-in
 * presets live in resources/Themes/presets; the project's own themes and the
 * chosen default live in ModuleState 'themes'. This controller serves the SPA
 * and the CRUD endpoints; ThemeRegistry / ThemeCompiler do the real work.
 */
class StyleController
{
    private array $project = [];

    public function __construct(
        private Config $config,
        private ModuleManager $moduleManager
    ) {}

    public function index(string $slug): string
    {
        $this->initProject($slug);

        $project = $this->project;
        $navItems = ProjectNav::items('/style');
        $baseUrl = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user = $_SESSION['user'];
        $pageTitle = 'Style';

        $themes = ThemeRegistry::catalogue($slug);
        $default = ThemeRegistry::activeDefault($slug);
        $tokenKeys = ThemeCompiler::tokenKeys();

        // Real designed pages, master templates + common pages (for the live preview) + icon stylesheets.
        $designer = ModuleState::load($slug, 'designer');
        $designerPages = $designer['pages'] ?? [];
        $designerTemplates = $designer['templates'] ?? [];
        $designerCommon = $designer['common'] ?? [];
        $iconCssHtml = SpaAssetManager::renderIconCss($baseUrl);

        // Shell palette names, navbar types + project navigation menu.
        $shellPalettes = ThemeCompiler::shellPaletteNames();
        $navbarTypes = NavbarRegistry::all();
        $menu = MenuModel::normalize(ThemeRegistry::menu($slug));
        $projectShell = ThemeRegistry::projectShell($slug) ?: null;

        // Real access vocabulary (roles + groups) the Menu Editor offers for
        // per-item visibility. Sourced from the Security module so that as an
        // admin defines groups / roles for the generated app, they surface here
        // automatically. Best-effort — a brand-new project has none yet.
        $roleVocab = $this->collectRoleVocab($slug);

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

    /**
     * Style module guide — an in-app, theme-aware reference for the theme
     * system: tokens, presets, layouts, the grid/menu editors and how it all
     * reaches the generated app.
     */
    public function guide(string $slug): string
    {
        $this->initProject($slug);

        $project   = $this->project;
        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');

        // The guide documents the real catalogue, so it can never drift from
        // what the compiler actually emits.
        $catalogue = ThemeCompiler::tokenCatalogue();
        $docs      = ThemeCompiler::tokenDocs();
        $authored  = ThemeCompiler::authoredProperties();
        $navbars   = NavbarRegistry::all();
        $presets   = ThemeRegistry::catalogue($slug);

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

    /**
     * Collect the project's real access vocabulary for the Menu Editor:
     *  - groups: rows from the Security module's `groups` table (id + name)
     *  - roles:  distinct values of the mapped user table's role field
     *
     * Everything is best-effort and defensive: a new project (no security
     * config, no groups table) simply returns empty arrays, and the editor
     * falls back to its built-in role suggestions. When the upcoming in-app
     * group/permission manager writes to these same stores, the entries appear
     * here with no further wiring.
     *
     * @return array{groups: array<int, array{id:int, name:string}>, roles: array<int, string>}
     */
    private function collectRoleVocab(string $slug): array
    {
        $groups = [];
        $roles = [];

        // Groups — defined in Security → Groups (`groups` table).
        try {
            $rows = DB::FetchAll('SELECT id, name FROM `groups` ORDER BY name') ?: [];
            foreach ($rows as $row) {
                $name = trim((string) ($row['name'] ?? ''));
                if ($name !== '') {
                    $groups[] = ['id' => (int) ($row['id'] ?? 0), 'name' => $name];
                }
            }
        } catch (\Throwable) {
            // No groups table yet — fine.
        }

        // Roles — distinct values of the mapped role field, per Security config.
        try {
            $security = ModuleState::load($slug, 'security');
            $table = $this->safeIdentifier((string) ($security['users_table'] ?? ''));
            $roleField = $this->safeIdentifier((string) ($security['role_field'] ?? ''));
            if ($table !== '' && $roleField !== '') {
                $col = DB::QuoteIdentifier($roleField);
                $rows = DB::FetchAll('SELECT DISTINCT ' . $col . ' AS r FROM ' . DB::QuoteIdentifier($table) . ' WHERE ' . $col . " IS NOT NULL AND $col <> ''") ?: [];
                foreach ($rows as $row) {
                    $r = trim((string) ($row['r'] ?? ''));
                    if ($r !== '' && !in_array($r, $roles, true)) {
                        $roles[] = $r;
                    }
                }
            }
        } catch (\Throwable) {
            // Table not created / mapped yet — fine.
        }
        natcasesort($roles);

        return ['groups' => $groups, 'roles' => array_values($roles)];
    }

    private function safeIdentifier(string $identifier): string
    {
        $identifier = trim($identifier);
        return preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $identifier) ? $identifier : '';
    }

    public function load(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        echo json_encode([
            'success' => true,
            'themes' => ThemeRegistry::catalogue($slug),
            'default' => ThemeRegistry::activeDefault($slug),
            'tokenKeys' => ThemeCompiler::tokenKeys(),
            'shellPalettes' => ThemeCompiler::shellPaletteNames(),
            'navbarTypes' => NavbarRegistry::all(),
            'menu' => MenuModel::normalize(ThemeRegistry::menu($slug)),
        ]);
        exit;
    }

    /**
     * Compile a DRAFT theme with the real ThemeCompiler and return both the CSS
     * and the fully resolved palette.
     *
     * The SPA used to re-implement the compiler in JavaScript, which meant the
     * live preview drifted from what the build actually emits (and knew nothing
     * about the ~76 derived tokens). Compiling server-side makes this endpoint
     * the single source of truth for the preview AND the Palette panel, so what
     * a designer sees is exactly what ships.
     */
    public function compile(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);

        $input = json_decode(file_get_contents('php://input'), true) ?? [];
        $theme = is_array($input['theme'] ?? null) ? $input['theme'] : [];
        $scope = (string) ($input['scope'] ?? ':root');
        // Only ever a preview selector or :root — never arbitrary CSS.
        if (!preg_match('/^[:#.\w-]{1,64}$/', $scope)) $scope = ':root';

        $css = ThemeCompiler::css($theme, $scope);

        echo json_encode([
            'success'   => true,
            'css'       => $css,
            'palette'   => self::resolvePalette($css, $scope),
            'catalogue' => ThemeCompiler::tokenCatalogue(),
            'docs'      => ThemeCompiler::tokenDocs(),
            // Which properties come straight from a Theme-tab field, vs. which
            // the compiler derives. The Palette panel badges them so a developer
            // knows what is safe to author and what will be recomputed.
            'authored'  => ThemeCompiler::authoredProperties(),
        ]);
        exit;
    }

    /**
     * Read the compiled CSS back into { light: {prop:value}, dark: {...} }.
     *
     * Parsing the emitted stylesheet (rather than recomputing) guarantees the
     * swatches show the literal values the browser will use. Dark inherits every
     * light value it does not override, mirroring the cascade.
     *
     * @return array{light: array<string,string>, dark: array<string,string>}
     */
    private static function resolvePalette(string $css, string $scope): array
    {
        $light = [];
        $dark  = [];
        // Split into blocks: "<selector> { ... }"
        if (preg_match_all('/([^{}]+)\{([^{}]*)\}/', $css, $blocks, PREG_SET_ORDER)) {
            foreach ($blocks as [, $sel, $body]) {
                $isDark = str_contains($sel, '.is-dark');
                if (!preg_match_all('/(--pp-[\w-]+)\s*:\s*([^;]+);/', $body, $decls, PREG_SET_ORDER)) continue;
                foreach ($decls as [, $prop, $val]) {
                    $val = trim($val);
                    if ($isDark) $dark[$prop] = $val; else $light[$prop] = $val;
                }
            }
        }
        // Dark inherits anything it doesn't restate.
        return ['light' => $light, 'dark' => array_replace($light, $dark)];
    }

    public function save(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        Security::RequireCsrf();

        $input = json_decode(file_get_contents('php://input'), true) ?? [];
        $theme = $input['theme'] ?? null;

        $state = ModuleState::load($slug, 'themes');
        $state['themes'] = is_array($state['themes'] ?? null) ? $state['themes'] : [];

        if (is_array($theme)) {
            $id = $this->slugifyId((string) ($theme['id'] ?? ''));
            if ($id === '') {
                http_response_code(400);
                echo json_encode(['success' => false, 'message' => 'A theme id is required.']);
                exit;
            }
            // presets are read-only — a save always lands in the project's own set
            $state['themes'][$id] = [
                'id' => $id,
                'label' => (string) ($theme['label'] ?? ucfirst($id)),
                'description' => (string) ($theme['description'] ?? ''),
                'base' => ($theme['base'] ?? null) ?: null,
                'tokens' => is_array($theme['tokens'] ?? null) ? $theme['tokens'] : [],
                'dark' => is_array($theme['dark'] ?? null) ? $theme['dark'] : [],
                'shell' => is_array($theme['shell'] ?? null) ? $theme['shell'] : [],
                'grid' => is_array($theme['grid'] ?? null) ? $theme['grid'] : [],
                'density' => (string) ($theme['density'] ?? 'normal'),
                'surface' => (($theme['surface'] ?? 'flat') === 'glass') ? 'glass' : 'flat',
                'css' => (string) ($theme['css'] ?? ''),
            ];
        }

        if (array_key_exists('default', $input) && is_string($input['default']) && $input['default'] !== '') {
            // Only accept a default that actually resolves to a theme — a shipped
            // preset or one of the project's saved themes (incl. any just added
            // above). Guards against a stray/typo'd id that would leave the Build
            // silently falling back to the 'default' preset.
            $known = array_keys($state['themes']);
            foreach (ThemeRegistry::presets() as $p) {
                if (!empty($p['id'])) $known[] = (string) $p['id'];
            }
            if (in_array($input['default'], $known, true)) {
                $state['default'] = $input['default'];
            }
        } elseif (is_array($theme) && empty($state['default'])) {
            // First saved theme becomes the active (built) theme automatically —
            // otherwise the Build would silently keep using the 'default' preset.
            $state['default'] = $this->slugifyId((string) ($theme['id'] ?? ''));
        }

        if (isset($input['menu']) && is_array($input['menu'])) {
            $state['menu'] = MenuModel::normalize($input['menu']);
        }

        // Project-level layout (shell) — independent of the chosen theme
        if (isset($input['shell']) && is_array($input['shell'])) {
            $state['shell'] = $input['shell'];
        }

        ModuleState::save($slug, 'themes', $state);

        echo json_encode([
            'success' => true,
            'themes' => ThemeRegistry::catalogue($slug),
            'default' => ThemeRegistry::activeDefault($slug),
        ]);
        exit;
    }

    public function delete(string $slug): void
    {
        header('Content-Type: application/json');
        $this->initProject($slug);
        Security::RequireCsrf();

        $input = json_decode(file_get_contents('php://input'), true) ?? [];
        $id = (string) ($input['id'] ?? '');

        $state = ModuleState::load($slug, 'themes');
        $state['themes'] = is_array($state['themes'] ?? null) ? $state['themes'] : [];

        if ($id !== '' && isset($state['themes'][$id])) {
            unset($state['themes'][$id]);
            if (($state['default'] ?? '') === $id) {
                $state['default'] = 'default';
            }
            ModuleState::save($slug, 'themes', $state);
        }

        echo json_encode([
            'success' => true,
            'themes' => ThemeRegistry::catalogue($slug),
            'default' => ThemeRegistry::activeDefault($slug),
        ]);
        exit;
    }

    private function slugifyId(string $id): string
    {
        $id = strtolower(trim($id));
        $id = (string) preg_replace('/[^a-z0-9]+/', '-', $id);
        return trim($id, '-');
    }

    private function initProject(string $slug): void
    {
        Security::RequireAuth();
        DB::Init();

        $project = DB::Select('projects', ['slug' => $slug, 'created_by' => Security::CurrentUserData()['id']]);
        if (empty($project) && Security::IsAdmin()) {
            $project = DB::Select('projects', ['slug' => $slug]);
        }
        if (empty($project)) {
            http_response_code(404);
            echo 'Project not found.';
            exit;
        }

        $this->project = $project[0];
        // Standalone (database-free) projects have no project DB — Style/theming
        // works from files only. Skip the project connection so it doesn't fatal
        // into a blank page (matches Finished/CustomFiles).
        if (\PpPhp\Core\ProjectDatabaseProfile::isStandaloneSlug($slug)) {
            return;
        }
        DB::Init($slug);
        DB::SetConnection($slug);
    }
}
