<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Constants\IntegrationSources;
use App\Modules\Cron\CronRepository;
use PDO;
use Throwable;

final class AllegroStatusSyncService
{
    private const DIRECTION_ALLEGRO_TO_ORDERPRO = 'allegro_to_orderpro';
    private const DIRECTION_ORDERPRO_TO_ALLEGRO = 'orderpro_to_allegro';

    private const FINAL_STATUSES = ['anulowane', 'cancelled', 'returned', 'zwrocone'];
    private const MAX_ORDERS_PER_RUN = 50;

    public function __construct(
        private readonly CronRepository $cronRepository,
        private readonly AllegroOrderImportService $orderImportService,
        private readonly AllegroApiClient $apiClient,
        private readonly AllegroTokenManager $tokenManager,
        private readonly AllegroStatusMappingRepository $statusMappings,
        private readonly AllegroOrderSyncStateRepository $syncStateRepository,
        private readonly AllegroIntegrationRepository $integrationRepository,
        private readonly PDO $pdo
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function sync(): array
    {
        $direction = trim($this->cronRepository->getStringSetting(
            'allegro_status_sync_direction',
            self::DIRECTION_ALLEGRO_TO_ORDERPRO
        ));
        if (!in_array($direction, [self::DIRECTION_ALLEGRO_TO_ORDERPRO, self::DIRECTION_ORDERPRO_TO_ALLEGRO], true)) {
            $direction = self::DIRECTION_ALLEGRO_TO_ORDERPRO;
        }

        if ($direction === self::DIRECTION_ORDERPRO_TO_ALLEGRO) {
            return $this->syncPushDirection();
        }

        return $this->syncPullDirection();
    }

    /**
     * @return array<string, mixed>
     */
    private function syncPullDirection(): array
    {
        $orders = $this->findOrdersNeedingStatusSync();

        $result = [
            'ok' => true,
            'direction' => self::DIRECTION_ALLEGRO_TO_ORDERPRO,
            'processed' => 0,
            'failed' => 0,
            'errors' => [],
        ];

        foreach ($orders as $order) {
            $sourceOrderId = (string) ($order['source_order_id'] ?? '');
            if ($sourceOrderId === '') {
                continue;
            }

            try {
                $this->orderImportService->importSingleOrder($sourceOrderId, 'status_sync');
                $result['processed']++;
                $this->markOrderStatusChecked((int) ($order['id'] ?? 0));
            } catch (Throwable $exception) {
                $result['failed']++;
                $errors = is_array($result['errors']) ? $result['errors'] : [];
                if (count($errors) < 20) {
                    $errors[] = [
                        'source_order_id' => $sourceOrderId,
                        'error' => $exception->getMessage(),
                    ];
                }
                $result['errors'] = $errors;
            }
        }

        return $result;
    }

    /**
     * @return array<string, mixed>
     */
    private function syncPushDirection(): array
    {
        $integrationId = $this->integrationRepository->getActiveIntegrationId();
        if ($integrationId <= 0) {
            return [
                'ok' => false,
                'direction' => self::DIRECTION_ORDERPRO_TO_ALLEGRO,
                'pushed' => 0,
                'skipped' => 0,
                'failed' => 0,
                'message' => 'Brak aktywnej integracji Allegro.',
                'errors' => [],
            ];
        }

        $reverseMap = $this->statusMappings->buildOrderproToAllegroMap();
        if ($reverseMap === []) {
            return [
                'ok' => true,
                'direction' => self::DIRECTION_ORDERPRO_TO_ALLEGRO,
                'pushed' => 0,
                'skipped' => 0,
                'failed' => 0,
                'message' => 'Brak mapowan statusow orderPRO -> Allegro.',
                'errors' => [],
            ];
        }

        [$accessToken, $environment] = $this->tokenManager->resolveToken();
        $lastStatusPushedAt = $this->syncStateRepository->getLastStatusPushedAt($integrationId);
        $orders = $this->findOrdersForPush($integrationId, $lastStatusPushedAt);

        if ($orders === []) {
            return [
                'ok' => true,
                'direction' => self::DIRECTION_ORDERPRO_TO_ALLEGRO,
                'pushed' => 0,
                'skipped' => 0,
                'failed' => 0,
                'message' => 'Brak zamowien do synchronizacji statusow.',
                'errors' => [],
            ];
        }

        $result = [
            'ok' => true,
            'direction' => self::DIRECTION_ORDERPRO_TO_ALLEGRO,
            'pushed' => 0,
            'skipped' => 0,
            'failed' => 0,
            'errors' => [],
        ];
        $latestPushedChangeAt = null;

        foreach ($orders as $order) {
            $sourceOrderId = trim((string) ($order['source_order_id'] ?? ''));
            $orderproStatusCode = strtolower(trim((string) ($order['orderpro_status_code'] ?? '')));
            if ($sourceOrderId === '' || $orderproStatusCode === '') {
                $result['skipped']++;
                continue;
            }

            $allegroStatusCode = $reverseMap[$orderproStatusCode] ?? null;
            if ($allegroStatusCode === null || trim($allegroStatusCode) === '') {
                $result['skipped']++;
                continue;
            }

            try {
                $resolved = $this->pushStatusWith401Retry(
                    $environment,
                    $accessToken,
                    $sourceOrderId,
                    $allegroStatusCode
                );
                $environment = $resolved['environment'];
                $accessToken = $resolved['token'];
                $result['pushed']++;

                $changeAt = trim((string) ($order['latest_change'] ?? ''));
                if ($changeAt !== '' && ($latestPushedChangeAt === null || $changeAt > $latestPushedChangeAt)) {
                    $latestPushedChangeAt = $changeAt;
                }
            } catch (Throwable $exception) {
                $result['failed']++;
                $errors = is_array($result['errors']) ? $result['errors'] : [];
                if (count($errors) < 20) {
                    $errors[] = [
                        'source_order_id' => $sourceOrderId,
                        'orderpro_status_code' => $orderproStatusCode,
                        'allegro_status_code' => $allegroStatusCode,
                        'error' => $exception->getMessage(),
                    ];
                }
                $result['errors'] = $errors;
            }
        }

        if ($latestPushedChangeAt !== null) {
            $this->syncStateRepository->updateLastStatusPushedAt($integrationId, $latestPushedChangeAt);
        }

        return $result;
    }

    /**
     * @return array<string, string>
     */
    private function pushStatusWith401Retry(
        string $environment,
        string $accessToken,
        string $checkoutFormId,
        string $allegroStatusCode
    ): array {
        try {
            $this->apiClient->updateCheckoutFormFulfillment(
                $environment,
                $accessToken,
                $checkoutFormId,
                $allegroStatusCode
            );

            return ['environment' => $environment, 'token' => $accessToken];
        } catch (Throwable $exception) {
            if (!str_contains($exception->getMessage(), 'ALLEGRO_HTTP_401')) {
                throw $exception;
            }
        }

        [$refreshedToken, $refreshedEnvironment] = $this->tokenManager->resolveToken();
        $this->apiClient->updateCheckoutFormFulfillment(
            $refreshedEnvironment,
            $refreshedToken,
            $checkoutFormId,
            $allegroStatusCode
        );

        return ['environment' => $refreshedEnvironment, 'token' => $refreshedToken];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function findOrdersNeedingStatusSync(): array
    {
        $placeholders = implode(',', array_fill(0, count(self::FINAL_STATUSES), '?'));

        try {
            $statement = $this->pdo->prepare(
                'SELECT id, source_order_id, external_status_id
                FROM orders
                WHERE source = ?
                  AND LOWER(COALESCE(external_status_id, "")) NOT IN (' . $placeholders . ')
                  AND (last_status_checked_at IS NULL OR source_updated_at > last_status_checked_at)
                ORDER BY source_updated_at DESC
                LIMIT ' . self::MAX_ORDERS_PER_RUN
            );
            $params = array_merge([IntegrationSources::ALLEGRO], self::FINAL_STATUSES);
            $statement->execute($params);

            return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function findOrdersForPush(int $integrationId, ?string $lastStatusPushedAt): array
    {
        $sinceDate = $lastStatusPushedAt;
        if ($sinceDate === null || trim($sinceDate) === '') {
            $sinceDate = date('Y-m-d H:i:s', strtotime('-24 hours'));
        }

        try {
            $statement = $this->pdo->prepare(
                'SELECT
                    o.id,
                    o.source_order_id,
                    o.external_status_id AS orderpro_status_code,
                    MAX(h.changed_at) AS latest_change
                FROM order_status_history h
                INNER JOIN orders o ON o.id = h.order_id
                WHERE o.source = :source
                  AND o.integration_id = :integration_id
                  AND h.change_source = :change_source
                  AND h.changed_at > :since_date
                GROUP BY o.id, o.source_order_id, o.external_status_id
                ORDER BY latest_change ASC
                LIMIT ' . self::MAX_ORDERS_PER_RUN
            );
            $statement->execute([
                'source' => IntegrationSources::ALLEGRO,
                'integration_id' => $integrationId,
                'change_source' => 'manual',
                'since_date' => $sinceDate,
            ]);

            return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable) {
            return [];
        }
    }

    private function markOrderStatusChecked(int $orderId): void
    {
        try {
            $statement = $this->pdo->prepare('UPDATE orders SET last_status_checked_at = NOW() WHERE id = ?');
            $statement->execute([$orderId]);
        } catch (Throwable) {
            // Blad zapisu znacznika nie powinien przerywac petli synchronizacji.
        }
    }
}
