<?php

namespace Opencart\Admin\Controller\Extension\Alsendo;

require_once(DIR_EXTENSION . 'alsendo/src/Database/DatabaseMigrator.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/Entity/AlsendoRegionEntity.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/OrderShipmentCodeDTO.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/MoneyDTO.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/OrderPreferredPickupRequestDTO.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/OrderShipmentSubmitDTO.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/OrderShipmentSubmitDTO.php');
require_once(DIR_EXTENSION . 'alsendo/src/DTO/Unified/OrderSubmitResultDTO.php');
require_once(DIR_EXTENSION . 'alsendo/src/Enums/OrderShipmentStatus.php');
require_once(DIR_EXTENSION . 'alsendo/src/Enums/PickupType.php');
require_once(DIR_EXTENSION . 'alsendo/src/Handlers/OrderPreSubmitSaveHandler.php');
require_once(DIR_EXTENSION . 'alsendo/src/Handlers/AlsendoShipmentStatusChangeHandler.php');
require_once(DIR_EXTENSION . 'alsendo/src/Handlers/UpdateOrderDetailsFromOpenCartOrder.php');
require_once(DIR_EXTENSION . 'alsendo/src/Helpers/Country.php');
require_once(DIR_EXTENSION . 'alsendo/src/Models/Config.php');
require_once(DIR_EXTENSION . 'alsendo/src/Models/OrderShipment.php');
require_once(DIR_EXTENSION . 'alsendo/src/Models/Service.php');
require_once(DIR_EXTENSION . 'alsendo/src/Models/WrapperResult.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/AdditionalSettingsDataProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/CommonDataProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/FullOrderProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/PickupLabelProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Services/MapWidgetService.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/OrderPickupPointProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/OrderShipmentProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/SenderAddressDataProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/ApaczkaPackageTypesProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Providers/ShippingSettingsDataProvider.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/AdditionalSettingsRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/ConfigRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/OrderPickupPointRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/OrderShipmentPackageRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/OrderShipmentRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/SenderAddressRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/ServicesRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/ShippingMethodRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/Repositories/ShippingSettingsRepository.php');
require_once(DIR_EXTENSION . 'alsendo/src/OrderDetails/CreateOrderDetailsFactory.php');
require_once(DIR_EXTENSION . 'alsendo/src/Services/OrderShipmentPackageService.php');
require_once(DIR_EXTENSION . 'alsendo/src/Services/PickupTypeService.php');
require_once(DIR_EXTENSION . 'alsendo/src/Services/WrapperService.php');
require_once(DIR_EXTENSION . 'alsendo/wrapper/src/Model/Order/Valuation/OrderValuation.php');
require_once(DIR_EXTENSION . 'alsendo/wrapper/src/Model/Order/Valuation/PriceTableItem.php');

use Alsendo\AlsendoWrapper\Model\Order\Valuation\OrderValuation;
use Opencart\Admin\Model\Extension\Alsendo\Module\AlsendoOrderDetailsPackage;
use Opencart\Extension\Alsendo\Database\DatabaseMigrator;
use Opencart\Extension\Alsendo\DTO\Entity\AbstractOrderDetailsPackageEntity;
use Opencart\Extension\Alsendo\DTO\Entity\AlsendoRegionEntity;
use Opencart\Extension\Alsendo\DTO\FullOrderDTO;
use Opencart\Extension\Alsendo\DTO\MoneyDTO;
use Opencart\Extension\Alsendo\DTO\OrderPreferredPickupRequestDTO;
use Opencart\Extension\Alsendo\DTO\OrderShipmentCodeDTO;
use Opencart\Extension\Alsendo\DTO\OrderShipmentSubmitDTO;
use Opencart\Extension\Alsendo\DTO\Unified\OrderSubmitResultDTO;
use Opencart\Extension\Alsendo\Enums\OrderShipmentStatus;
use Opencart\Extension\Alsendo\Enums\PickupType;
use Opencart\Extension\Alsendo\Handlers\AlsendoShipmentStatusChangeHandler;
use Opencart\Extension\Alsendo\Handlers\OrderPreSubmitSaveHandler;
use Opencart\Extension\Alsendo\Handlers\UpdateOrderDetailsFromOpenCartOrder;
use Opencart\Extension\Alsendo\Helpers\Country;
use Opencart\Extension\Alsendo\Models\Config;
use Opencart\Extension\Alsendo\Models\OrderShipment;
use Opencart\Extension\Alsendo\Models\Service;
use Opencart\Extension\Alsendo\Models\WrapperResult;
use Opencart\Extension\Alsendo\Providers\AdditionalSettingsDataProvider;
use Opencart\Extension\Alsendo\Providers\CommonDataProvider;
use Opencart\Extension\Alsendo\Providers\FullOrderProvider;
use Opencart\Extension\Alsendo\Providers\PickupLabelProvider;
use Opencart\Extension\Alsendo\Services\MapWidgetService;
use Opencart\Extension\Alsendo\Providers\OrderPickupPointProvider;
use Opencart\Extension\Alsendo\Providers\OrderShipmentProvider;
use Opencart\Extension\Alsendo\Providers\ApaczkaPackageTypesProvider;
use Opencart\Extension\Alsendo\Providers\SenderAddressDataProvider;
use Opencart\Extension\Alsendo\Providers\ShippingSettingsDataProvider;
use Opencart\Extension\Alsendo\Repositories\AdditionalSettingsRepository;
use Opencart\Extension\Alsendo\Repositories\ConfigRepository;
use Opencart\Extension\Alsendo\Repositories\OrderPickupPointRepository;
use Opencart\Extension\Alsendo\Repositories\OrderShipmentPackageRepository;
use Opencart\Extension\Alsendo\Repositories\OrderShipmentRepository;
use Opencart\Extension\Alsendo\Repositories\SenderAddressRepository;
use Opencart\Extension\Alsendo\Repositories\ServicesRepository;
use Opencart\Extension\Alsendo\Repositories\ShippingMethodRepository;
use Opencart\Extension\Alsendo\Repositories\ShippingSettingsRepository;
use Opencart\Extension\Alsendo\OrderDetails\CreateOrderDetailsFactory;
use Opencart\Extension\Alsendo\Services\OrderShipmentPackageService;
use Opencart\Extension\Alsendo\Services\PickupTypeService;
use Opencart\Extension\Alsendo\Services\WrapperService;
use Opencart\System\Engine\Controller;

class AlsendoOrder extends Controller {

    private AdditionalSettingsDataProvider $additionalSettingsDataProvider;
    private AdditionalSettingsRepository $additionalSettingsRepository;
    private CommonDataProvider $commonDataProvider;
    private ConfigRepository $configRepository;
    private FullOrderProvider $fullOrderProvider;
    private MapWidgetService $mapWidgetService;
    private OrderPickupPointProvider $orderPickupPointProvider;
    private OrderPreSubmitSaveHandler $orderPreSubmitSaveHandler;
    private OrderShipmentPackageService $orderShipmentPackageService;
    private OrderShipmentRepository $orderShipmentRepository;
    private OrderShipmentProvider $orderShipmentProvider;
    private SenderAddressDataProvider $senderAddressDataProvider;
    private SenderAddressRepository $senderAddressRepository;
    private ServicesRepository $servicesRepository;
    private ShippingMethodRepository $shippingMethodRepository;
    private ApaczkaPackageTypesProvider $apaczkaPackageTypesProvider;
    private ShippingSettingsDataProvider $shippingSettingsDataProvider;
    private ShippingSettingsRepository $shippingSettingsRepository;
    private WrapperService $wrapperService;
    private AlsendoRegionEntity $currentRegion;
    private DatabaseMigrator $databaseMigrator;
    private AlsendoShipmentStatusChangeHandler $alsendoShipmentStatusChangeHandler;
    private PickupLabelProvider $pickupLabelProvider;

