<?php
/**
 * Error Handler for FleekDash V2.2
 *
 * Lightweight error handler to:
 * 1. Prevent crashes from unserialize errors (commonly caused by external tools like WP-CLI search-replace)
 * 2. Suppress PHP Deprecated, Notice, and Warning errors site-wide when enabled (cleaner debug.log)
 * 3. Redirect WordPress automatic background update cron messages away from debug.log when enabled
 * 4. NEVER suppress PHP errors during critical WordPress operations (plugin/theme install/update)
 *
 * @package FleekDash\Core
 */

namespace FleekDash\Core;

// Load debug helpers
if (!function_exists('fleekdash_log_debug')) {
    require_once FLEEKDASH_PLUGIN_DIR . 'includes/Core/debug-helpers.php';
}

class ErrorHandler
{
    /** @var bool|null Cache for PHP notices suppression status (avoid multiple DB queries) */
    private static ?bool $noticesSuppressionEnabled = null;

    /** @var int|null error_reporting() level captured before the first global mask */
    private static ?int $preMaskErrorReporting = null;

    /** @var string|null ini error_log target saved before redirecting WP cron update noise */
    private static ?string $savedErrorLogTarget = null;

    private const WP_AUTO_UPDATE_HOOK = 'wp_maybe_auto_update';

    /**
     * Previous error handler (if any) - preserved for compatibility
     *
     * @var callable(int,string,string,int): bool|null|false
     */
    private static $previousHandler = null;

    /**
     * Track if we're in a critical WordPress operation (upgrader, file operations)
     * During these operations, we should NOT suppress any errors
     */
    private static bool $inCriticalOperation = false;

    /**
     * Initialize the error handler
     * Must be called EARLY (before other plugins set their handlers)
     *
     * ⚠️ NOTE (v2.2.9): Removed hook registration during init to prevent
     * conflicts with WordPress plugin installation process.
     * Critical operation detection is now done inline in isInCriticalOperation().
     */
    public static function init(): void
    {
        self::$previousHandler = set_error_handler([__CLASS__, 'handleError'], E_ALL);
        self::applyGlobalReportingMask();
        self::registerAutomaticUpdateLogSuppressionHooks();
        \add_filter('doing_it_wrong_trigger_error', [__CLASS__, 'filterDoingItWrongTrigger'], 10, 4);
        // wp_debug_mode() resets error_reporting(E_ALL) after our early mask — re-apply once WP is booted.
        \add_action('init', [__CLASS__, 'applyGlobalReportingMask'], 1);
    }

    /**
     * Block WP 6.7+ _doing_it_wrong() notices (e.g. health-check textdomain) from debug.log when suppression is on.
     * Does not affect real install/update PHP warnings during AJAX upgrader actions.
     *
     * @param bool   $trigger       Whether WordPress should trigger the notice
     * @param string $function_name Function name passed to _doing_it_wrong()
     * @param string $message       Message (unused)
     * @param string $version       Version (unused)
     */
    public static function filterDoingItWrongTrigger(
        $trigger,
        $function_name,
        $message,
        $version
    ): bool {
        unset($function_name, $message, $version);

        if (!$trigger || !self::readHidePhpNoticesOption()) {
            return (bool) $trigger;
        }

        return false;
    }

    /**
     * WordPress logs automatic update progress via error_log() when WP_DEBUG_LOG is on.
     * Redirect that output during wp_maybe_auto_update only (does not disable updates).
     */
    private static function registerAutomaticUpdateLogSuppressionHooks(): void
    {
        \add_action(self::WP_AUTO_UPDATE_HOOK, [self::class, 'redirectErrorLogAwayFromDebug'], -999999);
        \add_action(self::WP_AUTO_UPDATE_HOOK, [self::class, 'restoreErrorLogTarget'], 999999);
        \add_action('shutdown', [self::class, 'restoreErrorLogTarget'], 0);
    }

    public static function redirectErrorLogAwayFromDebug(): void
    {
        if (!self::readHidePhpNoticesOption() || self::$savedErrorLogTarget !== null) {
            return;
        }

        self::$savedErrorLogTarget = (string) \ini_get('error_log');

        if (\PHP_OS_FAMILY === 'Windows') {
            \ini_set('error_log', 'NUL');
            return;
        }

        $nullLog = \WP_CONTENT_DIR . '/fleekdash-null.log';
        if (!\file_exists($nullLog)) {
            \touch($nullLog);
        }

        \ini_set('error_log', $nullLog);
    }

    public static function restoreErrorLogTarget(): void
    {
        if (self::$savedErrorLogTarget === null) {
            return;
        }

        if (self::$savedErrorLogTarget !== '') {
            \ini_set('error_log', self::$savedErrorLogTarget);
        }

        self::$savedErrorLogTarget = null;
    }

    /**
     * Bitmask of noisy PHP levels hidden from logs when suppression is enabled.
     */
    public static function getSuppressedErrorLevels(): int
    {
        return E_DEPRECATED
            | E_USER_DEPRECATED
            | E_NOTICE
            | E_USER_NOTICE
            | E_WARNING
            | E_USER_WARNING;
    }

