<?php

declare(strict_types=1);

namespace App\Services;

final class CredibancoService
{
    public function probeTokenAndTransaction(array $settings, array $filters): array
    {
        $tokenResult = $this->generateToken($settings);
        if (!(bool) ($tokenResult['ok'] ?? false)) {
            return $tokenResult;
        }

        $accessToken = trim((string) ($tokenResult['access_token'] ?? ''));
        if ($accessToken === '') {
            return [
                'ok' => false,
                'message' => 'Credibanco no retornó access_token en la autenticación SSO.',
            ];
        }

        $queryResult = $this->queryTransactions($settings, $accessToken, $filters);
        if (!(bool) ($queryResult['ok'] ?? false)) {
            return $queryResult;
        }

        return [
            'ok' => true,
            'message' => 'Token y consulta de transacciones Credibanco validados correctamente.',
            'status_code' => (int) ($queryResult['status_code'] ?? 200),
            'token_expires_in' => (int) ($tokenResult['expires_in'] ?? 0),
            'rows_count' => (int) ($queryResult['rows_count'] ?? 0),
            'data' => is_array($queryResult['data'] ?? null) ? $queryResult['data'] : [],
        ];
    }

    public function generateToken(array $settings): array
    {
        $apiUrl = rtrim(trim((string) ($settings['api_url'] ?? '')), '/');
        $clientId = trim((string) ($settings['client_id'] ?? ''));
        $clientSecret = trim((string) ($settings['client_secret'] ?? ''));
        $tokenUrl = trim((string) ($settings['token_url'] ?? ''));

        if ($apiUrl === '') {
            return [
                'ok' => false,
                'message' => 'Falta la URL API de Credibanco.',
            ];
        }

        if ($clientId === '' || $clientSecret === '') {
            return [
                'ok' => false,
                'message' => 'Faltan credenciales Client ID / Client Secret de Credibanco.',
            ];
        }

        if (!function_exists('curl_init')) {
            return [
                'ok' => false,
                'message' => 'cURL no está disponible en PHP. No se puede conectar con Credibanco.',
            ];
        }

        $tokenEndpoints = $this->buildTokenEndpoints($apiUrl, $tokenUrl);
        $lastError = 'No fue posible generar el token SSO de Credibanco.';

        foreach ($tokenEndpoints as $endpoint) {
            $response = $this->request(
                'POST',
                $endpoint,
                [
                    'Accept: application/json',
                    'Content-Type: application/json',
                    'Operacion: 11',
                    'Client_id: ' . $clientId,
                    'Client_secret: ' . $clientSecret,
                ]
            );

            $parsed = $this->parseTokenResponse($response, $endpoint);
            if ((bool) ($parsed['ok'] ?? false)) {
                return $parsed;
            }
            $lastError = trim((string) ($parsed['message'] ?? $lastError));

            $fallback = $this->request(
                'POST',
                $endpoint,
                [
                    'Accept: application/json',
                    'Content-Type: application/x-www-form-urlencoded',
                    'Operacion: 11',
                    'Client_id: ' . $clientId,
                    'Client_secret: ' . $clientSecret,
                ],
                http_build_query([
                    'grant_type' => 'client_credentials',
                ])
            );

            $parsedFallback = $this->parseTokenResponse($fallback, $endpoint);
            if ((bool) ($parsedFallback['ok'] ?? false)) {
                return $parsedFallback;
            }
            $lastError = trim((string) ($parsedFallback['message'] ?? $lastError));
        }

        return [
            'ok' => false,
            'message' => $this->normalizeTokenErrorMessage($lastError, $tokenUrl === ''),
        ];
    }

