<?php

namespace MintyPHP\Http;

use MintyPHP\I18n;

final class ErrorRenderer
{
    /**
     * Renders the full developer debug page with all four panels.
     *
     * @param array{exception: array<string, mixed>, request: array<string, mixed>, environment: array<string, mixed>, queries: array<string, mixed>} $data
     */
    public static function renderDev(array $data): string
    {
        $exc = $data['exception'];
        $req = $data['request'];
        $env = $data['environment'];
        $qry = $data['queries'];

        $requestId = self::esc((string) ($req['request_id'] ?? 'unknown'));
        $statusCode = http_response_code() ?: 500;
        $statusText = self::statusText($statusCode);
        $queryCount = $qry['count'] ?? count($qry['queries'] ?? []);

        $css = self::loadAsset(__DIR__ . '/../../web/css/pages/app-error-debug.css');
        $js = self::loadAsset(__DIR__ . '/../../web/js/components/app-error-debug.js');

        $excClass = self::esc($exc['class'] ?? self::tr('Exception'));
        $excMessage = self::esc((string) ($exc['message'] ?? self::tr('An error occurred')));
        $excLocation = self::esc(self::tr(
            'in %s on line %s',
            self::shortenPath((string) ($exc['file'] ?? '')),
            (string) ($exc['line'] ?? '?')
        ));

        $exceptionPanel = self::renderExceptionPanel($exc);
        $requestPanel = self::renderRequestPanel($req);
        $environmentPanel = self::renderEnvironmentPanel($env);
        $queryPanel = self::renderQueryPanel($qry);

        $lang = self::esc(self::htmlLang());
        $statusTextEsc = self::esc($statusText);
        $requestIdLabel = self::esc(self::tr('Request ID'));
        $copyRequestIdTitle = self::esc(self::tr('Copy request ID'));
        $tabsLabel = self::esc(self::tr('Error details'));

        $tabException = self::esc(self::tr('Exception'));
        $tabRequest = self::esc(self::tr('Request'));
        $tabEnvironment = self::esc(self::tr('Environment'));
        $tabQueries = self::esc(self::tr('Queries'));

        return <<<HTML
        <!DOCTYPE html>
        <html lang="{$lang}">
        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>{$statusCode} {$statusTextEsc}</title>
          <style>{$css}</style>
        </head>
        <body class="app-error-debug">
          <header class="app-error-debug-header">
            <div class="app-error-debug-container app-error-debug-header__inner">
              <div class="app-error-debug-header__status">
                <span class="app-error-debug-header__code">{$statusCode}</span>
                <span class="app-error-debug-header__label">{$statusTextEsc}</span>
              </div>
              <div class="app-error-debug-request-id">
                {$requestIdLabel}: {$requestId}
                <button type="button" class="app-error-debug-copy-button" data-copy="{$requestId}" title="{$copyRequestIdTitle}" aria-label="{$copyRequestIdTitle}">
                  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/><path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/></svg>
                </button>
              </div>
            </div>
          </header>

          <main class="app-error-debug-container">
            <div class="app-error-debug-summary">
              <div class="app-error-debug-summary__class">{$excClass}</div>
              <div class="app-error-debug-summary__message">{$excMessage}</div>
              <div class="app-error-debug-summary__location">{$excLocation}</div>
            </div>

            <nav class="app-error-debug-tabs" role="tablist" aria-label="{$tabsLabel}">
              <button type="button" id="tab-exception" class="app-error-debug-tab active" role="tab" aria-selected="true" aria-controls="panel-exception" data-panel="panel-exception" tabindex="0">{$tabException}</button>
              <button type="button" id="tab-request" class="app-error-debug-tab" role="tab" aria-selected="false" aria-controls="panel-request" data-panel="panel-request" tabindex="-1">{$tabRequest}</button>
              <button type="button" id="tab-environment" class="app-error-debug-tab" role="tab" aria-selected="false" aria-controls="panel-environment" data-panel="panel-environment" tabindex="-1">{$tabEnvironment}</button>
              <button type="button" id="tab-queries" class="app-error-debug-tab" role="tab" aria-selected="false" aria-controls="panel-queries" data-panel="panel-queries" tabindex="-1">{$tabQueries} <span class="app-error-debug-tab__count">{$queryCount}</span></button>
            </nav>

            <div id="panel-exception" class="app-error-debug-panel active" role="tabpanel" aria-labelledby="tab-exception">{$exceptionPanel}</div>
            <div id="panel-request" class="app-error-debug-panel" role="tabpanel" aria-labelledby="tab-request">{$requestPanel}</div>
            <div id="panel-environment" class="app-error-debug-panel" role="tabpanel" aria-labelledby="tab-environment">{$environmentPanel}</div>
            <div id="panel-queries" class="app-error-debug-panel" role="tabpanel" aria-labelledby="tab-queries">{$queryPanel}</div>
          </main>

          <script>{$js}</script>
        </body>
        </html>
        HTML;
    }

