<?php

declare(strict_types=1);

namespace App;

use DateTimeImmutable;
use JsonException;
use RuntimeException;

final class CiotAPI
{
    private const DEFAULT_TIMEOUT_SECONDS = 30;
    private const DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;

    private string $environment;
    private string $baseUrl;
    private string $providerToken;
    private string $logFile;
    private int $timeout;
    private int $connectTimeout;
    private ?string $clientCertificatePath;
    private ?string $clientCertificatePassword;
    private string $clientCertificateType;

    /**
     * @param array{
     *   environment?: string,
    *   provider_token?: string,
     *   homologation_url?: string,
     *   production_url?: string,
     *   base_url?: string,
     *   log_file?: string,
     *   timeout?: int,
    *   connect_timeout?: int,
    *   client_certificate_path?: string|null,
    *   client_certificate_password?: string|null,
    *   client_certificate_type?: string
     * } $config
     */
    public function __construct(array $config)
    {
        $this->environment = strtolower((string)($config['environment'] ?? 'homologation'));
        $this->providerToken = trim((string)($config['provider_token'] ?? $config['bearer_token'] ?? ''));
        $this->timeout = (int)($config['timeout'] ?? self::DEFAULT_TIMEOUT_SECONDS);
        $this->connectTimeout = (int)($config['connect_timeout'] ?? self::DEFAULT_CONNECT_TIMEOUT_SECONDS);
        $this->logFile = (string)($config['log_file'] ?? dirname(__DIR__) . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . 'ciot.log');
        $this->clientCertificatePath = $this->normalizeNullableString($config['client_certificate_path'] ?? null);
        $this->clientCertificatePassword = $this->normalizeNullableString($config['client_certificate_password'] ?? null);
        $this->clientCertificateType = strtoupper((string)($config['client_certificate_type'] ?? 'P12'));

        $this->baseUrl = $this->resolveBaseUrl($config);
        $this->ensureLogDirectoryExists();
    }

    /**
     * @param array{
     *   cpf_motorista: string,
     *   nome_motorista: string,
     *   placa_veiculo: string,
     *   rntrc: string,
     *   valor_frete: float|int|string,
     *   origem: array{cidade: string, uf: string},
     *   destino: array{cidade: string, uf: string},
     *   mdfe?: array<string, mixed>,
     *   cte?: array<string, mixed>,
     *   referencia_externa?: string
     * } $operationData
     * @return array{sucesso: bool, mensagem: string, ciot: ?string, protocolo: ?string, data_emissao: ?string, http_status?: int, detalhes?: mixed}
     */
    public function emitirCiot(array $operationData): array
    {
        $validationErrors = $this->validateOperationData($operationData);

        if ($validationErrors !== []) {
            return $this->standardResponse(false, implode(' ', $validationErrors));
        }

        $payload = $this->buildPayload($operationData);

        try {
            $httpResponse = $this->postJson('/ciot/emissoes', $payload);
        } catch (RuntimeException $exception) {
            $this->writeLog('connection_error', [
                'message' => $exception->getMessage(),
                'payload' => $this->sanitizeForLog($payload),
            ]);

            return $this->standardResponse(false, 'Falha de comunicacao com o provedor CIOT: ' . $exception->getMessage());
        }

        return $this->handleProviderResponse($httpResponse);
    }

    /**
     * @param array{
     *   ciot?: string,
     *   numero_ciot?: string,
     *   protocolo?: string,
     *   data_encerramento?: string,
     *   motivo?: string,
     *   observacao?: string,
     *   cidade_encerramento?: array{cidade: string, uf: string},
     *   mdfe?: array<string, mixed>,
     *   cte?: array<string, mixed>,
     *   referencia_externa?: string
     * } $closureData
     * @return array<string, mixed>
     */
    public function encerrarCiot(array $closureData): array
    {
        $validationErrors = $this->validateClosureData($closureData);

        if ($validationErrors !== []) {
            return $this->standardResponse(false, implode(' ', $validationErrors), null, null, null, null, null, [
                'tipo_operacao' => 'encerramento',
            ]);
        }

        $payload = $this->buildClosurePayload($closureData);

        try {
            $httpResponse = $this->postJson('/ciot/encerramentos', $payload);
        } catch (RuntimeException $exception) {
            $this->writeLog('connection_error', [
                'message' => $exception->getMessage(),
                'payload' => $this->sanitizeForLog($payload),
            ]);

            return $this->standardResponse(false, 'Falha de comunicacao com o provedor CIOT: ' . $exception->getMessage(), null, null, null, null, null, [
                'tipo_operacao' => 'encerramento',
            ]);
        }

        return $this->handleClosureProviderResponse($httpResponse);
    }