    public function queryTransactions(array $settings, string $accessToken, array $filters): array
    {
        $apiUrl = rtrim(trim((string) ($settings['api_url'] ?? '')), '/');
        $clientId = trim((string) ($settings['client_id'] ?? ''));
        $accessToken = trim($accessToken);

        if ($apiUrl === '') {
            return [
                'ok' => false,
                'message' => 'Falta la URL API de Credibanco.',
            ];
        }

        if ($clientId === '') {
            return [
                'ok' => false,
                'message' => 'Falta el Client ID para consumir la consulta de transacciones.',
            ];
        }

        if ($accessToken === '') {
            return [
                'ok' => false,
                'message' => 'El token de Credibanco es obligatorio para consultar transacciones.',
            ];
        }

        $payload = $this->buildTransactionPayload($filters);
        if (!(bool) ($payload['ok'] ?? false)) {
            return $payload;
        }

        $endpoint = $apiUrl . '/v1/Consulta/Transacciones';
        $requestBody = is_array($payload['data'] ?? null) ? $payload['data'] : [];

        $headers = [
            'Accept: application/json',
            'Content-Type: application/json',
            'Authorization: Bearer ' . $accessToken,
            'Client_id: ' . $clientId,
            'Client_secret: ' . trim((string) ($settings['client_secret'] ?? '')),
            'Operacion: 11',
        ];

        $response = $this->request(
            'POST',
            $endpoint,
            $headers,
            (string) json_encode($requestBody, JSON_UNESCAPED_UNICODE)
        );

        if ((bool) ($response['ok'] ?? false)) {
            $data = is_array($response['data'] ?? null) ? $response['data'] : [];
            return [
                'ok' => true,
                'status_code' => (int) ($response['status_code'] ?? 200),
                'message' => 'Consulta de transacciones Credibanco exitosa.',
                'rows_count' => $this->countTransactionRows($data),
                'data' => $data,
            ];
        }

        $statusCode = (int) ($response['status_code'] ?? 0);
        if (in_array($statusCode, [403, 404, 405, 415], true)) {
            $authQuery = [
                'access_token' => $accessToken,
                'client_id' => $clientId,
                'client_secret' => trim((string) ($settings['client_secret'] ?? '')),
                'Operacion' => '11',
            ];
            $query = http_build_query(array_merge($requestBody, $authQuery));
            $url = $endpoint . ($query !== '' ? '?' . $query : '');
            $fallbackResponse = $this->request('GET', $url, [
                'Accept: application/json',
                'Authorization: Bearer ' . $accessToken,
                'Client_id: ' . $clientId,
                'Client_secret: ' . trim((string) ($settings['client_secret'] ?? '')),
                'Operacion: 11',
            ]);

            if (!(bool) ($fallbackResponse['ok'] ?? false)) {
                $fallbackResponse = $this->request(
                    'POST',
                    $endpoint,
                    [
                        'Accept: application/json',
                        'Content-Type: application/json',
                        'Authorization: Bearer ' . $accessToken,
                        'Client_id: ' . $clientId,
                        'Client_secret: ' . trim((string) ($settings['client_secret'] ?? '')),
                        'Operacion: 11',
                    ],
                    (string) json_encode(array_merge($requestBody, $authQuery), JSON_UNESCAPED_UNICODE)
                );
            }

            if ((bool) ($fallbackResponse['ok'] ?? false)) {
                $data = is_array($fallbackResponse['data'] ?? null) ? $fallbackResponse['data'] : [];
                return [
                    'ok' => true,
                    'status_code' => (int) ($fallbackResponse['status_code'] ?? 200),
                    'message' => 'Consulta de transacciones Credibanco exitosa.',
                    'rows_count' => $this->countTransactionRows($data),
                    'data' => $data,
                ];
            }

            return [
                'ok' => false,
                'status_code' => (int) ($fallbackResponse['status_code'] ?? 0),
                'message' => $this->extractApiMessage(
                    is_array($fallbackResponse['data'] ?? null) ? $fallbackResponse['data'] : [],
                    'Credibanco rechazó la consulta de transacciones.',
                    (string) ($fallbackResponse['raw_body'] ?? '')
                ),
                'data' => is_array($fallbackResponse['data'] ?? null) ? $fallbackResponse['data'] : [],
            ];
        }

        return [
            'ok' => false,
            'status_code' => $statusCode,
            'message' => $this->extractApiMessage(
                is_array($response['data'] ?? null) ? $response['data'] : [],
                'Credibanco rechazó la consulta de transacciones.',
                (string) ($response['raw_body'] ?? '')
            ),
            'data' => is_array($response['data'] ?? null) ? $response['data'] : [],
        ];
    }