    /**
     * Renders a clean production error page with request ID only.
     * Uses the same design tokens as the dev page for visual consistency.
     */
    public static function renderProd(string $requestId): string
    {
        $statusCode = http_response_code() ?: 500;
        $requestId = self::esc($requestId);
        $css = self::loadAsset(__DIR__ . '/../../web/css/pages/app-error-debug.css');
        $js = self::loadAsset(__DIR__ . '/../../web/js/components/app-error-debug.js');

        $lang = self::esc(self::htmlLang());
        $errorText = self::esc(self::tr('Error'));
        $requestIdLabel = self::esc(self::tr('Request ID'));
        $copyRequestIdTitle = self::esc(self::tr('Copy request ID'));
        $message = self::esc(self::tr('An unexpected error occurred. If the problem persists, please contact support with the reference below.'));
        $backLabel = self::esc(self::tr('Back to start'));

        return <<<HTML
        <!DOCTYPE html>
        <html lang="{$lang}">
        <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>{$statusCode} - {$errorText}</title>
          <style>
            {$css}
            .app-error-prod {
              display: flex; flex-direction: column; align-items: center; justify-content: center;
              min-height: 100dvh; padding: calc(var(--app-spacing) * 2); text-align: center;
              font-family: var(--app-font-family); background: var(--app-background-color); color: var(--app-color);
            }
            .app-error-prod__code {
              font-size: var(--text-5xl); font-weight: var(--font-bold);
              line-height: var(--leading-none); color: var(--app-muted-color);
            }
            .app-error-prod__message {
              margin-top: var(--app-spacing); font-size: var(--text-lg);
              color: var(--app-color); max-width: 28rem;
            }
            .app-error-prod__back {
              display: inline-block; margin-top: calc(var(--app-spacing) * 2);
              padding: calc(var(--app-spacing) * 0.5) calc(var(--app-spacing) * 1.25);
              font-size: var(--text-sm); text-decoration: none;
              border: 1px solid var(--app-border); border-radius: var(--app-border-radius);
              color: var(--app-color); background: var(--app-card-background-color);
            }
            .app-error-prod__back:hover { opacity: 0.8; }
          </style>
        </head>
        <body>
          <div class="app-error-prod">
            <div class="app-error-prod__code">{$statusCode}</div>
            <div class="app-error-prod__message">{$message}</div>
            <div class="app-error-debug-request-id" style="margin-top: calc(var(--app-spacing) * 1.5);">
              {$requestIdLabel}: {$requestId}
              <button type="button" class="app-error-debug-copy-button" data-copy="{$requestId}" title="{$copyRequestIdTitle}" aria-label="{$copyRequestIdTitle}"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/><path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/></svg></button>
            </div>
            <a class="app-error-prod__back" href="/">{$backLabel}</a>
          </div>
          <script>{$js}</script>
        </body>
        </html>
        HTML;
    }

