<?php
declare(strict_types=1);

namespace App\Modules\Settings;

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

/**
 * polkurier.pl Web Service API client.
 *
 * Kontrakt API zweryfikowany na podstawie oficjalnego SDK (https://github.com/Polkurier/polkurier-sdk):
 *  - Base URL: https://api.polkurier.pl/ (single endpoint, brak path per metoda)
 *  - HTTP POST, Content-Type: application/json (DOKLADNIE bez parametru charset)
 *  - Body: {"authorization": {"login", "token"}, "apimetod": "<method_name>", "data": {...}}
 *  - Sukces: top-level "status" === "success", tresc odpowiedzi w polu "response"
 *  - Blad: top-level "status" !== "success"; tresc bledu w polu "response" (string lub tablica)
 *
 * Phase 127: testConnection (apimetod=test_auth_api) - zweryfikowany na zywym koncie.
 * Phase 128: createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers/getParcelMachines.
 */
final class PolkurierApiClient
{
    private const API_URL = 'https://api.polkurier.pl/';
    private const PLATFORM = 'orderPRO';
    private const PLATFORM_VERSION = '1.0';

    public function __construct(private readonly int $timeoutSeconds = 30)
    {
    }

    /**
     * @return array{ok: bool, http_code: int, message: string}
     */
    public function testConnection(string $login, string $apiToken): array
    {
        try {
            $response = $this->call('test_auth_api', [
                'platform' => self::PLATFORM,
                'platform_version' => self::PLATFORM_VERSION,
            ], $login, $apiToken);
        } catch (RuntimeException $exception) {
            return [
                'ok' => false,
                'http_code' => $this->lastHttpCode,
                'message' => $exception->getMessage(),
            ];
        }

        $authorization = '';
        if (is_array($response) && isset($response['authorization'])) {
            $authorization = trim((string) $response['authorization']);
        }
        $message = $authorization !== ''
            ? 'Autoryzacja: ' . $authorization
            : 'Polaczenie OK (HTTP ' . $this->lastHttpCode . ').';

        return [
            'ok' => true,
            'http_code' => $this->lastHttpCode,
            'message' => $message,
        ];
    }

    /**
     * Zwraca liste dostepnych przewoznikow z konta polkurier.
     * apimetod=available_carriers; brak wymaganych parametrow.
     *
     * @return array<int, array<string, mixed>> Lista przewoznikow z polami:
     *   - servicecode (string) - kod uslugi do uzycia w 'courier' przy create_order
     *   - name (string) - czytelna nazwa
     *   - additional_data (array) - dodatkowe wymagania
     *   - foreign_shipments (bool)
     */
    public function getAvailableCarriers(string $login, string $apiToken): array
    {
        $response = $this->call('available_carriers', [], $login, $apiToken);

        if (is_array($response)) {
            // Polkurier moze zwracac liste bezposrednio jako array lub pod kluczem 'carriers'
            if (isset($response[0]) && is_array($response[0])) {
                return $response;
            }
            if (isset($response['carriers']) && is_array($response['carriers'])) {
                return $response['carriers'];
            }
        }

        return [];
    }

    /**
     * Tworzy zamowienie (paczke) w polkurier.
     * apimetod=create_order.
     *
     * @param array<string, mixed> $payload Struktura zgodna z SDK polkurier-sdk/CreateOrder:
     *   - shipmenttype (string, np. 'BOX')
     *   - courier (string, servicecode z available_carriers)
     *   - description (string)
     *   - sender (array: company, person, street, housenumber, flatnumber, postcode, city, email, phone, country, point_id)
     *   - recipient (array: same shape as sender)
     *   - packs (array of {length, width, height, weight, amount, type})
     *   - pickup (array: pickupdate, pickuptimefrom, pickuptimeto, nocourierorder)
     *   - COD (array: codtype, codamount, codbankaccount, return_cod) - optional
     *   - insurance (float) - optional
     *   - courierservice (array) - optional additional services
     *
     * @return array<string, mixed> Tresc z pola 'response' API. Typowe pola: 'orderno', 'cost', etc.
     */
    public function createShipment(string $login, string $apiToken, array $payload): array
    {
        $response = $this->call('create_order', $payload, $login, $apiToken);
        return is_array($response) ? $response : [];
    }

    /**
     * Pobiera etykiete dla zamowienia.
     * apimetod=get_label; wymagane: orderno (string lub array).
     *
     * @return array<string, mixed> Tresc z 'response'. Etykieta zwykle base64 w polu 'label' albo 'pdf'.
     */
    /**
     * Pobiera etykiete dla zamowienia.
     * API polkurier (apimetod=get_label) przyjmuje wylacznie `orderno: Array<String>`.
     * Rozmiar etykiety (A4 vs A6) jest sterowany w panelu klienta polkurier.pl
     * (Ustawienia konta -> Preferencje etykiet), nie przez parametry API.
     *
     * @return array<string, mixed> Tresc z 'response'. Etykieta base64 w polu 'file'.
     */
    public function getLabel(string $login, string $apiToken, string $orderno): array
    {
        $response = $this->call('get_label', [
            'orderno' => [$orderno],
        ], $login, $apiToken);

        return is_array($response) ? $response : [];
    }

