<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Constants\IntegrationSources;
use App\Core\Support\StringHelper;

final class ShopproOrderMapper
{
    /**
     * @param array<int, array<string, mixed>> $items
     * @return array<int, array{source_order_id:string,source_updated_at:string,payload:array<string,mixed>}>
     */
    public function buildCandidates(array $items, ?string $cursorUpdatedAt, ?string $cursorOrderId): array
    {
        $result = [];
        foreach ($items as $row) {
            $sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id']));
            $sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
            if ($sourceOrderId === '' || $sourceUpdatedAt === null) {
                continue;
            }
            if (!$this->isAfterCursor($sourceUpdatedAt, $sourceOrderId, $cursorUpdatedAt, $cursorOrderId)) {
                continue;
            }

            $result[] = [
                'source_order_id' => $sourceOrderId,
                'source_updated_at' => $sourceUpdatedAt,
                'payload' => $row,
            ];
        }

        usort($result, static function (array $a, array $b): int {
            $cmp = strcmp((string) ($a['source_updated_at'] ?? ''), (string) ($b['source_updated_at'] ?? ''));
            if ($cmp !== 0) {
                return $cmp;
            }

            return strcmp((string) ($a['source_order_id'] ?? ''), (string) ($b['source_order_id'] ?? ''));
        });

        return $result;
    }