    private static function renderExceptionPanel(array $exceptionData): string
    {
        $html = '<div class="app-error-debug-section"><div class="app-error-debug-section__title">' . self::esc(self::tr('Stack Trace')) . '</div>';
        $html .= '<ol class="app-error-debug-trace">';

        $frames = $exceptionData['trace'] ?? [];
        foreach ($frames as $i => $frame) {
            $file = self::esc(self::shortenPath((string) ($frame['file'] ?? '[internal]')));
            $line = self::esc((string) ($frame['line'] ?? '?'));
            $function = '';
            if (isset($frame['class'])) {
                $function = self::esc((string) $frame['class'] . '::' . (string) ($frame['function'] ?? '?'));
            } elseif (isset($frame['function'])) {
                $function = self::esc((string) $frame['function']);
            }

            $codeHtml = '';
            $snippet = $frame['code_snippet'] ?? [];
            if (!empty($snippet)) {
                $highlightLine = (int) ($frame['line'] ?? 0);
                $codeHtml = self::renderCodeBlock($snippet, $highlightLine);
            }

            $codeId = 'frame-code-' . $i;
            $toggleId = 'frame-toggle-' . $i;
            $isOpen = ($i === 0);
            $expanded = $isOpen ? 'true' : 'false';
            $openClass = $isOpen ? ' open' : '';

            $html .= <<<FRAME
            <li class="app-error-debug-frame{$openClass}">
              <button type="button" id="{$toggleId}" class="app-error-debug-frame__header app-error-debug-frame__toggle" aria-expanded="{$expanded}" aria-controls="{$codeId}">
                <span class="app-error-debug-frame__index">#{$i}</span>
                <span class="app-error-debug-frame__file">{$file}</span>:<span class="app-error-debug-frame__line">{$line}</span>
                <span class="app-error-debug-frame__function">{$function}</span>
              </button>
              <div id="{$codeId}" class="app-error-debug-frame__code" role="region" aria-labelledby="{$toggleId}">{$codeHtml}</div>
            </li>
            FRAME;
        }

        $html .= '</ol></div>';

        // Chained exceptions.
        if (isset($exceptionData['previous'])) {
            $prev = $exceptionData['previous'];
            $html .= '<div class="app-error-debug-chain">';
            $html .= '<div class="app-error-debug-chain__label">' . self::esc(self::tr('Caused by')) . '</div>';
            $html .= '<div class="app-error-debug-summary__class">' . self::esc((string) ($prev['class'] ?? self::tr('Exception'))) . '</div>';
            $html .= '<div class="app-error-debug-summary__message">' . self::esc((string) ($prev['message'] ?? '')) . '</div>';
            $html .= '<div class="app-error-debug-summary__location">' . self::esc(self::tr(
                'in %s on line %s',
                self::shortenPath((string) ($prev['file'] ?? '')),
                (string) ($prev['line'] ?? '?')
            )) . '</div>';
            $html .= '</div>';
        }

        return $html;
    }

    /**
     * @param array<int, string> $lines
     */
    private static function renderCodeBlock(array $lines, int $highlightLine): string
    {
        $html = '<table class="app-error-debug-code-table">';
        foreach ($lines as $lineNo => $code) {
            $isHighlight = ($lineNo === $highlightLine) ? ' highlight' : '';
            $lineNoEsc = self::esc((string) $lineNo);
            $codeEsc = self::esc($code);
            $html .= "<tr class=\"{$isHighlight}\"><td class=\"app-error-debug-code-table__line-no\">{$lineNoEsc}</td><td class=\"app-error-debug-code-table__code\">{$codeEsc}</td></tr>";
        }
        $html .= '</table>';

        return $html;
    }

