<?php

namespace Sitecake;

use Sitecake\Api\BootableInterface;
use Sitecake\Api\Exception\InvalidPluginDefinitionException;
use Sitecake\Api\Exception\MissingPluginException;
use Sitecake\Api\PluginInterface;
use Sitecake\Api\Sitecake as SitecakeApi;
use Sitecake\Content\DOM\ElementFactory;
use Sitecake\Content\MenuManager;
use Sitecake\Event\EventManager;
use Sitecake\Exception\InternalException;
use Sitecake\Http\Request;
use Sitecake\Http\Response;
use Sitecake\Http\Session;
use Sitecake\PageManager\PageManager;
use Sitecake\PageManager\TemplateManager;
use Sitecake\Plugin\PluginRegistry;
use Sitecake\Resources\Handler\ImageHandler;
use Sitecake\Resources\Handler\UploadHandler;
use Sitecake\Resources\ResourceManager;
use Sitecake\Services\ContentService;
use Sitecake\Services\ImageService;
use Sitecake\Services\PagesService;
use Sitecake\Services\ServiceRegistry;
use Sitecake\Services\SessionService;
use Sitecake\Services\SystemService;
use Sitecake\Services\UploadService;
use Sitecake\Util\Hash;

/**
 * Class Sitecake
 *
 * Main application class — singleton that owns the DI container and orchestrates
 * request handling.  Rewritten for pure PHP (no Silex / Pimple / Symfony).
 *
 * @package Sitecake
 */
class Sitecake
{
    const SITECAKE_TEMP_DIR_NAME = 'cms-temp';

    /** @var Sitecake */
    protected static $instance;

    /** @var Container */
    protected $app;

    /** @var string Absolute path to config directory */
    protected $configPath;

    protected bool $configInitialized = false;

    /** @var array Global configuration */
    protected static array $config = [];

    /** @var array Resolved filesystem paths */
    protected static array $paths = [];

    protected bool $requestHandlingInitialized = false;

    protected string $pluginPath;

    protected array $plugins = [];

    // ------------------------------------------------------------------

    protected function __construct(Container $app, string $configPath)
    {
        $this->app        = $app;
        $this->configPath = $configPath;
    }

    protected function __clone() {}

    /**
     * Create the singleton instance.
     *
     * @param Container $app
     * @param string    $configPath
     * @return Sitecake
     */
    public static function createInstance(Container $app, string $configPath): self
    {
        if (self::$instance) {
            throw new \LogicException('Sitecake is already instantiated.');
        }
        self::$instance = new static($app, $configPath);
        return self::$instance;
    }

    // ------------------------------------------------------------------
    // Boot
    // ------------------------------------------------------------------

    protected function boot(): void
    {
        $this->loadConfigurationAndPaths();
        $this->buildPaths();
        date_default_timezone_set(self::getConfig('timezone'));
        mb_internal_encoding(self::getConfig('encoding'));
        // Filesystem must be available before ensureDirStructure() is called
        $this->app->register(new \Sitecake\ServiceProviders\FilesystemServiceProvider());
        $this->app['filesystem.adapter.config'] = self::$paths['SITE_ROOT'];
        $this->ensureDirStructure();
        $this->app['local_cache.dir'] = $this->getCachePath();
        // Initialize core managers, services, action dispatcher
        $this->initialize();
        // Load plugins
        $this->setPluginPath(self::$paths['SITECAKE_DIR']);
        $this->loadPlugins();
    }

    // ------------------------------------------------------------------
    // Request entry point (replaces initializeRequestHandling + run)
    // ------------------------------------------------------------------

