<?php

namespace App\Core;

/**
 * ITSWEBER CMS – Application Bootstrap
 *
 * Central application class. Initializes core services,
 * handles the request lifecycle, and acts as a simple service container.
 */
class App
{
    private static ?App $instance = null;
    private array $services = [];
    private string $basePath;

    private function __construct(string $basePath)
    {
        $this->basePath = rtrim($basePath, '/');
    }

    /**
     * Initialize the application (singleton).
     */
    public static function init(string $basePath): self
    {
        if (self::$instance === null) {
            self::$instance = new self($basePath);
            self::$instance->bootstrap();
        }
        return self::$instance;
    }

    /**
     * Get the application instance.
     */
    public static function getInstance(): ?self
    {
        return self::$instance;
    }

    /**
     * Bootstrap core services.
     */
    private function bootstrap(): void
    {
        // Error reporting
        $this->setupErrorHandling();

        // Load configuration
        $this->registerService('config', new Config($this->basePath . '/app/Config'));

        // Session
        $session = new Session();
        $session->start();
        $this->registerService('session', $session);

        // Database (only if installed)
        if ($this->isInstalled()) {
            $dbConfig = $this->config()->get('database');
            if ($dbConfig) {
                $db = new Database($dbConfig);
                $this->registerService('database', $db);
            }
        }

        // CSRF
        $this->registerService('csrf', new Csrf($session));

        // Translator – determine system language
        $locale = 'de'; // Default fallback for all scenarios
        if ($this->isInstalled() && $this->hasService('database')) {
            $dbLocale = $this->getSetting('system_language', '');
            if ($dbLocale !== '' && Translator::isSupported($dbLocale)) {
                $locale = $dbLocale;
            }
        }
        // Session override (installer flow or admin language switch)
        $sessionLocale = $session->get('locale', '');
        if ($sessionLocale !== '' && Translator::isSupported($sessionLocale)) {
            $locale = $sessionLocale;
        }
        $this->registerService('translator', new Translator(
            $this->basePath . '/app/Lang',
            $locale,
            'de'
        ));

        // Auth
        if ($this->isInstalled() && $this->hasService('database')) {
            $this->registerService('auth', new Auth($this->service('database'), $session));
        }

        // View
        $themePath = $this->basePath . '/themes';
        $activeTheme = 'default';
        if ($this->isInstalled() && $this->hasService('database')) {
            $activeTheme = $this->getSetting('active_theme', 'default');
        }

        // Theme-Fallback: if the configured theme directory does not exist,
        // silently fall back to 'default' and persist a session flag so the
        // admin UI can show a persistent warning banner (ADR-088).
        if ($activeTheme !== 'default' && !is_dir($themePath . '/' . $activeTheme)) {
            $session->set('_iw_theme_fallback', $activeTheme);
            $activeTheme = 'default';
        } else {
            // Clear a stale fallback flag once the theme is available again.
            $session->remove('_iw_theme_fallback');
        }

        $this->registerService('view', new View($themePath, $activeTheme, $this->basePath));

        // Router
        $this->registerService('router', new Router($this));
    }

    /**
     * Run the application – dispatch the current request.
     */
    public function run(): void
    {
        $router = $this->service('router');
        $request = Request::capture();
        $this->registerService('request', $request);

        // Security headers (configurable via settings, default: on)
        $this->sendSecurityHeaders();

        // HTTP cache headers for frontend pages (configurable, default: off)
        $this->sendHttpCacheHeaders($request);

        // Check if installer should be shown
        if (!$this->isInstalled()) {
            $uri = $request->uri();
            if (strpos($uri, '/installer') !== 0) {
                Response::redirect('/installer');
                return;
            }
        }

        // Check for URL redirects before routing (SEO-friendly slug changes)
        if ($this->isInstalled() && $this->hasService('database')) {
            $this->handleRedirects($request);

            // Content scheduling pseudo-cron (debounced, max once per 60s)
            $this->runContentScheduler();

            // Marketplace integrity monitor (debounced, max once per 24h)
            $this->runIntegrityMonitor();

            // CMS Core update check (debounced, max once per TTL)
            $this->runCmsUpdateChecker();
        }

        // Page cache: serve cached response if available (B2)
        if ($this->isInstalled() && $this->tryServePageCache($request)) {
            return;
        }

        // Start output buffering for page cache capture
        $cacheCapture = $this->shouldCaptureForCache($request);
        if ($cacheCapture) {
            ob_start();
        }

        $router->dispatch($request);

        // Store output in page cache if applicable
        if ($cacheCapture) {
            $html = ob_get_clean();
            $this->storePageCache($request, $html);
            echo $html;
        }
    }

