<?php

namespace App\Http\Controllers\Admin;

use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Models\Setting;
use App\Sections\SectionRegistry;
use App\Services\ActivityLogger;

/**
 * ITSWEBER CMS – ThemeController
 *
 * Handles theme listing, activation, ZIP-based theme installation, and uninstallation.
 *
 * Security measures for theme upload (Phase 10a):
 *  - File extension + MIME-type validation
 *  - Pre-extraction ZIP-slip check (reject '..' in entry names)
 *  - Post-extraction realpath verification (belt-and-suspenders)
 *  - Slug whitelist regex ([a-z0-9][a-z0-9\-]*)
 *  - theme.json required fields (slug, name, version)
 *  - templates/ directory required
 *  - No write access outside themes/ and public/themes/
 *  - Temp dir always cleaned up (finally block)
 *
 * Security measures for theme uninstallation (Phase 13a):
 *  - CSRF validation
 *  - Slug whitelist validation (prevent path traversal)
 *  - 'default' theme permanently protected
 *  - Active theme cannot be uninstalled
 *  - Theme directory must exist before deletion
 */
class ThemeController extends Controller
{
    // ─────────────────────────────────────────────────────────
    // Public actions
    // ─────────────────────────────────────────────────────────

    public function index(Request $request, array $params = []): void
    {
        $themes           = $this->loadInstalledThemes();
        $activeTheme      = Setting::get('active_theme', 'default');
        $zipOk            = class_exists('ZipArchive');
        $permanentlyProtected = ['default'];

        $this->renderAdmin('themes/index', [
            'pageTitle'            => __('admin.themes.title'),
            'themes'               => $themes,
            'activeTheme'          => $activeTheme,
            'zipOk'                => $zipOk,
            'permanentlyProtected' => $permanentlyProtected,
            'auth'                 => $this->auth(),
            'csrf'                 => $this->csrf(),
        ]);
    }

    /**
     * POST /admin/themes/uninstall – remove an installed theme.
     *
     * Protection rules:
     *  1. 'default' is permanently protected (system fallback).
     *  2. The currently active theme cannot be uninstalled.
     *  3. Slug must pass whitelist validation (prevents path traversal).
     *  4. Theme directory must exist.
     */
    public function uninstall(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $slug = trim($request->input('theme', ''));

        if (empty($slug)) {
            set_flash('error', __('messages.error.no_slug_specified', ['type' => 'Theme']));
            Response::redirect(admin_url('themes'));
            return;
        }

        // ── Slug whitelist (belt-and-suspenders against path traversal) ──
        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', __('messages.error.invalid_slug', ['type' => 'Theme']));
            Response::redirect(admin_url('themes'));
            return;
        }

        // ── Permanently protected themes ─────────────────────────────────
        $permanentlyProtected = ['default'];
        if (in_array($slug, $permanentlyProtected, true)) {
            set_flash('error', __('messages.error.system_theme', ['name' => e($slug)]));
            Response::redirect(admin_url('themes'));
            return;
        }

        // ── Active theme protection ───────────────────────────────────────
        $activeTheme = Setting::get('active_theme', 'default');
        if ($slug === $activeTheme) {
            set_flash('error', __('messages.error.active_theme'));
            Response::redirect(admin_url('themes'));
            return;
        }

        // ── Theme must exist ──────────────────────────────────────────────
        $themeDir = $this->themesBasePath() . '/' . $slug;
        if (!is_dir($themeDir)) {
            set_flash('error', __('messages.error.not_found_named', ['type' => 'Theme', 'name' => e($slug)]));
            Response::redirect(admin_url('themes'));
            return;
        }

        // ── Read theme name for feedback message before deletion ──────────
        $themeName = $slug;
        $jsonFile  = $themeDir . '/theme.json';
        if (is_file($jsonFile)) {
            $meta = json_decode(file_get_contents($jsonFile), true);
            if (is_array($meta) && !empty($meta['name'])) {
                $themeName = $meta['name'];
            }
        }