    /**
     * Boot the application and handle one HTTP request.
     */
    public function handle(Request $request): Response
    {
        $this->boot();

        try {
            return $this->app['sc.action_dispatcher']->dispatch($request);
        } catch (InternalException $e) {
            $this->app['logger']->error((string)$e);
            return new Response(
                (string)$this->app['exception_handler']->handleException($e, true),
                500
            );
        } catch (\Exception $e) {
            $this->app['logger']->error((string)$e);
            $code = $e->getCode();
            $possibleCodes = [400, 401, 403, 404, 405, 500];
            if (empty($code) || !in_array($code, $possibleCodes, true)) {
                $code = 500;
            }
            return new Response(
                (string)$this->app['exception_handler']->handleException($e, true),
                $code
            );
        }
    }

    // ------------------------------------------------------------------
    // Configuration loading
    // ------------------------------------------------------------------

    protected function loadConfigurationAndPaths(): void
    {
        if (!$this->configInitialized) {
            $defaults = $this->loadDefaults();
            $this->loadPaths();
            $local = $this->loadLocalConfiguration();

            $this->app['config'] = self::$config = Hash::expand(array_merge($defaults, $local));
            // LOW-2: Sync debug flag so $app['debug'] and $config['debug'] are consistent
            if (isset(self::$config['debug'])) {
                $this->app['debug'] = (bool) self::$config['debug'];
            }
            $this->configInitialized = true;
        }
    }

    protected function loadDefaults(): array
    {
        return $this->loadConfigFile($this->configPath . DIRECTORY_SEPARATOR . 'default.php');
    }

    protected function loadLocalConfiguration(): array
    {
        $localFile = self::$paths['SITECAKE_DIR'] . DIRECTORY_SEPARATOR . 'config.php';
        if (file_exists($localFile)) {
            return $this->loadConfigFile($localFile);
        }
        return [];
    }

    protected function loadPaths(): void
    {
        self::$paths = $this->loadConfigFile($this->configPath . DIRECTORY_SEPARATOR . 'paths.php');
    }

    /**
     * Include a PHP config file and return the resulting array.
     * Supports both patterns:
     *   - File sets $config = [] and populates it (no return)
     *   - File builds $paths = [] / similar and returns it
     */
    protected function loadConfigFile(string $file): array
    {
        return (function ($f) {
            $config = [];
            $result = include $f;
            return is_array($result) ? $result : $config;
        })($file);
    }

    // ------------------------------------------------------------------
    // Path building
    // ------------------------------------------------------------------

