<?php

namespace App\Http\Controllers\Admin;

use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Services\PluginLoader;

/**
 * ITSWEBER CMS – PluginController (Phase 16)
 *
 * Handles plugin listing, ZIP-based plugin installation, and uninstallation.
 *
 * Security chain for plugin upload:
 *  - CSRF validation
 *  - PHP upload error translation
 *  - Size limit (10 MB)
 *  - Extension check (.zip only)
 *  - MIME-type check via finfo (belt-and-suspenders)
 *  - ZipArchive availability check
 *  - Pre-extraction ZIP-slip check ('..' entries, null bytes, absolute paths)
 *  - Post-extraction realpath verification
 *  - plugin.json required fields (slug, name, version)
 *  - Slug whitelist regex ([a-z0-9][a-z0-9\-]*)
 *  - plugin.php entry-point file required
 *  - Optional min_cms version check
 *  - Flat and one-level-wrapped ZIP layouts supported
 *  - Temp directory always cleaned up (finally block)
 *  - No write access outside plugins/ and public/plugins/
 */
class PluginController extends Controller
{
    // ─────────────────────────────────────────────────────────
    // Public actions
    // ─────────────────────────────────────────────────────────

    /**
     * GET /admin/plugins – list all installed plugins.
     */
    public function index(Request $request, array $params = []): void
    {
        $plugins       = $this->loadInstalledPlugins();
        $zipOk         = class_exists('ZipArchive');
        $activeSlugs   = PluginLoader::getActiveSlugs($this->appBasePath());

        $this->renderAdmin('plugins/index', [
            'pageTitle'    => 'Plugins',
            'plugins'      => $plugins,
            'zipOk'        => $zipOk,
            'activeSlugs'  => $activeSlugs,
            'auth'         => $this->auth(),
            'csrf'         => $this->csrf(),
        ]);
    }

    /**
     * POST /admin/plugins/activate – activate an installed plugin.
     */
    public function activate(Request $request, array $params = []): void
    {
        $this->validateCsrf();

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

        if (empty($slug) || !preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', 'Invalid plugin slug.');
            Response::redirect('/admin/plugins');
            return;
        }

        $pluginDir = $this->pluginsBasePath() . '/' . $slug;
        if (!is_dir($pluginDir)) {
            set_flash('error', 'Plugin "' . e($slug) . '" is not installed.');
            Response::redirect('/admin/plugins');
            return;
        }

        $base        = $this->appBasePath();
        $activeSlugs = PluginLoader::getActiveSlugs($base);

        if (!in_array($slug, $activeSlugs, true)) {
            $activeSlugs[] = $slug;
            if (!PluginLoader::saveActiveSlugs($base, $activeSlugs)) {
                set_flash('error', 'Could not save plugin activation state. Check write permissions on storage/.');
                Response::redirect('/admin/plugins');
                return;
            }
        }

        $pluginName = $this->readPluginName($slug);
        set_flash('success', 'Plugin "' . e($pluginName) . '" activated.');
        Response::redirect('/admin/plugins');
    }

    /**
     * POST /admin/plugins/deactivate – deactivate an installed plugin.
     */
    public function deactivate(Request $request, array $params = []): void
    {
        $this->validateCsrf();

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

        if (empty($slug) || !preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', 'Invalid plugin slug.');
            Response::redirect('/admin/plugins');
            return;
        }

        $base        = $this->appBasePath();
        $activeSlugs = PluginLoader::getActiveSlugs($base);
        $activeSlugs = array_values(array_filter($activeSlugs, fn($s) => $s !== $slug));

        if (!PluginLoader::saveActiveSlugs($base, $activeSlugs)) {
            set_flash('error', 'Could not save plugin activation state. Check write permissions on storage/.');
            Response::redirect('/admin/plugins');
            return;
        }

        $pluginName = $this->readPluginName($slug);
        set_flash('success', 'Plugin "' . e($pluginName) . '" deactivated.');
        Response::redirect('/admin/plugins');
    }

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

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

        // ── 1. File presence ──────────────────────────────────────────────
        if (empty($_FILES['plugin_zip']['tmp_name']) || !is_uploaded_file($_FILES['plugin_zip']['tmp_name'])) {
            set_flash('error', 'No file uploaded. Please select a plugin ZIP file.');
            Response::redirect('/admin/plugins/upload');
            return;
        }

        $file = $_FILES['plugin_zip'];

        // ── 2. PHP upload error codes ─────────────────────────────────────
        if ($file['error'] !== UPLOAD_ERR_OK) {
            set_flash('error', 'Upload failed: ' . $this->uploadErrorMessage($file['error']));
            Response::redirect('/admin/plugins/upload');
            return;
        }

        // ── 3. Size limit (max 10 MB) ─────────────────────────────────────
        $maxBytes = 10 * 1024 * 1024;
        if ($file['size'] > $maxBytes) {
            set_flash('error', 'Plugin ZIP exceeds the 10 MB size limit.');
            Response::redirect('/admin/plugins/upload');
            return;
        }

