<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Http\SslCertificateResolver;
use RuntimeException;

final class FakturowniaApiClient
{
    public function __construct(private readonly int $timeoutSeconds = 15)
    {
    }

    /**
     * @return array{ok: bool, http_code: int, message: string}
     */
    public function testConnection(string $accountPrefix, string $apiToken): array
    {
        $prefix = strtolower(trim($accountPrefix));
        $token = trim($apiToken);

        if ($prefix === '' || $token === '') {
            return [
                'ok' => false,
                'http_code' => 0,
                'message' => 'Brak prefiksu konta lub tokenu API.',
            ];
        }

        $url = $this->buildUrl($prefix, '/account.json') . '?api_token=' . rawurlencode($token);
        [$body, $httpCode, $curlError] = $this->httpGet($url);

        if ($curlError !== null) {
            return [
                'ok' => false,
                'http_code' => $httpCode,
                'message' => 'Blad polaczenia: ' . $curlError,
            ];
        }

        if ($httpCode >= 200 && $httpCode < 300) {
            return [
                'ok' => true,
                'http_code' => $httpCode,
                'message' => 'OK',
            ];
        }

        $message = $this->resolveErrorMessage($body);
        if ($message === '') {
            $message = 'HTTP ' . $httpCode;
        }

        return [
            'ok' => false,
            'http_code' => $httpCode,
            'message' => $message,
        ];
    }

    /**
     * POST /invoices.json — creates invoice in Fakturownia and returns parsed response.
     *
     * @param array{account_prefix: string, api_token: string} $settings
     * @param array<string, mixed> $invoiceFields
     * @return array{id: string, number: string, view_url: string, pdf_url: string, raw: array<string, mixed>}
     */
    public function createInvoice(array $settings, array $invoiceFields): array
    {
        $prefix = strtolower(trim((string) ($settings['account_prefix'] ?? '')));
        $apiToken = trim((string) ($settings['api_token'] ?? ''));

        if ($prefix === '' || $apiToken === '') {
            throw new RuntimeException('Brak prefiksu konta lub tokenu API.');
        }

        $url = $this->buildUrl($prefix, '/invoices.json');
        $body = json_encode([
            'api_token' => $apiToken,
            'invoice' => $invoiceFields,
        ], JSON_UNESCAPED_UNICODE);

        if ($body === false) {
            throw new RuntimeException('Nie udalo sie zakodowac payloadu (json_encode).');
        }

        [$rawBody, $httpCode, $curlError] = $this->httpPostJson($url, $body);

        if ($curlError !== null) {
            throw new RuntimeException('Blad polaczenia: ' . $curlError);
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            $msg = $this->resolveErrorMessage($rawBody);
            if ($msg === '' || strtolower($msg) === 'error') {
                $msg = 'raw body: ' . substr($rawBody, 0, 500);
            }
            error_log('[Fakturownia] createInvoice HTTP ' . $httpCode . ' | body=' . substr($rawBody, 0, 1000));
            throw new RuntimeException('HTTP ' . $httpCode . ': ' . $msg);
        }

        $decoded = json_decode($rawBody, true);
        if (!is_array($decoded)) {
            throw new RuntimeException('Niepoprawna odpowiedz JSON od Fakturowni.');
        }

        $id = $decoded['id'] ?? null;
        $number = $decoded['number'] ?? $decoded['full_number'] ?? null;

        if ($id === null || $number === null) {
            throw new RuntimeException('Odpowiedz Fakturowni nie zawiera id/number.');
        }

        $viewUrl = (string) ($decoded['view_url'] ?? '');
        $pdfUrl = $this->buildPdfUrl($prefix, (string) $id, $apiToken);

        return [
            'id' => (string) $id,
            'number' => (string) $number,
            'view_url' => $viewUrl,
            'pdf_url' => $pdfUrl,
            'raw' => $decoded,
        ];
    }

    /**
     * Build URL for direct PDF download/redirect (no fetch — used in 302 redirect).
     */
    public function buildPdfUrl(string $accountPrefix, string $invoiceId, string $apiToken): string
    {
        $prefix = strtolower(trim($accountPrefix));
        return $this->buildUrl($prefix, '/invoices/' . rawurlencode($invoiceId) . '.pdf')
            . '?api_token=' . rawurlencode($apiToken);
    }