    private function buildTransactionPayload(array $filters): array
    {
        $merchantCode = trim((string) ($filters['valorCodigoComercio'] ?? ''));
        $startAt = trim((string) ($filters['fechaInicio'] ?? ''));
        $endAt = trim((string) ($filters['fechaFin'] ?? ''));
        $rowsPerPage = max(1, (int) ($filters['filasPorPagina'] ?? 10));
        $page = max(1, (int) ($filters['pagina'] ?? 1));

        if ($merchantCode === '') {
            return [
                'ok' => false,
                'message' => 'El valorCodigoComercio es obligatorio para probar la consulta.',
            ];
        }

        if ($startAt === '' || $endAt === '') {
            return [
                'ok' => false,
                'message' => 'Debes informar fechaInicio y fechaFin para la consulta.',
            ];
        }

        $payload = [
            'valorCodigoComercio' => $merchantCode,
            'fechaInicio' => $startAt,
            'fechaFin' => $endAt,
            'filasPorPagina' => $rowsPerPage,
            'pagina' => $page,
        ];

        $optionalMap = [
            'valorNumericoTerminal',
            'codAutorizacion',
            'referencia',
            'utrnno',
            'numeroTarjeta',
        ];
        foreach ($optionalMap as $key) {
            $value = trim((string) ($filters[$key] ?? ''));
            if ($value !== '') {
                $payload[$key] = $value;
            }
        }

        return [
            'ok' => true,
            'data' => $payload,
        ];
    }

    private function parseTokenResponse(array $response, string $endpoint): array
    {
        if (!(bool) ($response['ok'] ?? false)) {
            return [
                'ok' => false,
                'status_code' => (int) ($response['status_code'] ?? 0),
                'message' => $this->extractApiMessage(
                    is_array($response['data'] ?? null) ? $response['data'] : [],
                    'Credibanco no permitió generar token en ' . $endpoint . '.',
                    (string) ($response['raw_body'] ?? '')
                ),
            ];
        }

        $payload = is_array($response['data'] ?? null) ? $response['data'] : [];
        $token = $this->findFirstString($payload, ['access_token', 'token', 'id_token']);
        if ($token === '') {
            $rawBody = trim((string) ($response['raw_body'] ?? ''));
            if (preg_match('/"access_token"\s*:\s*"([^"]+)"/i', $rawBody, $matches) === 1) {
                $token = trim((string) ($matches[1] ?? ''));
            } elseif ($this->looksLikeJwt($rawBody)) {
                $token = $rawBody;
            }
        }

        if ($token === '') {
            return [
                'ok' => false,
                'status_code' => (int) ($response['status_code'] ?? 0),
                'message' => 'Credibanco respondió sin access_token en ' . $endpoint . '.',
                'data' => $payload,
            ];
        }

        return [
            'ok' => true,
            'status_code' => (int) ($response['status_code'] ?? 200),
            'message' => 'Token SSO de Credibanco generado correctamente.',
            'access_token' => $token,
            'token_type' => trim((string) ($payload['token_type'] ?? 'Bearer')),
            'expires_in' => (int) ($payload['expires_in'] ?? 0),
            'data' => $payload,
        ];
    }

    private function buildTokenEndpoints(string $apiUrl, string $tokenUrl = ''): array
    {
        $apiUrl = rtrim(trim($apiUrl), '/');
        $tokenUrl = rtrim(trim($tokenUrl), '/');

        $candidates = [];
        if ($tokenUrl !== '') {
            $candidates[] = $tokenUrl;
        }

        $candidates[] = $apiUrl;
        $candidates[] = $apiUrl . '/token';
        $candidates[] = $apiUrl . '/oauth/token';
        $candidates[] = $apiUrl . '/auth/realms/3scale/protocol/openid-connect/token';
        $candidates[] = $apiUrl . '/auth/realms/3Scale/protocol/openid-connect/token';
        $candidates[] = $apiUrl . '/realms/3scale/protocol/openid-connect/token';

        $unique = [];
        foreach ($candidates as $candidate) {
            $candidate = trim((string) $candidate);
            if ($candidate === '' || !filter_var($candidate, FILTER_VALIDATE_URL)) {
                continue;
            }
            $unique[$candidate] = true;
        }

        return array_keys($unique);
    }