    /**
     * Try to serve a cached page. Returns true if cache hit was served.
     */
    private function tryServePageCache(Request $request): bool
    {
        if ($this->getSetting('page_cache_enabled', '0') !== '1') {
            return false;
        }
        if (!\App\Services\PageCache::shouldCache()) {
            return false;
        }

        $ttl   = (int) $this->getSetting('page_cache_ttl', '3600');
        $cache = new \App\Services\PageCache($this->basePath, $ttl);
        $html  = $cache->get($request->uri());

        if ($html === null) {
            return false;
        }

        header('X-Page-Cache: HIT');
        echo $html;
        return true;
    }

    /**
     * Check if we should capture output for caching.
     */
    private function shouldCaptureForCache(Request $request): bool
    {
        if (!$this->isInstalled()) {
            return false;
        }
        if ($this->getSetting('page_cache_enabled', '0') !== '1') {
            return false;
        }
        return \App\Services\PageCache::shouldCache();
    }

    /**
     * Store captured output in page cache.
     */
    private function storePageCache(Request $request, string $html): void
    {
        if ($html === '' || http_response_code() !== 200) {
            return;
        }

        $ttl   = (int) $this->getSetting('page_cache_ttl', '3600');
        $cache = new \App\Services\PageCache($this->basePath, $ttl);
        $cache->set($request->uri(), $html);
        header('X-Page-Cache: MISS');
    }

    /**
     * Register a service in the container.
     */
    public function registerService(string $name, object $service): void
    {
        $this->services[$name] = $service;
    }

    /**
     * Retrieve a service from the container.
     */
    public function service(string $name): object
    {
        if (!isset($this->services[$name])) {
            throw new \RuntimeException("Service '{$name}' not registered.");
        }
        return $this->services[$name];
    }

    /**
     * Check if a service exists.
     */
    public function hasService(string $name): bool
    {
        return isset($this->services[$name]);
    }

    /**
     * Shortcut: Config service.
     */
    public function config(): Config
    {
        return $this->services['config'];
    }

    /**
     * Shortcut: Database service.
     */
    public function db(): Database
    {
        return $this->services['database'];
    }

    /**
     * Get base path.
     */
    public function basePath(string $append = ''): string
    {
        return $this->basePath . ($append ? '/' . ltrim($append, '/') : '');
    }

    /**
     * Check if CMS is installed.
     */
    public function isInstalled(): bool
    {
        // Primary check: flag file
        if (file_exists($this->basePath . '/storage/.installed')) {
            return true;
        }

        // Secondary check: if database is configured and has users, consider installed.
        // This prevents re-running the installer if only the flag file was deleted.
        if ($this->hasService('database')) {
            try {
                $count = (int) $this->db()->fetchColumn("SELECT COUNT(*) FROM iw_users");
                if ($count > 0) {
                    // Restore the missing flag file
                    @file_put_contents($this->basePath . '/storage/.installed', date('Y-m-d H:i:s'));
                    return true;
                }
            } catch (\Exception $e) {
                // Database not yet set up – not installed
            }
        }

        return false;
    }

    /**
     * Get a setting from the database.
     */
    public function getSetting(string $key, string $default = ''): string
    {
        if (!$this->hasService('database')) {
            return $default;
        }
        try {
            $db = $this->db();
            $stmt = $db->query(
                "SELECT setting_value FROM iw_settings WHERE setting_key = :key LIMIT 1",
                ['key' => $key]
            );
            $row = $stmt->fetch(\PDO::FETCH_ASSOC);
            return $row ? $row['setting_value'] : $default;
        } catch (\Exception $e) {
            return $default;
        }
    }

    /**
     * Send HTTP cache headers for frontend GET requests.
     * Skipped for logged-in users, admin paths, and POST requests.
     */
    private function sendHttpCacheHeaders(Request $request): void
    {
        // Only for GET requests
        if ($request->method() !== 'GET') {
            header('Cache-Control: no-store, private');
            return;
        }

        $uri = $request->uri();

        // Skip admin and installer paths
        if (str_starts_with($uri, admin_url()) || str_starts_with($uri, '/installer')) {
            header('Cache-Control: no-store, private');
            return;
        }

        // Skip if user is logged in (dynamic content)
        if ($this->hasService('auth') && $this->service('auth')->check()) {
            header('Cache-Control: no-cache, private');
            return;
        }

        // Check if HTTP caching is enabled
        if (!$this->isInstalled() || !$this->hasService('database')) {
            return;
        }

        if ($this->getSetting('http_cache_enabled', '0') !== '1') {
            return;
        }

        $ttl = max(60, (int) $this->getSetting('http_cache_ttl', '3600'));

        header('Cache-Control: public, max-age=' . $ttl . ', must-revalidate');
        header('Vary: Accept-Encoding');
    }

    /**
     * Run the content scheduler (pseudo-cron) if the feature is enabled.
     */
    private function runContentScheduler(): void
    {
        if ($this->getSetting('content_scheduling_enabled', '1') === '0') {
            return;
        }

        try {
            $scheduler = new \App\Services\ContentScheduler(
                $this->basePath . '/storage'
            );
            $scheduler->runIfDue();
        } catch (\Exception $e) {
            // Pre-migration or DB error – skip silently
        }
    }