        // ── Remove themes/{slug}/ ─────────────────────────────────────────
        $this->removeDirectory($themeDir);

        // ── Remove public/themes/{slug}/ if it exists ─────────────────────
        $publicThemeDir = $this->appBasePath() . '/public/themes/' . $slug;
        if (is_dir($publicThemeDir)) {
            $this->removeDirectory($publicThemeDir);
        }

        set_flash('success', 'Theme "' . e($themeName) . '" has been uninstalled.');
        Response::redirect(admin_url('themes'));
    }

    public function activate(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        $slug = trim($request->input('theme', ''));

        if (empty($slug)) {
            set_flash('error', __('messages.error.no_slug_specified', ['type' => 'Theme']));
            Response::redirect(admin_url('themes'));
            return;
        }

        // Validate: theme directory must exist and contain a theme.json
        $themeDir = $this->themesBasePath() . '/' . $slug;
        if (!is_dir($themeDir) || !is_file($themeDir . '/theme.json')) {
            set_flash('error', __('messages.error.theme_not_found_invalid', ['name' => e($slug)]));
            Response::redirect(admin_url('themes'));
            return;
        }

        $oldTheme = Setting::get('active_theme', 'default');
        Setting::set('active_theme', $slug, 'branding');

        // Handle theme-bound section lifecycle (deactivate old, activate new + seed)
        $this->handleThemeSectionLifecycle($oldTheme, $slug);

        $this->clearPageCache();
        ActivityLogger::log('activate', 'theme', null, $slug);
        set_flash('success', 'Theme "' . e($slug) . '" activated. The site now uses this theme.');
        Response::redirect(admin_url('themes'));
    }

    /**
     * GET /admin/themes/upload – show the theme upload form.
     */
    public function uploadForm(Request $request, array $params = []): void
    {
        $this->renderAdmin('themes/upload', [
            'pageTitle' => __('admin.themes.install'),
            'auth'      => $this->auth(),
            'csrf'      => $this->csrf(),
        ]);
    }

    /**
     * POST /admin/themes/upload – process a theme ZIP upload.
     */
    public function handleUpload(Request $request, array $params = []): void
    {
        $this->validateCsrf();

        // ── 1. File presence ──────────────────────────────────────────────
        if (empty($_FILES['theme_zip']['tmp_name']) || !is_uploaded_file($_FILES['theme_zip']['tmp_name'])) {
            set_flash('error', __('messages.error.no_file_uploaded', ['type' => 'Theme']));
            Response::redirect(admin_url('themes/upload'));
            return;
        }

        $file = $_FILES['theme_zip'];

        // ── 2. PHP upload error codes ─────────────────────────────────────
        if ($file['error'] !== UPLOAD_ERR_OK) {
            set_flash('error', __('messages.error.upload_error', ['error' => $this->uploadErrorMessage($file['error'])]));
            Response::redirect(admin_url('themes/upload'));
            return;
        }

        // ── 3. Size limit (max 10 MB) ─────────────────────────────────────
        $maxBytes = 10 * 1024 * 1024;
        if ($file['size'] > $maxBytes) {
            set_flash('error', __('messages.error.zip_too_large', ['type' => 'Theme']));
            Response::redirect(admin_url('themes/upload'));
            return;
        }

        // ── 4. Extension check ────────────────────────────────────────────
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($ext !== 'zip') {
            set_flash('error', __('messages.error.only_zip_accepted', ['type' => 'Theme']));
            Response::redirect(admin_url('themes/upload'));
            return;
        }

        // ── 5. MIME-type check (fileinfo, belt-and-suspenders) ────────────
        if (class_exists('finfo')) {
            $finfo    = new \finfo(FILEINFO_MIME_TYPE);
            $mimeType = $finfo->file($file['tmp_name']);
            $allowedMime = ['application/zip', 'application/x-zip-compressed', 'application/x-zip'];
            if (!in_array($mimeType, $allowedMime, true)) {
                set_flash('error', __('messages.error.invalid_zip_mime', ['mime' => e($mimeType)]));
                Response::redirect(admin_url('themes/upload'));
                return;
            }
        }

        // ── 6. ZipArchive availability ────────────────────────────────────
        if (!class_exists('ZipArchive')) {
            set_flash('error', __('messages.error.zip_not_available', ['type' => 'Theme']));
            Response::redirect(admin_url('themes/upload'));
            return;
        }

        // ── 7. Temp directory ─────────────────────────────────────────────
        $tempDir = sys_get_temp_dir() . '/iw_theme_' . bin2hex(random_bytes(8));
        if (!mkdir($tempDir, 0700, true)) {
            set_flash('error', __('messages.error.temp_dir_failed'));
            Response::redirect(admin_url('themes/upload'));
            return;
        }

        try {
            // ── 8. Open ZIP ───────────────────────────────────────────────
            $zip = new \ZipArchive();
            $openResult = $zip->open($file['tmp_name']);
            if ($openResult !== true) {
                set_flash('error', __('messages.error.zip_open_failed', ['code' => (int)$openResult]));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            // ── 9. Pre-extraction ZIP-slip check ──────────────────────────
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $entryName = $zip->getNameIndex($i);
                if ($entryName === false) {
                    continue;
                }
                // Reject any entry with parent-directory traversal or null bytes
                if (strpos($entryName, '..') !== false || strpos($entryName, "\0") !== false) {
                    $zip->close();
                    set_flash('error', __('messages.error.zip_path_traversal', ['type' => 'Theme']));
                    Response::redirect(admin_url('themes/upload'));
                    return;
                }
                // Reject absolute paths
                if (substr($entryName, 0, 1) === '/' || (strlen($entryName) > 1 && $entryName[1] === ':')) {
                    $zip->close();
                    set_flash('error', __('messages.error.zip_absolute_path', ['type' => 'Theme']));
                    Response::redirect(admin_url('themes/upload'));
                    return;
                }
            }

            // ── 10. Extract ───────────────────────────────────────────────
            if (!$zip->extractTo($tempDir)) {
                $zip->close();
                set_flash('error', __('messages.error.zip_extract_failed'));
                Response::redirect(admin_url('themes/upload'));
                return;
            }
            $zip->close();

            // ── 11. Post-extraction realpath verification (belt-and-suspenders)
            $realTempBase = realpath($tempDir);
            if ($realTempBase === false) {
                set_flash('error', __('messages.error.zip_resolve_failed'));
                Response::redirect(admin_url('themes/upload'));
                return;
            }
            $realTempBase .= DIRECTORY_SEPARATOR;

            foreach ($this->recursiveFileList($tempDir) as $extractedFile) {
                $realExtracted = realpath($extractedFile);
                if ($realExtracted === false || strpos($realExtracted, $realTempBase) !== 0) {
                    set_flash('error', __('messages.error.zip_slip_detected'));
                    Response::redirect(admin_url('themes/upload'));
                    return;
                }
            }

            // ── 12. Locate theme.json (flat or one-level-wrapped) ─────────
            $themeRoot = $this->findThemeRoot($tempDir);
            if ($themeRoot === null) {
                set_flash('error', __('messages.error.no_manifest_found', ['type' => 'Theme', 'manifest' => 'theme.json']));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            // ── 13. Parse and validate theme.json ─────────────────────────
            $rawJson   = file_get_contents($themeRoot . '/theme.json');
            $themeJson = json_decode($rawJson, true);

            if (!is_array($themeJson)) {
                set_flash('error', __('messages.error.invalid_manifest_json', ['type' => 'Theme', 'manifest' => 'theme.json']));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            $slug    = trim($themeJson['slug']    ?? '');
            $name    = trim($themeJson['name']    ?? '');
            $version = trim($themeJson['version'] ?? '');

            if ($slug === '' || $name === '' || $version === '') {
                set_flash('error', __('messages.error.manifest_missing_fields', ['type' => 'Theme', 'manifest' => 'theme.json']));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            // ── 14. Slug whitelist validation ─────────────────────────────
            if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
                set_flash('error', __('messages.error.invalid_slug_format', ['type' => 'Theme', 'slug' => e($slug)]));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            // ── 15. templates/ directory required ─────────────────────────
            if (!is_dir($themeRoot . '/templates')) {
                set_flash('error', __('messages.error.theme_templates_missing'));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            // ── 16. Target path determination ─────────────────────────────
            $basePath       = $this->appBasePath();
            $themeTargetDir = $basePath . '/themes/' . $slug;
            $publicThemeDir = $basePath . '/public/themes/' . $slug;

            // ── 17. Protect core themes from accidental overwrite ─────────
            $protectedSlugs = ['default'];
            if (in_array($slug, $protectedSlugs, true)) {
                set_flash('error', __('messages.error.theme_slug_reserved', ['slug' => e($slug)]));
                Response::redirect(admin_url('themes/upload'));
                return;
            }

            // ── 18. Copy theme to themes/{slug}/ (skip assets/) ─────────
            $this->copyDirectoryExcluding($themeRoot, $themeTargetDir, ['assets']);

            // ── 19. Copy assets to public/themes/{slug}/assets/ ──────────
            // Support both legacy layout (assets/) and build-theme.php layout (__public_assets__/)
            $sourceAssetsDir = $themeRoot . '/assets';
            $altAssetsDir    = $themeRoot . '/__public_assets__';
            if (is_dir($sourceAssetsDir)) {
                $publicAssetsDir = $publicThemeDir . '/assets';
                $this->copyDirectory($sourceAssetsDir, $publicAssetsDir);
            } elseif (is_dir($altAssetsDir)) {
                $this->copyDirectory($altAssetsDir, $publicThemeDir);
            }

            // ── 20. Success ───────────────────────────────────────────────
            $isUpdate = is_dir($themeTargetDir) && count(glob($themeTargetDir . '/*')) > 0;
            $action   = isset($isUpdate) ? 'updated' : 'installed';
            set_flash('success',
                'Theme "' . e($name) . '" v' . e($version) . ' ' . $action . ' successfully. '
                . 'You can now activate it below.'
            );
            Response::redirect(admin_url('themes'));

        } finally {
            // ── 21. Always remove temp dir ────────────────────────────────
            $this->removeDirectory($tempDir);
        }
    }

    // ─────────────────────────────────────────────────────────
    // Theme-bound section lifecycle
    // ─────────────────────────────────────────────────────────

    /**
     * Handle theme-bound section activation/deactivation on theme switch.
     *
     * 1. Deactivate sections belonging to the OLD theme
     * 2. Activate sections belonging to the NEW theme (if they exist)
     * 3. Create + seed demo content for new theme sections that don't exist yet
     *
     * IMPORTANT: Only touches theme-EXCLUSIVE sections (themeSlug != null).
     * Generic sections (hero, intro, features, etc.) are NEVER modified.
     */
    private function handleThemeSectionLifecycle(string $oldTheme, string $newTheme): void
    {
        if ($oldTheme === $newTheme) return;

        $db = $this->db();
        $db->pdo()->beginTransaction();

        try {
            // Collect theme-bound section type keys from the registry
            $oldThemeTypes = $this->getThemeBoundTypeKeys($oldTheme);
            $newThemeTypes = $this->getThemeBoundTypeKeys($newTheme);

            // ── 1. Deactivate old theme's exclusive sections ──
            if (!empty($oldThemeTypes)) {
                $placeholders = implode(',', array_fill(0, count($oldThemeTypes), '?'));
                $db->query(
                    "UPDATE iw_page_sections SET is_active = 0
                     WHERE section_type IN ($placeholders)
                       AND is_active = 1",
                    array_values($oldThemeTypes)
                );
            }

            // ── 2. Activate existing / create missing sections for new theme ──
            foreach ($newThemeTypes as $typeKey) {
                $section = SectionRegistry::get($typeKey);
                if (!$section) continue;

                $existing = $db->fetch(
                    "SELECT id, content FROM iw_page_sections
                     WHERE section_type = ? AND page_context = 'homepage' LIMIT 1",
                    [$typeKey]
                );

                if ($existing) {
                    $db->query(
                        "UPDATE iw_page_sections SET is_active = 1 WHERE id = ?",
                        [(int) $existing['id']]
                    );
                } else {
                    $demoData = $section->defaultData();
                    $maxSort = $db->fetch(
                        "SELECT COALESCE(MAX(sort_order), 0) + 1 AS next_sort
                         FROM iw_page_sections WHERE page_context = 'homepage'"
                    );

                    $db->insert('iw_page_sections', [
                        'page_context'   => 'homepage',
                        'section_type'   => $typeKey,
                        'instance_label' => $section::label(),
                        'content'        => json_encode($demoData, JSON_UNESCAPED_UNICODE),
                        'is_active'      => 1,
                        'sort_order'     => (int) ($maxSort['next_sort'] ?? 99),
                        'created_at'     => date('Y-m-d H:i:s'),
                        'updated_at'     => date('Y-m-d H:i:s'),
                    ]);
                }
            }

            $db->pdo()->commit();
        } catch (\Throwable $e) {
            $db->pdo()->rollBack();
            error_log('Theme section lifecycle failed: ' . $e->getMessage());
        }
    }

    /**
     * Get all section type keys bound to a specific theme.
     */
    private function getThemeBoundTypeKeys(string $themeSlug): array
    {
        $keys = [];
        foreach (SectionRegistry::all() as $typeKey => $section) {
            if ($section::themeSlug() === $themeSlug) {
                $keys[] = $typeKey;
            }
        }
        return $keys;
    }

    // ─────────────────────────────────────────────────────────
    // Private helpers
    // ─────────────────────────────────────────────────────────

    /**
     * Absolute path to the project root (4 levels up from Admin/ThemeController.php).
     */
    private function appBasePath(): string
    {
        return dirname(__DIR__, 4);
    }

    /**
     * Absolute path to the themes/ directory.
     */
    private function themesBasePath(): string
    {
        return $this->appBasePath() . '/themes';
    }

    /**
     * Scan the themes/ directory and return metadata for all installed themes.
     */
    private function loadInstalledThemes(): array
    {
        $base   = $this->themesBasePath();
        $themes = [];

        foreach (glob($base . '/*', GLOB_ONLYDIR) as $dir) {
            $slug     = basename($dir);
            $jsonFile = $dir . '/theme.json';

            if (!is_file($jsonFile)) {
                continue;
            }

            $raw  = file_get_contents($jsonFile);
            $meta = json_decode($raw, true);

            if (!is_array($meta)) {
                continue;
            }

            // Resolve thumbnail URL: only expose if file is mirrored to public/
            $thumbnailRelative = $meta['thumbnail'] ?? null;
            $thumbnailUrl      = null;
            if ($thumbnailRelative !== null) {
                $publicThumbPath = $this->appBasePath()
                    . '/public/themes/' . $slug . '/'
                    . ltrim(str_replace('\\', '/', $thumbnailRelative), '/');
                if (is_file($publicThumbPath)) {
                    $thumbnailUrl = '/themes/' . $slug . '/'
                        . ltrim(str_replace('\\', '/', $thumbnailRelative), '/');
                }
            }

            $themes[$slug] = [
                'slug'         => $slug,
                'name'         => $meta['name']        ?? ucfirst($slug),
                'description'  => $meta['description'] ?? '',
                'version'      => $meta['version']     ?? '1.0.0',
                'author'       => $meta['author']      ?? '',
                'thumbnail'    => $meta['thumbnail']   ?? null,
                'thumbnailUrl' => $thumbnailUrl,
                'colors'       => $meta['colors']      ?? [],
            ];
        }

        ksort($themes);
        return $themes;
    }

    /**
     * Find the directory containing theme.json.
     * Supports flat layout (theme.json at root of temp dir) and
     * single-subdirectory wrapped layout (e.g. user zipped a folder).
     */
    private function findThemeRoot(string $tempDir): ?string
    {
        // Flat: theme.json at tempDir root
        if (is_file($tempDir . '/theme.json')) {
            return $tempDir;
        }

        // One-level-wrapped: theme.json inside a single subdirectory
        $subdirs = glob($tempDir . '/*', GLOB_ONLYDIR);
        if ($subdirs !== false && count($subdirs) === 1) {
            $subDir = $subdirs[0];
            if (is_file($subDir . '/theme.json')) {
                return $subDir;
            }
        }

        return null;
    }

    /**
     * Recursively copy a directory.
     */
    private function copyDirectory(string $src, string $dst): void
    {
        if (!is_dir($dst)) {
            mkdir($dst, 0755, true);
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($src, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $relative = substr($item->getPathname(), strlen($src));
            $target   = $dst . $relative;

            if ($item->isDir()) {
                if (!is_dir($target)) {
                    mkdir($target, 0755, true);
                }
            } else {
                copy($item->getPathname(), $target);
            }
        }
    }

    /**
     * Recursively copy a directory, skipping specified top-level subdirectories.
     */
    private function copyDirectoryExcluding(string $src, string $dst, array $excludeDirs): void
    {
        if (!is_dir($dst)) {
            mkdir($dst, 0755, true);
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($src, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $relative = ltrim(substr($item->getPathname(), strlen($src)), '/\\');
            $topDir   = explode(DIRECTORY_SEPARATOR, $relative)[0];
            $topDir   = explode('/', $topDir)[0]; // handle both separators

            if (in_array($topDir, $excludeDirs, true)) {
                continue;
            }

            $target = $dst . DIRECTORY_SEPARATOR . $relative;

            if ($item->isDir()) {
                if (!is_dir($target)) {
                    mkdir($target, 0755, true);
                }
            } else {
                $targetDir = dirname($target);
                if (!is_dir($targetDir)) {
                    mkdir($targetDir, 0755, true);
                }
                copy($item->getPathname(), $target);
            }
        }
    }

    /**
     * Recursively remove a directory.
     */
    private function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $item) {
            if ($item->isDir()) {
                rmdir($item->getPathname());
            } else {
                unlink($item->getPathname());
            }
        }

        rmdir($dir);
    }

    /**
     * List all files under a directory recursively.
     */
    private function recursiveFileList(string $dir): array
    {
        $files    = [];
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
        );

        foreach ($iterator as $item) {
            if ($item->isFile()) {
                $files[] = $item->getPathname();
            }
        }

        return $files;
    }

    /**
     * Human-readable PHP upload error message.
     */
    private function uploadErrorMessage(int $code): string
    {
        return match ($code) {
            UPLOAD_ERR_INI_SIZE   => 'The file exceeds the server\'s upload_max_filesize limit.',
            UPLOAD_ERR_FORM_SIZE  => 'The file exceeds the MAX_FILE_SIZE limit specified in the form.',
            UPLOAD_ERR_PARTIAL    => 'The file was only partially uploaded. Please try again.',
            UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
            UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary upload directory on the server.',
            UPLOAD_ERR_CANT_WRITE => 'Failed to write the file to disk. Check server permissions.',
            UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the upload.',
            default               => 'Unknown upload error (code ' . $code . ').',
        };
    }
}
