<?php

namespace classes;

use RuntimeException;
use Throwable;

class error_report_service
{
    private const ANSWER_MAX_LENGTH = 4000;
    private const NOTE_MAX_LENGTH = 2000;
    private const SCREENSHOT_MAX_BYTES = 6_000_000;
    private const JSON_MAX_LENGTH = 200_000;
    private const STATUS_OPEN = 'open';
    private const STATUS_RESOLVED = 'resolved';

    private bool $schemaEnsured = false;
    private error_report_store $store;

    public function __construct(?error_report_store $store = null)
    {
        $this->store = $store ?? new error_report_store();
    }

    public static function redactPayload(mixed $value, int $depth = 0): mixed
    {
        if ($depth > 8) {
            return '[depth-limit]';
        }

        if (is_array($value)) {
            $redacted = [];
            $index = 0;
            foreach ($value as $key => $item) {
                $index++;
                if ($index > 80) {
                    $redacted['[truncated]'] = 'More than 80 keys omitted.';
                    break;
                }

                $keyString = (string)$key;
                if (preg_match('/authorization|cookie|password|passwd|secret|token|api[_-]?key|session|credential|card|cpr|ssn/i', $keyString) === 1) {
                    $redacted[$key] = '[redacted]';
                    continue;
                }

                $redacted[$key] = self::redactPayload($item, $depth + 1);
            }
            return $redacted;
        }

        if (is_object($value)) {
            return self::redactPayload((array)$value, $depth + 1);
        }

        if (is_string($value) && strlen($value) > 4000) {
            return substr($value, 0, 4000) . "\n... [truncated]";
        }

        return $value;
    }

    public static function decodeScreenshotDataUri(string $dataUri): array
    {
        if (!preg_match('/^data:(image\/(?:png|jpeg|webp));base64,([a-zA-Z0-9+\/=\r\n]+)$/', trim($dataUri), $matches)) {
            throw new RuntimeException('Screenshot must be a PNG, JPEG, or WebP data URI.');
        }

        $contents = base64_decode(preg_replace('/\s+/', '', $matches[2]) ?? '', true);
        if ($contents === false || $contents === '') {
            throw new RuntimeException('Screenshot could not be decoded.');
        }

        if (strlen($contents) > self::SCREENSHOT_MAX_BYTES) {
            throw new RuntimeException('Screenshot is too large.');
        }

        return [
            'mime_type' => $matches[1],
            'contents' => $contents,
            'size_bytes' => strlen($contents),
        ];
    }

    public function createFromCurrentPrincipal(array $payload): array
    {
        $this->ensureSchema();
        $principal = $this->resolvePrincipal();
        $answers = $this->validatedAnswers($payload);

        if (!$this->acceptedDataCollection($payload['data_collection_accepted'] ?? null)) {
            throw new RuntimeException('Data collection acceptance is required.');
        }

        $context = is_array($payload['context'] ?? null) ? $payload['context'] : [];
        $storedScreenshot = $this->storeOptionalScreenshot($payload['screenshot'] ?? null, $context);
        $requestErrors = $this->boundedArray($payload['request_errors'] ?? ($context['request_errors'] ?? []), 25);
        $vueErrors = $this->boundedArray($payload['vue_errors'] ?? ($context['vue_errors'] ?? []), 25);
        $runtimeContext = $this->runtimeContext($payload, $context);
        $runtimeContext['screenshot_attachment'] = [
            'status' => $storedScreenshot['status'],
            'attached' => $storedScreenshot['key'] !== '',
            'mime_type' => $storedScreenshot['mime_type'] !== '' ? $storedScreenshot['mime_type'] : null,
            'size_bytes' => (int)$storedScreenshot['size_bytes'],
        ];

        $this->execute(
            "INSERT INTO error_reports (
                status,
                reporter_type,
                reporter_user_id,
                reporter_subuser_id,
                reporter_customer_number,
                reporter_customer_number_context,
                reporter_name,
                reporter_email,
                route_path,
                page_url,
                release_trace_id,
                frontend_version,
                api_version,
                screenshot_object_key,
                screenshot_mime_type,
                screenshot_size_bytes,
                before_error,
                expected,
                actual,
                request_error_count,
                vue_error_count,
                request_errors_json,
                vue_errors_json,
                runtime_context_json,
                data_collection_accepted,
                data_collection_accepted_at,
                data_collection_policy_version
            ) VALUES (
                'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), ?
            )",
            'siiiisssssssssisssiissss',
            [
                $principal['type'],
                $principal['user_id'],
                $principal['subuser_id'],
                $principal['customer_number'],
                $principal['customer_number_context'],
                $principal['name'],
                $principal['email'],
                $runtimeContext['route_path'],
                $runtimeContext['page_url'],
                $runtimeContext['release_trace_id'],
                $runtimeContext['frontend_version'],
                $runtimeContext['api_version'],
                $storedScreenshot['key'],
                $storedScreenshot['mime_type'],
                (int)$storedScreenshot['size_bytes'],
                $answers['before_error'],
                $answers['expected'],
                $answers['actual'],
                count($requestErrors),
                count($vueErrors),
                $this->jsonEncodeLimited(self::redactPayload($requestErrors)),
                $this->jsonEncodeLimited(self::redactPayload($vueErrors)),
                $this->jsonEncodeLimited(self::redactPayload($runtimeContext)),
                $runtimeContext['data_collection_policy_version'],
            ]
        );