    /**
     * Run the marketplace integrity monitor if the feature is enabled.
     */
    private function runIntegrityMonitor(): void
    {
        if ($this->getSetting('marketplace_integrity_monitor_enabled', '1') === '0') {
            return;
        }

        try {
            $monitor = new \App\Services\Marketplace\IntegrityMonitor($this->basePath);
            $monitor->runIfDue();
        } catch (\Exception $e) {
            // Pre-migration or DB error – skip silently
        }
    }

    /**
     * Run the CMS Core update checker (pseudo-cron) if the feature is enabled.
     */
    private function runCmsUpdateChecker(): void
    {
        if ($this->getSetting('cms_update_check_enabled', '1') !== '1') {
            return;
        }

        try {
            $cron = new \App\Services\CmsUpdate\CmsUpdatePseudoCron($this->basePath);
            $cron->runIfDue();
        } catch (\Exception $e) {
            // Pre-migration or DB error – skip silently
        }
    }

    /**
     * Check for URL redirects and send 301/302 if a match is found.
     */
    private function handleRedirects(Request $request): void
    {
        if ($this->getSetting('redirect_manager_enabled', '1') === '0') {
            return;
        }

        $uri = $request->uri();

        // Skip admin and installer paths
        if (str_starts_with($uri, admin_url()) || str_starts_with($uri, '/installer')) {
            return;
        }

        try {
            $redirect = $this->db()->fetch(
                "SELECT id, target_url, status_code FROM iw_redirects WHERE source_path = :path AND is_active = 1 LIMIT 1",
                ['path' => $uri]
            );

            if ($redirect) {
                // Increment hit counter (fire-and-forget)
                $this->db()->query(
                    "UPDATE iw_redirects SET hit_count = hit_count + 1 WHERE id = :id",
                    ['id' => (int) $redirect['id']]
                );

                Response::redirect($redirect['target_url'], (int) $redirect['status_code']);
            }
        } catch (\Exception $e) {
            // Table may not exist yet (pre-migration) – skip silently
        }
    }

    /**
     * Send security-related HTTP headers on every response.
     * Toggleable via setting 'security_headers_enabled' (default: on).
     */
    private function sendSecurityHeaders(): void
    {
        if ($this->isInstalled() && $this->hasService('database')) {
            $enabled = $this->getSetting('security_headers_enabled', '1');
            if ($enabled === '0') {
                return;
            }
        }

        // Prevent clickjacking – page cannot be embedded in iframes on other domains
        header('X-Frame-Options: SAMEORIGIN');

        // Prevent MIME-type sniffing attacks
        header('X-Content-Type-Options: nosniff');

        // Control referrer information sent to other sites
        header('Referrer-Policy: strict-origin-when-cross-origin');

        // Restrict browser features (camera, microphone, geolocation etc.)
        header('Permissions-Policy: camera=(), microphone=(), geolocation=()');

        // Enforce HTTPS (only when already on HTTPS to avoid lockout)
        if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
            header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
        }

        // Prevent XSS in older browsers (modern browsers use CSP)
        header('X-XSS-Protection: 1; mode=block');
    }

    /**
     * Setup error handling based on environment.
     */
    private function setupErrorHandling(): void
    {
        $isDebug = file_exists($this->basePath . '/.debug');
        if ($isDebug) {
            error_reporting(E_ALL);
            ini_set('display_errors', '1');
        } else {
            error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
            ini_set('display_errors', '0');
        }
        ini_set('log_errors', '1');

        $logFile = $this->basePath . '/storage/logs/error.log';
        ini_set('error_log', $logFile);

        // Log rotation: rotate when file exceeds 5 MB, keep max 5 archives
        $this->rotateLogIfNeeded($logFile, 5 * 1024 * 1024, 5);
    }

    /**
     * Rotate a log file if it exceeds the given size limit.
     */
    private function rotateLogIfNeeded(string $logFile, int $maxSize, int $keepFiles): void
    {
        if (!file_exists($logFile) || filesize($logFile) < $maxSize) {
            return;
        }

        $dir  = dirname($logFile);
        $base = pathinfo($logFile, PATHINFO_FILENAME);
        $ext  = pathinfo($logFile, PATHINFO_EXTENSION);

        // Rename current log with date suffix
        $archiveName = $dir . '/' . $base . '-' . date('Y-m-d-His') . '.' . $ext;
        @rename($logFile, $archiveName);

        // Clean up old archives beyond keepFiles limit
        $pattern  = $dir . '/' . $base . '-*.' . $ext;
        $archives = glob($pattern);
        if ($archives && count($archives) > $keepFiles) {
            sort($archives);
            $toDelete = array_slice($archives, 0, count($archives) - $keepFiles);
            foreach ($toDelete as $old) {
                @unlink($old);
            }
        }
    }
}