        // ── 4. Extension check ────────────────────────────────────────────
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($ext !== 'zip') {
            set_flash('error', 'Only .zip files are accepted as plugin packages.');
            Response::redirect('/admin/plugins/upload');
            return;
        }

        // ── 5. MIME-type check (finfo, 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', 'Uploaded file does not appear to be a valid ZIP archive (MIME: ' . e($mimeType) . ').');
                Response::redirect('/admin/plugins/upload');
                return;
            }
        }

        // ── 6. ZipArchive availability ────────────────────────────────────
        if (!class_exists('ZipArchive')) {
            set_flash('error', 'PHP ZipArchive extension is not available on this server. Plugin upload is not supported.');
            Response::redirect('/admin/plugins/upload');
            return;
        }

        // ── 7. Temp directory ─────────────────────────────────────────────
        $tempDir = sys_get_temp_dir() . '/iw_plugin_' . bin2hex(random_bytes(8));
        if (!mkdir($tempDir, 0700, true)) {
            set_flash('error', 'Could not create temporary extraction directory.');
            Response::redirect('/admin/plugins/upload');
            return;
        }

        try {
            // ── 8. Open ZIP ───────────────────────────────────────────────
            $zip        = new \ZipArchive();
            $openResult = $zip->open($file['tmp_name']);
            if ($openResult !== true) {
                set_flash('error', 'Could not open ZIP archive. The file may be corrupted (ZipArchive error: ' . (int)$openResult . ').');
                Response::redirect('/admin/plugins/upload');
                return;
            }

            // ── 9. Pre-extraction ZIP-slip check ──────────────────────────
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $entryName = $zip->getNameIndex($i);
                if ($entryName === false) {
                    continue;
                }
                if (strpos($entryName, '..') !== false || strpos($entryName, "\0") !== false) {
                    $zip->close();
                    set_flash('error', 'Security: Plugin ZIP contains unsafe path entries (path traversal detected). Installation aborted.');
                    Response::redirect('/admin/plugins/upload');
                    return;
                }
                if (substr($entryName, 0, 1) === '/' || (strlen($entryName) > 1 && $entryName[1] === ':')) {
                    $zip->close();
                    set_flash('error', 'Security: Plugin ZIP contains absolute path entries. Installation aborted.');
                    Response::redirect('/admin/plugins/upload');
                    return;
                }
            }

            // ── 10. Extract ───────────────────────────────────────────────
            if (!$zip->extractTo($tempDir)) {
                $zip->close();
                set_flash('error', 'Failed to extract ZIP archive. The file may be corrupted or the server lacks write permissions.');
                Response::redirect('/admin/plugins/upload');
                return;
            }
            $zip->close();

            // ── 11. Post-extraction realpath verification ─────────────────
            $realTempBase = realpath($tempDir);
            if ($realTempBase === false) {
                set_flash('error', 'Security: Could not resolve temp directory path after extraction.');
                Response::redirect('/admin/plugins/upload');
                return;
            }
            $realTempBase .= DIRECTORY_SEPARATOR;

            foreach ($this->recursiveFileList($tempDir) as $extractedFile) {
                $realExtracted = realpath($extractedFile);
                if ($realExtracted === false || strpos($realExtracted, $realTempBase) !== 0) {
                    set_flash('error', 'Security: ZIP-slip path escape detected after extraction. Installation aborted.');
                    Response::redirect('/admin/plugins/upload');
                    return;
                }
            }

            // ── 12. Locate plugin.json (flat or one-level-wrapped) ────────
            $pluginRoot = $this->findPluginRoot($tempDir);
            if ($pluginRoot === null) {
                set_flash('error', 'Invalid plugin package: no plugin.json found. The ZIP must contain a plugin.json at the root or inside a single subdirectory.');
                Response::redirect('/admin/plugins/upload');
                return;
            }

            // ── 13. Parse and validate plugin.json ────────────────────────
            $rawJson    = file_get_contents($pluginRoot . '/plugin.json');
            $pluginJson = json_decode($rawJson, true);

            if (!is_array($pluginJson)) {
                set_flash('error', 'Invalid plugin package: plugin.json is not valid JSON.');
                Response::redirect('/admin/plugins/upload');
                return;
            }

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

            if ($slug === '' || $name === '' || $version === '') {
                set_flash('error', 'Invalid plugin package: plugin.json must contain "slug", "name", and "version" fields.');
                Response::redirect('/admin/plugins/upload');
                return;
            }

            // ── 14. Slug whitelist validation ─────────────────────────────
            if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
                set_flash('error', 'Invalid plugin slug "' . e($slug) . '". Slug must start with a lowercase letter or digit and contain only lowercase letters, digits, and hyphens.');
                Response::redirect('/admin/plugins/upload');
                return;
            }

            // ── 15. plugin.php entry-point required ───────────────────────
            if (!is_file($pluginRoot . '/plugin.php')) {
                set_flash('error', 'Invalid plugin package: a plugin.php entry-point file is required.');
                Response::redirect('/admin/plugins/upload');
                return;
            }

            // ── 16. Optional min_cms version check ────────────────────────
            $minCms = trim($pluginJson['min_cms'] ?? '');
            if ($minCms !== '') {
                $currentVersion = defined('CMS_VERSION') ? CMS_VERSION : (cms_version() ?? '1.0.0');
                if (version_compare($currentVersion, $minCms, '<')) {
                    set_flash('error',
                        'Plugin "' . e($name) . '" requires CMS version ' . e($minCms) . ' or higher. '
                        . 'Your installed version is ' . e($currentVersion) . '. Please update the CMS first.'
                    );
                    Response::redirect('/admin/plugins/upload');
                    return;
                }
            }

            // ── 17. Copy plugin to plugins/{slug}/ ────────────────────────
            $basePath        = $this->appBasePath();
            $pluginTargetDir = $basePath . '/plugins/' . $slug;
            $isUpdate        = is_dir($pluginTargetDir);

            $this->copyDirectory($pluginRoot, $pluginTargetDir);

            // ── 18. Mirror assets to public/plugins/{slug}/assets/ ────────
            $sourceAssetsDir = $pluginTargetDir . '/assets';
            if (is_dir($sourceAssetsDir)) {
                $publicAssetsDir = $basePath . '/public/plugins/' . $slug . '/assets';
                $this->copyDirectory($sourceAssetsDir, $publicAssetsDir);
            }

            // ── 19. Success ───────────────────────────────────────────────
            $action = $isUpdate ? 'updated' : 'installed';
            set_flash('success',
                'Plugin "' . e($name) . '" v' . e($version) . ' ' . $action . ' successfully.'
            );
            Response::redirect('/admin/plugins');

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

    /**
     * POST /admin/plugins/uninstall – remove an installed plugin.
     *
     * Protection rules:
     *  1. Slug must pass whitelist validation (prevents path traversal).
     *  2. Plugin directory must exist.
     */
    public function uninstall(Request $request, array $params = []): void
    {
        $this->validateCsrf();

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

        if (empty($slug)) {
            set_flash('error', 'No plugin specified.');
            Response::redirect('/admin/plugins');
            return;
        }

        // ── Slug whitelist ────────────────────────────────────────────────
        if (!preg_match('/^[a-z0-9][a-z0-9\-]*$/', $slug)) {
            set_flash('error', 'Invalid plugin slug.');
            Response::redirect('/admin/plugins');
            return;
        }

        // ── Plugin must exist ─────────────────────────────────────────────
        $pluginDir = $this->pluginsBasePath() . '/' . $slug;
        if (!is_dir($pluginDir)) {
            set_flash('error', 'Plugin "' . e($slug) . '" not found.');
            Response::redirect('/admin/plugins');
            return;
        }

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

        // ── Remove plugins/{slug}/ ────────────────────────────────────────
        $this->removeDirectory($pluginDir);

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

        // ── Remove from active list if present (Phase 17) ─────────────────
        $base        = $this->appBasePath();
        $activeSlugs = PluginLoader::getActiveSlugs($base);
        if (in_array($slug, $activeSlugs, true)) {
            $activeSlugs = array_values(array_filter($activeSlugs, fn($s) => $s !== $slug));
            PluginLoader::saveActiveSlugs($base, $activeSlugs);
        }

        set_flash('success', 'Plugin "' . e($pluginName) . '" has been uninstalled.');
        Response::redirect('/admin/plugins');
    }

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

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

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

    /**
     * Read the plugin display name from plugin.json (falls back to slug).
     */
    private function readPluginName(string $slug): string
    {
        $jsonFile = $this->pluginsBasePath() . '/' . $slug . '/plugin.json';
        if (is_file($jsonFile)) {
            $meta = json_decode(file_get_contents($jsonFile), true);
            if (is_array($meta) && !empty($meta['name'])) {
                return $meta['name'];
            }
        }
        return $slug;
    }

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

        if (!is_dir($base)) {
            return $plugins;
        }

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

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

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

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

            $plugins[$slug] = [
                'slug'        => $slug,
                'name'        => $meta['name']        ?? ucfirst($slug),
                'description' => $meta['description'] ?? '',
                'version'     => $meta['version']     ?? '1.0.0',
                'author'      => $meta['author']      ?? '',
                'min_cms'     => $meta['min_cms']     ?? '',
            ];
        }

        ksort($plugins);
        return $plugins;
    }

    /**
     * Find the directory containing plugin.json.
     * Supports flat layout and single-subdirectory wrapped layout.
     */
    private function findPluginRoot(string $tempDir): ?string
    {
        if (is_file($tempDir . '/plugin.json')) {
            return $tempDir;
        }

        $subdirs = glob($tempDir . '/*', GLOB_ONLYDIR);
        if ($subdirs !== false && count($subdirs) === 1) {
            $subDir = $subdirs[0];
            if (is_file($subDir . '/plugin.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 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 . ').',
        };
    }
}