    public function __construct($registry)
    {
        parent::__construct($registry);

        $this->databaseMigrator = new DatabaseMigrator($registry, $this->db,\DB_PREFIX);

        if (!$this->databaseMigrator->isInstalled()) {
            return;
        }

        $this->load->model('extension/alsendo/module/alsendo_region');
        /** @var AlsendoRegionEntity $currentRegion */
        $this->currentRegion = $this->model_extension_alsendo_module_alsendo_region->getCurrentRegion();

        $this->configRepository = new ConfigRepository($this->db, \DB_PREFIX);
        $this->servicesRepository = new ServicesRepository($this->db, \DB_PREFIX);
        $this->orderShipmentRepository = new OrderShipmentRepository($this->db, \DB_PREFIX);
        $this->shippingSettingsRepository = new ShippingSettingsRepository($this->db, \DB_PREFIX);

        $this->additionalSettingsRepository = new AdditionalSettingsRepository($this->db, \DB_PREFIX);
        $this->additionalSettingsDataProvider = new AdditionalSettingsDataProvider($this->additionalSettingsRepository);

        $this->senderAddressRepository = new SenderAddressRepository($this->db, \DB_PREFIX);

        $this->wrapperService = new WrapperService(
            $registry,
            DIR_EXTENSION . 'alsendo/',
            $this->servicesRepository,
            $this->additionalSettingsDataProvider,
            $this->senderAddressRepository,
            $this->configRepository
        );

        $orderPickupPointRepository = new OrderPickupPointRepository($this->db, \DB_PREFIX);
        $this->orderPickupPointProvider = new OrderPickupPointProvider($orderPickupPointRepository);

        $orderShipmentPackageRepository = new OrderShipmentPackageRepository($this->db, \DB_PREFIX);

        $this->orderPreSubmitSaveHandler = new OrderPreSubmitSaveHandler($registry, $this->db);

        $this->commonDataProvider = new CommonDataProvider($this->language, $this->url, $this->session);
        $this->fullOrderProvider = new FullOrderProvider($registry, $this->db);
        $this->mapWidgetService = new MapWidgetService($this->currentRegion->code);
        $this->orderShipmentProvider = new OrderShipmentProvider($this->orderShipmentRepository, $orderShipmentPackageRepository);
        $this->shippingSettingsDataProvider = new ShippingSettingsDataProvider($this->shippingSettingsRepository);
        $this->apaczkaPackageTypesProvider = new ApaczkaPackageTypesProvider();

        $this->shippingMethodRepository = new ShippingMethodRepository($registry, $this->db, \DB_PREFIX);

        $this->senderAddressDataProvider = new SenderAddressDataProvider($this->senderAddressRepository);

        $this->orderShipmentPackageService = new OrderShipmentPackageService(
            new OrderShipmentPackageRepository($this->db, \DB_PREFIX)
        );

        $this->alsendoShipmentStatusChangeHandler = new AlsendoShipmentStatusChangeHandler($registry, $this->db);

        $this->load->language('extension/alsendo/main');
        $this->load->language('extension/alsendo/validation');

        $this->pickupLabelProvider = new PickupLabelProvider($this->language, $this->currentRegion->code);
    }

