<?php

namespace App\Core;

/**
 * ErrorHandler — Global PHP Error and Exception Handler
 *
 * Registers three PHP global handlers:
 *   1. set_error_handler()         — converts PHP errors to ErrorException
 *   2. set_exception_handler()     — catches all uncaught Throwables
 *   3. register_shutdown_function() — catches fatal errors missed by #1/#2
 *
 * Behaviour is environment-aware:
 *   APP_DEBUG=true  → developer debug page (stack trace, file, line)
 *   APP_DEBUG=false → friendly error view (500.php); log entry written
 *
 * Public helpers used by the Router (INIT-007):
 *   ErrorHandler::renderNotFound()   → 404 response
 *   ErrorHandler::renderForbidden()  → 403 response
 *   ErrorHandler::renderMaintenance() → 503 response
 *
 * Dependencies:
 *   env()    — INIT-005
 *   config() — INIT-005
 *   Logger   — INIT-006 (same task, loaded before this via Autoloader)
 *   ROOT_PATH constant — INIT-003
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.7, SAD v1.1 Ch.13, INIT-006
 */
class ErrorHandler
{
    // ─────────────────────────────────────────────────────────────────────────
    // REGISTRATION
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Register all three PHP error/exception/shutdown handlers.
     * Called once from Bootstrap::registerErrorHandlers() (INIT-003).
     *
     * @return void
     */
    public static function register(): void
    {
        set_error_handler([self::class, 'handleError']);
        set_exception_handler([self::class, 'handleException']);
        register_shutdown_function([self::class, 'handleShutdown']);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // HANDLER — PHP Errors (E_WARNING, E_NOTICE, E_USER_ERROR, etc.)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Convert PHP errors into ErrorException instances.
     *
     * PHP errors that match the current error_reporting() mask are converted
     * into ErrorException and thrown. This makes all errors catchable with
     * try/catch and ensures they are logged and displayed consistently.
     *
     * The @ operator temporarily sets error_reporting to 0. When this handler
     * is called with errno=0, the error was suppressed — we honour that.
     *
     * @param int    $errno   Error level constant (E_WARNING, E_NOTICE, etc.)
     * @param string $errstr  Error message.
     * @param string $errfile Absolute path of the file where the error occurred.
     * @param int    $errline Line number in $errfile.
     *
     * @return bool  false = let PHP's internal handler also process this error.
     * @throws \ErrorException
     */
    public static function handleError(
        int    $errno,
        string $errstr,
        string $errfile,
        int    $errline
    ): bool {
        // Error suppressed with @ operator — honour the suppression.
        if (!(error_reporting() & $errno)) {
            return false;
        }

        throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // HANDLER — Uncaught Exceptions and Errors
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Handle any uncaught Throwable (Exception or Error).
     *
     * In debug mode   → render a developer-friendly debug page.
     * In production   → log the exception; render 500.php to the user.
     *
     * @param \Throwable $throwable The uncaught exception or error.
     * @return void
     */
    public static function handleException(\Throwable $throwable): void
    {
        $debug = env('APP_DEBUG', false);

        // Always log in production. In debug, logging is optional but useful.
        if (!$debug) {
            Logger::error($throwable->getMessage(), [
                'class' => get_class($throwable),
                'code'  => $throwable->getCode(),
                'file'  => $throwable->getFile(),
                'line'  => $throwable->getLine(),
                'trace' => self::trimTrace($throwable->getTraceAsString()),
            ]);
        }

        // Discard any partial output already sent to the browser.
        self::clearOutputBuffers();

        http_response_code(500);

        if ($debug) {
            self::renderDebugPage($throwable);
        } else {
            self::renderView('500');
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // HANDLER — Fatal Errors via register_shutdown_function()
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Catch fatal errors that set_error_handler() cannot intercept.
     *
     * Fatal error types caught here:
     *   E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE
     *
     * Non-fatal errors are handled by handleError(); this function
     * intentionally only acts on fatal errors to avoid double-handling.
     *
     * @return void
     */
    public static function handleShutdown(): void
    {
        $error = error_get_last();

        if ($error === null) {
            return;
        }

        $fatalTypes = [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_USER_ERROR];

        if (!in_array($error['type'], $fatalTypes, true)) {
            return;
        }

        $throwable = new \ErrorException(
            $error['message'],
            0,
            $error['type'],
            $error['file'],
            $error['line']
        );

        self::handleException($throwable);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PUBLIC HELPERS — Used by Router (INIT-007) and Middleware (INIT-009)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Render a 404 Not Found response.
     * Called by the Router when no route matches the request.
     *
     * @return void
     */
    public static function renderNotFound(): void
    {
        self::clearOutputBuffers();
        http_response_code(404);
        self::renderView('404');
    }

    /**
     * Render a 403 Forbidden response.
     * Called by Middleware when authorization fails.
     *
     * @return void
     */
    public static function renderForbidden(): void
    {
        self::clearOutputBuffers();
        http_response_code(403);
        self::renderView('403');
    }

    /**
     * Render a 503 Maintenance Mode response.
     * Called by Bootstrap or Middleware when APP_MAINTENANCE=true.
     *
     * @return void
     */
    public static function renderMaintenance(): void
    {
        self::clearOutputBuffers();
        http_response_code(503);
        header('Retry-After: 3600');
        self::renderView('maintenance');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE — Rendering
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Require and display an error view file.
     *
     * Views are in app/views/errors/{name}.php and are self-contained
     * HTML documents. They MUST NOT depend on the layout system —
     * the layout engine may itself be broken when an error occurs.
     *
     * @param string $name View name without extension (e.g. '500', '404').
     * @return void
     */
    private static function renderView(string $name): void
    {
        $viewFile = ROOT_PATH . '/app/views/errors/' . $name . '.php';

        if (file_exists($viewFile)) {
            header('Content-Type: text/html; charset=UTF-8');
            ob_start();
            require $viewFile;
            $output = (string) ob_get_clean();

            // Rewrite URLs to include base path if necessary
            $base = base_path();
            if ($base !== '') {
                $pattern = '#(href|src|action)="/(?!/)([^"]*)"#i';
                $output = preg_replace_callback($pattern, function ($matches) use ($base) {
                    $attr = $matches[1];
                    $path = $matches[2];
                    $trimmedBase = ltrim($base, '/');
                    
                    // If path already starts with the base path prefix, do not prepend it again.
                    if ($path === $trimmedBase || strpos($path, $trimmedBase . '/') === 0) {
                        return $attr . '="/' . $path . '"';
                    }
                    return $attr . '="' . $base . '/' . $path . '"';
                }, $output);
            }

            echo $output;
        } else {
            // Absolute fallback — the error view itself is missing.
            header('Content-Type: text/plain; charset=UTF-8');
            echo 'An error occurred. Please try again later.';
        }
    }

    /**
     * Render an HTML developer debug page for uncaught exceptions.
     * Only shown when APP_DEBUG=true. Never shown in production.
     *
     * @param \Throwable $throwable The exception to display.
     * @return void
     */
    private static function renderDebugPage(\Throwable $throwable): void
    {
        $class   = htmlspecialchars(get_class($throwable), ENT_QUOTES, 'UTF-8');
        $message = htmlspecialchars($throwable->getMessage(), ENT_QUOTES, 'UTF-8');
        $file    = htmlspecialchars($throwable->getFile(), ENT_QUOTES, 'UTF-8');
        $line    = $throwable->getLine();
        $trace   = htmlspecialchars($throwable->getTraceAsString(), ENT_QUOTES, 'UTF-8');
        $method  = htmlspecialchars($_SERVER['REQUEST_METHOD'] ?? '', ENT_QUOTES, 'UTF-8');
        $uri     = htmlspecialchars($_SERVER['REQUEST_URI'] ?? '', ENT_QUOTES, 'UTF-8');
        $env     = htmlspecialchars(env('APP_ENV', 'local'), ENT_QUOTES, 'UTF-8');

        header('Content-Type: text/html; charset=UTF-8');

        echo <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Inveetaire — {$class}</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;padding:2rem;min-height:100vh}
    .badge{display:inline-block;background:#ef4444;color:#fff;font-size:.7rem;font-weight:700;letter-spacing:.08em;padding:.2rem .6rem;border-radius:9999px;text-transform:uppercase;margin-bottom:1rem}
    h1{font-size:1.5rem;font-weight:700;color:#f87171;margin-bottom:.5rem;word-break:break-word}
    .message{font-size:1rem;color:#fca5a5;margin-bottom:1.5rem;line-height:1.6}
    .meta{display:grid;gap:.5rem;margin-bottom:1.5rem}
    .meta-row{display:flex;gap:.75rem;align-items:baseline}
    .meta-label{font-size:.75rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:.06em;min-width:70px;flex-shrink:0}
    .meta-value{font-family:'Consolas',monospace;font-size:.85rem;color:#94a3b8;word-break:break-all}
    .meta-value strong{color:#e2e8f0}
    .section-title{font-size:.7rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#475569;margin-bottom:.75rem;padding-bottom:.5rem;border-bottom:1px solid #1e293b}
    pre{background:#020617;border:1px solid #1e293b;border-radius:.5rem;padding:1.25rem;font-family:'Consolas',monospace;font-size:.78rem;line-height:1.7;color:#94a3b8;overflow-x:auto;white-space:pre-wrap;word-break:break-word}
    .card{background:#1e293b;border-radius:.75rem;padding:1.5rem;margin-bottom:1rem}
    .env-chip{display:inline-block;background:#0f4c81;color:#7dd3fc;font-size:.7rem;padding:.15rem .5rem;border-radius:4px;font-family:monospace}
    footer{margin-top:2rem;font-size:.7rem;color:#334155}
  </style>
</head>
<body>
  <div class="badge">DEBUG MODE</div>
  <h1>{$class}</h1>
  <p class="message">{$message}</p>

  <div class="card">
    <div class="section-title">Location</div>
    <div class="meta">
      <div class="meta-row">
        <span class="meta-label">File</span>
        <span class="meta-value">{$file}<strong>:{$line}</strong></span>
      </div>
      <div class="meta-row">
        <span class="meta-label">Request</span>
        <span class="meta-value"><strong>{$method}</strong> {$uri}</span>
      </div>
      <div class="meta-row">
        <span class="meta-label">Env</span>
        <span class="meta-value"><span class="env-chip">{$env}</span></span>
      </div>
    </div>
  </div>

  <div class="card">
    <div class="section-title">Stack Trace</div>
    <pre>{$trace}</pre>
  </div>

  <footer>Inveetaire &mdash; Debug output is only visible when APP_DEBUG=true.</footer>
</body>
</html>
HTML;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE — Utilities
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Clear all active output buffers.
     * Ensures no partial page output is sent before an error view.
     *
     * @return void
     */
    private static function clearOutputBuffers(): void
    {
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
    }

    /**
     * Trim the stack trace to a safe length for log entries.
     * Full traces can be very large; 4000 chars is sufficient for diagnosis.
     *
     * @param string $trace Full stack trace string.
     * @return string
     */
    private static function trimTrace(string $trace): string
    {
        if (strlen($trace) > 4000) {
            return substr($trace, 0, 4000) . ' ... [truncated]';
        }
        return $trace;
    }
}
