<?php

namespace Sitecake\Error;

/**
 * Global PHP error / exception handler.
 *
 * Rewritten to remove dependencies on Monolog, PSR-3, and Symfony HttpKernel.
 * Loggers are any object with a log(string $level, string $message, array $context) method.
 * Formatters are any callable(array $error): mixed.
 */
class ErrorHandler
{
    const SUPPRESS_NONE = 0;
    const SUPPRESS_NEXT = 1;
    const SUPPRESS_ALL  = 2;

    // Log-level string constants (replaces Monolog\Logger::ERROR etc.)
    const LEVEL_ERROR   = 'error';
    const LEVEL_WARNING = 'warning';
    const LEVEL_NOTICE  = 'notice';

    /** @var object[] Objects with log(level, message, context) */
    protected static array $loggers = [];

    /** @var callable[] */
    protected static array $formatters = [];

    /** @var ErrorHandler */
    protected static $handler;

    protected static int $suppressed = self::SUPPRESS_NONE;

    protected bool $debug = false;

    public function __construct(bool $debug = false)
    {
        $this->debug = $debug;
    }

    /**
     * Register as the global PHP error/exception handler.
     *
     * @param int         $level      Error reporting level (e.g. E_ALL)
     * @param object|null $logger     Any object with log(level, message, context)
     * @param callable|null $formatter callable(array $error): mixed
     */
    public static function register(int $level, object $logger = null, callable $formatter = null): self
    {
        if ($logger !== null) {
            self::$loggers[] = $logger;
        }
        if ($formatter !== null) {
            self::$formatters[] = $formatter;
        }

        self::$handler = new static();

        error_reporting($level);
        set_error_handler([self::$handler, 'handleError'], $level);
        set_exception_handler([self::$handler, 'wrapAndHandleException']);

        register_shutdown_function(function () {
            $error = error_get_last();
            if (!is_array($error)) {
                return;
            }
            $fatalErrors = [E_USER_ERROR, E_ERROR, E_PARSE];
            if (in_array($error['type'], $fatalErrors, true)) {
                self::$handler->handleFatalError(
                    $error['type'],
                    $error['message'],
                    $error['file'],
                    $error['line']
                );
            }
        });

        return self::$handler;
    }

    public function setDebug(bool $debug): self
    {
        $this->debug = $debug;
        return $this;
    }

    public function handleFatalError(int $code, string $description, string $file, int $line, $context = null): bool
    {
        http_response_code(500);

        $data = [
            'level'       => self::LEVEL_ERROR,
            'level_name'  => 'Error',
            'code'        => $code,
            'description' => $description,
            'file'        => $file,
            'line'        => $line,
            'error'       => 'Fatal Error',
            'context'     => (array)$context,
        ];

        self::$handler->logError(self::LEVEL_ERROR, $data);
        self::$handler->displayError($data);

        return true;
    }

    public function wrapAndHandleException($exception): void
    {
        if ($exception instanceof \Error) {
            $exception = new PHP7ErrorException($exception);
        }
        self::$handler->handleException($exception);
    }

    protected function logError($level, array $data = []): bool
    {
        if ($level instanceof \Exception) {
            $message = self::getMessage($level);
            $level   = self::LEVEL_ERROR;
        } else {
            $message = sprintf(
                '%s (%s): %s in [%s, line %s]',
                $data['error'],
                $data['code'],
                $data['description'],
                $data['file'],
                $data['line']
            );
            ob_start();
            debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
            $trace = ob_get_clean();
            $message .= "\nStack Trace:\n" . $trace . "\n\n";
        }

        foreach (self::$loggers as $logger) {
            $logger->log($level, $message, $data['context'] ?? []);
        }

        return true;
    }

    protected static function getMessage(\Exception $exception): string
    {
        return sprintf(
            '%s (%s): %s in [%s, line %s]',
            get_class($exception),
            $exception->getCode(),
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine()
        ) . "\nStack Trace:\n" . $exception->getTraceAsString() . "\n\n";
    }

    protected function displayError(array $error, bool $return = false)
    {
        if (self::$suppressed !== self::SUPPRESS_NONE && !$return) {
            if (self::$suppressed === self::SUPPRESS_NEXT) {
                self::$suppressed = self::SUPPRESS_NONE;
            }
            return '';
        }

        if (!$this->debug) {
            return '';
        }

        if (PHP_SAPI === 'cli') {
            var_dump($error);
            return null;
        }

        $formatted = self::$handler->format($error);

        if ($return) {
            return $formatted;
        }

        if ($this->debug) {
            echo $formatted;
        }

        return null;
    }

    protected function format(array $error)
    {
        foreach (self::$formatters as $formatter) {
            $error = $formatter($error);
        }
        return $error;
    }

    public function handleError(int $code, string $description, string $file = null, int $line = null, $context = null): bool
    {
        if (error_reporting() === 0) {
            return false;
        }

        [$error, $log] = self::mapErrorCode($code);

        if ($log === self::LEVEL_ERROR) {
            return self::$handler->handleFatalError($code, $description, $file ?? '', $line ?? 0, $context);
        }

        $data = [
            'level_name'  => ucfirst($error),
            'level'       => $log,
            'code'        => $code,
            'error'       => ucfirst($error),
            'description' => $description,
            'file'        => $file,
            'line'        => $line,
            'extra'       => [],
            'context'     => $context,
        ];

        self::$handler->logError($error, $data);
        self::$handler->displayError($data);

        return true;
    }

    protected static function mapErrorCode(int $code): array
    {
        $levelMap = [
            E_PARSE            => self::LEVEL_ERROR,
            E_ERROR            => self::LEVEL_ERROR,
            E_CORE_ERROR       => self::LEVEL_ERROR,
            E_COMPILE_ERROR    => self::LEVEL_ERROR,
            E_USER_ERROR       => self::LEVEL_ERROR,
            E_WARNING          => self::LEVEL_WARNING,
            E_USER_WARNING     => self::LEVEL_WARNING,
            E_COMPILE_WARNING  => self::LEVEL_WARNING,
            E_RECOVERABLE_ERROR => self::LEVEL_WARNING,
            E_NOTICE           => self::LEVEL_NOTICE,
            E_USER_NOTICE      => self::LEVEL_NOTICE,
            E_STRICT           => self::LEVEL_NOTICE,
            E_DEPRECATED       => self::LEVEL_NOTICE,
            E_USER_DEPRECATED  => self::LEVEL_NOTICE,
        ];

        $error = $levelMap[$code] ?? self::LEVEL_NOTICE;
        return [$error, $error];   // both are string now
    }

    public function handleException(\Exception $exception, bool $return = false)
    {
        $trace = $exception->getTrace();
        $data = [
            'level'       => self::LEVEL_ERROR,
            'level_name'  => get_class($exception),
            'function'    => $trace[0]['function'] ?? false,
            'class'       => $trace[0]['class'] ?? false,
            'type'        => $trace[0]['type'] ?? false,
            'file'        => $exception->getFile(),
            'line'        => $exception->getLine(),
            'description' => $exception->getMessage(),
            'code'        => $exception->getCode(),
            'error'       => get_class($exception),
            'extra'       => [],
            'context'     => [],
        ];

        self::$handler->logError($exception);

        if ($return) {
            return self::displayError($data, true);
        }

        self::$handler->displayError($data, false);
        return null;
    }

    public static function suppress(int $level = self::SUPPRESS_NEXT): void
    {
        self::$suppressed = $level;
    }
}
