<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Support\StringHelper;
use App\Modules\Orders\OrderImportRepository;
use App\Modules\Orders\OrdersRepository;
use DateTimeImmutable;
use Throwable;

final class ShopproOrdersSyncService
{
    public function __construct(
        private readonly ShopproIntegrationsRepository $integrations,
        private readonly ShopproOrderSyncStateRepository $syncState,
        private readonly ShopproApiClient $apiClient,
        private readonly OrderImportRepository $orderImportRepository,
        private readonly ShopproStatusMappingRepository $statusMappings,
        private readonly OrdersRepository $orders,
        private readonly ShopproOrderMapper $mapper,
        private readonly ShopproProductImageResolver $imageResolver
    ) {
    }

    /**
     * @param array<string, mixed> $options
     * @return array<string, mixed>
     */
    public function sync(array $options = []): array
    {
        $maxPages = max(1, min(20, (int) ($options['max_pages'] ?? 3)));
        $pageLimit = max(1, min(100, (int) ($options['page_limit'] ?? 50)));
        $maxOrders = max(1, min(1000, (int) ($options['max_orders'] ?? 200)));
        $ignoreOrdersFetchEnabled = !empty($options['ignore_orders_fetch_enabled']);
        $allowedIntegrationIds = $this->normalizeIntegrationIds($options['allowed_integration_ids'] ?? []);

        $result = [
            'processed' => 0,
            'imported_created' => 0,
            'imported_updated' => 0,
            'failed' => 0,
            'skipped' => 0,
            'checked_integrations' => 0,
            'errors' => [],
        ];

        foreach ($this->integrations->listIntegrations() as $integration) {
            $integrationId = (int) ($integration['id'] ?? 0);
            if ($integrationId <= 0) {
                continue;
            }
            if ($allowedIntegrationIds !== [] && !isset($allowedIntegrationIds[$integrationId])) {
                continue;
            }
            if (empty($integration['is_active']) || empty($integration['has_api_key'])) {
                continue;
            }
            if (!$ignoreOrdersFetchEnabled && empty($integration['orders_fetch_enabled'])) {
                continue;
            }

            $result['checked_integrations'] = (int) $result['checked_integrations'] + 1;
            $this->syncOneIntegration($integration, $maxPages, $pageLimit, $maxOrders, $result);
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $integration
     * @param array<string, mixed> $result
     */
    private function syncOneIntegration(array $integration, int $maxPages, int $pageLimit, int $maxOrders, array &$result): void
    {
        $integrationId = (int) ($integration['id'] ?? 0);
        $state = $this->syncState->getState($integrationId);
        $this->syncState->markRunStarted($integrationId, new DateTimeImmutable('now'));

        try {
            $statusMap = $this->buildStatusMap($integrationId);
            $cursorUpdatedAt = StringHelper::nullableString((string) ($state['last_synced_updated_at'] ?? ''));
            $cursorOrderId = StringHelper::nullableString((string) ($state['last_synced_source_order_id'] ?? ''));
            $startDate = $this->resolveStartDate(
                (string) ($integration['orders_fetch_start_date'] ?? ''),
                $cursorUpdatedAt
            );
            $baseUrl = trim((string) ($integration['base_url'] ?? ''));
            $apiKey = $this->integrations->getApiKeyDecrypted($integrationId);
            $timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10)));
            $productImageCache = [];

            if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') {
                throw new \RuntimeException('Brak poprawnych danych API dla integracji.');
            }

            $latestUpdatedAt = $cursorUpdatedAt;
            $latestOrderId = $cursorOrderId;
            $shouldStop = false;

            for ($page = 1; $page <= $maxPages; $page++) {
                $items = $this->fetchOrdersPage($baseUrl, (string) $apiKey, $timeout, $page, $pageLimit, $startDate);
                if ($items === []) {
                    break;
                }
                $candidates = $this->mapper->buildCandidates($items, $cursorUpdatedAt, $cursorOrderId);
                $this->processPageCandidates(
                    $candidates, $integrationId, $baseUrl, (string) $apiKey, $timeout,
                    $statusMap, $maxOrders, $result, $productImageCache, $shouldStop,
                    $latestUpdatedAt, $latestOrderId
                );
                if ($shouldStop || count($items) < $pageLimit) {
                    break;
                }
            }

