<?php
declare(strict_types=1);

namespace App\Modules\Shipments;

use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\ApaczkaApiClient;
use App\Modules\Settings\ApaczkaIntegrationRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Exceptions\ShipmentException;
use Throwable;

final class ApaczkaShipmentService implements ShipmentProviderInterface
{
    private const PICKUP_DATE_RETRY_DAYS = 7;

    /**
     * @var array<string, array{street:string,postal_code:string,city:string}>
     */
    private array $pointAddressCache = [];

    public function __construct(
        private readonly ApaczkaIntegrationRepository $integrationRepository,
        private readonly ApaczkaApiClient $apiClient,
        private readonly ShipmentPackageRepository $packages,
        private readonly CompanySettingsRepository $companySettings,
        private readonly OrdersRepository $ordersRepository
    ) {
    }

    public function code(): string
    {
        return 'apaczka';
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function getDeliveryServices(): array
    {
        [$appId, $appSecret] = $this->requireCredentials();
        return $this->apiClient->getServiceStructure($appId, $appSecret);
    }

    /**
     * @param array<string, mixed> $formData
     * @return array<string, mixed>
     */
    public function createShipment(int $orderId, array $formData): array
    {
        $order = $this->ordersRepository->findDetails($orderId);
        if ($order === null) {
            throw new ShipmentException('Zamowienie nie znalezione.');
        }

        [$appId, $appSecret] = $this->requireCredentials();

        $sender = $this->companySettings->getSenderAddress();
        $this->validateSenderAddress($sender);

        $orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
        $sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));

        $deliveryMethodId = trim((string) ($formData['delivery_method_id'] ?? ''));
        if ($deliveryMethodId === '') {
            throw new ShipmentException('Nie podano uslugi Apaczka.');
        }

        $serviceDefinition = $this->resolveServiceDefinition($appId, $appSecret, $deliveryMethodId);
        $receiverAddress = $this->buildReceiverAddress($order, $formData, $appId, $appSecret);
        $senderAddress = $this->normalizeSender($sender);
        $senderPointId = trim((string) ($formData['sender_point_id'] ?? ''));
        if ($senderPointId !== '') {
            $this->applyPointIdentifiers($senderAddress, $senderPointId);
        }

        $weightKg = max(0.001, (float) ($formData['weight_kg'] ?? 1.0));
        $lengthCm = max(1.0, (float) ($formData['length_cm'] ?? 25.0));
        $widthCm = max(1.0, (float) ($formData['width_cm'] ?? 20.0));
        $heightCm = max(1.0, (float) ($formData['height_cm'] ?? 8.0));

        $insuranceAmount = max(0.0, (float) ($formData['insurance_amount'] ?? 0));
        $codAmount = max(0.0, (float) ($formData['cod_amount'] ?? 0));