    private static function renderRequestPanel(array $requestData): string
    {
        $html = '';

        $html .= self::renderInfoSection(self::tr('Request'), [
            self::tr('Method') => $requestData['method'] ?? 'GET',
            self::tr('URL') => $requestData['url'] ?? '',
            self::tr('IP') => $requestData['ip'] ?? '',
            self::tr('User Agent') => $requestData['user_agent'] ?? '',
            self::tr('Channel') => $requestData['channel'] ?? 'web',
        ]);

        $headers = $requestData['headers'] ?? [];
        if (!empty($headers)) {
            $html .= self::renderInfoSection(self::tr('Headers'), $headers);
        }

        $get = $requestData['get'] ?? [];
        if (!empty($get)) {
            $html .= self::renderInfoSection(self::tr('GET Parameters'), array_map(
                fn ($v): string => is_string($v) ? $v : (string) json_encode($v, JSON_UNESCAPED_SLASHES),
                $get,
            ));
        }

        $post = $requestData['post'] ?? [];
        if (!empty($post)) {
            $html .= self::renderInfoSection(self::tr('POST Parameters'), array_map(
                fn ($v): string => is_string($v) ? $v : (string) json_encode($v, JSON_UNESCAPED_SLASHES),
                $post,
            ));
        }

        $session = $requestData['session_keys'] ?? [];
        if (!empty($session)) {
            $html .= self::renderInfoSection(self::tr('Session (keys + types)'), $session);
        }

        $html .= self::renderInfoSection(self::tr('Context'), array_filter([
            self::tr('User ID') => $requestData['user_id'] ?? null,
            self::tr('Tenant ID') => $requestData['tenant_id'] ?? null,
        ], fn ($v): bool => $v !== null));

        return $html;
    }

    private static function renderEnvironmentPanel(array $envData): string
    {
        $html = '';

        $html .= self::renderInfoSection(self::tr('PHP'), [
            self::tr('Version') => $envData['php_version'] ?? PHP_VERSION,
            self::tr('SAPI') => $envData['php_sapi'] ?? PHP_SAPI,
            self::tr('Memory (current)') => self::formatBytes((int) ($envData['memory_current'] ?? 0)),
            self::tr('Memory (peak)') => self::formatBytes((int) ($envData['memory_peak'] ?? 0)),
        ]);

        $env = $envData['env'] ?? [];
        if (!empty($env)) {
            $html .= self::renderInfoSection(self::tr('Application'), $env);
        }

        $modules = $envData['modules'] ?? [];
        if (!empty($modules)) {
            $html .= self::renderInfoSection(self::tr('Active Modules'), array_combine(
                array_map(fn ($m): string => (string) $m, $modules),
                array_fill(0, count($modules), self::tr('Active')),
            ) ?: []);
        }

        $extensions = $envData['extensions'] ?? [];
        if (!empty($extensions)) {
            $html .= '<div class="app-error-debug-section"><div class="app-error-debug-section__title">'
                . self::esc(self::tr('Loaded Extensions (%d)', count($extensions)))
                . '</div>';
            $html .= '<div style="font-family: var(--app-font-family-monospace); font-size: var(--text-xs); color: var(--app-muted-color); padding: calc(var(--app-spacing) * 0.5); background: var(--app-card-background-color); border-radius: var(--app-border-radius); border: 1px solid var(--app-border);">';
            $html .= self::esc(implode(', ', $extensions));
            $html .= '</div></div>';
        }

        return $html;
    }

    private static function renderQueryPanel(array $queryData): string
    {
        if (!($queryData['available'] ?? false)) {
            return '<div class="app-error-debug-empty">' . self::esc(self::tr('Query tracking is not available (Debugger disabled or not initialized).')) . '</div>';
        }

        $queries = $queryData['queries'] ?? [];
        if (empty($queries)) {
            return '<div class="app-error-debug-empty">' . self::esc(self::tr('No queries were executed before the error occurred.')) . '</div>';
        }

        $html = '<div class="app-error-debug-section"><div class="app-error-debug-section__title">'
            . self::esc(self::tr('Queries (%d)', count($queries)))
            . '</div>';

        foreach ($queries as $i => $query) {
            $sql = is_string($query) ? $query : ($query[0] ?? $query['query'] ?? '?');
            $duration = '';
            if (is_array($query) && isset($query[1])) {
                $durationMs = round((float) $query[1] * 1000, 2);
                $duration = $durationMs . ' ms';
            }

            $querySqlId = 'query-sql-' . $i;
            $queryToggleId = 'query-toggle-' . $i;

            $html .= '<div class="app-error-debug-query">';
            $html .= '<button type="button" id="' . self::esc($queryToggleId) . '" class="app-error-debug-query__header app-error-debug-query__toggle" aria-expanded="false" aria-controls="' . self::esc($querySqlId) . '">';
            $html .= '<span>' . self::esc(self::truncate($sql, 120)) . '</span>';
            if ($duration !== '') {
                $html .= '<span class="app-error-debug-query__duration">' . self::esc($duration) . '</span>';
            }
            $html .= '</button>';
            $html .= '<div id="' . self::esc($querySqlId) . '" class="app-error-debug-query__sql" role="region" aria-labelledby="' . self::esc($queryToggleId) . '">' . self::esc($sql) . '</div>';
            $html .= '</div>';
        }

        $html .= '</div>';

        return $html;
    }