    protected function buildPaths(): void
    {
        if (($siteRoot = self::getConfig('SITE_ROOT')) !== null) {
            self::$paths['SITE_ROOT'] = $siteRoot;
        }
        if (($clientBaseUrl = self::getConfig('SITECAKE_CLIENT_BASE_URL')) !== null) {
            self::$paths['SITECAKE_CLIENT_BASE_URL'] = $clientBaseUrl;
        }

        self::$paths['EDITOR_JS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/editor-v4/sitecake-editor.iife.js';

        self::$paths['EDITOR_CSS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/editor-v4/style.css';

        self::$paths['PAGEMANAGER_JS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/pagemanager/js/pagemanager.' . SITECAKE_VERSION . '.js';

        self::$paths['PAGEMANAGER_VENDORS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/pagemanager/js/vendor.' . SITECAKE_VERSION . '.js';

        self::$paths['PAGEMANAGER_CSS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/pagemanager/css/pagemanager.' . SITECAKE_VERSION . '.css';

        self::$paths['GALLERY_LIGHTBOX_JS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/editor-v4/sc-gallery-lightbox.iife.js';

        self::$paths['GALLERY_LIGHTBOX_CSS_URL'] =
            self::$paths['SITECAKE_CLIENT_BASE_URL'] . '/editor-v4/sc-gallery-lightbox.css';
    }

    // ------------------------------------------------------------------
    // Directory structure
    // ------------------------------------------------------------------

    protected function ensureDirStructure(): void
    {
        if (!$this->app['filesystem']->ensureDir(self::SITECAKE_TEMP_DIR_NAME)) {
            throw new \LogicException(
                'Could not ensure that the directory /' . self::SITECAKE_TEMP_DIR_NAME . ' is present and writable.'
            );
        }

        // LOW-5: Protect cms-temp from direct HTTP access
        $this->protectTempDir($this->app['filesystem']->abs(self::SITECAKE_TEMP_DIR_NAME));

        $workingDir = $this->app['filesystem']->randomDir(self::SITECAKE_TEMP_DIR_NAME);
        if ($workingDir === false) {
            throw new \LogicException(
                'Could not ensure that the work directory in /' . self::SITECAKE_TEMP_DIR_NAME . ' is present and writable.'
            );
        }
        self::$paths['SITECAKE_WORKING_DIR'] = $workingDir;

        $tmpPath = $this->app['filesystem']->ensureDir(self::$paths['SITECAKE_WORKING_DIR'] . '/tmp');
        if ($tmpPath === false) {
            throw new \LogicException(
                'Could not ensure that the directory ' . self::$paths['SITECAKE_WORKING_DIR'] . '/tmp is present and writable.'
            );
        }
        self::$paths['TMP'] = $tmpPath;

        $cachePath = $this->app['filesystem']->ensureDir(self::$paths['TMP'] . '/cache');
        if ($cachePath === false) {
            throw new \LogicException(
                'Could not ensure that the directory ' . self::$paths['TMP'] . '/cache is present and writable.'
            );
        }
        self::$paths['CACHE'] = $cachePath;

        $logsPath = $this->app['filesystem']->ensureDir(self::$paths['SITECAKE_WORKING_DIR'] . '/logs');
        if ($logsPath === false) {
            throw new \LogicException(
                'Could not ensure that the directory ' . self::$paths['SITECAKE_WORKING_DIR'] . '/logs is present and writable.'
            );
        }
        self::$paths['LOGS'] = $logsPath;
    }

    /**
     * LOW-5: Write .htaccess, web.config, and index.php into the cms-temp
     * directory to block direct HTTP access on Apache, IIS, and generic servers.
     *
     * Draft images (stored in cms-temp) must be served to the editor so the
     * user can see uploaded images before publishing. We allow image file
     * extensions while blocking everything else (sessions, PHP, configs).
     */
    private function protectTempDir(string $absDir): void
    {
        $htaccess = $absDir . '/.htaccess';
        $content  =
            "# Block all access to protect sessions and working files\n" .
            "Order deny,allow\n" .
            "Deny from all\n\n" .
            "# Allow draft images so the editor can display them before publishing\n" .
            "<FilesMatch \"\\.(jpg|jpeg|png|gif|webp|svg|bmp|avif)$\">\n" .
            "  Order allow,deny\n" .
            "  Allow from all\n" .
            "</FilesMatch>\n";

        // Write if missing, or upgrade installs that have the old block-all version
        if (!file_exists($htaccess) || strpos((string)file_get_contents($htaccess), 'FilesMatch') === false) {
            file_put_contents($htaccess, $content);
        }

        $webconfig = $absDir . '/web.config';
        if (!file_exists($webconfig)) {
            file_put_contents(
                $webconfig,
                '<?xml version="1.0"?><configuration><system.webServer><authorization>'
                . '<deny users="*"/></authorization></system.webServer></configuration>'
            );
        }

        $index = $absDir . '/index.php';
        if (!file_exists($index)) {
            file_put_contents($index, '<?php header("Location: /"); exit;');
        }
    }

    // ------------------------------------------------------------------
    // Service initialization (formerly split across ServiceProviders)
    // ------------------------------------------------------------------

    protected function initialize(): void
    {
        // --- Native PHP session setup (replaces SessionServiceProvider) ---
        $this->initializeSession();

        // --- Event bus (replaces EventBusServiceProvider) ---
        $this->app['event_bus'] = function ($app) {
            return EventManager::instance();
        };

        // --- Core managers ---
        $this->app['sc.cache'] = function ($app) {
            // Use the absolute filesystem path so Cache::get/save works regardless
            // of PHP's CWD. PHP-FPM sets CWD to the script directory (e.g.
            // /var/www/html/test/sitecake/4.0.18/src), so the relative working-dir
            // path would resolve to a different (non-existent) location. CLI tools
            // and chdir-based contexts use the project root, which is why this bug
            // was invisible to direct PHP-CLI testing.
            return new Cache($app['filesystem']->abs(self::getCachePath()));
        };

        $this->app['sc.resource_manager'] = function ($app) {
            return new ResourceManager($app['filesystem'], $app['sc.cache']);
        };
        $this->app->extend('sc.resource_manager', function (ResourceManager $rm) {
            $rm->registerHandler(ImageHandler::class, [
                'validExtensions' => self::getConfig('image.valid_extensions'),
                'imageDirName'    => self::getConfig('image.directory_name'),
            ]);
            $rm->registerHandler(UploadHandler::class, [
                'forbiddenExtensions' => self::getConfig('upload.forbidden_extensions'),
                'uploadsDirName'      => self::getConfig('upload.directory_name'),
            ]);
            // SourceFileHandler must be registered HERE — not in PageManager — so
            // hasSupportingHandler('*.html') returns true before any ResourceManager
            // operation that triggers findResources()/saveLastModified() and caches
            // the resources list. Otherwise: a non-page action (e.g. _image upload)
            // runs first → findResources() drops HTML files from cache → containers
            // metadata is lost → saveDraft() silently saves nothing → publish empty.
            $rm->registerHandler(\Sitecake\PageManager\SourceFileHandler::class);
            $rm->ignore(self::SITECAKE_TEMP_DIR_NAME . '/');
            $rm->ignore(\Sitecake\PageManager\PageManager::SC_PAGES_FILENAME);
            return $rm;
        });

        $this->app['sc.backup_manager'] = function ($app) {
            return new BackupManager($app['filesystem'], $app['sc.resource_manager']);
        };

        $this->app['sc.site'] = function ($app) {
            return new Site($app['filesystem'], $app['sc.resource_manager'], $app['sc.backup_manager']);
        };

        $this->app['sc.template_manager'] = function ($app) {
            return new TemplateManager($app['sc.resource_manager']);
        };

        $this->app['sc.page_manager'] = function ($app) {
            return new PageManager(
                $app['sc.resource_manager'],
                $app['sc.template_manager'],
                $app['sc.cache'],
                $app['filesystem']
            );
        };

        $this->app['sc.menu_manager'] = function ($app) {
            return new MenuManager($app['sc.site'], $app['sc.page_manager'], $app['sc.resource_manager']);
        };

        $this->app['sc.flock'] = function ($app) {
            return new FileLock($app['filesystem'], $this->getTmpPath());
        };

        $this->app['sc.session_manager'] = function ($app) {
            return new SessionManager($app['sc.session'], $app['sc.flock']);
        };

        // --- Public API & plugin registry ---
        $this->app['sc.api'] = function ($app) {
            return new SitecakeApi($app);
        };

        $this->app['sc.plugin_registry'] = function ($app) {
            return new PluginRegistry(
                self::$paths['PLUGINS_DIR'],
                $app['sc.api'],
                function (string $file) { return $this->loadConfigFile($file); }
            );
        };

        // --- Services ---
        $this->app['sc.service_registry'] = function ($app) {
            return new ServiceRegistry($app['sc.site']);
        };
        $this->app->extend('sc.service_registry', function (ServiceRegistry $sr, $app) {
            $sr->register(ContentService::class, [
                'pageManager' => $app['sc.page_manager'],
            ]);
            $sr->register(ImageService::class, [
                'resourceManager'   => $app['sc.resource_manager'],
                'metadata'          => $app['sc.cache'],
                'imageExtensions'   => self::getConfig('image.valid_extensions'),
                'imgDirName'        => self::getConfig('image.directory_name'),
                'srcSetWidths'      => self::getConfig('image.srcset_widths'),
                'srcSetQuality'     => self::getConfig('image.srcset_qualities'),
                'srcSetWidthMaxDiff' => self::getConfig('image.srcset_width_maxdiff'),
            ]);
            $sr->register(PagesService::class, [
                'pageManager'     => $app['sc.page_manager'],
                'menuManager'     => $app['sc.menu_manager'],
                'resourceManager' => $app['sc.resource_manager'],
                'metadata'        => $app['sc.cache'],
            ]);
            $sr->register(SessionService::class, [
                'CREDENTIALS_PATH' => self::$paths['CREDENTIALS_PATH'],
                'sessionManager'   => $app['sc.session_manager'],
                'pageManager'      => $app['sc.page_manager'],
                'resourceManager'  => $app['sc.resource_manager'],
            ]);
            $sr->register(SystemService::class, []);
            $sr->register(UploadService::class, [
                'resourceManager'    => $app['sc.resource_manager'],
                'metadata'           => $app['sc.cache'],
                'forbiddenExtensions' => self::getConfig('upload.forbidden_extensions'),
                'uploadDirName'       => self::getConfig('upload.directory_name'),
            ]);
            return $sr;
        });

        // --- Action dispatcher ---
        $this->app['sc.action_dispatcher'] = function ($app) {
            $dispatcher = new ActionDispatcher(
                $app['sc.session_manager'],
                $app['sc.service_registry'],
                $app['exception_handler']
            );
            $dispatcher->setDefaultAction('_pages', 'render');
            $dispatcher->setLoginAction('_session', 'login');
            return $dispatcher;
        };
    }

    // ------------------------------------------------------------------
    // Native PHP session setup (replaces SessionServiceProvider)
    // ------------------------------------------------------------------

    protected function initializeSession(): void
    {
        $config      = self::$config['session'] ?? [];
        $saveHandler = $config['save_handler'] ?? 'files';

        if ($saveHandler !== 'files' && !empty($saveHandler)) {
            // Redis
            if ($saveHandler === 'redis') {
                $server = $config['options']['server'] ?? ['127.0.0.1', 6379];
                ini_set('session.save_handler', 'redis');
                ini_set('session.save_path', "tcp://{$server[0]}:{$server[1]}");
            }
            // Memcache / Memcached
            elseif (in_array($saveHandler, ['memcache', 'memcached'], true)) {
                $servers = $config['options']['servers'] ?? [['127.0.0.1', 11211]];
                ini_set('session.save_handler', $saveHandler);
                $savePath = implode(',', array_map(
                    fn($s) => "tcp://{$s[0]}:{$s[1]}",
                    $servers
                ));
                ini_set('session.save_path', $savePath);
            }
        } else {
            // File-based sessions — make sure the path is writable
            $savePath = ini_get('session.save_path');
            if (empty($savePath) || !@is_writable($savePath)) {
                $fallback = $this->getTmpPath();
                if (DIRECTORY_SEPARATOR !== '/') {
                    $fallback = str_replace('/', DIRECTORY_SEPARATOR, $fallback);
                }
                session_save_path($fallback);
            }
        }

        $this->app['sc.session'] = function ($app) {
            return new Session();
        };
    }

    // ------------------------------------------------------------------
    // Plugin loading
    // ------------------------------------------------------------------

    protected function setPluginPath(string $baseSourceDir): void
    {
        $this->pluginPath = $baseSourceDir . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR;
    }

    protected function getPluginsPath(string $plugin = null): string
    {
        if ($plugin === null) {
            return $this->pluginPath;
        }
        if (!isset($this->plugins[$plugin])) {
            throw new MissingPluginException(['plugin' => $plugin]);
        }
        return $this->pluginPath . strtolower($plugin);
    }

    protected function loadPlugins(): void
    {
        $dir = new \DirectoryIterator($this->getPluginsPath());
        foreach ($dir as $fileInfo) {
            if (!$fileInfo->isDot() && $fileInfo->isDir()) {
                $name          = $fileInfo->getBasename();
                $config        = self::getConfig($name);
                $bootstrapFile = $fileInfo->getRealPath() . DIRECTORY_SEPARATOR . 'bootstrap.php';

                if (file_exists($bootstrapFile)) {
                    $pluginLoader    = \Closure::bind(function ($bootstrapFile, $sitecake, $config) {
                        return include $bootstrapFile;
                    }, null);
                    $pluginInstance  = $pluginLoader($bootstrapFile, $this->app['sc.api'], $config);
                    if (!($pluginInstance instanceof PluginInterface)) {
                        throw new InvalidPluginDefinitionException(['plugin' => $name]);
                    }
                    $this->app['sc.plugin_registry']->add($name, $pluginInstance);
                } else {
                    $class = $this->app['sc.plugin_registry']->resolveClassName($name);
                    if (!class_exists($class)) {
                        throw new MissingPluginException(['plugin' => $class]);
                    }
                    if (!is_subclass_of($class, PluginInterface::class)) {
                        throw new InvalidPluginDefinitionException(['plugin' => $name]);
                    }
                    $pluginInstance = $this->app['sc.plugin_registry']->load($class, $config);
                }

                if ($pluginInstance instanceof BootableInterface) {
                    $pluginInstance->boot();
                }
            }
        }
    }

    // ------------------------------------------------------------------
    // Static API
    // ------------------------------------------------------------------

    public static function getConfig(string $key = null, $default = null)
    {
        if ($key === null) {
            return self::$config;
        }
        if (strpos($key, '.') === false) {
            return self::$config[$key] ?? null;
        }
        $return = self::$config;
        foreach (explode('.', $key) as $k) {
            if (!is_array($return) || !isset($return[$k])) {
                return $default;
            }
            $return = $return[$k];
        }
        return $return;
    }

    public static function isDebugMode(): bool
    {
        return (bool)(self::$instance->app['debug'] ?? false);
    }

    public static function resourceManager()
    {
        return self::$instance->app['sc.resource_manager'];
    }

    public static function eventBus(): EventManager
    {
        return self::$instance->app['event_bus'];
    }

    public static function cache()
    {
        return self::$instance->app['sc.cache'];
    }

    public function getFilesystem()
    {
        return $this->app['filesystem'];
    }

    public static function getPath(string $key = null)
    {
        if ($key !== null && array_key_exists($key, self::$paths)) {
            return self::$paths[$key];
        }
        return null;
    }

    public static function getPaths(): array
    {
        return self::$paths;
    }

    public function getTmpPath(): string
    {
        return self::$paths['TMP'];
    }

    public static function getLogsPath(): string
    {
        return self::$paths['LOGS'];
    }

    public static function getCachePath(): string
    {
        return self::$paths['CACHE'];
    }

    public function getResourceManager()
    {
        return $this->app['sc.resource_manager'];
    }

    public function getLogger()
    {
        return $this->app['logger'];
    }

    public function services()
    {
        return $this->app['sc.service_registry'];
    }

    public function getSessionManager()
    {
        return $this->app['sc.session_manager'];
    }

    public function getSession()
    {
        return $this->app['sc.session'];
    }

    public function getFileLocker()
    {
        return $this->app['sc.flock'];
    }

    public function getElementFactory()
    {
        return $this->app['sc.element_factory'] ?? null;
    }

    public function getApi()
    {
        return $this->app['sc.api'];
    }

    // Helpers for finding previous version (used in upgrade)
    protected function previousVersion(): string
    {
        $versionDirs = $this->app['filesystem']->listPatternPaths('sitecake', '/^[0-9]\.[0-9]\.[0-9]$/');
        if (!empty($versionDirs) && count($versionDirs) > 1) {
            sort($versionDirs);
            array_pop($versionDirs);
            return array_pop($versionDirs);
        }
        return '';
    }
}
