<?php

namespace MintyPHP\Module\Audit\Service;

use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepository;
use MintyPHP\Service\Audit\ApiAuditServiceInterface;

class ApiAuditService implements ApiAuditServiceInterface
{
    private const RETENTION_DAYS = 90;
    private const MAX_QUERY_KEYS = 30;
    private const ALLOWED_QUERY_KEYS = [
        'active',
        'status',
        'method',
        'order',
        'dir',
        'sort',
        'page',
        'per_page',
        'limit',
        'offset',
        'cursor',
        'from',
        'to',
        'created_from',
        'created_to',
        'tenant_id',
        'tenant_ids',
        'user_id',
        'user_ids',
        'id',
        'ids',
        'request_id',
        'error_code',
        'search',
    ];

    private ?array $context = null;

    public function __construct(
        private readonly ApiAuditLogRepository $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->buildQueryMetadataJson($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()));
        $ipHash = $ip !== '' ? RequestContext::hashValue($ip) : null;

        $userAgent = trim((string) ($requestContext['user_agent'] ?? $this->requestRuntime->userAgent()));
        $userAgentHash = $userAgent !== '' ? RequestContext::hashValue($userAgent) : null;

        $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' => $ipHash,
            'user_agent' => $userAgentHash,
        ];

        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 buildQueryMetadataJson(array $query): ?string
    {
        if (!$query) {
            return null;
        }

        $keys = $this->extractAllowedQueryKeys($query);
        if ($keys === []) {
            return null;
        }

        $encoded = json_encode(
            ['keys' => $keys],
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );

        return is_string($encoded) ? $encoded : null;
    }

    /**
     * @param array<string, mixed> $query
     * @return list<string>
     */
    private function extractAllowedQueryKeys(array $query): array
    {
        $keys = [];
        foreach ($query as $rawKey => $_value) {
            if (count($keys) >= self::MAX_QUERY_KEYS) {
                break;
            }

            $key = $this->normalizeQueryKey((string) $rawKey);
            if ($key === '' || $this->isSensitiveKey($key) || !$this->isAllowedQueryKey($key)) {
                continue;
            }

            if (!in_array($key, $keys, true)) {
                $keys[] = $key;
            }
        }

        return $keys;
    }

    private function normalizeQueryKey(string $rawKey): string
    {
        $key = strtolower(trim($rawKey));
        if ($key === '') {
            return '';
        }

        $bracketPos = strpos($key, '[');
        if ($bracketPos !== false) {
            $key = substr($key, 0, $bracketPos);
        }

        $dotPos = strpos($key, '.');
        if ($dotPos !== false) {
            $key = substr($key, 0, $dotPos);
        }

        return trim($key);
    }

    private function isAllowedQueryKey(string $key): bool
    {
        return in_array($key, self::ALLOWED_QUERY_KEYS, true);
    }

    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',
            'email',
            'to_email',
            'uuid',
        ], true)) {
            return true;
        }

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