    private function buildUrl(string $prefix, string $path): string
    {
        return 'https://' . $prefix . '.fakturownia.pl' . $path;
    }

    /**
     * @return array{0: string, 1: int, 2: ?string}
     */
    private function httpGet(string $url): array
    {
        $ch = curl_init($url);
        if ($ch === false) {
            return ['', 0, 'Nie udalo sie zainicjowac cURL.'];
        }

        $opts = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPGET => true,
            CURLOPT_TIMEOUT => $this->timeoutSeconds,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER => [
                'Accept: application/json',
                'User-Agent: orderPRO/1.0',
            ],
        ];

        $caPath = SslCertificateResolver::resolve();
        if ($caPath !== null) {
            $opts[CURLOPT_CAINFO] = $caPath;
        }

        curl_setopt_array($ch, $opts);
        $rawBody = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);

        if ($rawBody === false) {
            return ['', $httpCode, $curlError !== '' ? $curlError : 'Brak odpowiedzi z API.'];
        }

        return [(string) $rawBody, $httpCode, null];
    }

    /**
     * @return array{0: string, 1: int, 2: ?string}
     */
    private function httpPostJson(string $url, string $body): array
    {
        $ch = curl_init($url);
        if ($ch === false) {
            return ['', 0, 'Nie udalo sie zainicjowac cURL.'];
        }

        $opts = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_TIMEOUT => $this->timeoutSeconds,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Accept: application/json',
                'User-Agent: orderPRO/1.0',
            ],
        ];

        $caPath = SslCertificateResolver::resolve();
        if ($caPath !== null) {
            $opts[CURLOPT_CAINFO] = $caPath;
        }

        curl_setopt_array($ch, $opts);
        $rawBody = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);

        if ($rawBody === false) {
            return ['', $httpCode, $curlError !== '' ? $curlError : 'Brak odpowiedzi z API.'];
        }

        return [(string) $rawBody, $httpCode, null];
    }

    private function resolveErrorMessage(string $body): string
    {
        $trimmed = ltrim($body, "\xEF\xBB\xBF \t\n\r\0\x0B");
        if ($trimmed === '') {
            return '';
        }

        $decoded = json_decode($trimmed, true);
        if (is_array($decoded)) {
            $fieldErrors = $this->flattenFieldErrors($decoded['errors'] ?? null);
            if ($fieldErrors !== '') {
                $code = isset($decoded['code']) && is_string($decoded['code']) ? trim($decoded['code']) : '';
                return $code !== '' && strtolower($code) !== 'error'
                    ? $code . ' — ' . $fieldErrors
                    : $fieldErrors;
            }

            foreach (['message', 'error'] as $key) {
                if (isset($decoded[$key]) && is_string($decoded[$key]) && trim($decoded[$key]) !== '' && trim($decoded[$key]) !== 'error') {
                    return trim($decoded[$key]);
                }
            }

            if (isset($decoded['code']) && is_string($decoded['code']) && trim($decoded['code']) !== '') {
                return trim($decoded['code']) . ' (body: ' . substr($trimmed, 0, 300) . ')';
            }
        }

        $snippet = trim(strip_tags($trimmed));
        return substr($snippet, 0, 300);
    }

    /**
     * Flatten Fakturownia per-field errors structure like:
     *   {"buyer_name": ["can't be blank"], "positions": ["are invalid"]}
     * Into: "buyer_name: can't be blank; positions: are invalid"
     */
    private function flattenFieldErrors(mixed $errors): string
    {
        if (!is_array($errors) || $errors === []) {
            return '';
        }

        $parts = [];
        foreach ($errors as $field => $value) {
            $msgs = [];
            if (is_string($value)) {
                $msgs[] = trim($value);
            } elseif (is_array($value)) {
                foreach ($value as $v) {
                    if (is_string($v)) {
                        $msgs[] = trim($v);
                    } elseif (is_array($v)) {
                        $msgs[] = json_encode($v, JSON_UNESCAPED_UNICODE) ?: '';
                    }
                }
            }
            $msgs = array_filter($msgs, static fn (string $m): bool => $m !== '');
            if ($msgs === []) {
                continue;
            }
            $parts[] = (is_string($field) ? $field . ': ' : '') . implode(', ', $msgs);
        }

        return implode('; ', $parts);
    }
}
