<?php
declare(strict_types=1);

namespace App\Modules\Shipments;

use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Exceptions\ShipmentException;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\AllegroApiClient;
use App\Modules\Settings\AllegroTokenManager;
use App\Modules\Settings\CompanySettingsRepository;
use RuntimeException;
use Throwable;

final class AllegroShipmentService implements ShipmentProviderInterface
{
    public function __construct(
        private readonly AllegroTokenManager $tokenManager,
        private readonly AllegroApiClient $apiClient,
        private readonly ShipmentPackageRepository $packages,
        private readonly CompanySettingsRepository $companySettings,
        private readonly OrdersRepository $ordersRepository
    ) {
    }

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

    /**
     * @return array<int, array<string, mixed>>
     */
    public function getDeliveryServices(): array
    {
        [$accessToken, $env] = $this->tokenManager->resolveToken();
        $response = $this->apiClient->getDeliveryServices($env, $accessToken);

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

    /**
     * @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.');
        }

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

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

        $receiverAddress = $this->buildReceiverAddress($order, $formData);
        $senderAddress = $sender;
        if (trim((string) ($formData['sender_point_id'] ?? '')) !== '') {
            $senderAddress['point'] = trim((string) $formData['sender_point_id']);
        }

        $packageType = strtoupper(trim((string) ($formData['package_type'] ?? 'PACKAGE')));
        $lengthCm = (float) ($formData['length_cm'] ?? $company['default_package_length_cm']);
        $widthCm = (float) ($formData['width_cm'] ?? $company['default_package_width_cm']);
        $heightCm = (float) ($formData['height_cm'] ?? $company['default_package_height_cm']);
        $weightKg = (float) ($formData['weight_kg'] ?? $company['default_package_weight_kg']);
        $labelFormat = strtoupper(trim((string) ($formData['label_format'] ?? $company['default_label_format'])));
        if (!in_array($labelFormat, ['PDF', 'ZPL'], true)) {
            $labelFormat = 'PDF';
        }

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

        $commandId = $this->generateUuid();

        $apiPayload = [
            'commandId' => $commandId,
            'input' => [
                'deliveryMethodId' => $deliveryMethodId,
                'sender' => $senderAddress,
                'receiver' => $receiverAddress,
                'referenceNumber' => substr($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId, 0, 35),
                'packages' => [[
                    'type' => $packageType,
                    'length' => ['value' => $lengthCm, 'unit' => 'CENTIMETER'],
                    'width' => ['value' => $widthCm, 'unit' => 'CENTIMETER'],
                    'height' => ['value' => $heightCm, 'unit' => 'CENTIMETER'],
                    'weight' => ['value' => $weightKg, 'unit' => 'KILOGRAMS'],
                ]],
                'labelFormat' => $labelFormat,
            ],
        ];

        $insuranceAmount = (float) ($formData['insurance_amount'] ?? 0);
        if ($insuranceAmount > 0) {
            $apiPayload['input']['insurance'] = [
                'amount' => number_format($insuranceAmount, 2, '.', ''),
                'currency' => strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))),
            ];
        }

        $codAmount = (float) ($formData['cod_amount'] ?? 0);
        if ($codAmount > 0) {
            // Allegro WZA manages COD funds internally - iban/ownerName are not accepted.
            $apiPayload['input']['cashOnDelivery'] = [
                'amount' => number_format($codAmount, 2, '.', ''),
                'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
            ];
        }

        $credentialsId = trim((string) ($formData['credentials_id'] ?? ''));
        if ($credentialsId !== '') {
            $apiPayload['input']['credentialsId'] = $credentialsId;
        }

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

        [$accessToken, $env] = $this->tokenManager->resolveToken();

        try {
            $response = $this->apiClient->createShipment($env, $accessToken, $apiPayload);
        } catch (RuntimeException $exception) {
            if (trim($exception->getMessage()) === 'ALLEGRO_HTTP_401') {
                [$accessToken, $env] = $this->tokenManager->resolveToken();
                $response = $this->apiClient->createShipment($env, $accessToken, $apiPayload);
            } else {
                $this->packages->update($packageId, [
                    'status' => 'error',
                    'error_message' => $exception->getMessage(),
                ]);
                throw $exception;
            }
        }

        $returnedCommandId = trim((string) ($response['commandId'] ?? $commandId));
        $this->packages->update($packageId, [
            'command_id' => $returnedCommandId,
            'payload_json' => $response,
        ]);

        return [
            'package_id' => $packageId,
            'command_id' => $returnedCommandId,
        ];
    }

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

        $commandId = trim((string) ($package['command_id'] ?? ''));
        if ($commandId === '') {
            throw new ShipmentException('Brak command_id dla tej paczki.');
        }

        [$accessToken, $env] = $this->tokenManager->resolveToken();
        $response = $this->apiClient->getShipmentCreationStatus($env, $accessToken, $commandId);

        $status = strtoupper(trim((string) ($response['status'] ?? '')));
        $shipmentId = trim((string) ($response['shipmentId'] ?? ''));

        if ($status === 'SUCCESS' && $shipmentId !== '') {
            $details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
            $detailPackages = is_array($details['packages'] ?? null) ? $details['packages'] : [];
            $trackingNumber = trim((string) ($detailPackages[0]['waybill'] ?? ''));
            $carrierId = trim((string) ($package['carrier_id'] ?? ''));
            if ($carrierId === '') {
                $carrierId = trim((string) ($details['carrier'] ?? ''));
            }

            $this->packages->update($packageId, [
                'status' => 'created',
                'shipment_id' => $shipmentId,
                'tracking_number' => $trackingNumber !== '' ? $trackingNumber : null,
                'carrier_id' => $carrierId !== '' ? $carrierId : ($package['carrier_id'] ?? null),
                'payload_json' => $details,
            ]);

            $this->pushWaybillToAllegroCheckoutForm(
                (int) ($package['order_id'] ?? 0),
                $trackingNumber,
                $carrierId,
                $accessToken,
                $env
            );

            return [
                'status' => 'created',
                'shipment_id' => $shipmentId,
                'tracking_number' => $trackingNumber,
            ];
        }

        if ($status === 'ERROR') {
            $errors = is_array($response['errors'] ?? null) ? $response['errors'] : [];
            $messages = [];
            foreach ($errors as $err) {
                if (is_array($err)) {
                    $messages[] = trim((string) ($err['message'] ?? ($err['userMessage'] ?? '')));
                }
            }
            $errorMsg = implode('; ', array_filter($messages)) ?: 'Blad tworzenia przesylki.';

            $this->packages->update($packageId, [
                'status' => 'error',
                'error_message' => $errorMsg,
                'payload_json' => $response,
            ]);

            return ['status' => 'error', 'error' => $errorMsg];
        }

        return ['status' => 'in_progress'];
    }

    /**
     * @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 = trim((string) ($package['shipment_id'] ?? ''));
        if ($shipmentId === '') {
            throw new ShipmentException('Przesylka nie zostala jeszcze utworzona.');
        }

        [$accessToken, $env] = $this->tokenManager->resolveToken();
        $labelFormat = trim((string) ($package['label_format'] ?? 'PDF'));
        $pageSize = 'A6';
        $binary = $this->apiClient->getShipmentLabel($env, $accessToken, [$shipmentId], $pageSize);

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

        $ext = $labelFormat === 'ZPL' ? 'zpl' : 'pdf';
        $filename = 'label_' . $packageId . '_' . $shipmentId . '.' . $ext;
        $filePath = $dir . '/' . $filename;
        file_put_contents($filePath, $binary);

        $updateFields = [
            'status' => 'label_ready',
            'label_path' => 'labels/' . $filename,
        ];

        // Refresh tracking number if not yet saved (may not have been available at creation time).
        if (trim((string) ($package['tracking_number'] ?? '')) === '') {
            try {
                $details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
                $detailPackages = is_array($details['packages'] ?? null) ? $details['packages'] : [];
                $trackingNumber = trim((string) ($detailPackages[0]['waybill'] ?? ''));
                $carrierId = trim((string) ($package['carrier_id'] ?? ''));
                if ($carrierId === '') {
                    $carrierId = trim((string) ($details['carrier'] ?? ''));
                }

                if ($trackingNumber !== '') {
                    $updateFields['tracking_number'] = $trackingNumber;
                    if ($carrierId !== '') {
                        $updateFields['carrier_id'] = $carrierId;
                    }

                    $this->pushWaybillToAllegroCheckoutForm(
                        (int) ($package['order_id'] ?? 0),
                        $trackingNumber,
                        $carrierId,
                        $accessToken,
                        $env
                    );
                }
            } catch (Throwable) {
                // non-critical - label is still saved
            }
        }

        $this->packages->update($packageId, $updateFields);

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

    /**
     * @param array<string, mixed>|null $orderDetails
     * @param array<string, mixed> $formData
     * @return array<string, mixed>
     */
    private function buildReceiverAddress(?array $orderDetails, array $formData): 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;
            }
        }

        $addr = $deliveryAddr ?? $customerAddr ?? [];

        $email = trim((string) ($formData['receiver_email'] ?? ($addr['email'] ?? '')));
        $receiver = [
            'name' => trim((string) ($formData['receiver_name'] ?? ($addr['name'] ?? ''))),
            'street' => trim((string) ($formData['receiver_street'] ?? ($addr['street_name'] ?? ''))),
            'city' => trim((string) ($formData['receiver_city'] ?? ($addr['city'] ?? ''))),
            'postalCode' => trim((string) ($formData['receiver_postal_code'] ?? ($addr['zip_code'] ?? ''))),
            'countryCode' => strtoupper(trim((string) ($formData['receiver_country_code'] ?? ($addr['country'] ?? 'PL')))),
            'phone' => trim((string) ($formData['receiver_phone'] ?? ($addr['phone'] ?? ''))),
            'email' => $email,
        ];

        $buyerEmail = trim((string) ($customerAddr['email'] ?? $email));
        if ($buyerEmail !== '') {
            $receiver['hashedMail'] = hash('sha256', strtolower($buyerEmail));
        }

        $company = trim((string) ($formData['receiver_company'] ?? ($addr['company_name'] ?? '')));
        if ($company !== '') {
            $receiver['company'] = $company;
        }

        $pointId = trim((string) ($formData['receiver_point_id'] ?? ($addr['parcel_external_id'] ?? '')));
        if ($pointId !== '') {
            $receiver['point'] = $pointId;
        }

        return $receiver;
    }

    /**
     * @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).');
        }
    }

    private function pushWaybillToAllegroCheckoutForm(
        int $orderId,
        string $trackingNumber,
        string $carrierId,
        string $accessToken,
        string $environment
    ): void {
        if ($orderId <= 0) {
            return;
        }

        $waybill = trim($trackingNumber);
        if ($waybill === '') {
            return;
        }

        $orderDetails = $this->ordersRepository->findDetails($orderId);
        if ($orderDetails === null) {
            return;
        }

        $order = is_array($orderDetails['order'] ?? null) ? $orderDetails['order'] : [];
        $source = strtolower(trim((string) ($order['source'] ?? '')));
        if ($source !== 'allegro') {
            return;
        }

        $checkoutFormId = trim((string) ($order['source_order_id'] ?? ''));
        if ($checkoutFormId === '') {
            return;
        }

        $normalizedCarrierId = trim($carrierId);
        if ($normalizedCarrierId === '') {
            return;
        }

        $carrierName = (string) ($this->packages->resolveCarrierName('allegro_wza', $normalizedCarrierId) ?? '');
        $carrierName = trim($carrierName);
        if ($carrierName === '') {
            $carrierName = $normalizedCarrierId;
        }

        try {
            $this->apiClient->addShipmentToOrder(
                $environment,
                $accessToken,
                $checkoutFormId,
                $waybill,
                $normalizedCarrierId,
                $carrierName
            );
        } catch (RuntimeException $exception) {
            if (trim($exception->getMessage()) !== 'ALLEGRO_HTTP_401') {
                return;
            }

            try {
                [$refreshedToken, $refreshedEnvironment] = $this->tokenManager->resolveToken();
                $this->apiClient->addShipmentToOrder(
                    $refreshedEnvironment,
                    $refreshedToken,
                    $checkoutFormId,
                    $waybill,
                    $normalizedCarrierId,
                    $carrierName
                );
            } catch (Throwable) {
                // non-critical - local shipment remains created
            }
        } catch (Throwable) {
            // non-critical - local shipment remains created
        }
    }

    private function generateUuid(): string
    {
        $data = random_bytes(16);
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }
}