<?php

namespace MintyPHP\Module\Audit\Http;

use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Module\Audit\Service\SystemAuditService;
use MintyPHP\Service\Audit\ApiSystemAuditReporterInterface;

class ApiSystemAuditReporter implements ApiSystemAuditReporterInterface
{
    /**
     * @var array{
     *   started_at: float,
     *   method: string,
     *   path: string,
     *   finished: bool
     * }|null
     */
    private ?array $context = null;

    public function __construct(private readonly SystemAuditService $systemAuditService)
    {
    }

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

        RequestContext::ensureStarted();
        $requestContext = RequestContext::context();

        $method = strtoupper(trim((string) ($requestContext['method'] ?? 'GET')));
        if ($method === '') {
            $method = 'GET';
        }

        $path = trim((string) ($requestContext['path'] ?? ''));

        $this->context = [
            'started_at' => microtime(true),
            'method' => $method,
            'path' => $path,
            'finished' => false,
        ];
    }

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

        $this->context['finished'] = true;

        $method = strtoupper(trim((string) $this->context['method']));
        $path = trim((string) $this->context['path']);
        $statusCode = $this->normalizeStatusCode($statusCode);

        if (!$this->shouldRecord($method, $path, $statusCode)) {
            return;
        }

        $durationMs = max(0, (int) round((microtime(true) - (float) $this->context['started_at']) * 1000));
        $securityEndpoint = $this->isSecurityEndpoint($path);
        $writeMethod = $this->isWriteMethod($method);
        $normalizedErrorCode = trim((string) ($errorCode ?? ''));

        $metadata = [
            'endpoint_key' => $this->endpointKey($path),
            'status_code' => $statusCode,
            'status_class' => $this->statusClass($statusCode),
            'duration_ms' => $durationMs,
            'auth_mode' => $this->authMode(),
            'write_method' => $writeMethod,
            'security_endpoint' => $securityEndpoint,
        ];

        $this->systemAuditService->record(
            'api.request',
            $this->outcomeFromStatusCode($statusCode),
            [
                'actor_user_id' => ApiAuth::isAuthenticated() ? ApiAuth::userId() : null,
                'error_code' => $normalizedErrorCode !== '' ? substr($normalizedErrorCode, 0, 100) : null,
                'metadata' => $metadata,
            ]
        );
    }

    // Recording strategy: always log writes and server errors; for reads, only log
    // failures on security-sensitive endpoints (auth, tokens) to keep volume manageable.
    private function shouldRecord(string $method, string $path, int $statusCode): bool
    {
        if ($method === 'OPTIONS') {
            return false;
        }

        if ($this->isWriteMethod($method)) {
            return true;
        }

        if ($statusCode >= 500) {
            return true;
        }

        return $this->isSecurityEndpoint($path) && $statusCode >= 400;
    }

    private function isWriteMethod(string $method): bool
    {
        return in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true);
    }

    private function isSecurityEndpoint(string $path): bool
    {
        $path = '/' . ltrim(strtolower(trim($path)), '/');
        if ($path === '/api/v1/auth/login' || $path === '/api/v1/me/password') {
            return true;
        }

        return str_starts_with($path, '/api/v1/me/tokens');
    }

    // Normalizes dynamic path segments (IDs, UUIDs, token selectors) so audit queries
    // can group by endpoint pattern rather than individual resource IDs.
    private function endpointKey(string $path): string
    {
        $path = '/' . ltrim(trim($path), '/');
        if ($path === '/') {
            return '/';
        }

        $segments = array_values(array_filter(explode('/', trim($path, '/')), static fn (string $segment): bool => $segment !== ''));
        if ($segments === []) {
            return '/';
        }

        $normalized = [];
        foreach ($segments as $segment) {
            $value = strtolower(trim($segment));
            if ($value === '') {
                continue;
            }

            if (ctype_digit($value)) {
                $normalized[] = '{id}';
                continue;
            }

            if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $value) === 1) {
                $normalized[] = '{uuid}';
                continue;
            }

            if (preg_match('/^[a-f0-9]{24}$/i', $value) === 1) {
                $normalized[] = '{token_selector}';
                continue;
            }

            $normalized[] = substr($value, 0, 64);
        }

        return '/' . implode('/', $normalized);
    }

    private function outcomeFromStatusCode(int $statusCode): string
    {
        if ($statusCode >= 500) {
            return SystemAuditOutcome::Failed->value;
        }
        if ($statusCode >= 400) {
            return SystemAuditOutcome::Denied->value;
        }
        return SystemAuditOutcome::Success->value;
    }

    private function statusClass(int $statusCode): string
    {
        if ($statusCode >= 100 && $statusCode <= 199) {
            return '1xx';
        }
        if ($statusCode >= 200 && $statusCode <= 299) {
            return '2xx';
        }
        if ($statusCode >= 300 && $statusCode <= 399) {
            return '3xx';
        }
        if ($statusCode >= 400 && $statusCode <= 499) {
            return '4xx';
        }
        if ($statusCode >= 500 && $statusCode <= 599) {
            return '5xx';
        }

        return 'unknown';
    }

    private function authMode(): string
    {
        if (ApiAuth::isAuthenticated() || ApiAuth::extractBearerToken() !== '') {
            return 'token';
        }

        return 'public';
    }

    private function normalizeStatusCode(int $statusCode): int
    {
        return ($statusCode >= 100 && $statusCode <= 999) ? $statusCode : 500;
    }
}