        return $this->get($this->insertId());
    }

    public function list(array $filters = []): array
    {
        $this->ensureSchema();

        $where = ['1 = 1'];
        $types = '';
        $params = [];
        $status = $this->statusFilter($filters['status'] ?? self::STATUS_OPEN);
        if ($status !== 'all') {
            $where[] = 'status = ?';
            $types .= 's';
            $params[] = $status;
        }

        $search = trim((string)($filters['q'] ?? $filters['search'] ?? ''));
        if ($search !== '') {
            $where[] = '(route_path LIKE ? OR page_url LIKE ? OR before_error LIKE ? OR actual LIKE ? OR reporter_name LIKE ? OR reporter_email LIKE ?)';
            $types .= 'ssssss';
            $like = '%' . $search . '%';
            array_push($params, $like, $like, $like, $like, $like, $like);
        }

        $limit = min(200, max(1, (int)($filters['limit'] ?? 50)));
        $offset = max(0, (int)($filters['offset'] ?? 0));
        $types .= 'ii';
        $params[] = $limit;
        $params[] = $offset;

        $items = $this->selectRows(
            "SELECT id, status, reporter_type, reporter_user_id, reporter_subuser_id,
                    reporter_customer_number, reporter_customer_number_context, reporter_name, reporter_email,
                    route_path, page_url, release_trace_id, frontend_version, api_version,
                    screenshot_mime_type, screenshot_size_bytes, before_error, expected, actual,
                    request_error_count, vue_error_count, resolved_at, resolved_by_user_id, created_at, updated_at
             FROM error_reports
             WHERE " . implode(' AND ', $where) . "
             ORDER BY created_at DESC
             LIMIT ? OFFSET ?",
            $types,
            $params
        );

        return [
            'items' => array_map(fn(array $row): array => $this->publicReport($row, false), $items),
            'counts' => $this->counts(),
            'limit' => $limit,
            'offset' => $offset,
        ];
    }

    public function get(int $id): array
    {
        $this->ensureSchema();
        $row = $this->selectOne('SELECT * FROM error_reports WHERE id = ? LIMIT 1', 'i', [$id]);
        if ($row === null) {
            throw new RuntimeException('Error report not found.');
        }

        return $this->publicReport($row, true);
    }

    public function updateStatus(int $id, string $status, ?string $resolutionNote, ?int $actorUserId): array
    {
        $this->ensureSchema();
        $status = self::normalizeStatus($status);
        $note = $resolutionNote !== null ? $this->trimmedString($resolutionNote, self::NOTE_MAX_LENGTH, false) : null;

        if ($status === self::STATUS_RESOLVED) {
            $this->execute(
                'UPDATE error_reports SET status = ?, resolved_at = NOW(), resolved_by_user_id = ?, resolution_note = ? WHERE id = ?',
                'sisi',
                [$status, $actorUserId, $note, $id]
            );
        } else {
            $this->execute(
                'UPDATE error_reports SET status = ?, resolved_at = NULL, resolved_by_user_id = NULL, resolution_note = ? WHERE id = ?',
                'ssi',
                [$status, $note, $id]
            );
        }

        return $this->get($id);
    }

    public static function normalizeStatus(string $status): string
    {
        $status = strtolower(trim($status));
        if (!in_array($status, [self::STATUS_OPEN, self::STATUS_RESOLVED], true)) {
            throw new RuntimeException('Invalid error report status.');
        }
        return $status;
    }

    private function validatedAnswers(array $payload): array
    {
        return [
            'before_error' => $this->requiredAnswer($payload, ['before_error', 'what_were_you_doing_before_error_occurred']),
            'expected' => $this->requiredAnswer($payload, ['expected', 'what_did_you_expect_would_happen']),
            'actual' => $this->requiredAnswer($payload, ['actual', 'what_actually_happened']),
        ];
    }

    private function requiredAnswer(array $payload, array $keys): string
    {
        foreach ($keys as $key) {
            if (array_key_exists($key, $payload)) {
                return $this->trimmedString((string)$payload[$key], self::ANSWER_MAX_LENGTH, true);
            }
        }

        throw new RuntimeException('Missing required answer.');
    }

    private function trimmedString(string $value, int $maxLength, bool $required): string
    {
        $value = trim($value);
        if ($required && $value === '') {
            throw new RuntimeException('Required text fields must not be empty.');
        }

        if (strlen($value) > $maxLength) {
            return substr($value, 0, $maxLength);
        }

        return $value;
    }

    private function acceptedDataCollection(mixed $value): bool
    {
        return $value === true || $value === 1 || $value === '1' || $value === 'true';
    }

    private function storeOptionalScreenshot(mixed $value, array $context): array
    {
        if (!is_scalar($value) && !$value instanceof \Stringable && $value !== null) {
            return $this->emptyScreenshotAttachment('invalid');
        }

        $dataUri = trim((string)($value ?? ''));
        if ($dataUri === '') {
            return $this->emptyScreenshotAttachment($this->contextScreenshotStatus($context) ?? 'not_provided');
        }

        try {
            $screenshot = self::decodeScreenshotDataUri($dataUri);
        } catch (RuntimeException $exception) {
            $message = strtolower($exception->getMessage());
            return $this->emptyScreenshotAttachment(str_contains($message, 'too large') ? 'too_large' : 'invalid');
        }

        try {
            $storedScreenshot = $this->store->storeScreenshot($screenshot['mime_type'], $screenshot['contents']);
        } catch (Throwable) {
            return $this->emptyScreenshotAttachment('storage_failed');
        }

        return [
            'key' => (string)($storedScreenshot['key'] ?? ''),
            'mime_type' => (string)($storedScreenshot['mime_type'] ?? $screenshot['mime_type']),
            'size_bytes' => (int)($storedScreenshot['size_bytes'] ?? $screenshot['size_bytes']),
            'status' => 'stored',
        ];
    }

    private function emptyScreenshotAttachment(string $status): array
    {
        return [
            'key' => '',
            'mime_type' => '',
            'size_bytes' => 0,
            'status' => $status,
        ];
    }

    private function contextScreenshotStatus(array $context): ?string
    {
        $attachment = $context['screenshot_attachment'] ?? null;
        $status = is_array($attachment) ? ($attachment['status'] ?? null) : null;
        $status ??= $context['screenshot_capture_status'] ?? $context['screenshot_status'] ?? null;

        return $this->normalizeEmptyScreenshotStatus($status);
    }

    private function normalizeEmptyScreenshotStatus(mixed $status): ?string
    {
        $status = strtolower(trim((string)$status));
        if (in_array($status, ['capture_failed', 'not_provided'], true)) {
            return $status;
        }

        return null;
    }

    private function runtimeContext(array $payload, array $context): array
    {
        return [
            'route_path' => $this->nullableString($payload['route_path'] ?? $context['route_path'] ?? $context['route'] ?? null, 512),
            'page_url' => $this->nullableString($payload['page_url'] ?? $context['page_url'] ?? $context['url'] ?? null, 1024),
            'release_trace_id' => $this->nullableString($payload['release_trace_id'] ?? $context['release_trace_id'] ?? $context['trace_id'] ?? $this->releaseRequestContext('trace_id'), 64),
            'frontend_version' => $this->nullableString($payload['frontend_version'] ?? $context['frontend_version'] ?? $this->releaseRequestContext('frontend_version'), 128),
            'api_version' => $this->nullableString($payload['api_version'] ?? $context['api_version'] ?? $this->releaseRequestContext('backend_version'), 128),
            'viewport' => is_array($context['viewport'] ?? null) ? $context['viewport'] : null,
            'user_agent' => $this->nullableString($context['user_agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? null), 1024),
            'captured_at' => $this->nullableString($context['captured_at'] ?? null, 64),
            'data_collection_policy_version' => $this->nullableString($payload['data_collection_policy_version'] ?? $context['data_collection_policy_version'] ?? 'error-report-v1', 64) ?? 'error-report-v1',
        ];
    }

    private function releaseRequestContext(string $key): ?string
    {
        $context = is_array($GLOBALS['RELEASE_REQUEST_CONTEXT'] ?? null) ? $GLOBALS['RELEASE_REQUEST_CONTEXT'] : [];
        return isset($context[$key]) ? (string)$context[$key] : null;
    }

    private function nullableString(mixed $value, int $maxLength): ?string
    {
        if ($value === null) {
            return null;
        }
        $value = trim((string)$value);
        if ($value === '') {
            return null;
        }
        return substr($value, 0, $maxLength);
    }

    private function boundedArray(mixed $value, int $limit): array
    {
        return is_array($value) ? array_slice(array_values($value), 0, $limit) : [];
    }

    private function statusFilter(mixed $status): string
    {
        $status = strtolower(trim((string)$status));
        if ($status === '' || $status === self::STATUS_OPEN) {
            return self::STATUS_OPEN;
        }
        if ($status === self::STATUS_RESOLVED || $status === 'all') {
            return $status;
        }
        return self::STATUS_OPEN;
    }

    private function counts(): array
    {
        $rows = $this->selectRows('SELECT status, COUNT(*) AS count FROM error_reports GROUP BY status');
        $counts = [
            self::STATUS_OPEN => 0,
            self::STATUS_RESOLVED => 0,
            'all' => 0,
        ];
        foreach ($rows as $row) {
            $status = (string)($row['status'] ?? '');
            $count = (int)($row['count'] ?? 0);
            if (isset($counts[$status])) {
                $counts[$status] = $count;
            }
            $counts['all'] += $count;
        }
        return $counts;
    }

    private function resolvePrincipal(): array
    {
        $auth = new authentication();

        try {
            $subuser = $auth->get_subuser();
            if ($subuser !== false) {
                return [
                    'type' => 'subuser',
                    'user_id' => null,
                    'subuser_id' => (int)$subuser->id,
                    'customer_number' => null,
                    'customer_number_context' => $this->headerInt('X-Customer-Number'),
                    'name' => $this->safeObjectValue($subuser, 'name') ?: $this->safeObjectValue($subuser, 'username'),
                    'email' => $this->safeObjectValue($subuser, 'email'),
                ];
            }
        } catch (Throwable) {
        }

        try {
            $user = $auth->get_user();
            if ($user !== false) {
                return [
                    'type' => 'user',
                    'user_id' => (int)$user->id,
                    'subuser_id' => null,
                    'customer_number' => isset($user->customer_number) ? (int)$user->customer_number->value() : null,
                    'customer_number_context' => null,
                    'name' => $this->safeObjectValue($user, 'display_name'),
                    'email' => $this->safeObjectValue($user, 'email'),
                ];
            }
        } catch (Throwable) {
        }

        throw new RuntimeException('Authentication failed. Invalid or missing token.');
    }

    private function headerInt(string $name): ?int
    {
        $headers = function_exists('getallheaders') ? getallheaders() : [];
        foreach ($headers as $key => $value) {
            if (strcasecmp((string)$key, $name) === 0) {
                $int = (int)$value;
                return $int > 0 ? $int : null;
            }
        }
        $serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
        $int = (int)($_SERVER[$serverKey] ?? 0);
        return $int > 0 ? $int : null;
    }

    private function safeObjectValue(object $object, string $property): ?string
    {
        try {
            if (!isset($object->{$property}) || !method_exists($object->{$property}, 'value')) {
                return null;
            }
            $value = $object->{$property}->value();
            return $value === null ? null : substr((string)$value, 0, 255);
        } catch (Throwable) {
            return null;
        }
    }

    private function publicReport(array $row, bool $includeDetail): array
    {
        $screenshotMimeType = trim((string)($row['screenshot_mime_type'] ?? ''));
        $screenshotSizeBytes = isset($row['screenshot_size_bytes']) ? (int)$row['screenshot_size_bytes'] : 0;
        $hasScreenshot = $screenshotMimeType !== '' && $screenshotSizeBytes > 0;

        $report = [
            'id' => (int)$row['id'],
            'status' => (string)$row['status'],
            'reporter' => [
                'type' => $row['reporter_type'] ?? null,
                'user_id' => isset($row['reporter_user_id']) ? (int)$row['reporter_user_id'] : null,
                'subuser_id' => isset($row['reporter_subuser_id']) ? (int)$row['reporter_subuser_id'] : null,
                'customer_number' => isset($row['reporter_customer_number']) ? (int)$row['reporter_customer_number'] : null,
                'customer_number_context' => isset($row['reporter_customer_number_context']) ? (int)$row['reporter_customer_number_context'] : null,
                'name' => $row['reporter_name'] ?? null,
                'email' => $row['reporter_email'] ?? null,
            ],
            'route_path' => $row['route_path'] ?? null,
            'page_url' => $row['page_url'] ?? null,
            'release_trace_id' => $row['release_trace_id'] ?? null,
            'frontend_version' => $row['frontend_version'] ?? null,
            'api_version' => $row['api_version'] ?? null,
            'screenshot' => $hasScreenshot ? [
                'mime_type' => $screenshotMimeType,
                'size_bytes' => $screenshotSizeBytes,
            ] : null,
            'answers' => [
                'before_error' => $row['before_error'] ?? '',
                'expected' => $row['expected'] ?? '',
                'actual' => $row['actual'] ?? '',
            ],
            'request_error_count' => isset($row['request_error_count']) ? (int)$row['request_error_count'] : 0,
            'vue_error_count' => isset($row['vue_error_count']) ? (int)$row['vue_error_count'] : 0,
            'resolved_at' => $row['resolved_at'] ?? null,
            'resolved_by_user_id' => isset($row['resolved_by_user_id']) ? (int)$row['resolved_by_user_id'] : null,
            'created_at' => $row['created_at'] ?? null,
            'updated_at' => $row['updated_at'] ?? null,
        ];

        if ($includeDetail) {
            if ($hasScreenshot) {
                $objectKey = trim((string)($row['screenshot_object_key'] ?? ''));
                $report['screenshot']['url'] = $this->store->screenshotUrl($objectKey);
                $report['screenshot']['object_key'] = $objectKey !== '' ? $objectKey : null;
            }
            $report['request_errors'] = $this->jsonDecode($row['request_errors_json'] ?? null);
            $report['vue_errors'] = $this->jsonDecode($row['vue_errors_json'] ?? null);
            $report['runtime_context'] = $this->jsonDecode($row['runtime_context_json'] ?? null);
            $report['data_collection'] = [
                'accepted' => (bool)($row['data_collection_accepted'] ?? false),
                'accepted_at' => $row['data_collection_accepted_at'] ?? null,
                'policy_version' => $row['data_collection_policy_version'] ?? null,
            ];
            $report['resolution_note'] = $row['resolution_note'] ?? null;
        }

        return $report;
    }

    private function ensureSchema(): void
    {
        if ($this->schemaEnsured) {
            return;
        }
        error_report_schema_bootstrap::ensureTables();
        $this->schemaEnsured = true;
    }

    private function selectOne(string $sql, string $types = '', array $params = []): ?array
    {
        $rows = $this->selectRows($sql, $types, $params);
        return $rows[0] ?? null;
    }

    private function selectRows(string $sql, string $types = '', array $params = []): array
    {
        global $db;
        if ($types === '') {
            $result = $db->query($sql);
            return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
        }

        $stmt = $db->prepare($sql);
        if ($stmt === false) {
            throw new RuntimeException('Could not prepare error report query.');
        }
        $stmt->bind_param($types, ...$params);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
    }

    private function execute(string $sql, string $types = '', array $params = []): void
    {
        global $db;
        if ($types === '') {
            $db->query($sql);
            return;
        }

        $stmt = $db->prepare($sql);
        if ($stmt === false) {
            throw new RuntimeException('Could not prepare error report statement.');
        }
        $stmt->bind_param($types, ...$params);
        $stmt->execute();
    }

    private function insertId(): int
    {
        global $db;
        return (int)$db->insert_id();
    }

    private function jsonEncodeLimited(mixed $value): string
    {
        $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            throw new RuntimeException('Could not encode error report JSON payload.');
        }
        if (strlen($json) <= self::JSON_MAX_LENGTH) {
            return $json;
        }

        $truncated = [
            '[truncated]' => 'Payload exceeded ' . self::JSON_MAX_LENGTH . ' bytes.',
            'preview' => substr($json, 0, self::JSON_MAX_LENGTH),
        ];
        $encoded = json_encode($truncated, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return $encoded === false ? '{}' : $encoded;
    }

    private function jsonDecode(mixed $value): array
    {
        if (!is_string($value) || trim($value) === '') {
            return [];
        }
        $decoded = json_decode($value, true);
        return is_array($decoded) ? $decoded : [];
    }
}