    /**
     * @param array<string, mixed> $items
     */
    private static function renderInfoSection(string $title, array $items): string
    {
        if (empty($items)) {
            return '';
        }

        $html = '<div class="app-error-debug-section"><div class="app-error-debug-section__title">' . self::esc($title) . '</div>';
        $html .= '<table class="app-error-debug-info-table">';

        foreach ($items as $key => $value) {
            $keyEsc = self::esc((string) $key);
            $valueStr = (string) $value;
            $isRedacted = ($valueStr === '[REDACTED]');
            $valueEsc = $isRedacted
                ? '<span class="app-error-debug-redacted">[REDACTED]</span>'
                : self::esc($valueStr);
            $html .= "<tr><th>{$keyEsc}</th><td>{$valueEsc}</td></tr>";
        }

        $html .= '</table></div>';

        return $html;
    }

    private static function loadAsset(string $path): string
    {
        $resolved = realpath($path);
        if ($resolved === false || !is_file($resolved)) {
            return '/* asset not found: ' . basename($path) . ' */';
        }

        return (string) file_get_contents($resolved);
    }

    public static function esc(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    private static function shortenPath(string $path): string
    {
        $root = getcwd();
        if ($root !== false && str_starts_with($path, $root . '/')) {
            return substr($path, strlen($root) + 1);
        }

        return $path;
    }

    private static function statusText(int $code): string
    {
        return match ($code) {
            400 => self::tr('Bad Request'),
            401 => self::tr('Unauthorized'),
            403 => self::tr('Forbidden'),
            404 => self::tr('Not Found'),
            405 => self::tr('Method Not Allowed'),
            408 => self::tr('Request Timeout'),
            422 => self::tr('Unprocessable Entity'),
            429 => self::tr('Too Many Requests'),
            500 => self::tr('Internal Server Error'),
            502 => self::tr('Bad Gateway'),
            503 => self::tr('Service Unavailable'),
            default => self::tr('Error'),
        };
    }

    private static function formatBytes(int $bytes): string
    {
        if ($bytes < 1024) {
            return $bytes . ' B';
        }
        if ($bytes < 1024 * 1024) {
            return round($bytes / 1024, 1) . ' KB';
        }

        return round($bytes / (1024 * 1024), 1) . ' MB';
    }

    private static function truncate(string $text, int $length): string
    {
        if (mb_strlen($text) <= $length) {
            return $text;
        }

        return mb_substr($text, 0, $length) . '...';
    }

    private static function tr(string $text, mixed ...$args): string
    {
        if (function_exists('t')) {
            try {
                /** @var string $translated */
                $translated = t($text, ...$args);
                return $translated;
            } catch (\Throwable) {
                // Rendering must stay resilient, even if translation bootstrap failed.
            }
        }

        if ($args === []) {
            return $text;
        }

        return (string) @vsprintf($text, $args);
    }

    private static function htmlLang(): string
    {
        $locale = trim((string) (I18n::$locale ?? I18n::$defaultLocale ?? 'en'));
        if ($locale === '') {
            return 'en';
        }

        $locale = strtolower(str_replace('_', '-', $locale));
        if (preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $locale) === 1) {
            return $locale;
        }

        if (preg_match('/^[a-z]{2,3}/', $locale, $matches) === 1) {
            return $matches[0];
        }

        return 'en';
    }
}