    /**
     * Lower error_reporting() for noisy levels (any plugin/theme/core).
     * Safe to call multiple times; first call snapshots the pre-mask level for restore.
     */
    public static function applyGlobalReportingMask(): void
    {
        if (!self::isNoticesSuppressionEnabled()) {
            return;
        }

        $currentLevel = \error_reporting();
        if (self::$preMaskErrorReporting === null) {
            self::$preMaskErrorReporting = $currentLevel;
        }

        \error_reporting($currentLevel & ~self::getSuppressedErrorLevels());
    }

    /**
     * Restore error_reporting() after suppression is turned off.
     */
    public static function restoreGlobalReportingMask(): void
    {
        if (self::$preMaskErrorReporting === null) {
            return;
        }

        \error_reporting(self::$preMaskErrorReporting);
        self::$preMaskErrorReporting = null;
    }

    /**
     * Active plugin/theme/core install or update (AJAX upgrader only).
     * Viewing update-core.php in an iframe is not critical — suppression stays on for noisy notices.
     */
    public static function isInCriticalOperation(): bool
    {
        return self::$inCriticalOperation || self::isAjaxPluginOrThemeAction();
    }

    private static function isAjaxPluginOrThemeAction(): bool
    {
        if (!defined('DOING_AJAX') || !DOING_AJAX) {
            return false;
        }

        $action = isset($_REQUEST['action'])
            ? \sanitize_text_field((string) \wp_unslash($_REQUEST['action']))
            : '';
        $criticalActions = [
            'install-plugin',
            'update-plugin',
            'install-theme',
            'update-theme',
            'upload-plugin',
            'upload-theme',
            'update-core',
        ];

        return \in_array($action, $criticalActions, true);
    }

    /**
     * Check if PHP notices suppression is enabled
     * Uses cached value to avoid multiple DB queries per request
     */
    private static function readHidePhpNoticesOption(): bool
    {
        $hide_notices = \get_option('fleekdash_hide_php_notices', DebugLogger::HIDE_PHP_NOTICES_DEFAULT);

        if (\is_string($hide_notices)) {
            return $hide_notices === '1' || $hide_notices === 'true';
        }

        return (bool) $hide_notices;
    }

    private static function isNoticesSuppressionEnabled(): bool
    {
        if (self::isInCriticalOperation()) {
            return false;
        }

        if (self::$noticesSuppressionEnabled !== null) {
            return self::$noticesSuppressionEnabled;
        }

        self::$noticesSuppressionEnabled = self::readHidePhpNoticesOption();

        return self::$noticesSuppressionEnabled;
    }

    /**
     * Handle PHP errors - suppress unserialize errors, deprecated notices, and warnings
     * CRITICAL: Never suppresses errors during plugin/theme install/update operations
     *
     * @return bool True to suppress error, false to let WordPress handle it
     */
    public static function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
    {
        $suppress = false;

        if (!self::isInCriticalOperation()) {
            if (self::isFleekDashFile($errfile) && \strpos($errstr, 'unserialize(): Error at offset') !== false) {
                fleekdash_log_debug("FleekDash: Suppressed unserialize error: $errstr in $errfile:$errline");
                $suppress = true;
            } elseif (
                !self::isFilesystemRelatedWarning($errstr)
                && self::isNoticesSuppressionEnabled()
                && self::isSuppressibleNoticeLevel($errno)
            ) {
                $suppress = true;
            }
        }

        if ($suppress) {
            return true;
        }

        return self::delegateToPreviousHandler($errno, $errstr, $errfile, $errline);
    }

    private static function isFilesystemRelatedWarning(string $errstr): bool
    {
        return \strpos($errstr, 'file_get_contents') !== false
            || \strpos($errstr, 'file_put_contents') !== false
            || \strpos($errstr, 'copy(') !== false
            || \strpos($errstr, 'unlink(') !== false
            || \strpos($errstr, 'mkdir(') !== false
            || \strpos($errstr, 'rmdir(') !== false
            || \strpos($errstr, 'fopen(') !== false
            || \strpos($errstr, 'fwrite(') !== false
            || \strpos($errstr, 'is_readable') !== false
            || \strpos($errstr, 'is_writable') !== false
            || \strpos($errstr, 'PCLZIP') !== false
            || \strpos($errstr, 'ZipArchive') !== false;
    }

    private static function isSuppressibleNoticeLevel(int $errno): bool
    {
        return (bool) ($errno & self::getSuppressedErrorLevels());
    }

    /**
     * FleekDash-only paths (e.g. unserialize recovery), not used for global notice suppression.
     */
    private static function isFleekDashFile(string $file): bool
    {
        $pluginDir = \defined('FLEEKDASH_PLUGIN_DIR') ? \FLEEKDASH_PLUGIN_DIR : '';
        $normalizedFile = \wp_normalize_path($file);
        $normalizedPluginDir = \wp_normalize_path($pluginDir);

        return $normalizedPluginDir !== '' && \strpos($normalizedFile, $normalizedPluginDir) === 0;
    }

    /**
     * Delegate to the error handler that was installed before FleekDash.
     */
    private static function delegateToPreviousHandler(int $errno, string $errstr, string $errfile, int $errline): bool
    {
        $handled = false;
        if (self::$previousHandler && \is_callable(self::$previousHandler)) {
            $handled = (bool) \call_user_func(self::$previousHandler, $errno, $errstr, $errfile, $errline);
        }

        return $handled;
    }

    /**
     * Clear the suppression cache (call when setting changes)
     * This ensures the new setting takes effect immediately
     */
    public static function clearCache(): void
    {
        self::$noticesSuppressionEnabled = null;
    }
}