        $apiPayload = [
            'service_id' => ctype_digit($deliveryMethodId) ? (int) $deliveryMethodId : $deliveryMethodId,
            'address' => [
                'sender' => $senderAddress,
                'receiver' => $receiverAddress,
            ],
            'shipment' => [[
                'shipment_type_code' => 'PACZKA',
                'dimension1' => (int) round($lengthCm),
                'dimension2' => (int) round($widthCm),
                'dimension3' => (int) round($heightCm),
                'weight' => (float) round($weightKg, 3),
                'is_nstd' => 0,
            ]],
            'content' => 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId),
            'comment' => 'orderPRO ' . ($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId),
        ];
        $pickup = $this->buildPickupPayload($serviceDefinition, $senderPointId, $formData);
        if ($pickup !== []) {
            $apiPayload['pickup'] = $pickup;
        }
        $this->validateServiceRequirements($serviceDefinition, $receiverAddress, $senderAddress);

        if ($insuranceAmount > 0) {
            $apiPayload['shipment_value'] = (int) round($insuranceAmount * 100);
            $apiPayload['shipment_currency'] = strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN')));
        }

        if ($codAmount > 0) {
            $apiPayload['cod'] = [
                'amount' => (int) round($codAmount * 100),
                'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
            ];
        }

        $carrierLabel = trim((string) ($serviceDefinition['name'] ?? ''));
        if ($carrierLabel === '') {
            $carrierLabel = (string) ($this->packages->resolveCarrierName('apaczka', $deliveryMethodId) ?? '');
        }

        $packageId = $this->packages->create([
            'order_id' => $orderId,
            'provider' => 'apaczka',
            'delivery_method_id' => $deliveryMethodId,
            'credentials_id' => null,
            'command_id' => null,
            'status' => 'pending',
            'carrier_id' => $carrierLabel !== '' ? $carrierLabel : null,
            'package_type' => strtoupper(trim((string) ($formData['package_type'] ?? 'PACKAGE'))),
            'weight_kg' => $weightKg,
            'length_cm' => $lengthCm,
            'width_cm' => $widthCm,
            'height_cm' => $heightCm,
            'insurance_amount' => $insuranceAmount > 0 ? $insuranceAmount : null,
            'insurance_currency' => strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))),
            'cod_amount' => $codAmount > 0 ? $codAmount : null,
            'cod_currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
            'label_format' => 'PDF',
            'receiver_point_id' => trim((string) ($formData['receiver_point_id'] ?? '')),
            'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
            'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
            'payload_json' => $apiPayload,
        ]);

        try {
            $response = $this->sendOrderWithPickupFallback($appId, $appSecret, $apiPayload);
        } catch (Throwable $exception) {
            $errorMessage = $this->buildShipmentErrorMessage(
                $exception,
                $serviceDefinition,
                $receiverAddress,
                $senderAddress,
                $apiPayload
            );
            $this->packages->update($packageId, [
                'status' => 'error',
                'error_message' => $errorMessage,
            ]);
            throw new ShipmentException($errorMessage, 0, $exception);
        }

        $orderResponse = is_array($response['response']['order'] ?? null) ? $response['response']['order'] : [];
        $apaczkaOrderId = (int) ($orderResponse['id'] ?? 0);
        $tracking = trim((string) ($orderResponse['waybill_number'] ?? ''));

        $this->packages->update($packageId, [
            'status' => $apaczkaOrderId > 0 ? 'created' : 'pending',
            'shipment_id' => $apaczkaOrderId > 0 ? (string) $apaczkaOrderId : null,
            'tracking_number' => $tracking !== '' ? $tracking : null,
            'payload_json' => $response,
        ]);

        return [
            'package_id' => $packageId,
            'command_id' => $apaczkaOrderId > 0 ? (string) $apaczkaOrderId : null,
        ];
    }

    /**
     * @param array<string, mixed> $apiPayload
     * @return array<string, mixed>
     */
    private function sendOrderWithPickupFallback(string $appId, string $appSecret, array &$apiPayload): array
    {
        $attempt = 0;
        while (true) {
            try {
                return $this->apiClient->sendOrder($appId, $appSecret, $apiPayload);
            } catch (Throwable $exception) {
                if (
                    !$this->isPickupDateUnavailableError($exception)
                    || !$this->shiftPickupDateToNextBusinessDay($apiPayload)
                    || $attempt >= self::PICKUP_DATE_RETRY_DAYS
                ) {
                    throw $exception;
                }
                $attempt++;
            }
        }
    }

    private function isPickupDateUnavailableError(Throwable $exception): bool
    {
        $message = strtolower(trim($exception->getMessage()));
        if ($message === '') {
            return false;
        }

        return str_contains($message, 'pickup not available for selected day')
            || str_contains($message, "can\\u2019t place an order today")
            || str_contains($message, "can't place an order today")
            || str_contains($message, 'change its date to another working day')
            || str_contains($message, 'time between cut-off hours for pickup is too short')
            || str_contains($message, 'an order for the same day can be placed up till');
    }

    /**
     * @param array<string, mixed> $apiPayload
     */
    private function shiftPickupDateToNextBusinessDay(array &$apiPayload): bool
    {
        $pickup = is_array($apiPayload['pickup'] ?? null) ? $apiPayload['pickup'] : null;
        if ($pickup === null) {
            return false;
        }

        $pickupType = strtoupper(trim((string) ($pickup['type'] ?? '')));
        $pickupDate = trim((string) ($pickup['date'] ?? ''));
        if ($pickupType !== 'COURIER' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $pickupDate) !== 1) {
            return false;
        }

        $nextDateTimestamp = strtotime('+1 day', strtotime($pickupDate));
        if ($nextDateTimestamp === false) {
            return false;
        }

        $apiPayload['pickup']['date'] = $this->normalizeCourierPickupDate(date('Y-m-d', $nextDateTimestamp));
        return true;
    }

    /**
     * @return array<string, mixed>
     */
    public function checkCreationStatus(int $packageId): array
    {
        $package = $this->packages->findById($packageId);
        if ($package === null) {
            throw new ShipmentException('Paczka nie znaleziona.');
        }

        $shipmentId = max(0, (int) ($package['shipment_id'] ?? 0));
        if ($shipmentId <= 0) {
            return ['status' => 'in_progress'];
        }

        [$appId, $appSecret] = $this->requireCredentials();
        $details = $this->apiClient->getOrderDetails($appId, $appSecret, $shipmentId);
        $order = is_array($details['response']['order'] ?? null) ? $details['response']['order'] : [];
        $tracking = trim((string) ($order['waybill_number'] ?? ($package['tracking_number'] ?? '')));

        $this->packages->update($packageId, [
            'status' => 'created',
            'tracking_number' => $tracking !== '' ? $tracking : null,
            'payload_json' => $details,
        ]);

        return [
            'status' => 'created',
            'shipment_id' => (string) $shipmentId,
            'tracking_number' => $tracking,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function downloadLabel(int $packageId, string $storagePath): array
    {
        $package = $this->packages->findById($packageId);
        if ($package === null) {
            throw new ShipmentException('Paczka nie znaleziona.');
        }

        $shipmentId = max(0, (int) ($package['shipment_id'] ?? 0));
        if ($shipmentId <= 0) {
            throw new ShipmentException('Przesylka nie zostala jeszcze utworzona.');
        }

        [$appId, $appSecret] = $this->requireCredentials();
        try {
            $response = $this->apiClient->getWaybill($appId, $appSecret, $shipmentId);
        } catch (Throwable $exception) {
            $message = trim($exception->getMessage());
            if ($this->isLabelUnavailableError($message)) {
                $this->packages->update($packageId, [
                    'status' => 'error',
                    'error_message' => $message,
                ]);
            }
            throw $exception;
        }

        $waybillRaw = $response['response']['waybill'] ?? null;
        $base64 = '';
        if (is_string($waybillRaw)) {
            $base64 = trim($waybillRaw);
        } elseif (is_array($waybillRaw)) {
            $base64 = trim((string) ($waybillRaw['data'] ?? ''));
        }
        if ($base64 === '') {
            throw new ShipmentException('Apaczka nie zwrocila danych etykiety.');
        }

        $binary = base64_decode($base64, true);
        if (!is_string($binary) || $binary === '') {
            throw new ShipmentException('Nie mozna odczytac etykiety Apaczka.');
        }

        $dir = rtrim($storagePath, '/\\') . '/labels';
        if (!is_dir($dir)) {
            mkdir($dir, 0775, true);
        }

        $filename = 'label_' . $packageId . '_' . $shipmentId . '.pdf';
        $filePath = $dir . '/' . $filename;
        file_put_contents($filePath, $binary);

        $this->packages->update($packageId, [
            'status' => 'label_ready',
            'label_path' => 'labels/' . $filename,
            'payload_json' => $response,
        ]);

        return [
            'label_path' => 'labels/' . $filename,
            'full_path' => $filePath,
        ];
    }

    private function isLabelUnavailableError(string $message): bool
    {
        $normalized = strtolower(trim($message));
        if ($normalized === '') {
            return false;
        }

        return str_contains($normalized, 'label is not available for this order');
    }

    /**
     * @return array{0: string, 1: string}
     */
    private function requireCredentials(): array
    {
        $credentials = $this->integrationRepository->getApiCredentials();
        $appId = trim((string) ($credentials['app_id'] ?? ''));
        $appSecret = trim((string) ($credentials['app_secret'] ?? ''));

        if ($appId === '' || $appSecret === '') {
            throw new IntegrationConfigException('Brak konfiguracji Apaczka (app_id/app_secret).');
        }

        return [$appId, $appSecret];
    }

    /**
     * @param array<string, mixed>|null $orderDetails
     * @param array<string, mixed> $formData
     * @return array<string, mixed>
     */
    private function buildReceiverAddress(
        ?array $orderDetails,
        array $formData,
        string $appId,
        string $appSecret
    ): array
    {
        $addresses = is_array($orderDetails['addresses'] ?? null) ? $orderDetails['addresses'] : [];
        $deliveryAddr = null;
        $customerAddr = null;
        foreach ($addresses as $addr) {
            $type = (string) ($addr['address_type'] ?? '');
            if ($type === 'delivery') {
                $deliveryAddr = $addr;
            }
            if ($type === 'customer') {
                $customerAddr = $addr;
            }
        }

        $delivery = is_array($deliveryAddr) ? $deliveryAddr : [];
        $customer = is_array($customerAddr) ? $customerAddr : [];
        $deliveryStreet = $this->composeStreetLine($delivery);
        $customerStreet = $this->composeStreetLine($customer);
        $pickupMeta = $this->parsePickupMeta(
            trim((string) ($delivery['parcel_name'] ?? '')),
            trim((string) ($delivery['parcel_external_id'] ?? ''))
        );

        $receiverPointId = trim((string) ($formData['receiver_point_id'] ?? ($delivery['parcel_external_id'] ?? '')));
        $pointAddress = $receiverPointId !== ''
            ? $this->resolvePointAddress($appId, $appSecret, $receiverPointId)
            : ['street' => '', 'postal_code' => '', 'city' => ''];

        $name = $this->resolveStringField(
            $formData,
            'receiver_name',
            [
                $delivery['name'] ?? null,
                $customer['name'] ?? null,
                $delivery['company_name'] ?? null,
                $customer['company_name'] ?? null,
                $orderDetails['order']['customer_login'] ?? null,
                'Klient',
            ]
        );
        $street = $this->resolveStringField(
            $formData,
            'receiver_street',
            [
                $deliveryStreet,
                $pickupMeta['street'] ?? null,
                $pointAddress['street'] ?? null,
                $customerStreet,
                $receiverPointId !== '' ? ('Punkt odbioru ' . $receiverPointId) : null,
            ]
        );
        $city = $this->resolveStringField(
            $formData,
            'receiver_city',
            [
                $delivery['city'] ?? null,
                $pickupMeta['city'] ?? null,
                $pointAddress['city'] ?? null,
                $customer['city'] ?? null,
            ]
        );
        $postalCode = $this->resolveStringField(
            $formData,
            'receiver_postal_code',
            [
                $delivery['zip_code'] ?? null,
                $pickupMeta['postal_code'] ?? null,
                $pointAddress['postal_code'] ?? null,
                $customer['zip_code'] ?? null,
            ]
        );
        $countryCode = strtoupper($this->resolveStringField(
            $formData,
            'receiver_country_code',
            [$delivery['country'] ?? null, $customer['country'] ?? null, 'PL']
        ));
        $phone = $this->resolveStringField(
            $formData,
            'receiver_phone',
            [$delivery['phone'] ?? null, $customer['phone'] ?? null]
        );
        $email = $this->resolveStringField(
            $formData,
            'receiver_email',
            [$delivery['email'] ?? null, $customer['email'] ?? null]
        );

        if ($receiverPointId !== '') {
            if ($street === '') {
                $street = 'Punkt odbioru ' . $receiverPointId;
            }
            if ($city === '') {
                $city = $this->resolveStringField($formData, 'receiver_city', [$customer['city'] ?? null, 'Warszawa']);
            }
            if ($postalCode === '') {
                $postalCode = $this->resolveStringField($formData, 'receiver_postal_code', [$customer['zip_code'] ?? null, '00-000']);
            }
        }

        if ($name === '' || $street === '' || $city === '' || $postalCode === '' || $countryCode === '') {
            throw new ShipmentException('Brak wymaganych danych adresowych odbiorcy.');
        }

        $receiver = [
            'name' => $name,
            'line1' => $street,
            'postal_code' => $postalCode,
            'city' => $city,
            'country_code' => $countryCode,
        ];
        $contactPerson = $this->resolveStringField(
            $formData,
            'receiver_contact_person',
            [$name, $customer['name'] ?? null, $delivery['name'] ?? null]
        );
        if ($contactPerson !== '') {
            $receiver['contact_person'] = $contactPerson;
            $receiver['person'] = $contactPerson;
        }
        if ($receiverPointId !== '') {
            $this->applyPointIdentifiers($receiver, $receiverPointId);
        }

        if ($phone !== '') {
            $receiver['phone'] = $phone;
        }
        if ($email !== '') {
            $receiver['email'] = $email;
        }

        return $receiver;
    }

    /**
     * @param array<string, mixed> $address
     */
    private function applyPointIdentifiers(array &$address, string $pointId): void
    {
        $normalizedPointId = trim($pointId);
        if ($normalizedPointId === '') {
            return;
        }

        $address['point'] = $normalizedPointId;
        $address['foreign_address_id'] = $normalizedPointId;
        $address['point_id'] = $normalizedPointId;
    }

    /**
     * @return array{street:string,postal_code:string,city:string}
     */
    private function resolvePointAddress(string $appId, string $appSecret, string $pointId): array
    {
        $normalizedPointId = strtoupper(trim($pointId));
        if ($normalizedPointId === '') {
            return ['street' => '', 'postal_code' => '', 'city' => ''];
        }
        if (isset($this->pointAddressCache[$normalizedPointId])) {
            return $this->pointAddressCache[$normalizedPointId];
        }

        $types = ['parcel_locker', 'pickup_point'];
        foreach ($types as $type) {
            try {
                $points = $this->apiClient->getPoints($appId, $appSecret, $type);
            } catch (Throwable) {
                continue;
            }
            foreach ($points as $point) {
                if (!is_array($point)) {
                    continue;
                }
                $candidateId = strtoupper(trim((string) ($point['id'] ?? $point['point_id'] ?? $point['code'] ?? '')));
                if ($candidateId === '' || $candidateId !== $normalizedPointId) {
                    continue;
                }

                $resolved = [
                    'street' => trim((string) ($point['street'] ?? $point['line1'] ?? $point['address'] ?? $point['street_name'] ?? '')),
                    'postal_code' => trim((string) ($point['postal_code'] ?? $point['zip_code'] ?? $point['zip'] ?? $point['postcode'] ?? '')),
                    'city' => trim((string) ($point['city'] ?? $point['town'] ?? '')),
                ];
                $this->pointAddressCache[$normalizedPointId] = $resolved;
                return $resolved;
            }
        }

        $this->pointAddressCache[$normalizedPointId] = ['street' => '', 'postal_code' => '', 'city' => ''];
        return $this->pointAddressCache[$normalizedPointId];
    }

    /**
     * @param array<string, mixed> $address
     */
    private function composeStreetLine(array $address): string
    {
        $street = trim((string) ($address['street_name'] ?? ''));
        $number = trim((string) ($address['street_number'] ?? ''));
        if ($street === '') {
            return '';
        }
        if ($number === '') {
            return $street;
        }

        return trim($street . ' ' . $number);
    }

    /**
     * @param array<string, mixed> $formData
     * @param array<int, mixed> $fallbacks
     */
    private function resolveStringField(array $formData, string $fieldName, array $fallbacks): string
    {
        $direct = trim((string) ($formData[$fieldName] ?? ''));
        if ($direct !== '') {
            return $direct;
        }

        foreach ($fallbacks as $fallback) {
            $value = trim((string) $fallback);
            if ($value !== '') {
                return $value;
            }
        }

        return '';
    }

    /**
     * @return array{point_id:string,street:string,postal_code:string,city:string}
     */
    private function parsePickupMeta(string $parcelName, string $parcelExternalId): array
    {
        $label = trim($parcelName);
        $pointId = trim($parcelExternalId);
        if ($label === '') {
            return [
                'point_id' => $pointId,
                'street' => '',
                'postal_code' => '',
                'city' => '',
            ];
        }

        $addressPart = $label;
        if (str_contains($label, '|')) {
            $parts = explode('|', $label, 2);
            $left = trim((string) ($parts[0] ?? ''));
            $right = trim((string) ($parts[1] ?? ''));
            if ($pointId === '' && $left !== '') {
                $pointId = $left;
            }
            if ($right !== '') {
                $addressPart = $right;
            }
        }

        $street = '';
        $postalCode = '';
        $city = '';
        if (preg_match('/^(.*?),\s*(\d{2}-\d{3})\s+(.+)$/u', $addressPart, $matches) === 1) {
            $street = trim((string) ($matches[1] ?? ''));
            $postalCode = trim((string) ($matches[2] ?? ''));
            $city = trim((string) ($matches[3] ?? ''));
        } elseif (preg_match('/(\d{2}-\d{3})\s+(.+)$/u', $addressPart, $matches) === 1) {
            $postalCode = trim((string) ($matches[1] ?? ''));
            $city = trim((string) ($matches[2] ?? ''));
            $street = trim(preg_replace('/\s*\d{2}-\d{3}\s+.+$/u', '', $addressPart) ?? '');
        }

        return [
            'point_id' => $pointId,
            'street' => $street,
            'postal_code' => $postalCode,
            'city' => $city,
        ];
    }

    /**
     * @param array<string, mixed> $sender
     * @return array<string, mixed>
     */
    private function normalizeSender(array $sender): array
    {
        $result = [
            'name' => trim((string) ($sender['name'] ?? $sender['company'] ?? '')),
            'line1' => trim((string) ($sender['street'] ?? '')),
            'postal_code' => trim((string) ($sender['postalCode'] ?? '')),
            'city' => trim((string) ($sender['city'] ?? '')),
            'country_code' => strtoupper(trim((string) ($sender['countryCode'] ?? 'PL'))),
            'phone' => trim((string) ($sender['phone'] ?? '')),
            'email' => trim((string) ($sender['email'] ?? '')),
        ];
        $contactPerson = trim((string) ($sender['contactPerson'] ?? $sender['name'] ?? ''));
        if ($contactPerson !== '') {
            $result['contact_person'] = $contactPerson;
            $result['person'] = $contactPerson;
        }

        return $result;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function resolveServiceDefinition(string $appId, string $appSecret, string $serviceId): ?array
    {
        if ($serviceId === '') {
            return null;
        }

        try {
            $services = $this->apiClient->getServiceStructure($appId, $appSecret);
        } catch (Throwable) {
            return null;
        }

        foreach ($services as $service) {
            if (!is_array($service)) {
                continue;
            }
            $currentServiceId = trim((string) ($service['service_id'] ?? $service['id'] ?? ''));
            if ($currentServiceId === $serviceId) {
                return $service;
            }
        }

        return null;
    }

    /**
     * @param array<string, mixed>|null $serviceDefinition
     * @param array<string, mixed> $receiverAddress
     * @param array<string, mixed> $senderAddress
     */
    private function validateServiceRequirements(
        ?array $serviceDefinition,
        array $receiverAddress,
        array $senderAddress
    ): void {
        if ($serviceDefinition === null) {
            return;
        }

        $requiresReceiverPoint = ((int) ($serviceDefinition['door_to_point'] ?? 0) === 1)
            || ((int) ($serviceDefinition['point_to_point'] ?? 0) === 1);
        $pickupCourierMode = (int) ($serviceDefinition['pickup_courier'] ?? 0);
        $requiresSenderPoint = $pickupCourierMode === 0;
        $receiverPoint = trim((string) ($receiverAddress['foreign_address_id'] ?? ''));
        $senderPoint = trim((string) ($senderAddress['foreign_address_id'] ?? ''));
        $serviceName = trim((string) ($serviceDefinition['name'] ?? ''));

        if ($requiresReceiverPoint && $receiverPoint === '') {
            throw new ShipmentException(
                'Wybrana usluga Apaczka (' . ($serviceName !== '' ? $serviceName : 'ID')
                . ') wymaga punktu odbioru (`receiver_point_id`).'
            );
        }
        if ($requiresSenderPoint && $senderPoint === '') {
            throw new ShipmentException(
                'Wybrana usluga Apaczka (' . ($serviceName !== '' ? $serviceName : 'ID')
                . ') wymaga punktu nadania (`sender_point_id`).'
            );
        }

    }

    /**
     * @param array<string, mixed>|null $serviceDefinition
     */
    private function resolvePickupType(?array $serviceDefinition, string $senderPointId): string
    {
        $hasSenderPoint = trim($senderPointId) !== '';
        if ($hasSenderPoint) {
            return 'SELF';
        }

        if ($serviceDefinition === null) {
            return 'COURIER';
        }

        $pickupCourierMode = (int) ($serviceDefinition['pickup_courier'] ?? 0);
        if ($pickupCourierMode === 0) {
            return 'SELF';
        }

        return 'COURIER';
    }

    /**
     * @param array<string, mixed>|null $serviceDefinition
     * @param array<string, mixed> $formData
     * @return array<string, mixed>
     */
    private function buildPickupPayload(?array $serviceDefinition, string $senderPointId, array $formData): array
    {
        $pickupType = $this->resolvePickupType($serviceDefinition, $senderPointId);
        if ($pickupType === '') {
            return [];
        }

        $pickup = ['type' => $pickupType];
        if ($pickupType !== 'COURIER') {
            return $pickup;
        }

        $pickupDate = trim((string) ($formData['pickup_date'] ?? ''));
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $pickupDate) !== 1) {
            $pickupDate = date('Y-m-d');
        }
        $pickupDate = $this->normalizeCourierPickupDate($pickupDate);

        // If pickup is today and current time past safe cutoff, move to next business day
        if ($pickupDate === date('Y-m-d') && (int) date('H') >= 16) {
            $nextDay = date('Y-m-d', strtotime('+1 day', strtotime($pickupDate)));
            $pickupDate = $this->normalizeCourierPickupDate($nextDay);
        }

        $hoursFrom = trim((string) ($formData['pickup_hours_from'] ?? ''));
        if (preg_match('/^\d{2}:\d{2}$/', $hoursFrom) !== 1) {
            $hoursFrom = '09:00';
        }
        $hoursTo = trim((string) ($formData['pickup_hours_to'] ?? ''));
        if (preg_match('/^\d{2}:\d{2}$/', $hoursTo) !== 1) {
            $hoursTo = '16:00';
        }
        [$hoursFrom, $hoursTo] = $this->normalizeCourierPickupHours($hoursFrom, $hoursTo);

        $pickup['date'] = $pickupDate;
        $pickup['hours_from'] = $hoursFrom;
        $pickup['hours_to'] = $hoursTo;

        return $pickup;
    }

    private function normalizeCourierPickupDate(string $pickupDate): string
    {
        $ts = strtotime($pickupDate);
        if ($ts === false) {
            $ts = time();
        }

        $today = date('Y-m-d');
        if (date('Y-m-d', $ts) < $today) {
            $ts = time();
        }

        // Apaczka rejects Sunday as pickup date.
        $weekday = (int) date('N', $ts);
        if ($weekday === 7) {
            $ts = strtotime('+1 day', $ts);
        }

        return date('Y-m-d', $ts);
    }

    /**
     * @return array{0:string,1:string}
     */
    private function normalizeCourierPickupHours(string $hoursFrom, string $hoursTo): array
    {
        $normalizedFrom = $hoursFrom;
        $normalizedTo = $hoursTo;
        $maxTo = '16:00';

        if ($this->compareTime($normalizedTo, $maxTo) > 0) {
            $normalizedTo = $maxTo;
        }
        if ($this->compareTime($normalizedFrom, $normalizedTo) >= 0) {
            $normalizedFrom = '09:00';
        }
        if ($this->compareTime($normalizedFrom, $normalizedTo) >= 0) {
            $normalizedFrom = '08:00';
        }
        if ($this->compareTime($normalizedFrom, $normalizedTo) >= 0) {
            $normalizedFrom = '00:00';
        }

        return [$normalizedFrom, $normalizedTo];
    }

    private function compareTime(string $left, string $right): int
    {
        return strcmp($left, $right);
    }

    /**
     * @param array<string, mixed>|null $serviceDefinition
     * @param array<string, mixed> $receiverAddress
     * @param array<string, mixed> $senderAddress
     * @param array<string, mixed> $apiPayload
     */
    private function buildShipmentErrorMessage(
        Throwable $exception,
        ?array $serviceDefinition,
        array $receiverAddress,
        array $senderAddress,
        array $apiPayload
    ): string {
        $message = trim($exception->getMessage());
        if ($message === '') {
            $message = 'Nieznany blad tworzenia przesylki Apaczka.';
        }

        $isValuationError = stripos($message, 'Brak wyceny dla podanych parametr') !== false;
        $isPickupMethodError = stripos($message, 'Niepoprawny sposób nadania przesyłki') !== false
            || stripos($message, 'Niepoprawny sposob nadania przesylki') !== false;
        if (!$isValuationError && !$isPickupMethodError) {
            return $message;
        }

        $serviceId = trim((string) ($apiPayload['service_id'] ?? ''));
        $serviceName = trim((string) ($serviceDefinition['name'] ?? ''));
        $supplier = trim((string) ($serviceDefinition['supplier'] ?? ''));
        $receiverPoint = trim((string) ($receiverAddress['foreign_address_id'] ?? ''));
        $senderPoint = trim((string) ($senderAddress['foreign_address_id'] ?? ''));
        $weight = (string) ($apiPayload['shipment'][0]['weight'] ?? '0');
        $width = (string) ((int) ($apiPayload['shipment'][0]['dimension2'] ?? 0));
        $height = (string) ((int) ($apiPayload['shipment'][0]['dimension3'] ?? 0));
        $length = (string) ((int) ($apiPayload['shipment'][0]['dimension1'] ?? 0));
        $parts = [];
        $parts[] = 'service_id=' . ($serviceId !== '' ? $serviceId : '-');
        if ($serviceName !== '') {
            $parts[] = 'service_name=' . $serviceName;
        }
        if ($supplier !== '') {
            $parts[] = 'supplier=' . $supplier;
        }
        $parts[] = 'receiver_point_id=' . ($receiverPoint !== '' ? $receiverPoint : '(brak)');
        $parts[] = 'sender_point_id=' . ($senderPoint !== '' ? $senderPoint : '(brak)');
        $parts[] = 'gabaryt_cm=' . $length . 'x' . $width . 'x' . $height;
        $parts[] = 'waga_kg=' . $weight;
        $pickupType = trim((string) ($apiPayload['pickup']['type'] ?? ''));
        $pickupDate = trim((string) ($apiPayload['pickup']['date'] ?? ''));
        $pickupFrom = trim((string) ($apiPayload['pickup']['hours_from'] ?? ''));
        $pickupTo = trim((string) ($apiPayload['pickup']['hours_to'] ?? ''));
        if ($pickupType !== '') {
            $parts[] = 'pickup.type=' . $pickupType;
        }
        if ($pickupDate !== '') {
            $parts[] = 'pickup.date=' . $pickupDate;
        }
        if ($pickupFrom !== '' || $pickupTo !== '') {
            $parts[] = 'pickup.hours=' . ($pickupFrom !== '' ? $pickupFrom : '-') . '-' . ($pickupTo !== '' ? $pickupTo : '-');
        }

        $hint = 'Sprawdz zgodnosc typu uslugi z typem punktu oraz uzupelnij wymagane punkty nadania/odbioru.';
        if ($isPickupMethodError) {
            $hint = 'Apaczka odrzucila sposob nadania. Dla uslug punktowych uzupelnij `sender_point_id` '
                . 'lub wybierz usluge z odbiorem przez kuriera (door_to_point).';
        }
        if ($receiverPoint !== '' && str_starts_with(strtoupper($receiverPoint), 'POP-') && strtoupper($supplier) === 'INPOST') {
            $hint = 'Prefiks punktu (`POP-`) moze byc mapowany roznie zaleznie od konfiguracji przewoznika. '
                . 'Jesli to punkt InPost, zignoruj ta sugestie.';
        }

        return $message . ' Diagnostyka: ' . implode('; ', $parts) . '. ' . $hint;
    }

    /**
     * @param array<string, mixed> $sender
     */
    private function validateSenderAddress(array $sender): void
    {
        $required = ['street', 'city', 'postalCode', 'phone', 'email'];
        foreach ($required as $field) {
            if (trim((string) ($sender[$field] ?? '')) === '') {
                throw new IntegrationConfigException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak: ' . $field . ').');
            }
        }

        $name = trim((string) ($sender['name'] ?? ''));
        $company = trim((string) ($sender['company'] ?? ''));
        if ($name === '' && $company === '') {
            throw new IntegrationConfigException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak nazwy/firmy).');
        }
        $contactPerson = trim((string) ($sender['contactPerson'] ?? $name ?? ''));
        if ($contactPerson === '') {
            throw new IntegrationConfigException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak osoby kontaktowej).');
        }
    }
}