    public function showFullForm(): void {
        if (isset($this->request->get['order_id'])) {
            $factory = new CreateOrderDetailsFactory($this->registry);
            $createOrderDetails = $factory->getCreateOrderDetailsForRegion($this->currentRegion);
            $createOrderDetails->createIfNotExists((int)$this->request->get['order_id'], false, $this->currentRegion);
        }

        $mapAssets = $this->mapWidgetService->getAssetUrls();
        $this->document->addScript($mapAssets['js']);
        $this->document->addStyle($mapAssets['css']);
        $this->document->addScript('/extension/alsendo/admin/view/javascript/alsendo-map.js');
        $this->document->addScript('/extension/alsendo/admin/view/javascript/get-quote.js');
        $this->document->addScript('/extension/alsendo/admin/view/javascript/combobox.js');
        $this->document->addStyle('/extension/alsendo/admin/view/css/style.css');

        $this->document->setTitle($this->language->get('text_order_details'));

        $this->load->language('sale/order');
        $this->load->language('extension/alsendo/main');
        $this->load->language('extension/alsendo/validation');

        if (isset($this->request->get['order_id'])) {
            $orderId = (int)$this->request->get['order_id'];
        } else {
            $orderId = 0;
        }

        $existingOrderShipment = $this->orderShipmentProvider->getLatestNotCancelledByOrderId($orderId);
        if ($existingOrderShipment) {
            $this->response->redirect($this->url->link('sale/order.info', [
                'user_token' => $this->session->data['user_token'],
                'order_id' => $orderId,
            ]));
        }

        $data['breadcrumbs'] = [];

        $data['breadcrumbs'][] = [
            'text' => $this->language->get('text_home'),
            'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'])
        ];

        $data['breadcrumbs'][] = [
            'text' => $this->language->get('text_orders'),
            'href' => $this->url->link('sale/order', 'user_token=' . $this->session->data['user_token'])
        ];

        $data['breadcrumbs'][] = [
            'text' => $this->language->get('Alsendo'),
            'href' => 'javascript:void(0)'
        ];

        $data['breadcrumbs'][] = [
            'text' => sprintf('%s: %s', $this->language->get('column_order_id'), $orderId),
            'href' => $this->url->link('sale/order.info', 'user_token=' . $this->session->data['user_token'] . '&order_id='.$orderId)
        ];

        $data['user_token'] = $this->session->data['user_token'];

        if ($orderId) {
            $this->load->model('sale/order');
            $this->load->model('extension/alsendo/module/alsendo_order_details');
            $orderInfo = $this->model_sale_order->getOrder($orderId);
            $orderDetailsEntity = $this->model_extension_alsendo_module_alsendo_order_details->getOrderDetailsByOrderId($orderId);
            $orderPickupPoint = $this->orderPickupPointProvider->getOrderPickupPointByOrderId($orderId);
            $data['order_pickup_point'] = $orderPickupPoint?->toArray();
            $data['order_id'] = $orderId;
            $data['order_info'] = $orderInfo;
            $data['order_details'] = $orderDetailsEntity;
            $hasShippingMethodCode = !empty($orderInfo['shipping_method']['code']);

            try {
                if (!$hasShippingMethodCode) {
                    throw new \Exception();
                }
                $orderShipmentCodeDTO = new OrderShipmentCodeDTO($orderInfo['shipping_method']['code']);
                $data['order_info']['shipping_method']['alsendo_service_id'] = $orderShipmentCodeDTO->serviceId;
                $data['order_info']['shipping_method']['shipping_method_id'] = $orderShipmentCodeDTO->shippingMethodId;

                $currentService = $this->servicesRepository->getServiceById($orderShipmentCodeDTO->serviceId);
                $data['order_info']['shipping_method']['external_service_id'] = $currentService?->getExternalId() ?? null;

                if ($currentService) {
                    $data['order_info']['shipping_method']['courier_service_name'] = $currentService->getCustomName() ?? $currentService->getName();
                    $data['order_info']['shipping_method']['ship_via_pickup_point_allowed'] = $currentService->isShipViaPickupPointAllowed();
                } else {
                    $data['order_info']['shipping_method']['courier_service_name'] = $this->language->get('any_service_text');
                    $data['order_info']['shipping_method']['ship_via_pickup_point_allowed'] = true;
                }
            } catch (\Throwable $exception) {
                $data['order_info']['shipping_method']['alsendo_service_id'] = null;
                $data['order_info']['shipping_method']['shipping_method_id'] = null;
                $data['order_info']['shipping_method']['courier_service_name'] = '-';
                $data['order_info']['shipping_method']['ship_via_pickup_point_allowed'] = true;
            }
        }

        $data['ship_via_pickup_point_allowed'] = $data['order_info']['shipping_method']['ship_via_pickup_point_allowed'] ?? true;

        $shippingSettingsData = $this->shippingSettingsDataProvider->getData();
        $data['shipping_settings_templates'] = $shippingSettingsData['shipping_settings_templates'];
        $data['has_main_shipping_template'] = $shippingSettingsData['has_main_shipping_template'];

        $senderAddressData = $this->senderAddressDataProvider->getData();
        $data['sender_address_templates'] = $senderAddressData['sender_address_templates'];

        $config = $this->configRepository->getConfig();
        $commonData = $this->commonDataProvider->getData($config);
        $commonData['current_store_currency'] = $this->config->get('config_currency');
        $data = array_merge($data, $commonData);

        $pickupTypeService = new PickupTypeService($config->getRegion());
        $data['pickup_types'] = $pickupTypeService->getAllowedPickupTypes();
        $data['pickup_type_labels'] = $this->pickupLabelProvider->getPickupTypeLabels();

        $availableServices = $this->servicesRepository->getSelectedServices();
        $data['available_services'] = [];
        foreach ($availableServices as $service) {
            $data['available_services'][] = $service->toArray();
        }

        $data['header'] = $this->load->controller('common/header');
        $data['column_left'] = $this->load->controller('common/column_left');
        $data['footer'] = $this->load->controller('common/footer');

        $data['map_operators_by_service_id'] = $this->servicesRepository->getMapOperatorMappings($this->currentRegion);

        $countries = [];
        $countryTranslatableMap = Country::getTranslatableMap();
        foreach ($countryTranslatableMap as $isoCode => $translationKey) {
            $countries[$isoCode] = $this->language->get($translationKey);
        }
        asort($countries);
        $data['countries'] = $countries;

        $additionalSettings = $this->additionalSettingsDataProvider->getData()['additional_settings'] ?? [];

        $data['default_pickup_hour_from'] = $additionalSettings['pickup_hour_from'] ?? null;
        $data['default_pickup_hour_to'] = $additionalSettings['pickup_hour_to'] ?? null;
        $data['map_config_json'] = json_encode($this->mapWidgetService->getClientConfig());
        $data['current_region'] = $this->currentRegion->code;

        $data['address_book_list'] = [];
        $data['sender_address_external_id_preselect'] = null;
        if ($this->currentRegion->code === 'czechia' && $config->isComplete()) {
            $addressBookResult = $this->wrapperService->getAddressBookList($config);
            if ($addressBookResult->isSuccess() && !empty($addressBookResult->getData()['addresses'])) {
                $data['address_book_list'] = $addressBookResult->getData()['addresses'];
            }
            if (!empty($orderDetailsEntity) && $orderDetailsEntity->sender !== null) {
                $data['sender_address_external_id_preselect'] = $orderDetailsEntity->sender->externalId ?? null;
            }
            if ($data['sender_address_external_id_preselect'] === null) {
                $mainTemplate = $this->senderAddressRepository->getMainTemplate();
                if ($mainTemplate && $mainTemplate->getExternalId()) {
                    $data['sender_address_external_id_preselect'] = $mainTemplate->getExternalId();
                }
            }
        }

        $externalServiceId = $data['order_info']['shipping_method']['external_service_id'] ?? null;
        $isAnyService = empty($data['order_info']['shipping_method']['alsendo_service_id']) || (string)($data['order_info']['shipping_method']['alsendo_service_id'] ?? '') === '0';
        $mapOperators = $data['map_operators_by_service_id'] ?? [];
        $data['sender_dropoff_map_operator'] = (!$isAnyService && $externalServiceId && isset($mapOperators[$externalServiceId])) ? $mapOperators[$externalServiceId] : null;

        $data['region'] = $this->currentRegion->code;

        if ($this->currentRegion->code === 'romania' && $config->isComplete()) {
            $structResult = $this->wrapperService->getServiceStructure($config);
            if ($structResult->isSuccess() && !empty($structResult->getData()['package_types'])) {
                $data['romania_package_types'] = $structResult->getData()['package_types'];
            }
        }
        $data['romania_package_types'] = $data['romania_package_types'] ?? [];
        $mainShippingTemplate = $this->shippingSettingsRepository->getMainTemplate();
        $data['default_package_type_ro'] = ($mainShippingTemplate && $this->currentRegion->code === 'romania') ? $mainShippingTemplate->getPackageType() : null;

        if ($this->currentRegion->code === 'poland') {
            $data['poland_package_types'] = $this->apaczkaPackageTypesProvider->getPackageTypes();
        }
        $data['poland_package_types'] = $data['poland_package_types'] ?? [];
        $data['default_package_type_pl'] = ($mainShippingTemplate && $this->currentRegion->code === 'poland') ? $mainShippingTemplate->getPackageType() : null;

        $allAdditionalSettings = $this->additionalSettingsDataProvider->getData()['additional_settings'] ?? [];
        $data['auto_declared_value'] = $allAdditionalSettings['auto_declared_value'] ?? false;
        $data['order_total'] = isset($orderInfo) ? ($this->getOrderTotalInCurrency($orderInfo) ?? 0) : 0;
        $data['text_price_na'] = $this->language->get('text_price_na');

        $this->response->setOutput($this->load->view('extension/alsendo/module/alsendo_order_show_full_form', $data));
    }

    private function getOrderTotalInCurrency(array $orderInfo): ?float
    {
        try {
            $totalInCurrency = $this->registry->currency->format(
                $orderInfo['total'],
                $orderInfo['currency_code'],
                $orderInfo['currency_value'],
                false
            );
        } catch (\Throwable $e) {
            $totalInCurrency = $orderInfo['total'] ?? null;
        }
        return $totalInCurrency !== null ? (float) $totalInCurrency : null;
    }

    public function getQuote(): void {
        $config = $this->configRepository->getConfig();

        $byOrderId = $this->request->post['by_order_id'] ?? false;
        $byOrderId = filter_var($byOrderId, FILTER_VALIDATE_BOOLEAN);

        if ($byOrderId) {
            $orderId = $this->request->post['order_id'];
        } else {
            $templateIdToName = $this->shippingSettingsRepository->getIdNameMap();
            $dto = OrderShipmentSubmitDTO::fromArray($this->request->post, $templateIdToName);
            $this->orderPreSubmitSaveHandler->save($dto, $this->currentRegion);

            $orderId = $dto->orderId;
        }

        $fullOrderDTO = $this->fullOrderProvider->getByOrderId($orderId);
        $result = $this->wrapperService->getOrderValuation($config, $fullOrderDTO);

        $selectedPickupType = $fullOrderDTO->getSenderPackagePickupType();

        $resultData = $result->getData();
        if (empty($resultData)) {
            $this->response->addHeader('Content-Type: application/json');
            $this->response->setOutput(json_encode($result->toArray()));
            return;
        }

        /** @var OrderValuation[] $orderValuations */
        $orderValuations = $resultData;

        $availableServices = $this->servicesRepository->getSelectedServices();

        $filteredServicesValuation = [];

        $config = $this->configRepository->getConfig();
        $currency = $this->config->get('config_currency');

        $mappedServiceExternalId = $fullOrderDTO->getSelectedServiceExternalId();
        $mappedSupplier = '';
        if ($mappedServiceExternalId) {
            foreach ($availableServices as $svc) {
                if ((string)$svc->getExternalId() === (string)$mappedServiceExternalId) {
                    $mappedSupplier = strtolower($svc->getSupplier() ?? '');
                    break;
                }
            }
        }

        foreach ($availableServices as $service) {
            if (empty($service->getExternalId())) {
                continue;
            }

            $serviceId = (string) $service->getExternalId();

            foreach ($orderValuations as $orderValuation) {

                if (empty($orderValuation->priceTable)
                    || empty($orderValuation->priceTable->price)
                    || empty($orderValuation->priceTable->priceGross)
                ) {
                    continue;
                }

                if ($config->getRegion() === 'czechia') {
                    $sameService = strtolower($service->getName()) === strtolower($orderValuation->carrier);
                } elseif ($config->getRegion() === 'romania') {
                    $sameService = strtolower($serviceId) === strtolower($orderValuation->serviceId)
                        || str_starts_with(strtolower($orderValuation->serviceId), strtolower($serviceId));
                } else {
                    $sameService = $serviceId == $orderValuation->serviceId;
                }

                if ($sameService) {

                    $price = $orderValuation->priceTable->price;
                    $priceGross = $orderValuation->priceTable->priceGross;

                    if (in_array($config->getRegion(), ['czechia', 'romania'])) {
                        $price = (int) round((float) $orderValuation->priceTable->price * 100);
                        $priceGross = (int) round((float) $orderValuation->priceTable->priceGross * 100);
                    }

                    if ($config->getRegion() === 'romania' && !empty($mappedServiceExternalId)) {
                        if (!str_contains($mappedServiceExternalId, $orderValuation->serviceId)) {
                            continue;
                        }
                    }

                    $filteredServicesValuation[] = [
                        'price_net_display' => (new MoneyDTO($price, $currency))->toSymbolString(),
                        'price_gross_display' => (new MoneyDTO($priceGross, $currency))->toSymbolString(),
                        'service_id' => $service->getId(),
                        'external_id' => $service->getExternalId(),
                        'service_name' => $service->getName(),
                        'is_delivery_to_point' => $service->isDeliveryToPoint(),
                        'map_operator' => $service->getMapOperatorName(),
                        'logo_url' => $service->getLogoUrl(),
                        'no_price' => false,
                        'is_mapped' => ((string)$service->getExternalId() === (string)$mappedServiceExternalId),
                    ];
                    break;
                }
            }
        }

        if ($this->currentRegion->code === 'romania') {
            if ($mappedSupplier !== '') {
                $quotedServiceIds = array_column($filteredServicesValuation, 'service_id');
                foreach ($availableServices as $service) {
                    if (in_array($service->getId(), $quotedServiceIds)) continue;
                    $svcSupplier = strtolower($service->getSupplier() ?? '');
                    if ($svcSupplier !== $mappedSupplier) continue;

                    $filteredServicesValuation[] = [
                        'price_net_display' => '',
                        'price_gross_display' => '',
                        'service_id' => $service->getId(),
                        'external_id' => $service->getExternalId(),
                        'service_name' => $service->getName(),
                        'is_delivery_to_point' => $service->isDeliveryToPoint(),
                        'map_operator' => $service->getMapOperatorName(),
                        'logo_url' => $service->getLogoUrl(),
                        'no_price' => true,
                        'is_mapped' => ((string)$service->getExternalId() === (string)$mappedServiceExternalId),
                    ];
                }
            }
        }

        usort($filteredServicesValuation, function($a, $b) {
            if ($a['is_mapped'] !== $b['is_mapped']) return $a['is_mapped'] ? -1 : 1;
            if ($a['no_price'] !== $b['no_price']) return $a['no_price'] ? 1 : -1;
            return 0;
        });

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($filteredServicesValuation));
    }

    public function downloadShipmentWaybill(): void {
        $alsendoOrderId = $this->request->post['alsendo_order_id'] ?? null;

        $this->response->addHeader('Content-Type: application/json');

        if (!$alsendoOrderId) {
            $this->response->setOutput(json_encode([
                'success' => false,
                'message' => 'Missing shipment order id',
                'error' => 'Missing shipment order id',
            ]));
            return;
        }

        $config = $this->configRepository->getConfig();

        $waybillResponse = $this->wrapperService->getWaybill($config, (string) $alsendoOrderId);

        if (!$waybillResponse->isSuccess() || $waybillResponse->getError()) {
            $this->response->setOutput(json_encode($waybillResponse->toArray()));
            return;
        }

        $data = $waybillResponse->getData();

        if (empty($data['type']) || empty($data['waybill'])) {
            $this->response->setOutput(json_encode([
                'success' => false,
                'message' => 'No waybill data',
                'error' => 'No waybill data',
            ]));
            return;
        }

        if ($data['type'] === 'pdf') {
            $this->response->setOutput(json_encode([
                'success' => true,
                'waybill' => $data['waybill'],
                'type' => 'pdf',
            ]));
        }
    }

    public function bulkDownloadShipmentWaybillsByOrderIds(): void {
        $orderIds = $this->request->post['ids'] ?? [];
        if (!$orderIds) {
            return;
        }

        $config = $this->configRepository->getConfig();
        $pdfFiles = [];

        foreach ($orderIds as $orderId) {
            $orderShipment = $this->orderShipmentProvider->getLatestNotCancelledByOrderId($orderId);
            if (!$orderShipment) continue;

            $alsendoOrderId = $orderShipment->getExternalId();
            $waybillResponse = $this->wrapperService->getWaybill($config, (string) $alsendoOrderId);
            if ($waybillResponse->getError()) continue;

            $data = $waybillResponse->getData();
            if ($data['type'] === 'pdf') {
                $pdfFiles[] = [
                    'filename' => 'waybill_' . (int)$alsendoOrderId . '.pdf',
                    'content' => base64_decode($data['waybill'])
                ];
            }
        }

        if (count($pdfFiles) === 0) {
            return;
        }

        if (count($pdfFiles) === 1) {
            $this->response->addHeader('Content-Type: application/pdf');
            $this->response->addHeader('Content-Disposition: attachment; filename="' . $pdfFiles[0]['filename'] . '"');
            $this->response->setOutput($pdfFiles[0]['content']);
            return;
        }

        $zip = new \ZipArchive();
        $zipName = tempnam(sys_get_temp_dir(), 'waybills_') . '.zip';
        if ($zip->open($zipName, \ZipArchive::CREATE) !== true) {
            return;
        }

        foreach ($pdfFiles as $file) {
            $zip->addFromString($file['filename'], $file['content']);
        }
        $zip->close();

        $this->response->addHeader('Content-Type: application/zip');
        $this->response->addHeader('Content-Disposition: attachment; filename="waybills.zip"');
        $this->response->setOutput(file_get_contents($zipName));
        unlink($zipName);
    }


    public function validatePreOrderShipment(): void {
        $templateIdToName = $this->shippingSettingsRepository->getIdNameMap();
        $dto = OrderShipmentSubmitDTO::fromArray($this->request->post, $templateIdToName);
        $this->orderPreSubmitSaveHandler->save($dto, $this->currentRegion);
        $fullOrderDTO = $this->fullOrderProvider->getByOrderId($dto->orderId);

        $validationErrors = $this->getValidationErrors($fullOrderDTO, $dto->shipmentSelectedService);
        $jsonResponse = [
            'success' => empty($validationErrors),
            'errors' => $validationErrors,
        ];

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($jsonResponse));
    }

    public function saveOrderDetails(): void {
        try {
            $templateIdToName = $this->shippingSettingsRepository->getIdNameMap();
            $dto = OrderShipmentSubmitDTO::fromArray($this->request->post, $templateIdToName);

            $this->orderPreSubmitSaveHandler->save($dto, $this->currentRegion);

            $this->response->addHeader('Content-Type: application/json');
            $this->response->setOutput(json_encode([
                'success' => true,
                'message' => $this->language->get('success_order_details_saved'),
            ]));
        } catch (\Throwable $e) {
            $this->response->addHeader('Content-Type: application/json');
            $this->response->setOutput(json_encode([
                'success' => false,
                'error' => $this->language->get('text_operation_not_successful'),
            ]));
        }
    }

    public function submitOrderShipment(): void {
        $byOrderId = $this->request->post['by_order_id'] ?? false;
        $byOrderId = filter_var($byOrderId, FILTER_VALIDATE_BOOLEAN);

        $shipmentSelectedServiceRaw = $this->request->post['shipment_selected_service'] ?? null;
        $shipmentSelectedService = isset($this->request->post['shipment_selected_service']) ? (int) $this->request->post['shipment_selected_service'] : null;
        $useConfigPickupOnFirstAttempt = !isset($this->request->post['shipment_selected_service']) || $shipmentSelectedServiceRaw === '' || $shipmentSelectedServiceRaw === null;

        if ($byOrderId) {
            $orderId = $this->request->post['order_id'];
            $existingShipment = $this->orderShipmentRepository->getLatestNotCancelledByOrderId((int)$orderId);
            if ($existingShipment) {
                $this->response->addHeader('Content-Type: application/json');
                $this->response->setOutput(json_encode([
                    'success' => false,
                    'message' => $this->language->get('text_error_shipment_already_exists'),
                    'error' => $this->language->get('text_error_shipment_already_exists'),
                ]));
                return;
            }
        } else {
            $templateIdToName = $this->shippingSettingsRepository->getIdNameMap();
            $dto = OrderShipmentSubmitDTO::fromArray($this->request->post, $templateIdToName);
            $this->orderPreSubmitSaveHandler->save($dto, $this->currentRegion);

            $orderId = $dto->orderId;
        }

        $preferredPickupRequestDTO = OrderPreferredPickupRequestDTO::tryCreateFromPayload($this->request->post);

        $config = $this->configRepository->getConfig();
        $resultArray = $this->submitOrderShipmentById(
            $orderId,
            $config,
            $shipmentSelectedService,
            $preferredPickupRequestDTO,
            0,
            $useConfigPickupOnFirstAttempt
        );

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($resultArray));
    }

    public function resetPickupPoint(): void
    {
        $this->response->addHeader('Content-Type: application/json');
        $orderId = isset($this->request->post['order_id']) ? (int)$this->request->post['order_id'] : 0;
        if (!$orderId) {
            $this->response->setOutput(json_encode(['success' => false]));
            return;
        }
        $orderPickupPointRepository = new OrderPickupPointRepository($this->db, \DB_PREFIX);
        $orderPickupPointRepository->deleteByOrderId($orderId);

        $this->registry->load->model('extension/alsendo/module/alsendo_order_pickup_points');
        $this->registry->model_extension_alsendo_module_alsendo_order_pickup_points->removePickupPointDataByOrderId($orderId);

        $this->response->setOutput(json_encode(['success' => true]));
    }

    public function cancel(): void {
        $orderId = $this->request->post['alsendo_order_id'] ?? null;

        if (!$orderId) {
            return;
        }

        $config = $this->configRepository->getConfig();

        $resultArray = $this->submitCancelByOrderId((int)$orderId, $config);

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode($resultArray));
    }

    public function bulkSubmitOrderShipment(): void {
        $orderIds = $this->request->post['ids'] ?? [];
        $shippableOrderIds = [];

        foreach ($orderIds as $orderId) {
            $orderShipment = $this->orderShipmentProvider->getLatestNotCancelledByOrderId($orderId);

             if ($orderShipment === null) {
                 $shippableOrderIds[] = $orderId;
            }
        }

        $message = '';
        $errors = [];
        $success = [];

        if (empty($shippableOrderIds)) {
            $message = $this->language->get('text_there_are_no_orders_to_be_sent');
        }

        $config = $this->configRepository->getConfig();
        foreach ($shippableOrderIds as $shippableOrderId) {
            $result = $this->submitOrderShipmentById((int)$shippableOrderId, $config, null, null, 0, true);

            if ($result['success'] === false) {
                if (!empty($result['errors'])) {
                    $errors[$shippableOrderId] = implode(', ', $result['errors']);
                } else {
                    $errors[$shippableOrderId] = $result['error'] ?? $result['message'] ?? $this->language->get('text_unknown_error');
                }
            }

            if ($result['success'] === true) {
                $successHtml = $this->getExistingShipmentCellHtml($result['data']['waybill_number'] ?? 'N/A');
                $success[$shippableOrderId] = $successHtml;
            }
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode([
            'success' => $success,
            'message' => $message,
            'errors' => $errors,
        ]));
    }

    private function getExistingShipmentCellHtml(?string $waybillNumber = null): string {
        if (empty($waybillNumber)) {
            $waybillNumber = 'N/A';
        }

        return sprintf(
            '%s <br> %s: <span style="font-weight:bold;">%s</span>',
            $this->language->get('text_order_shipment_submitted_success'),
            $this->language->get('text_waybill_number'),
            $waybillNumber
        );
    }

    public function bulkCancel(): void {
        $orderIds = $this->request->post['ids'] ?? [];
        $cancelableOrderIds = [];

        foreach ($orderIds as $orderId) {
            $orderShipment = $this->orderShipmentProvider->getLatestNotCancelledByOrderId($orderId);

            if ($orderShipment !== null) {
                $cancelableOrderIds[] = $orderId;
            }
        }

        $message = '';
        $errors = [];
        $success = [];

        if (empty($cancelableOrderIds)) {
            $message = $this->language->get('text_there_are_no_orders_to_be_cancelled');
        }

        $config = $this->configRepository->getConfig();
        foreach ($cancelableOrderIds as $cancelableOrderId) {
            $result = $this->submitCancelByOrderId((int)$cancelableOrderId, $config);

            if ($result['success'] === false) {
                $errors[$cancelableOrderId] = $result['error'] ?? $result['message'] ?? $this->language->get('text_unknown_error');
            }

            if ($result['success'] === true) {
                $success[$cancelableOrderId] = $this->language->get('text_order_shipment_cancelled_success');
            }
        }

        $this->response->addHeader('Content-Type: application/json');
        $this->response->setOutput(json_encode([
            'success' => $success,
            'message' => $message,
            'errors' => $errors,
        ]));
    }


    public function appendBulkOperationsViewButton(string $route, array &$data, &$output): void {
        $sectionHtml = $this->load->view('extension/alsendo/supplements/order_bulk_operation_button', [
            'user_token' => $this->session->data['user_token']
        ]);

        $output = preg_replace(
            '/(<div id="content">)/',
            '$1' . $sectionHtml,
            $output,
            1
        );
    }

    public function showOrderListWithBulkOperation(): void {
        $this->load->language('sale/order');
        $this->document->addStyle('/extension/alsendo/admin/view/css/style.css');

        $data['breadcrumbs'] = [];
        $data['breadcrumbs'][] = [
            'text' => $this->language->get('text_home'),
            'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'])
        ];

        $data['breadcrumbs'][] = [
            'text' => $this->language->get('text_orders'),
            'href' => $this->url->link('sale/order', 'user_token=' . $this->session->data['user_token'])
        ];

        $data['breadcrumbs'][] = [
            'text' => $this->language->get('text_bulk_send_orders'),
            'href' => 'javascript:void(0)'
        ];

        $selectedIds = $this->request->get['ids'] ?? [];
        $selectedIds = is_array($selectedIds) ? array_map('intval', $selectedIds) : array_map('intval', array_filter(explode(',', $selectedIds)));

        $this->load->model('sale/order');
        $this->load->model('extension/alsendo/module/alsendo_order_details');
        $updateOrderDetails = new UpdateOrderDetailsFromOpenCartOrder($this->registry, $this->db);
        foreach ($selectedIds as $orderId) {
            $orderInfo = $this->model_sale_order->getOrder($orderId);
            $orderDetails = $this->model_extension_alsendo_module_alsendo_order_details->getOrderDetailsByOrderId($orderId);
            if ($orderInfo && $orderDetails && !empty($orderInfo['shipping_method']['code'])) {
                $updateOrderDetails->sync($orderInfo, $orderDetails);
            }
        }

        $data['list'] = $this->getList($selectedIds);

        $data['user_token'] = $this->session->data['user_token'];

        $data['header'] = $this->load->controller('common/header');
        $data['column_left'] = $this->load->controller('common/column_left');
        $data['footer'] = $this->load->controller('common/footer');

        $this->response->setOutput($this->load->view('extension/alsendo/module/alsendo_order_bulk_operations', $data));
    }

    /**
     * @param int[] $selectedIds
     */
    public function list(array $selectedIds): void
    {
        $this->load->language('sale/order');

        $this->response->setOutput($this->getList($selectedIds));
    }

    /**
     * @param int[] $selectedIds
     */
    protected function getList(array $selectedIds): string
    {
        $data['action'] = $this->url->link('sale/order.list', 'user_token=' . $this->session->data['user_token']);

        $data['orders'] = [];

        $this->load->model('sale/order');

        $createFactory = new CreateOrderDetailsFactory($this->registry);
        $createOrderDetails = $createFactory->getCreateOrderDetailsForRegion($this->currentRegion);

        foreach ($selectedIds as $orderId) {
            $createOrderDetails->createIfNotExists((int) $orderId, false, $this->currentRegion);

            $fullOrder = $this->fullOrderProvider->getByOrderId($orderId);

            $hasShipment = $fullOrder->hasNonCancelledShipmentGoingOn();
            $data['orders'][] = [
                'order_id'        => $orderId,
                'full_order'      => $fullOrder,
                'external_shipment_id' => $fullOrder->getLastNonCancelledShipmentExternalId(),
                'has_shipment'    => $hasShipment,
                'shipment_cell_html' => $hasShipment ? $this->getExistingShipmentCellHtml($fullOrder->getLastNonCancelledWaybillNumber()) : null,
                'view'            => $this->url->link('sale/order.info', 'user_token=' . $this->session->data['user_token'] . '&order_id='.$orderId),
                'view_full_form'  => $this->url->link('extension/alsendo/alsendo_order.showFullForm', 'user_token=' . $this->session->data['user_token'] . '&order_id='.$orderId)
            ];
        }

        $this->load->language('extension/alsendo/main');
        $data['user_token'] = $this->session->data['user_token'];
        $data['sender_address_templates'] = array_map(fn($t) => $t->toArray(), $this->senderAddressRepository->getAllTemplates());
        $data['shipping_settings_templates'] = array_map(fn($t) => $t->toArray(), $this->shippingSettingsRepository->getAllTemplates());
        $data['text_operation_not_successful'] = $this->language->get('text_operation_not_successful');

        return $this->load->view('extension/alsendo/module/alsendo_order_bulk_operations_list', $data);
    }

    private function submitOrderShipmentById(
        int $orderId,
        Config $config,
        ?int $shipmentSelectedService = null,
        ?OrderPreferredPickupRequestDTO $orderPreferredPickupRequestDTO = null,
        int $pickupDateRetryCount = 0,
        bool $useConfigPickupOnFirstAttempt = false,
    ): array {
        $fullOrderDTO = $this->fullOrderProvider->getByOrderId($orderId);
        $validationErrors = $this->getValidationErrors($fullOrderDTO, $shipmentSelectedService);

        if (!empty($validationErrors)) {
            return [
                'success' => false,
                'errors' => $validationErrors
            ];
        }

        $additionalSettingsData = $this->additionalSettingsDataProvider->getData();
        $additionalSettings = $additionalSettingsData['additional_settings'] ?? [];

        if ($pickupDateRetryCount === 0 && $useConfigPickupOnFirstAttempt && $fullOrderDTO->orderDetails?->shipment !== null) {
            $preferredPickupDate = !empty($additionalSettings['sameday_dispatch'])
                ? date('Y-m-d')
                : $this->getNextWorkingDay(date('Y-m-d'));
            $preferredPickupHourFrom = $additionalSettings['pickup_hour_from'] ?? null;
            $preferredPickupHourTo = $additionalSettings['pickup_hour_to'] ?? null;

            if ($orderPreferredPickupRequestDTO) {
                $orderPreferredPickupRequestDTO->date = $preferredPickupDate;
                $orderPreferredPickupRequestDTO->hourFrom = $preferredPickupHourFrom;
                $orderPreferredPickupRequestDTO->hourTo = $preferredPickupHourTo;
            }

            $s = $fullOrderDTO->orderDetails->shipment;
            $this->load->model('extension/alsendo/module/alsendo_order_details_shipment');
            $this->model_extension_alsendo_module_alsendo_order_details_shipment->updateShipment($s->id, [
                'shipping_method_id' => $s->shippingMethodId,
                'shipping_method_name' => $s->shippingMethodName,
                'service_id' => $s->serviceId,
                'external_service_id' => $s->externalServiceId,
                'shipping_service_name' => $s->shippingServiceName,
                'map_operator' => $s->mapOperator,
                'preferred_pickup_date' => $preferredPickupDate,
                'preferred_pickup_hour_from' => $preferredPickupHourFrom,
                'preferred_pickup_hour_to' => $preferredPickupHourTo,
                'shipment_pickup_point' => $s->shipmentPickupPoint,
                'shipment_pickup_point_display' => $s->shipmentPickupPointDisplay,
            ]);
            $fullOrderDTO = $this->fullOrderProvider->getByOrderId($orderId);
        }

        if (
            $orderPreferredPickupRequestDTO
            && $fullOrderDTO->orderDetails?->shipment !== null
            && !($pickupDateRetryCount === 0 && $useConfigPickupOnFirstAttempt)
        ) {
            $s = $fullOrderDTO->orderDetails->shipment;
            $this->load->model('extension/alsendo/module/alsendo_order_details_shipment');
            $this->model_extension_alsendo_module_alsendo_order_details_shipment->updateShipment($s->id, [
                'shipping_method_id' => $s->shippingMethodId,
                'shipping_method_name' => $s->shippingMethodName,
                'service_id' => $s->serviceId,
                'external_service_id' => $s->externalServiceId,
                'shipping_service_name' => $s->shippingServiceName,
                'map_operator' => $s->mapOperator,
                'preferred_pickup_date' => $orderPreferredPickupRequestDTO->date,
                'preferred_pickup_hour_from' => $orderPreferredPickupRequestDTO->hourFrom,
                'preferred_pickup_hour_to' => $orderPreferredPickupRequestDTO->hourTo,
                'shipment_pickup_point' => $s->shipmentPickupPoint,
                'shipment_pickup_point_display' => $s->shipmentPickupPointDisplay,
            ]);
            $fullOrderDTO = $this->fullOrderProvider->getByOrderId($orderId);
        }
        try {
            $result = $this->wrapperService->sendOrder($config, $fullOrderDTO, $shipmentSelectedService);
            $resultData = OrderSubmitResultDTO::fromWrapperResult($result);
            if ($result->isSuccess()) {
                if ($resultData->id) {
                    $orderShipment = new OrderShipment(
                        $this->currentRegion->id,
                        null,
                        $orderId,
                        $resultData->toArray(),
                        null,
                        null,
                        $resultData->id,
                        OrderShipmentStatus::NEW
                    );
                    $orderShipmentId = $this->orderShipmentRepository->upsert($orderShipment);
                    $this->alsendoShipmentStatusChangeHandler->handle($orderShipment);
                    $this->orderShipmentPackageService->createFromOrderShipmentSubmit($fullOrderDTO, $orderShipmentId);
                }
            }
        } catch (\Throwable $e) {
            var_dump($e); die();
        }

        $samedayDispatch = !empty($additionalSettingsData['additional_settings']['sameday_dispatch']);
        if (
            $samedayDispatch
            && $pickupDateRetryCount < 3
            && $result instanceof WrapperResult
            && $result->isErroredPossiblyCausedByPickupDate()
        ) {
            $preferredDate = null;
            if ($orderPreferredPickupRequestDTO && !empty($orderPreferredPickupRequestDTO->date)) {
                $preferredDate = $orderPreferredPickupRequestDTO->date;
            } elseif (!empty($fullOrderDTO->orderDetails?->shipment->preferredPickupDate)) {
                $preferredDate = $fullOrderDTO->orderDetails->shipment->preferredPickupDate;
            }

            if (!empty($preferredDate) && !empty($fullOrderDTO->orderDetails?->shipment?->id)) {
                $nextPreferredDate = $pickupDateRetryCount === 0
                    ? $this->addDays($preferredDate, 1)
                    : $this->getNextWorkingDay($preferredDate);

                $this->load->model('extension/alsendo/module/alsendo_order_details_shipment');
                $this->model_extension_alsendo_module_alsendo_order_details_shipment->updateShipment($fullOrderDTO->orderDetails->shipment->id, [
                    'preferred_pickup_date' => $nextPreferredDate,
                ]);

                if ($orderPreferredPickupRequestDTO) {
                    $orderPreferredPickupRequestDTO->date = $nextPreferredDate;
                }

                return $this->submitOrderShipmentById(
                    $orderId,
                    $config,
                    $shipmentSelectedService,
                    $orderPreferredPickupRequestDTO,
                    $pickupDateRetryCount + 1,
                    $useConfigPickupOnFirstAttempt
                );
            }
        }

        return $result->toArray();
    }

    private function addDays(string $date, int $days): string
    {
        $dt = new \DateTimeImmutable($date);
        return $dt->modify('+' . $days . ' day')->format('Y-m-d');
    }

    private function getNextWorkingDay(string $date): string
    {
        $dt = new \DateTimeImmutable($date);
        $dt = $dt->modify('+1 day');
        while ((int) $dt->format('N') >= 6) {
            $dt = $dt->modify('+1 day');
        }
        return $dt->format('Y-m-d');
    }

    private function submitCancelByOrderId(int $orderId, Config $config): array {
        $alsendoOrderShipment = $this->orderShipmentProvider->getLatestNotCancelledByOrderId($orderId);

        if (!$alsendoOrderShipment) {
            return [
                'success' => false,
                'error' => $this->language->get('text_error_order_shipment_not_found'),
                'message' => $this->language->get('text_error_order_shipment_not_found'),
            ];
        }

        $externalShipmentIds = [$alsendoOrderShipment->getExternalId()];

        if ($config->getRegion() === 'czechia') {
            $waybillNumbers = $alsendoOrderShipment->getData()['waybillNumbers'] ?? [];
            $externalShipmentIds = !empty($waybillNumbers) ? $waybillNumbers : $externalShipmentIds;
        }

        $result = $this->wrapperService->cancelOrder($config, $externalShipmentIds);

        if ($result->isSuccess()) {
            $shipmentData = $alsendoOrderShipment->getData();
            $shipmentData['is_cancelled'] = true;
            $alsendoOrderShipment->setStatus(OrderShipmentStatus::CANCELLED);
            $alsendoOrderShipment->setData($shipmentData);
            $this->orderShipmentRepository->upsert($alsendoOrderShipment);
            $this->alsendoShipmentStatusChangeHandler->handle($alsendoOrderShipment);
        }

        return $result->toArray();
    }

    private function getValidationErrors(FullOrderDTO $fullOrderDTO, ?int $shipmentSelectedService = null): array
    {
        $errors = [];

        $selectedService = $this->validateAndGetService($fullOrderDTO, $errors, $shipmentSelectedService);

        $this->validateSenderFields($fullOrderDTO, $errors);
        $this->validateShippingFields($fullOrderDTO, $errors, $selectedService);
        $this->validatePackageFields($fullOrderDTO, $errors);
        $this->validateShipmentFields($fullOrderDTO, $errors, $selectedService);

        return $errors;
    }

    private function getFieldTranslation(string $fieldName): string
    {
        $translationKey = 'field_' . $fieldName;
        $translation = $this->language->get($translationKey);

        if (empty($translation)) {
            return ucfirst(str_replace('_', ' ', $fieldName));
        }

        return $translation;
    }

    private function getTranslatedMessage(string $messageKey, string $fieldName, array $replacements = []): string
    {
        $message = $this->language->get($messageKey);
        $fieldTranslation = $this->getFieldTranslation($fieldName);

        $message = str_replace('[[FIELD]]', $fieldTranslation, $message);

        foreach ($replacements as $placeholder => $value) {
            $message = str_replace('[[' . strtoupper($placeholder) . ']]', $value, $message);
        }

        return $message;
    }

    private function validateAndGetService(
        FullOrderDTO $fullOrderDTO,
        array &$errors,
        ?int $shipmentSelectedService = null
    ): ?Service {
        if (!$shipmentSelectedService) {
            $serviceId = $fullOrderDTO->getSelectedServiceId();
        } else {
            $serviceId = $shipmentSelectedService;
        }

        if (empty($serviceId)) {
            $errors['shipment_selected_service'] = $this->language->get('service_not_selected');
            return null;
        }

        $service = $this->servicesRepository->getServiceById($serviceId);
        if (!$service) {
            $errors['shipment_selected_service'] = $this->language->get('selected_service_not_found');
            return null;
        }

        return $service;
    }

    private function validateSenderFields(FullOrderDTO $fullOrderDTO, array &$errors): void
    {
        if ($this->currentRegion->code === 'czechia' && !empty($fullOrderDTO->orderDetails?->sender?->externalId)) {
            return;
        }

        $sender = $fullOrderDTO->orderDetails?->sender;
        $this->validateStringValue($sender?->companyName ?? null, 'sender_company_name', 50, $errors, true);
        $this->validateStringValue($sender?->fullName ?? null, 'sender_full_name', 101, $errors, true);
        $this->validateStringValue($sender?->street ?? null, 'sender_street', 50, $errors);
        $this->validateStringValue($sender?->houseNumber ?? null, 'sender_house_number', 10, $errors);
        $this->validateStringValue($sender?->apartmentNumber ?? null, 'sender_apartment_number', 10, $errors, true);
        $this->validateCountryCodeValue($sender?->country ?? null, 'sender_country', $errors);
        $this->validateStringValue($sender?->postalCode ?? null, 'sender_postal_code', 50, $errors);
        $this->validateStringValue($sender?->city ?? null, 'sender_city', 50, $errors);
        $this->validatePhoneNumberValue($sender?->phoneNumber ?? null, 'sender_phone_number', $errors);
        $this->validateEmailValue($sender?->email ?? null, 'sender_email', $errors);
    }

    private function validateShippingFields(FullOrderDTO $fullOrderDTO, array &$errors, ?Service $service): void
    {
        $addressType = $fullOrderDTO->getShippingAddressType();

        if ($addressType === 'company') {
            $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_company'], 'shipping_company_name', 50, $errors);
        }

        $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_firstname'], 'shipping_first_name', 50, $errors, true);
        $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_lastname'], 'shipping_last_name', 50, $errors, true);
        $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_address_1'], 'shipping_address_one', 100, $errors);
        $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_address_2'], 'shipping_address_two', 50, $errors, true);
        $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_postcode'], 'shipping_postal_code', 50, $errors);
        $this->validateStringValue($fullOrderDTO->openCartOrder['shipping_city'], 'shipping_city', 50, $errors);
        $this->validatePhoneNumberValue($fullOrderDTO->getShippingPhoneNumber(), 'shipping_phone_number', $errors);
        $this->validateEmailValue($fullOrderDTO->openCartOrder['email'], 'shipping_email', $errors);

        if ($service && $service->isDeliveryToPoint()) {
            $this->validatePickupPointValue($fullOrderDTO->getShippingPickupPoint(), 'shipping_pickup_point', $errors);
        }
    }

    private function validatePackageFields(FullOrderDTO $fullOrderDTO, array &$errors): void
    {
        /** @var AbstractOrderDetailsPackageEntity $firstPackage */
        $firstPackage = $fullOrderDTO->orderDetails->packageDetails->packages[0] ?? [];

        $this->validatePositiveNumberValue($firstPackage->packageWidth, 'package_width', $errors);
        $this->validatePositiveNumberValue($firstPackage->packageLength, 'package_length', $errors);
        $this->validatePositiveNumberValue($firstPackage->packageHeight, 'package_height', $errors);
        $this->validatePositiveNumberValue($firstPackage->packageWidth, 'package_weight', $errors);
        $this->validateStringValue($fullOrderDTO->orderDetails->packageDetails->packageContent, 'package_content', 255, $errors, true);

        $this->validateCodAmountValue($fullOrderDTO->orderDetails->packageDetails->codValue->toFloat(), 'package_cod', $errors);
        $this->validateDeclaredValueValue($fullOrderDTO->orderDetails->packageDetails->declaredValue->toFloat(), 'package_declared_value', $errors);
    }

    private function validateShipmentFields(FullOrderDTO $fullOrderDTO, array &$errors, ?Service $service): void
    {
        $pickupType = $fullOrderDTO->getSenderPackagePickupType();

        $reflection = new \ReflectionClass(PickupType::class);
        $validPickupTypes = array_values($reflection->getConstants());

        if (!in_array($pickupType, $validPickupTypes)) {
            $errors['selected_pickup_type'] = $this->language->get('pickup_type_invalid');
        }

        if ($pickupType === 'COURIER') {
            $this->validateFutureDateValue($fullOrderDTO->orderDetails->shipment->preferredPickupDate, 'shipment_preferred_pickup_date', $errors);
        }

        if ($pickupType === 'SELF') {
            //$this->validatePickupPointValue($fullOrderDTO->getShipmentPickupPoint(), 'shipment_pickup_point', $errors);
        }
    }

    private function validateStringLength(array $payload, string $field, int $maxLength, array &$errors, bool $allowEmpty = false): void
    {
        $value = $payload[$field] ?? '';

        if (!$allowEmpty && empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_string($value)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_string', $field);
            return;
        }

        if (strlen($value) > $maxLength) {
            $errors[$field] = $this->getTranslatedMessage('max_length_exceeded', $field, ['length' => $maxLength]);
        }
    }

    private function validateCountryCode(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_string($value) || strlen($value) !== 2) {
            $errors[$field] = $this->getTranslatedMessage('must_be_exactly_2_chars', $field);
        }
    }

    private function validatePhoneNumber(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_string($value)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_string', $field);
            return;
        }

        if (!preg_match('/^[\d\s\+]+$/', $value)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_phone_number', $field);
            return;
        }

        if (strlen($value) > 16) {
            $errors[$field] = $this->getTranslatedMessage('must_not_exceed_digits', $field, ['length' => 16]);
        }
    }

    private function validateEmail(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_email', $field);
        }
    }

    private function validatePositiveNumber(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_numeric($value) || (float)$value <= 0) {
            $errors[$field] = $this->getTranslatedMessage('must_be_number_higher_than_zero', $field);
        }
    }

    private function validatePositiveInteger(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_numeric($value) || (int)$value <= 0) {
            $errors[$field] = $this->getTranslatedMessage('must_be_positive_number', $field);
        }
    }

    private function validateCodAmount(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if ($value === '') {
            return;
        }

        if (!is_numeric($value) || (float)$value <= 0) {
            $errors[$field] = $this->getTranslatedMessage('must_be_number_higher_than_zero', $field);
        }
    }

    private function validatePickupPoint(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        $decoded = json_decode(html_entity_decode($value), true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_json', $field);
            return;
        }

        if (empty($decoded['code'])) {
            $errors[$field] = $this->getTranslatedMessage('must_contain_valid_code', $field);
        }
    }

    private function validateFutureDate(array $payload, string $field, array &$errors): void
    {
        $value = $payload[$field] ?? '';

        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        $date = strtotime($value);
        if ($date === false) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_date', $field);
            return;
        }

        $today = strtotime(date('Y-m-d'));
        if ($date < $today) {
            $errors[$field] = $this->getTranslatedMessage('must_be_today_or_future', $field);
        }
    }

    private function validateStringValue(?string $value, string $field, int $maxLength, array &$errors, bool $allowEmpty = false): void
    {
        if (!$allowEmpty && empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_string($value) && !is_null($value)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_string', $field);
            return;
        }

        if ($value && strlen($value) > $maxLength) {
            $errors[$field] = $this->getTranslatedMessage('max_length_exceeded', $field, ['length' => $maxLength]);
        }
    }

    private function validateCountryCodeValue(?string $value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_string($value) || strlen($value) !== 2) {
            $errors[$field] = $this->getTranslatedMessage('must_be_exactly_2_chars', $field);
        }
    }

    private function validatePhoneNumberValue(?string $value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_string($value)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_string', $field);
            return;
        }

        if (!preg_match('/^[\d\s\+]+$/', $value)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_phone_number', $field);
            return;
        }

        if (strlen($value) > 16) {
            $errors[$field] = $this->getTranslatedMessage('must_not_exceed_digits', $field, ['length' => 16]);
        }
    }

    private function validateEmailValue(?string $value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_email', $field);
        }
    }

    private function validatePositiveNumberValue(mixed $value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_numeric($value) || (float)$value <= 0) {
            $errors[$field] = $this->getTranslatedMessage('must_be_number_higher_than_zero', $field);
        }
    }

    private function validatePositiveIntegerValue(mixed $value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (!is_numeric($value) || (int)$value <= 0) {
            $errors[$field] = $this->getTranslatedMessage('must_be_positive_number', $field);
        }
    }

    private function validateCodAmountValue($value, string $field, array &$errors): void
    {
        if ($value === null) {
            return;
        }

        if ($value && (!is_numeric($value) || ((int) $value)  <= 0)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_number_higher_than_zero', $field);
        }
    }

    private function validateDeclaredValueValue($value, string $field, array &$errors): void
    {
        if ($value === null) {
            return;
        }

        if ($value && (!is_numeric($value) || ((int) $value)  <= 0)) {
            $errors[$field] = $this->getTranslatedMessage('must_be_number_higher_than_zero', $field);
        }
    }

    private function validatePickupPointValue($value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        if (is_string($value)) {
            $decoded = json_decode(html_entity_decode($value), true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                $errors[$field] = $this->getTranslatedMessage('must_be_valid_json', $field);
                return;
            }
            $value = $decoded;
        }

        if (!is_array($value) || empty($value['code'])) {
            $errors[$field] = $this->getTranslatedMessage('must_contain_valid_code', $field);
        }
    }

    private function validateFutureDateValue(?string $value, string $field, array &$errors): void
    {
        if (empty($value)) {
            $errors[$field] = $this->getTranslatedMessage('field_required', $field);
            return;
        }

        $date = strtotime($value);
        if ($date === false) {
            $errors[$field] = $this->getTranslatedMessage('must_be_valid_date', $field);
            return;
        }

        $today = strtotime(date('Y-m-d'));
        if ($date < $today) {
            $errors[$field] = $this->getTranslatedMessage('must_be_today_or_future', $field);
        }
    }
}