    /**
     * @param array<string, mixed> $payload
     * @param array<string, string> $statusMap
     * @param array<int, string> $productImagesById
     * @return array{
     *   order:array<string,mixed>,
     *   addresses:array<int,array<string,mixed>>,
     *   items:array<int,array<string,mixed>>,
     *   payments:array<int,array<string,mixed>>,
     *   shipments:array<int,array<string,mixed>>,
     *   notes:array<int,array<string,mixed>>,
     *   status_history:array<int,array<string,mixed>>
     * }
     */
    public function mapOrderAggregate(
        int $integrationId,
        array $payload,
        array $statusMap,
        string $fallbackOrderId,
        string $fallbackUpdatedAt,
        array $productImagesById = []
    ): array {
        $sourceOrderId = $this->normalizeOrderId($this->readPath($payload, ['id', 'order_id', 'external_order_id']));
        if ($sourceOrderId === '') {
            $sourceOrderId = $fallbackOrderId;
        }

        $sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add', 'date_order']));
        $sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
        if ($sourceUpdatedAt === null) {
            $sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
        }

        $originalStatus = strtolower(trim((string) $this->readPath($payload, ['status', 'status_code', 'order_status'])));
        $effectiveStatus = $statusMap[$originalStatus] ?? $originalStatus;
        if ($effectiveStatus === '') {
            $effectiveStatus = 'new';
        }

        $currency = trim((string) $this->readPath($payload, ['currency', 'totals.currency']));
        if ($currency === '') {
            $currency = 'PLN';
        }

        $totalGross = $this->toFloatOrNull($this->readPath($payload, [
            'total_gross', 'total_with_tax', 'summary.total', 'totals.gross', 'summary', 'amount',
        ]));
        $transportCost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
        if ($totalGross === null && $transportCost !== null) {
            $productsSum = 0.0;
            $hasProducts = false;
            $rawItemsForSummary = $this->readPath($payload, ['products', 'items', 'order_items']);
            if (is_array($rawItemsForSummary)) {
                foreach ($rawItemsForSummary as $rawItem) {
                    if (!is_array($rawItem)) {
                        continue;
                    }
                    $itemPrice = $this->toFloatOrNull($this->readPath($rawItem, [
                        'price_brutto', 'price_gross', 'gross_price', 'price',
                    ]));
                    $itemQty = $this->toFloatOrDefault($this->readPath($rawItem, ['quantity', 'qty']), 1.0);
                    if ($itemPrice === null) {
                        continue;
                    }
                    $hasProducts = true;
                    $productsSum += ($itemPrice * $itemQty);
                }
            }
            if ($hasProducts) {
                $totalGross = $productsSum + $transportCost;
            }
        }
        $totalNet = $this->toFloatOrNull($this->readPath($payload, ['total_net', 'total_without_tax', 'totals.net']));
        $totalPaid = $this->toFloatOrNull($this->readPath($payload, ['total_paid', 'payments.total_paid', 'payment.total', 'paid_amount']));
        $paidFlag = $this->readPath($payload, ['paid', 'is_paid']);
        $isPaid = $this->normalizePaidFlag($paidFlag);
        if ($totalPaid === null) {
            if ($isPaid && $totalGross !== null) {
                $totalPaid = $totalGross;
            }
        }

        $deliveryLabel = $this->buildDeliveryMethodLabel($payload);

        $order = [
            'integration_id' => $integrationId,
            'source' => IntegrationSources::SHOPPRO,
            'source_order_id' => $sourceOrderId,
            'external_order_id' => $sourceOrderId,
            'external_platform_id' => IntegrationSources::SHOPPRO,
            'external_platform_account_id' => null,
            'external_status_id' => $effectiveStatus,
            'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
            'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
            'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
            'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
                'transport_id', 'shipping.method_id', 'delivery.method_id',
            ])),
            'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [
                'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
            ])),
            'is_invoice' => $this->resolveInvoiceRequested($payload),
            'is_encrypted' => false,
            'is_canceled_by_buyer' => false,
            'currency' => $currency,
            'total_without_tax' => $totalNet,
            'total_with_tax' => $totalGross,
            'total_paid' => $totalPaid,
            'send_date_min' => null,
            'send_date_max' => null,
            'ordered_at' => $sourceCreatedAt,
            'source_created_at' => $sourceCreatedAt,
            'source_updated_at' => $sourceUpdatedAt,
            'preferences_json' => null,
            'payload_json' => $payload,
            'fetched_at' => date('Y-m-d H:i:s'),
        ];

        $addresses = $this->mapAddresses($payload);
        $items = $this->mapItems($payload, $productImagesById);
        $payments = $this->mapPayments($payload, $currency, $totalPaid);
        $shipments = $this->mapShipments($payload);
        $notes = $this->mapNotes($payload);
        $statusHistory = [[
            'from_status_id' => null,
            'to_status_id' => $effectiveStatus,
            'changed_at' => $sourceUpdatedAt,
            'change_source' => 'import',
            'comment' => $originalStatus !== '' ? 'shopPRO status: ' . $originalStatus : null,
            'payload_json' => null,
        ]];

        return [
            'order' => $order,
            'addresses' => $addresses,
            'items' => $items,
            'payments' => $payments,
            'shipments' => $shipments,
            'notes' => $notes,
            'status_history' => $statusHistory,
        ];
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<int, array<string, mixed>>
     */
    private function mapAddresses(array $payload): array
    {
        $result = [];

        $customerData = $this->buildCustomerAddress($payload);
        $result[] = $customerData['address'];

        $invoiceAddress = $this->buildInvoiceAddress(
            $payload,
            $customerData['name'],
            $customerData['email'],
            $customerData['phone']
        );
        if ($invoiceAddress !== null) {
            $result[] = $invoiceAddress;
        }

        $deliveryAddress = $this->buildDeliveryAddress(
            $payload,
            $customerData['name'],
            $customerData['email'],
            $customerData['phone']
        );
        if ($deliveryAddress !== null) {
            $result[] = $deliveryAddress;
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array{address:array<string,mixed>,name:?string,email:?string,phone:?string}
     */
    private function buildCustomerAddress(array $payload): array
    {
        $customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
            'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname',
            'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname',
            'first_name', 'firstname', 'client_name', 'imie',
        ]));
        $customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [
            'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname',
            'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname',
            'last_name', 'lastname', 'client_surname', 'nazwisko',
        ]));
        $customerName = StringHelper::nullableString((string) $this->readPath($payload, [
            'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name',
            'receiver.name', 'client', 'customer_full_name', 'client_full_name',
        ])) ?? $this->composeName($customerFirstName, $customerLastName, 'Klient');

        $customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [
            'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email',
            'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail',
        ]));
        $customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [
            'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone',
            'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone',
            'phone_number', 'client_phone_number',
        ]));

        $address = [
            'address_type' => 'customer',
            'name' => $customerName ?? 'Klient',
            'phone' => $customerPhone,
            'email' => $customerEmail,
            'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
                'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street',
                'address.street', 'street', 'client_street', 'ulica',
            ])),
            'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
                'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number',
                'billing_address.house_number', 'client.address.street_number', 'address.street_number',
                'house_number', 'street_no', 'street_number', 'nr_domu',
            ])),
            'city' => StringHelper::nullableString((string) $this->readPath($payload, [
                'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city',
                'address.city', 'city', 'client_city', 'miejscowosc',
            ])),
            'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
                'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode',
                'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip',
                'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy',
            ])),
            'country' => StringHelper::nullableString((string) $this->readPath($payload, [
                'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country',
                'address.country', 'country', 'kraj',
            ])),
            'payload_json' => [
                'buyer' => $this->readPath($payload, ['buyer']),
                'customer' => $this->readPath($payload, ['customer']),
                'billing_address' => $this->readPath($payload, ['billing_address']),
                'buyer_address' => $this->readPath($payload, ['buyer_address']),
                'address' => $this->readPath($payload, ['address']),
            ],
        ];

        return ['address' => $address, 'name' => $customerName, 'email' => $customerEmail, 'phone' => $customerPhone];
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>|null
     */
    private function buildDeliveryAddress(array $payload, ?string $customerName, ?string $customerEmail, ?string $customerPhone): ?array
    {
        $deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
            'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
            'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
            'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name',
        ]));
        $deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [
            'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname',
            'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname',
            'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name',
        ]));
        $deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [
            'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name',
            'receiver.name', 'delivery_name', 'shipping_name',
        ])) ?? $this->composeName($deliveryFirstName, $deliveryLastName, null);

        $pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point']));
        $fields = [
            'name' => $deliveryName ?? StringHelper::nullableString($this->buildDeliveryMethodLabel($payload)),
            'phone' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone',
                'receiver.phone', 'delivery_phone', 'shipping_phone',
            ])) ?? $customerPhone,
            'email' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email',
                'receiver.email', 'delivery_email', 'shipping_email',
            ])) ?? $customerEmail,
            'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street',
                'receiver.address.street', 'delivery_street', 'shipping_street',
            ])) ?? StringHelper::nullableString($pickupData['street'] ?? ''),
            'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number',
                'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number',
                'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number',
            ])),
            'city' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city',
                'receiver.address.city', 'delivery_city', 'shipping_city',
            ])) ?? StringHelper::nullableString($pickupData['city'] ?? ''),
            'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode',
                'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode',
                'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode',
                'shipping_zip', 'shipping_postcode',
            ])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''),
            'country' => StringHelper::nullableString((string) $this->readPath($payload, [
                'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country',
                'receiver.address.country', 'delivery_country', 'shipping_country',
            ])),
            'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''),
            'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''),
            'payload_json' => [
                'delivery' => $this->readPath($payload, ['delivery']),
                'shipping' => $this->readPath($payload, ['shipping']),
                'delivery_address' => $this->readPath($payload, ['delivery_address']),
                'shipping_address' => $this->readPath($payload, ['shipping_address']),
                'receiver' => $this->readPath($payload, ['receiver']),
                'inpost_paczkomat' => $this->readPath($payload, ['inpost_paczkomat']),
                'orlen_point' => $this->readPath($payload, ['orlen_point']),
            ],
        ];

        if (!$this->hasAddressData($fields)) {
            return null;
        }

        return [
            'address_type' => 'delivery',
            'name' => $fields['name'] ?? $customerName ?? 'Dostawa',
            'phone' => $fields['phone'] ?? null,
            'email' => $fields['email'] ?? $customerEmail,
            'street_name' => $fields['street_name'] ?? null,
            'street_number' => $fields['street_number'] ?? null,
            'city' => $fields['city'] ?? null,
            'zip_code' => $fields['zip_code'] ?? null,
            'country' => $fields['country'] ?? null,
            'parcel_external_id' => $fields['parcel_external_id'] ?? null,
            'parcel_name' => $fields['parcel_name'] ?? null,
            'payload_json' => is_array($fields['payload_json'] ?? null) ? $fields['payload_json'] : null,
        ];
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function resolveInvoiceRequested(array $payload): bool
    {
        $explicitInvoice = $this->readPath($payload, ['is_invoice', 'invoice.required', 'invoice']);
        if (!empty($explicitInvoice)) {
            return true;
        }

        $companyName = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
            'firm_name', 'company_name', 'client_company', 'buyer_company',
        ]));
        $taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
            'firm_nip', 'company_nip', 'tax_id', 'nip',
        ]));

        return $companyName !== null || $taxNumber !== null;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>|null
     */
    private function buildInvoiceAddress(
        array $payload,
        ?string $customerName,
        ?string $customerEmail,
        ?string $customerPhone
    ): ?array {
        $companyName = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
            'firm_name', 'company_name', 'client_company', 'buyer_company',
        ]));
        $companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
            'firm_nip', 'company_nip', 'tax_id', 'nip',
        ]));
        $invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
            'buyer.first_name', 'customer.first_name', 'client_name',
        ]));
        $invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
            'buyer.last_name', 'customer.last_name', 'client_surname',
        ]));
        $invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');

        $streetName = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
            'firm_street', 'company_street',
        ]));
        $streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
            'billing_address.street_number', 'billing_address.house_number',
            'billing.street_number', 'house_number', 'street_number',
        ]));
        $city = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
            'firm_city', 'company_city',
        ]));
        $zipCode = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
            'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
            'firm_postal_code', 'company_postal_code',
        ]));
        $country = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
            'firm_country', 'company_country',
        ]));
        $email = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
        ])) ?? $customerEmail;
        $phone = StringHelper::nullableString((string) $this->readPath($payload, [
            'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
        ])) ?? $customerPhone;

        $hasInvoiceData = $companyName !== null
            || $companyTaxNumber !== null
            || $streetName !== null
            || $city !== null
            || $zipCode !== null;
        if (!$hasInvoiceData) {
            return null;
        }

        return [
            'address_type' => 'invoice',
            'name' => $invoiceName ?? 'Faktura',
            'phone' => $phone,
            'email' => $email,
            'street_name' => $streetName,
            'street_number' => $streetNumber,
            'city' => $city,
            'zip_code' => $zipCode,
            'country' => $country,
            'company_tax_number' => $companyTaxNumber,
            'company_name' => $companyName,
            'payload_json' => [
                'invoice' => $this->readPath($payload, ['invoice']),
                'billing' => $this->readPath($payload, ['billing']),
                'billing_address' => $this->readPath($payload, ['billing_address']),
                'firm_name' => $this->readPath($payload, ['firm_name']),
                'firm_nip' => $this->readPath($payload, ['firm_nip']),
                'firm_street' => $this->readPath($payload, ['firm_street']),
                'firm_postal_code' => $this->readPath($payload, ['firm_postal_code']),
                'firm_city' => $this->readPath($payload, ['firm_city']),
            ],
        ];
    }

    private function composeName(?string $firstName, ?string $lastName, ?string $fallback): ?string
    {
        $name = trim(trim((string) $firstName) . ' ' . trim((string) $lastName));
        if ($name !== '') {
            return $name;
        }

        $fallbackValue = trim((string) $fallback);
        return $fallbackValue !== '' ? $fallbackValue : null;
    }

    /**
     * @param array<string, mixed> $address
     */
    private function hasAddressData(array $address): bool
    {
        $fields = ['name', 'phone', 'email', 'street_name', 'street_number', 'city', 'zip_code', 'country'];
        foreach ($fields as $field) {
            $value = trim((string) ($address[$field] ?? ''));
            if ($value !== '') {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<string, mixed> $payload
     * @param array<int, string> $productImagesById
     * @return array<int, array<string, mixed>>
     */
    private function mapItems(array $payload, array $productImagesById = []): array
    {
        $rawItems = $this->readPath($payload, ['items']);
        if (!is_array($rawItems)) {
            $rawItems = $this->readPath($payload, ['order_items']);
        }
        if (!is_array($rawItems)) {
            $rawItems = $this->readPath($payload, ['products']);
        }
        if (!is_array($rawItems)) {
            return [];
        }

        $result = [];
        $sort = 0;
        foreach ($rawItems as $row) {
            if (!is_array($row)) {
                continue;
            }
            $name = trim((string) $this->readPath($row, ['name', 'title']));
            if ($name === '') {
                continue;
            }

            $productId = (int) $this->readPath($row, ['product_id']);
            $parentProductId = (int) $this->readPath($row, ['parent_product_id']);
            $mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
            if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) {
                $mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]);
            }
            if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) {
                $mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]);
            }

            $result[] = [
                'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
                'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
                'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])),
                'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
                'original_name' => $name,
                'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])),
                'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])),
                'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])),
                'media_url' => $mediaUrl,
                'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0),
                'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])),
                'item_status' => null,
                'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])),
                'item_type' => 'product',
                'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
                'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
                'sort_order' => $sort++,
                'payload_json' => $row,
                'personalization' => $this->extractPersonalization($row),
            ];
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $row
     */
    private function extractPersonalization(array $row): ?string
    {
        $raw = $this->readPath($row, ['custom_fields']);
        if ($raw === null || $raw === '' || $raw === false) {
            return null;
        }

        $text = str_replace(['<br>', '<br/>', '<br />'], "\n", (string) $raw);
        $text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $text = trim($text);

        return $text !== '' ? $text : null;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<int, array<string, mixed>>
     */
    private function mapPayments(array $payload, string $currency, ?float $totalPaid): array
    {
        $paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
        if ($paymentMethod === null && $totalPaid === null) {
            return [];
        }

        return [[
            'source_payment_id' => null,
            'external_payment_id' => null,
            'payment_type_id' => $paymentMethod ?? 'unknown',
            'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
            'amount' => $totalPaid,
            'currency' => $currency,
            'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
            'payload_json' => null,
        ]];
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<int, array<string, mixed>>
     */
    private function mapShipments(array $payload): array
    {
        $tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
        if ($tracking === null) {
            return [];
        }

        return [[
            'source_shipment_id' => null,
            'external_shipment_id' => null,
            'tracking_number' => $tracking,
            'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [
                'delivery_method', 'shipping.method', 'transport', 'transport_description',
            ]) ?? 'unknown')),
            'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
            'media_uuid' => null,
            'payload_json' => null,
        ]];
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<int, array<string, mixed>>
     */
    private function mapNotes(array $payload): array
    {
        $comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
        if ($comment === null) {
            return [];
        }

        return [[
            'source_note_id' => null,
            'note_type' => 'message',
            'created_at_external' => null,
            'comment' => $comment,
            'payload_json' => null,
        ]];
    }

    private function normalizeOrderId(mixed $value): string
    {
        return trim((string) $value);
    }

    private function normalizePaidFlag(mixed $value): bool
    {
        if ($value === true) {
            return true;
        }
        if ($value === false || $value === null) {
            return false;
        }

        $normalized = strtolower(trim((string) $value));
        return in_array($normalized, ['1', 'true', 'yes', 'paid'], true);
    }

    private function mapPaymentStatus(array $payload, bool $isPaid): ?int
    {
        if ($isPaid) {
            return 2;
        }

        $raw = strtolower(trim((string) $this->readPath($payload, ['payment_status', 'payment.status'])));
        if ($raw === '') {
            return 0;
        }

        return match ($raw) {
            'paid', 'finished', 'completed' => 2,
            'partially_paid', 'in_progress' => 1,
            'cancelled', 'canceled', 'failed', 'unpaid', 'not_paid' => 0,
            default => 0,
        };
    }

    private function sanitizePlainText(string $value): string
    {
        $withoutTags = strip_tags($value);
        $decoded = html_entity_decode($withoutTags, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        return trim(preg_replace('/\s+/', ' ', $decoded) ?? '');
    }

    private function buildDeliveryMethodLabel(array $payload): string
    {
        $label = $this->sanitizePlainText((string) $this->readPath($payload, [
            'delivery_method', 'shipping.method', 'delivery.method', 'shipping_method', 'delivery_name', 'shipping_name',
            'transport', 'transport_description',
        ]));
        $cost = $this->toFloatOrNull($this->readPath($payload, ['transport_cost', 'delivery_cost', 'shipping.cost']));
        if ($label !== '' && $cost !== null) {
            $label .= ': ' . $this->formatMoneyCompact($cost) . ' zł';
        }

        return $label;
    }

    private function formatMoneyCompact(float $amount): string
    {
        $formatted = number_format($amount, 2, '.', '');
        return rtrim(rtrim($formatted, '0'), '.');
    }

    /**
     * @return array{code:string,label:string,street:string,zip_code:string,city:string}
     */
    private function parsePickupPoint(string $raw): array
    {
        $value = trim($this->sanitizePlainText($raw));
        if ($value === '') {
            return ['code' => '', 'label' => '', 'street' => '', 'zip_code' => '', 'city' => ''];
        }

        $code = '';
        $address = $value;
        $parts = preg_split('/\s*\|\s*/', $value);
        if (is_array($parts) && count($parts) >= 2) {
            $code = trim((string) ($parts[0] ?? ''));
            $address = trim((string) ($parts[1] ?? ''));
        }

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

        return [
            'code' => $code,
            'label' => $value,
            'street' => $street,
            'zip_code' => $zip,
            'city' => $city,
        ];
    }

    private function isAfterCursor(string $sourceUpdatedAt, string $sourceOrderId, ?string $cursorUpdatedAt, ?string $cursorOrderId): bool
    {
        if ($cursorUpdatedAt === null || $cursorUpdatedAt === '') {
            return true;
        }
        if ($sourceUpdatedAt > $cursorUpdatedAt) {
            return true;
        }
        if ($sourceUpdatedAt < $cursorUpdatedAt) {
            return false;
        }
        if ($cursorOrderId === null || $cursorOrderId === '') {
            return true;
        }

        return strcmp($sourceOrderId, $cursorOrderId) > 0;
    }

    private function toFloatOrNull(mixed $value): ?float
    {
        if ($value === null || $value === '') {
            return null;
        }
        if (is_string($value)) {
            $value = str_replace(',', '.', trim($value));
        }
        if (!is_numeric($value)) {
            return null;
        }

        return (float) $value;
    }

    private function toFloatOrDefault(mixed $value, float $default): float
    {
        $result = $this->toFloatOrNull($value);
        return $result ?? $default;
    }

    private function readPath(mixed $payload, array $paths): mixed
    {
        foreach ($paths as $path) {
            $value = $this->readSinglePath($payload, (string) $path);
            if ($value !== null && $value !== '') {
                return $value;
            }
        }

        return null;
    }

    private function readSinglePath(mixed $payload, string $path): mixed
    {
        if ($path === '') {
            return null;
        }

        $segments = explode('.', $path);
        $current = $payload;
        foreach ($segments as $segment) {
            if (!is_array($current) || !array_key_exists($segment, $current)) {
                return null;
            }
            $current = $current[$segment];
        }

        return $current;
    }
}