    /**
     * @param array<string, mixed> $config
     */
    private function resolveBaseUrl(array $config): string
    {
        if (!empty($config['base_url'])) {
            return rtrim((string)$config['base_url'], '/');
        }

        $homologationUrl = (string)($config['homologation_url'] ?? 'https://homologacao.exemplo-ciot.com.br/api/v1');
        $productionUrl = (string)($config['production_url'] ?? 'https://api.exemplo-ciot.com.br/v1');

        return match ($this->environment) {
            'production', 'producao' => rtrim($productionUrl, '/'),
            'homologation', 'homologacao', 'sandbox' => rtrim($homologationUrl, '/'),
            default => throw new RuntimeException('Ambiente CIOT invalido. Use homologation ou production.'),
        };
    }

    /**
     * @param array<string, mixed> $operationData
     * @return list<string>
     */
    private function validateOperationData(array $operationData): array
    {
        $errors = [];

        $cpf = (string)($operationData['cpf_motorista'] ?? '');
        if (!$this->isValidCpf($cpf)) {
            $errors[] = 'CPF do motorista invalido.';
        }

        $driverName = trim((string)($operationData['nome_motorista'] ?? ''));
        if (strlen($driverName) < 3) {
            $errors[] = 'Nome do motorista deve conter pelo menos 3 caracteres.';
        }

        $plate = (string)($operationData['placa_veiculo'] ?? '');
        if (!$this->isValidVehiclePlate($plate)) {
            $errors[] = 'Placa do veiculo invalida.';
        }

        $rntrc = preg_replace('/\D+/', '', (string)($operationData['rntrc'] ?? ''));
        if ($rntrc === '' || strlen($rntrc) < 8 || strlen($rntrc) > 14) {
            $errors[] = 'RNTRC invalido.';
        }

        $freightValue = $operationData['valor_frete'] ?? null;
        if (!$this->isValidFreightValue($freightValue)) {
            $errors[] = 'Valor do frete deve ser numerico e maior que zero.';
        }

        foreach (['origem' => 'origem', 'destino' => 'destino'] as $key => $label) {
            if (!isset($operationData[$key]) || !is_array($operationData[$key])) {
                $errors[] = 'Cidade/UF de ' . $label . ' nao informada.';
                continue;
            }

            $city = trim((string)($operationData[$key]['cidade'] ?? ''));
            $state = strtoupper(trim((string)($operationData[$key]['uf'] ?? '')));

            if ($city === '') {
                $errors[] = 'Cidade de ' . $label . ' nao informada.';
            }

            if (!$this->isValidBrazilianState($state)) {
                $errors[] = 'UF de ' . $label . ' invalida.';
            }
        }

        return $errors;
    }

    /**
     * @param array<string, mixed> $closureData
     * @return list<string>
     */
    private function validateClosureData(array $closureData): array
    {
        $errors = [];
        $ciot = preg_replace('/\D+/', '', (string)($closureData['ciot'] ?? $closureData['numero_ciot'] ?? ''));

        if ($ciot === '') {
            $errors[] = 'Numero do CIOT e obrigatorio para encerramento.';
        }

        $closureDate = trim((string)($closureData['data_encerramento'] ?? ''));
        if ($closureDate !== '' && strtotime($closureDate) === false) {
            $errors[] = 'Data de encerramento invalida.';
        }

        if (isset($closureData['cidade_encerramento'])) {
            if (!is_array($closureData['cidade_encerramento'])) {
                $errors[] = 'Cidade/UF de encerramento deve ser um objeto.';
            } else {
                $city = trim((string)($closureData['cidade_encerramento']['cidade'] ?? ''));
                $state = strtoupper(trim((string)($closureData['cidade_encerramento']['uf'] ?? '')));

                if ($city === '') {
                    $errors[] = 'Cidade de encerramento nao informada.';
                }

                if (!$this->isValidBrazilianState($state)) {
                    $errors[] = 'UF de encerramento invalida.';
                }
            }
        }

        return $errors;
    }