            $this->syncState->markRunSuccess($integrationId, new DateTimeImmutable('now'), $latestUpdatedAt, $latestOrderId);
        } catch (Throwable $exception) {
            $this->syncState->markRunFailed($integrationId, new DateTimeImmutable('now'), $exception->getMessage());
            $result['failed'] = (int) $result['failed'] + 1;
            $errors = is_array($result['errors']) ? $result['errors'] : [];
            if (count($errors) < 20) {
                $errors[] = ['integration_id' => $integrationId, 'error' => $exception->getMessage()];
            }
            $result['errors'] = $errors;
        }
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function fetchOrdersPage(string $baseUrl, string $apiKey, int $timeout, int $page, int $pageLimit, ?string $startDate): array
    {
        $orders = $this->apiClient->fetchOrders($baseUrl, $apiKey, $timeout, $page, $pageLimit, $startDate);
        if (($orders['ok'] ?? false) !== true) {
            throw new \RuntimeException((string) ($orders['message'] ?? 'Blad pobierania listy zamowien.'));
        }

        return is_array($orders['items'] ?? null) ? $orders['items'] : [];
    }

    /**
     * @param array<int, array<string, mixed>> $candidates
     * @param array<string, string> $statusMap
     * @param array<string, mixed> $result
     * @param array<int, string> $productImageCache
     */
    private function processPageCandidates(
        array $candidates,
        int $integrationId,
        string $baseUrl,
        string $apiKey,
        int $timeout,
        array $statusMap,
        int $maxOrders,
        array &$result,
        array &$productImageCache,
        bool &$shouldStop,
        ?string &$latestUpdatedAt,
        ?string &$latestOrderId
    ): void {
        foreach ($candidates as $candidate) {
            if ((int) $result['processed'] >= $maxOrders) {
                $shouldStop = true;
                break;
            }

            $sourceOrderId = (string) ($candidate['source_order_id'] ?? '');
            $sourceUpdatedAt = (string) ($candidate['source_updated_at'] ?? '');
            $rawOrder = is_array($candidate['payload'] ?? null) ? $candidate['payload'] : [];

            $details = $this->apiClient->fetchOrderById($baseUrl, $apiKey, $timeout, $sourceOrderId);
            if (($details['ok'] ?? false) === true && is_array($details['order'] ?? null)) {
                $detailsOrder = (array) $details['order'];
                foreach (['products', 'summary', 'paid', 'transport_cost', 'transport', 'transport_description',
                    'client_name', 'client_surname', 'client_email', 'client_phone', 'client_city',
                    'client_street', 'client_postal_code'] as $protectedKey) {
                    if (array_key_exists($protectedKey, $rawOrder)) {
                        unset($detailsOrder[$protectedKey]);
                    }
                }
                $rawOrder = array_replace($rawOrder, $detailsOrder);
            }

            $this->importOneOrder(
                $integrationId, $sourceOrderId, $sourceUpdatedAt, $rawOrder,
                $baseUrl, $apiKey, $timeout, $statusMap, $result, $productImageCache
            );

            if ($latestUpdatedAt === null || $sourceUpdatedAt > $latestUpdatedAt) {
                $latestUpdatedAt = $sourceUpdatedAt;
                $latestOrderId = $sourceOrderId;
            } elseif ($latestUpdatedAt === $sourceUpdatedAt && ($latestOrderId === null || strcmp($sourceOrderId, $latestOrderId) > 0)) {
                $latestOrderId = $sourceOrderId;
            }
        }
    }

    /**
     * @param array<string, mixed> $rawOrder
     * @param array<string, string> $statusMap
     * @param array<string, mixed> $result
     * @param array<int, string> $productImageCache
     */
    private function importOneOrder(
        int $integrationId,
        string $sourceOrderId,
        string $sourceUpdatedAt,
        array $rawOrder,
        string $baseUrl,
        string $apiKey,
        int $timeout,
        array $statusMap,
        array &$result,
        array &$productImageCache
    ): void {
        try {
            $productImages = $this->imageResolver->resolveProductImagesForOrder(
                $baseUrl, $apiKey, $timeout, $rawOrder, $productImageCache
            );
            $aggregate = $this->mapper->mapOrderAggregate(
                $integrationId, $rawOrder, $statusMap, $sourceOrderId, $sourceUpdatedAt, $productImages
            );
            $save = $this->orderImportRepository->upsertOrderAggregate(
                $aggregate['order'],
                $aggregate['addresses'],
                $aggregate['items'],
                $aggregate['payments'],
                $aggregate['shipments'],
                $aggregate['notes'],
                $aggregate['status_history']
            );
            $result['processed'] = (int) $result['processed'] + 1;
            if (!empty($save['created'])) {
                $result['imported_created'] = (int) $result['imported_created'] + 1;
            } else {
                $result['imported_updated'] = (int) $result['imported_updated'] + 1;
            }
            $wasCreated = !empty($save['created']);
            $savedOrderId = (int) ($save['order_id'] ?? 0);
            $summary = $wasCreated
                ? 'Import zamowienia z shopPRO'
                : 'Zaktualizowano zamowienie z shopPRO (re-import)';
            $details = [
                'integration_id' => $integrationId,
                'source_order_id' => $sourceOrderId,
                'source_updated_at' => $sourceUpdatedAt,
                'created' => $wasCreated,
                'trigger' => 'orders_sync',
                'trigger_label' => 'Synchronizacja zamowien',
            ];

            if (!$this->orders->shouldSkipDuplicateImportActivity($savedOrderId, $details)) {
                $this->orders->recordActivity(
                    $savedOrderId,
                    'import',
                    $summary,
                    $details,
                    'import',
                    'shopPRO'
                );
            }
        } catch (Throwable $exception) {
            $result['failed'] = (int) $result['failed'] + 1;
            $errors = is_array($result['errors']) ? $result['errors'] : [];
            if (count($errors) < 20) {
                $errors[] = [
                    'integration_id' => $integrationId,
                    'source_order_id' => $sourceOrderId,
                    'error' => $exception->getMessage(),
                ];
            }
            $result['errors'] = $errors;
        }
    }

    /**
     * @param mixed $rawIds
     * @return array<int, true>
     */
    private function normalizeIntegrationIds(mixed $rawIds): array
    {
        if (!is_array($rawIds)) {
            return [];
        }

        $result = [];
        foreach ($rawIds as $rawId) {
            $id = (int) $rawId;
            if ($id <= 0) {
                continue;
            }
            $result[$id] = true;
        }

        return $result;
    }

    /**
     * @return array<string, string>
     */
    private function buildStatusMap(int $integrationId): array
    {
        $rows = $this->statusMappings->listByIntegration($integrationId);
        $map = [];
        foreach ($rows as $row) {
            $shopCode = strtolower(trim((string) ($row['shoppro_status_code'] ?? '')));
            $orderCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? '')));
            if ($shopCode === '' || $orderCode === '') {
                continue;
            }
            $map[$shopCode] = $orderCode;
        }

        return $map;
    }

    private function resolveStartDate(string $settingsDate, ?string $cursorUpdatedAt): ?string
    {
        $settings = trim($settingsDate);
        $cursor = StringHelper::nullableString((string) $cursorUpdatedAt);
        if ($settings === '' && $cursor === null) {
            return null;
        }
        if ($settings === '') {
            return $cursor;
        }
        if ($cursor === null) {
            return $settings;
        }

        return $cursor > $settings ? $cursor : $settings;
    }

}
