<?php

namespace MintyPHP\Service\Audit;

use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Repository\Audit\ApiAuditLogRepositoryInterface;

class ApiAuditService
{
    private const RETENTION_DAYS = 90;
    private const MAX_USER_AGENT_LENGTH = 255;
    private const MAX_STRING_LENGTH = 500;
    private const MAX_ARRAY_ITEMS = 30;

    private ?array $context = null;

    public function __construct(
        private readonly ApiAuditLogRepositoryInterface $apiAuditLogRepository,
        private readonly RequestRuntimeInterface $requestRuntime
    ) {
    }

    public function startRequestContext(): void
    {
        if ($this->context !== null) {
            return;
        }

        RequestContext::ensureStarted();
        RequestContext::setChannel('api');
        $requestContext = RequestContext::context();

        $method = strtoupper(trim($this->requestRuntime->method()));
        if ($method === 'OPTIONS') {
            $this->context = ['active' => false, 'finished' => true];
            return;
        }

        $path = trim($this->requestRuntime->path());
        if ($path === '') {
            $this->context = ['active' => false, 'finished' => true];
            return;
        }

        $this->context = [
            'active' => true,
            'finished' => false,
            'started_at' => microtime(true),
            'request_id' => (string) ($requestContext['request_id'] ?? RequestContext::id()),
            'method' => (string) ($requestContext['method'] ?? substr($method !== '' ? $method : 'GET', 0, 8)),
            'path' => (string) ($requestContext['path'] ?? substr($path, 0, 255)),
            'query_json' => $this->buildRedactedQueryJson($this->requestRuntime->queryParams()),
        ];
    }

    public function currentRequestId(): ?string
    {
        if (!is_array($this->context)) {
            return null;
        }

        $requestId = trim((string) ($this->context['request_id'] ?? ''));
        return $requestId !== '' ? $requestId : null;
    }

    public function finish(int $statusCode, ?string $errorCode = null): void
    {
        if (!is_array($this->context)) {
            return;
        }
        if (!empty($this->context['finished'])) {
            return;
        }
        $this->context['finished'] = true;

        if (empty($this->context['active'])) {
            return;
        }

        $durationMs = null;
        if (isset($this->context['started_at'])) {
            $durationMs = (int) round((microtime(true) - (float) $this->context['started_at']) * 1000);
            if ($durationMs < 0) {
                $durationMs = 0;
            }
        }

        $statusCode = ($statusCode >= 100 && $statusCode <= 999) ? $statusCode : 500;
        $normalizedErrorCode = trim((string) ($errorCode ?? ''));
        if ($normalizedErrorCode === '') {
            $normalizedErrorCode = null;
        } elseif (strlen($normalizedErrorCode) > 100) {
            $normalizedErrorCode = substr($normalizedErrorCode, 0, 100);
        }

        $requestContext = RequestContext::context();

        $ip = trim((string) ($requestContext['ip'] ?? $this->requestRuntime->ip()));
        if ($ip === '') {
            $ip = null;
        } elseif (strlen($ip) > 45) {
            $ip = substr($ip, 0, 45);
        }

        $userAgent = trim((string) ($requestContext['user_agent'] ?? $this->requestRuntime->userAgent()));
        if ($userAgent === '') {
            $userAgent = null;
        } elseif (strlen($userAgent) > self::MAX_USER_AGENT_LENGTH) {
            $userAgent = substr($userAgent, 0, self::MAX_USER_AGENT_LENGTH);
        }

        $userId = ApiAuth::isAuthenticated() ? ApiAuth::userId() : 0;
        $tenantId = ApiAuth::isAuthenticated() ? (int) (ApiAuth::tenantId() ?? 0) : 0;
        $apiTokenId = ApiAuth::isAuthenticated() ? (int) (ApiAuth::tokenId() ?? 0) : 0;
        $tokenTenantId = ApiAuth::isAuthenticated() ? (int) (ApiAuth::scopedTenantId() ?? 0) : 0;

        $payload = [
            'request_id' => (string) ($this->context['request_id'] ?? RequestContext::id()),
            'method' => (string) ($this->context['method'] ?? ''),
            'path' => (string) ($this->context['path'] ?? ''),
            'query_json' => $this->context['query_json'] ?? null,
            'status_code' => $statusCode,
            'duration_ms' => $durationMs,
            'error_code' => $normalizedErrorCode,
            'user_id' => $userId > 0 ? $userId : null,
            'tenant_id' => $tenantId > 0 ? $tenantId : null,
            'api_token_id' => $apiTokenId > 0 ? $apiTokenId : null,
            'token_tenant_id' => $tokenTenantId > 0 ? $tokenTenantId : null,
            'ip' => $ip,
            'user_agent' => $userAgent,
        ];

        try {
            $this->apiAuditLogRepository->create($payload);
        } catch (\Throwable $exception) {
            // Never break API responses because of audit logging failures.
        }
    }

    public function listPaged(array $filters): array
    {
        return $this->apiAuditLogRepository->listPaged($filters);
    }

    public function find(int $id): ?array
    {
        return $this->apiAuditLogRepository->find($id);
    }

    public function filterOptions(int $limit = 200): array
    {
        return $this->apiAuditLogRepository->listFilterOptions($limit);
    }

    public function purgeExpired(): int
    {
        return $this->apiAuditLogRepository->purgeOlderThanDays(self::RETENTION_DAYS);
    }

    private function buildRedactedQueryJson(array $query): ?string
    {
        if (!$query) {
            return null;
        }
        $redacted = $this->redactArray($query);
        if ($redacted === []) {
            return null;
        }
        $encoded = json_encode($redacted, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return is_string($encoded) ? $encoded : null;
    }

    private function redactArray(array $data): array
    {
        $result = [];
        $i = 0;
        foreach ($data as $rawKey => $value) {
            if ($i >= self::MAX_ARRAY_ITEMS) {
                break;
            }
            $key = trim((string) $rawKey);
            if ($key === '') {
                continue;
            }

            if ($this->isSensitiveKey($key)) {
                $result[$key] = '[REDACTED]';
            } else {
                $result[$key] = $this->normalizeValue($value);
            }
            $i++;
        }
        return $result;
    }

    private function normalizeValue(mixed $value): mixed
    {
        if (is_array($value)) {
            return $this->redactArray($value);
        }

        if (is_bool($value) || is_int($value) || is_float($value) || $value === null) {
            return $value;
        }

        $string = trim((string) $value);
        if ($string === '') {
            return '';
        }
        if (strlen($string) > self::MAX_STRING_LENGTH) {
            return substr($string, 0, self::MAX_STRING_LENGTH);
        }
        return $string;
    }

    private function isSensitiveKey(string $key): bool
    {
        $key = strtolower(trim($key));
        if ($key === '') {
            return false;
        }

        if (in_array($key, [
            'token',
            'access_token',
            'refresh_token',
            'id_token',
            'secret',
            'password',
            'authorization',
            'api_key',
            'client_secret',
            'key',
        ], true)) {
            return true;
        }

        return str_contains($key, 'token')
            || str_contains($key, 'secret')
            || str_contains($key, 'password')
            || str_contains($key, 'authorization')
            || str_ends_with($key, '_key');
    }
}