    private function isValidCpf(string $cpf): bool
    {
        $cpf = preg_replace('/\D+/', '', $cpf) ?? '';

        if (strlen($cpf) !== 11 || preg_match('/^(\d)\1{10}$/', $cpf) === 1) {
            return false;
        }

        for ($digitPosition = 9; $digitPosition < 11; $digitPosition++) {
            $sum = 0;
            for ($index = 0; $index < $digitPosition; $index++) {
                $sum += (int)$cpf[$index] * (($digitPosition + 1) - $index);
            }

            $calculatedDigit = ((10 * $sum) % 11) % 10;
            if ((int)$cpf[$digitPosition] !== $calculatedDigit) {
                return false;
            }
        }

        return true;
    }

    private function isValidVehiclePlate(string $plate): bool
    {
        $plate = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $plate) ?? '');

        return preg_match('/^[A-Z]{3}[0-9][A-Z0-9][0-9]{2}$/', $plate) === 1;
    }

    private function isValidFreightValue(mixed $value): bool
    {
        $normalizedValue = $this->parseMoneyValue($value);

        return $normalizedValue !== null && $normalizedValue > 0;
    }

    private function isValidBrazilianState(string $state): bool
    {
        $validStates = [
            'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG',
            'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO',
        ];

        return in_array($state, $validStates, true);
    }

    /**
     * @param array<string, mixed> $operationData
     * @return array<string, mixed>
     */
    private function buildPayload(array $operationData): array
    {
        return [
            'motorista' => [
                'cpf' => preg_replace('/\D+/', '', (string)$operationData['cpf_motorista']),
                'nome' => trim((string)$operationData['nome_motorista']),
            ],
            'veiculo' => [
                'placa' => strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string)$operationData['placa_veiculo']) ?? ''),
                'rntrc' => preg_replace('/\D+/', '', (string)$operationData['rntrc']),
            ],
            'frete' => [
                'valor' => $this->normalizeMoney($operationData['valor_frete']),
            ],
            'origem' => [
                'cidade' => trim((string)$operationData['origem']['cidade']),
                'uf' => strtoupper(trim((string)$operationData['origem']['uf'])),
            ],
            'destino' => [
                'cidade' => trim((string)$operationData['destino']['cidade']),
                'uf' => strtoupper(trim((string)$operationData['destino']['uf'])),
            ],
            'integracoes_futuras' => [
                'mdfe' => $operationData['mdfe'] ?? null,
                'cte' => $operationData['cte'] ?? null,
            ],
            'referencia_externa' => $operationData['referencia_externa'] ?? null,
            'ambiente' => $this->environment,
        ];
    }

    /**
     * @param array<string, mixed> $closureData
     * @return array<string, mixed>
     */
    private function buildClosurePayload(array $closureData): array
    {
        $closureDate = trim((string)($closureData['data_encerramento'] ?? ''));

        return [
            'ciot' => preg_replace('/\D+/', '', (string)($closureData['ciot'] ?? $closureData['numero_ciot'] ?? '')),
            'protocolo' => trim((string)($closureData['protocolo'] ?? '')) ?: null,
            'data_encerramento' => $closureDate !== '' ? date(DATE_ATOM, (int)strtotime($closureDate)) : (new DateTimeImmutable())->format(DATE_ATOM),
            'motivo' => trim((string)($closureData['motivo'] ?? 'ENTREGA_CONCLUIDA')),
            'observacao' => trim((string)($closureData['observacao'] ?? '')) ?: null,
            'cidade_encerramento' => isset($closureData['cidade_encerramento']) && is_array($closureData['cidade_encerramento']) ? [
                'cidade' => trim((string)$closureData['cidade_encerramento']['cidade']),
                'uf' => strtoupper(trim((string)$closureData['cidade_encerramento']['uf'])),
            ] : null,
            'integracoes_futuras' => [
                'mdfe' => $closureData['mdfe'] ?? null,
                'cte' => $closureData['cte'] ?? null,
            ],
            'referencia_externa' => $closureData['referencia_externa'] ?? null,
            'ambiente' => $this->environment,
        ];
    }

    private function normalizeMoney(mixed $value): float
    {
        $normalizedValue = $this->parseMoneyValue($value) ?? 0.0;

        return round($normalizedValue, 2);
    }

    private function parseMoneyValue(mixed $value): ?float
    {
        if (is_int($value) || is_float($value)) {
            return (float)$value;
        }

        if (!is_string($value)) {
            return null;
        }

        $normalized = preg_replace('/\s+/', '', trim($value)) ?? '';
        if ($normalized === '') {
            return null;
        }

        $lastComma = strrpos($normalized, ',');
        $lastDot = strrpos($normalized, '.');

        if ($lastComma !== false && $lastDot !== false) {
            $normalized = $lastComma > $lastDot
                ? str_replace('.', '', str_replace(',', '.', $normalized))
                : str_replace(',', '', $normalized);
        } elseif ($lastComma !== false) {
            $normalized = str_replace(',', '.', $normalized);
        }

        return is_numeric($normalized) ? (float)$normalized : null;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array{status: int, body: ?array<string, mixed>, raw_body: string, headers: array<string, mixed>}
     */
    private function postJson(string $path, array $payload): array
    {
        if (str_starts_with($this->baseUrl, 'mock://')) {
            return $this->postJsonToInternalMock($path, $payload);
        }

        $url = $this->baseUrl . '/' . ltrim($path, '/');

        try {
            $jsonPayload = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        } catch (JsonException $exception) {
            throw new RuntimeException('Nao foi possivel serializar o payload JSON: ' . $exception->getMessage(), 0, $exception);
        }

        $this->writeLog('request', [
            'method' => 'POST',
            'url' => $url,
            'payload' => $this->sanitizeForLog($payload),
        ]);

        $curl = curl_init($url);
        if ($curl === false) {
            throw new RuntimeException('Nao foi possivel inicializar cURL.');
        }

        $headers = [
            'Accept: application/json',
            'Content-Type: application/json',
            'User-Agent: CiotAPI-PHP/1.0',
        ];

        if ($this->providerToken !== '') {
            $headers[] = 'Authorization: Bearer ' . $this->providerToken;
        }

        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POSTFIELDS => $jsonPayload,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);

        if ($this->clientCertificatePath !== null) {
            if (!is_file($this->clientCertificatePath) || !is_readable($this->clientCertificatePath)) {
                throw new RuntimeException('Certificado cliente nao encontrado ou sem permissao de leitura: ' . $this->clientCertificatePath);
            }

            curl_setopt($curl, CURLOPT_SSLCERT, $this->clientCertificatePath);
            curl_setopt($curl, CURLOPT_SSLCERTTYPE, $this->clientCertificateType);

            if ($this->clientCertificatePassword !== null) {
                curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $this->clientCertificatePassword);
            }
        }

        $rawBody = curl_exec($curl);
        $curlError = curl_error($curl);
        $curlErrno = curl_errno($curl);
        $httpStatus = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        if ($rawBody === false) {
            $message = $curlErrno === CURLE_OPERATION_TIMEDOUT
                ? 'Timeout ao comunicar com o provedor CIOT.'
                : 'Erro cURL ' . $curlErrno . ': ' . $curlError;

            throw new RuntimeException($message);
        }

        $decodedBody = null;
        if (trim($rawBody) !== '') {
            try {
                $decodedBody = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
            } catch (JsonException $exception) {
                $this->writeLog('invalid_json_response', [
                    'http_status' => $httpStatus,
                    'raw_body' => $rawBody,
                    'error' => $exception->getMessage(),
                ]);

                return [
                    'status' => $httpStatus,
                    'body' => null,
                    'raw_body' => $rawBody,
                    'headers' => [],
                ];
            }
        }

        $response = [
            'status' => $httpStatus,
            'body' => is_array($decodedBody) ? $decodedBody : null,
            'raw_body' => $rawBody,
            'headers' => [],
        ];

        $this->writeLog('response', [
            'http_status' => $httpStatus,
            'body' => $this->sanitizeForLog($response['body'] ?? $rawBody),
        ]);

        return $response;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array{status: int, body: ?array<string, mixed>, raw_body: string, headers: array<string, mixed>}
     */
    private function postJsonToInternalMock(string $path, array $payload): array
    {
        $this->writeLog('request', [
            'method' => 'POST',
            'url' => $this->baseUrl . '/' . ltrim($path, '/'),
            'payload' => $this->sanitizeForLog($payload),
        ]);

        if ($path === '/ciot/emissoes') {
            foreach (['motorista', 'veiculo', 'frete', 'origem', 'destino'] as $requiredKey) {
                if (!isset($payload[$requiredKey]) || !is_array($payload[$requiredKey])) {
                    return $this->mockResponse(400, ['mensagem' => 'Campo obrigatorio ausente: ' . $requiredKey]);
                }
            }

            $seed = hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
            $ciot = substr(preg_replace('/\D+/', '', base_convert(substr($seed, 0, 12), 16, 10)), 0, 12);
            $protocol = 'HML-' . strtoupper(substr($seed, 12, 12));

            return $this->mockResponse(201, [
                'mensagem' => 'CIOT homologacao Render emitido com sucesso.',
                'ciot' => str_pad($ciot, 12, '0', STR_PAD_LEFT),
                'protocolo' => $protocol,
                'data_emissao' => date(DATE_ATOM),
            ]);
        }

        if ($path === '/ciot/encerramentos') {
            $ciot = preg_replace('/\D+/', '', (string)($payload['ciot'] ?? ''));
            if ($ciot === '') {
                return $this->mockResponse(400, ['mensagem' => 'Numero do CIOT e obrigatorio para encerramento.']);
            }

            return $this->mockResponse(200, [
                'mensagem' => 'CIOT homologacao Render encerrado com sucesso.',
                'ciot' => $ciot,
                'protocolo' => $payload['protocolo'] ?? 'ENC-HML-' . strtoupper(substr(hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)), 0, 10)),
                'data_encerramento' => $payload['data_encerramento'] ?? date(DATE_ATOM),
                'motivo' => $payload['motivo'] ?? 'ENTREGA_CONCLUIDA',
            ]);
        }

        return $this->mockResponse(404, ['mensagem' => 'Endpoint mock nao encontrado.']);
    }

    /**
     * @param array<string, mixed> $body
     * @return array{status: int, body: ?array<string, mixed>, raw_body: string, headers: array<string, mixed>}
     */
    private function mockResponse(int $status, array $body): array
    {
        $this->writeLog('response', [
            'http_status' => $status,
            'body' => $this->sanitizeForLog($body),
        ]);

        return [
            'status' => $status,
            'body' => $body,
            'raw_body' => json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '',
            'headers' => [],
        ];
    }

    /**
     * @param array{status: int, body: ?array<string, mixed>, raw_body: string, headers: array<string, mixed>} $httpResponse
     * @return array{sucesso: bool, mensagem: string, ciot: ?string, protocolo: ?string, data_emissao: ?string, http_status?: int, detalhes?: mixed}
     */
    private function handleProviderResponse(array $httpResponse): array
    {
        $status = $httpResponse['status'];
        $body = $httpResponse['body'];

        if ($body === null) {
            return $this->standardResponse(false, 'Resposta invalida do provedor CIOT.', null, null, null, $status, $httpResponse['raw_body']);
        }

        return match ($status) {
            200, 201 => $this->standardResponse(
                true,
                (string)($body['mensagem'] ?? 'CIOT emitido com sucesso.'),
                $this->readStringValue($body, ['ciot', 'numero_ciot', 'numeroCiot']),
                $this->readStringValue($body, ['protocolo', 'numero_protocolo', 'numeroProtocolo']),
                $this->readStringValue($body, ['data_emissao', 'dataEmissao']) ?? (new DateTimeImmutable())->format(DATE_ATOM),
                $status,
                $body
            ),
            400 => $this->standardResponse(false, (string)($body['mensagem'] ?? 'Requisicao invalida para emissao do CIOT.'), null, null, null, $status, $body),
            401 => $this->standardResponse(false, 'Nao autorizado pelo provedor CIOT. Verifique as credenciais configuradas no servidor.', null, null, null, $status, $body),
            403 => $this->standardResponse(false, 'Acesso negado pelo provedor CIOT.', null, null, null, $status, $body),
            404 => $this->standardResponse(false, 'Endpoint de emissao CIOT nao encontrado.', null, null, null, $status, $body),
            429 => $this->standardResponse(false, 'Limite de requisicoes excedido. Tente novamente mais tarde.', null, null, null, $status, $body),
            500 => $this->standardResponse(false, 'Erro interno no provedor CIOT.', null, null, null, $status, $body),
            default => $this->standardResponse(false, 'Resposta HTTP inesperada do provedor CIOT: ' . $status, null, null, null, $status, $body),
        };
    }

    /**
     * @param array{status: int, body: ?array<string, mixed>, raw_body: string, headers: array<string, mixed>} $httpResponse
     * @return array<string, mixed>
     */
    private function handleClosureProviderResponse(array $httpResponse): array
    {
        $status = $httpResponse['status'];
        $body = $httpResponse['body'];

        if ($body === null) {
            return $this->standardResponse(false, 'Resposta invalida do provedor CIOT.', null, null, null, $status, $httpResponse['raw_body'], [
                'tipo_operacao' => 'encerramento',
            ]);
        }

        return match ($status) {
            200, 201 => $this->standardResponse(
                true,
                (string)($body['mensagem'] ?? 'CIOT encerrado com sucesso.'),
                $this->readStringValue($body, ['ciot', 'numero_ciot', 'numeroCiot']),
                $this->readStringValue($body, ['protocolo', 'numero_protocolo', 'numeroProtocolo']),
                $this->readStringValue($body, ['data_emissao', 'dataEmissao']),
                $status,
                $body,
                [
                    'tipo_operacao' => 'encerramento',
                    'data_encerramento' => $this->readStringValue($body, ['data_encerramento', 'dataEncerramento']) ?? (new DateTimeImmutable())->format(DATE_ATOM),
                ]
            ),
            400 => $this->standardResponse(false, (string)($body['mensagem'] ?? 'Requisicao invalida para encerramento do CIOT.'), null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
            401 => $this->standardResponse(false, 'Nao autorizado pelo provedor CIOT. Verifique as credenciais configuradas no servidor.', null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
            403 => $this->standardResponse(false, 'Acesso negado pelo provedor CIOT.', null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
            404 => $this->standardResponse(false, 'Endpoint de encerramento CIOT nao encontrado.', null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
            429 => $this->standardResponse(false, 'Limite de requisicoes excedido. Tente novamente mais tarde.', null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
            500 => $this->standardResponse(false, 'Erro interno no provedor CIOT.', null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
            default => $this->standardResponse(false, 'Resposta HTTP inesperada do provedor CIOT: ' . $status, null, null, null, $status, $body, ['tipo_operacao' => 'encerramento']),
        };
    }

    /**
     * @param array<string, mixed> $body
     * @param list<string> $keys
     */
    private function readStringValue(array $body, array $keys): ?string
    {
        foreach ($keys as $key) {
            if (isset($body[$key]) && trim((string)$body[$key]) !== '') {
                return trim((string)$body[$key]);
            }
        }

        return null;
    }

    /**
     * @return array{sucesso: bool, mensagem: string, ciot: ?string, protocolo: ?string, data_emissao: ?string, http_status?: int, detalhes?: mixed}
     */
    private function standardResponse(
        bool $success,
        string $message,
        ?string $ciot = null,
        ?string $protocol = null,
        ?string $issueDate = null,
        ?int $httpStatus = null,
        mixed $details = null,
        array $extra = []
    ): array {
        $response = [
            'sucesso' => $success,
            'mensagem' => $message,
            'ciot' => $ciot,
            'protocolo' => $protocol,
            'data_emissao' => $issueDate,
        ];

        if ($httpStatus !== null) {
            $response['http_status'] = $httpStatus;
        }

        if ($details !== null) {
            $response['detalhes'] = $details;
        }

        return array_merge($response, $extra);
    }

    private function ensureLogDirectoryExists(): void
    {
        $directory = dirname($this->logFile);
        if (!is_dir($directory) && !mkdir($directory, 0750, true) && !is_dir($directory)) {
            throw new RuntimeException('Nao foi possivel criar o diretorio de logs: ' . $directory);
        }
    }

    /**
     * @param array<string, mixed>|string|null $data
     */
    private function writeLog(string $event, array|string|null $data): void
    {
        $entry = [
            'timestamp' => (new DateTimeImmutable())->format(DATE_ATOM),
            'environment' => $this->environment,
            'event' => $event,
            'data' => $data,
        ];

        try {
            $line = json_encode($entry, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;
            file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
        } catch (JsonException) {
            file_put_contents($this->logFile, '[' . date(DATE_ATOM) . '] Falha ao gravar log JSON.' . PHP_EOL, FILE_APPEND | LOCK_EX);
        }
    }

    private function sanitizeForLog(mixed $data): mixed
    {
        if (!is_array($data)) {
            return $data;
        }

        $sanitized = [];
        foreach ($data as $key => $value) {
            $normalizedKey = strtolower((string)$key);
            if (in_array($normalizedKey, ['authorization', 'bearer_token', 'token', 'access_token'], true)) {
                $sanitized[$key] = '***';
                continue;
            }

            $sanitized[$key] = is_array($value) ? $this->sanitizeForLog($value) : $value;
        }

        return $sanitized;
    }

    private function normalizeNullableString(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        $normalized = trim((string)$value);

        return $normalized === '' ? null : $normalized;
    }
}