    private function normalizeTokenErrorMessage(string $lastError, bool $missingTokenUrl): string
    {
        $message = trim($lastError);
        $normalized = strtolower($message);

        if ($missingTokenUrl && str_contains($normalized, 'authentication parameters missing')) {
            return 'Credibanco respondió "Authentication parameters missing". Normalmente esto ocurre cuando falta configurar la URL SSO de token por ambiente. En la documentación indica que este endpoint se solicita al comercial asignado.';
        }

        if ($message !== '') {
            return $message;
        }

        if ($missingTokenUrl) {
            return 'No fue posible generar token SSO. Credibanco puede requerir una URL SSO específica por ambiente (solicitada al comercial asignado).';
        }

        return 'No fue posible generar token SSO. Verifica endpoint, Client ID y Client Secret en Credibanco.';
    }

    private function request(string $method, string $url, array $headers, ?string $body = null): array
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 25);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        }

        $rawBody = curl_exec($ch);
        $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if (!is_string($rawBody)) {
            $rawBody = '';
        }

        $payload = json_decode($rawBody, true);
        if (!is_array($payload)) {
            $payload = [];
        }

        if ($error !== '') {
            return [
                'ok' => false,
                'status_code' => $statusCode,
                'message' => 'Error de red conectando con Credibanco: ' . $error,
                'data' => $payload,
                'raw_body' => $rawBody,
            ];
        }

        return [
            'ok' => $statusCode >= 200 && $statusCode < 300,
            'status_code' => $statusCode,
            'data' => $payload,
            'raw_body' => $rawBody,
        ];
    }

    private function countTransactionRows(array $payload): int
    {
        $candidates = [
            'transacciones',
            'transactions',
            'listaTransacciones',
            'result',
            'data',
            'payload',
        ];

        foreach ($candidates as $key) {
            if (!isset($payload[$key]) || !is_array($payload[$key])) {
                continue;
            }

            $value = $payload[$key];
            if ($this->isListArray($value)) {
                return count($value);
            }

            foreach ($value as $nested) {
                if (is_array($nested) && $this->isListArray($nested)) {
                    return count($nested);
                }
            }
        }

        return 0;
    }

    private function isListArray(array $value): bool
    {
        $expectedIndex = 0;
        foreach ($value as $key => $_) {
            if (!is_int($key) || $key !== $expectedIndex) {
                return false;
            }
            $expectedIndex++;
        }

        return true;
    }

    private function looksLikeJwt(string $token): bool
    {
        $token = trim($token);
        return (bool) preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $token);
    }

    private function extractApiMessage(array $payload, string $fallback, string $rawBody = ''): string
    {
        $message = $this->findFirstString($payload, [
            'message',
            'Message',
            'error',
            'Error',
            'detail',
            'Detail',
            'description',
            'Description',
            'error_description',
        ]);
        if ($message !== '') {
            return $message;
        }

        if (isset($payload['errors']) && is_array($payload['errors'])) {
            $parts = [];
            foreach ($payload['errors'] as $error) {
                if (is_string($error) && trim($error) !== '') {
                    $parts[] = trim($error);
                    continue;
                }

                if (is_array($error)) {
                    $candidate = $this->findFirstString($error, [
                        'message',
                        'detail',
                        'description',
                        'code',
                    ]);
                    if ($candidate !== '') {
                        $parts[] = $candidate;
                    }
                }
            }
            if ($parts !== []) {
                return implode(' | ', array_slice($parts, 0, 3));
            }
        }

        $plain = trim((string) preg_replace('/\s+/', ' ', strip_tags($rawBody)));
        if ($plain !== '') {
            return substr($plain, 0, 240);
        }

        return $fallback;
    }

    private function findFirstString(array $payload, array $keys): string
    {
        $bags = [$payload];
        foreach (['data', 'payload', 'result', 'response'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $bags[] = $payload[$key];
            }
        }

        foreach ($bags as $bag) {
            foreach ($keys as $key) {
                $value = trim((string) ($bag[$key] ?? ''));
                if ($value !== '') {
                    return $value;
                }
            }
        }

        return '';
    }
}