    /**
     * Pobiera status zamowienia.
     * apimetod=get_status; wymagane: orderno.
     *
     * @return array<string, mixed> Tresc z 'response'. Pola: status, statuscode, statusdate, deliverydate, url.
     */
    public function getStatus(string $login, string $apiToken, string $orderno): array
    {
        $response = $this->call('get_status', [
            'orderno' => $orderno,
        ], $login, $apiToken);

        return is_array($response) ? $response : [];
    }

    /**
     * Anuluje zamowienie.
     * apimetod=cancel_order; wymagane: orderno.
     *
     * @return array<string, mixed>
     */
    public function cancelOrder(string $login, string $apiToken, string $orderno): array
    {
        $response = $this->call('cancel_order', [
            'orderno' => $orderno,
        ], $login, $apiToken);

        return is_array($response) ? $response : [];
    }

    /**
     * Pobiera liste paczkomatow InPost.
     * apimetod=inpost_parcel_machines (deprecated wg SDK, ale nadal dziala — alternatywa: get_courier_point).
     *
     * @return array<int, array<string, mixed>>
     */
    public function getInpostParcelMachines(string $login, string $apiToken, bool $codAvailable = false): array
    {
        $response = $this->call('inpost_parcel_machines', [
            'cod_available' => $codAvailable,
            'parcel_send' => false,
        ], $login, $apiToken);

        if (is_array($response)) {
            if (isset($response[0]) && is_array($response[0])) {
                return $response;
            }
            if (isset($response['machines']) && is_array($response['machines'])) {
                return $response['machines'];
            }
        }

        return [];
    }

    /**
     * Generyczny lookup punktow odbioru per courier.
     * apimetod=get_courier_point (SDK nowsze API).
     *
     * @return array<int, array<string, mixed>>
     */
    public function getCourierPoints(string $login, string $apiToken, string $courier, ?string $postcode = null): array
    {
        $data = ['courier' => $courier];
        if ($postcode !== null && $postcode !== '') {
            $data['postcode'] = $postcode;
        }
        $response = $this->call('get_courier_point', $data, $login, $apiToken);

        if (is_array($response)) {
            if (isset($response[0]) && is_array($response[0])) {
                return $response;
            }
            if (isset($response['points']) && is_array($response['points'])) {
                return $response['points'];
            }
        }

        return [];
    }

    /**
     * Wspolny wrapper dla wszystkich apimetod.
     * Sukces -> zwraca tresc pola 'response'. Blad -> rzuca RuntimeException z trescia z 'response'.
     *
     * @param array<string, mixed> $data
     * @return mixed Tresc pola 'response' z API.
     */
    private function call(string $apimetod, array $data, string $login, string $apiToken): mixed
    {
        $payload = [
            'authorization' => [
                'login' => trim($login),
                'token' => trim($apiToken),
            ],
            'apimetod' => $apimetod,
            'data' => $data,
        ];

        [$body, $httpCode, $curlError] = $this->postJson($payload);
        $this->lastHttpCode = $httpCode;

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

        $decoded = json_decode(ltrim($body, "\xEF\xBB\xBF \t\n\r\0\x0B"), true);
        if (!is_array($decoded)) {
            $snippet = substr(trim(strip_tags($body)), 0, 240);
            throw new RuntimeException('Niepoprawna odpowiedz JSON polkurier (HTTP ' . $httpCode . '): ' . $snippet);
        }

        $status = strtolower(trim((string) ($decoded['status'] ?? '')));
        $responseField = $decoded['response'] ?? null;

        if ($status === 'success' && $httpCode >= 200 && $httpCode < 300) {
            return $responseField;
        }

        // Error path
        $errorMessage = $this->extractErrorMessage($responseField, $decoded, $status, $httpCode);
        throw new RuntimeException($errorMessage);
    }

    private int $lastHttpCode = 0;

    /**
     * @param mixed $responseField
     * @param array<string, mixed> $decoded
     */
    private function extractErrorMessage(mixed $responseField, array $decoded, string $status, int $httpCode): string
    {
        if (is_string($responseField)) {
            $msg = trim($responseField);
            if ($msg !== '') {
                return $msg;
            }
        }
        if (is_array($responseField)) {
            $msg = trim((string) (
                $responseField['error_message']
                ?? $responseField['errorMessage']
                ?? $responseField['message']
                ?? $responseField['error']
                ?? ''
            ));
            if ($msg === '') {
                $jsonDump = json_encode($responseField, JSON_UNESCAPED_UNICODE);
                if (is_string($jsonDump)) {
                    $msg = substr($jsonDump, 0, 280);
                }
            }
            if ($msg !== '') {
                return $msg;
            }
        }

        $msg = trim((string) (
            $decoded['error_message']
            ?? $decoded['errorMessage']
            ?? $decoded['message']
            ?? $decoded['error']
            ?? ''
        ));
        if ($msg !== '') {
            return $msg;
        }

        return $status !== ''
            ? 'polkurier status: ' . $status . ' (HTTP ' . $httpCode . ')'
            : 'polkurier HTTP ' . $httpCode;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array{0: string, 1: int, 2: ?string}
     */
    private function postJson(array $payload): array
    {
        $ch = curl_init(self::API_URL);
        if ($ch === false) {
            return ['', 0, 'Nie udalo sie zainicjowac cURL.'];
        }

        $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($encoded === false) {
            return ['', 0, 'Nie udalo sie zakodowac payloadu JSON.'];
        }

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

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

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

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

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