<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Constants\IntegrationSources;
use App\Core\Support\StringHelper;
use App\Modules\Automation\AutomationService;
use App\Modules\Orders\OrdersRepository;
use PDO;
use Throwable;

final class ShopproPaymentStatusSyncService
{
    private const PAID_STATUS = 2;
    private const UNPAID_STATUS = 0;

    /**
     * @var array<int, string>
     */
    private const DEFAULT_FINAL_STATUS_CODES = [
        'wyslane',
        'zrealizowane',
        'anulowane',
        'cancelled',
        'canceled',
        'delivered',
        'returned',
        'shipped',
    ];

    public function __construct(
        private readonly ShopproIntegrationsRepository $integrations,
        private readonly ShopproApiClient $apiClient,
        private readonly OrdersRepository $orders,
        private readonly PDO $pdo,
        private readonly ?AutomationService $automation = null
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function sync(array $options = []): array
    {
        $perIntegrationLimit = max(1, min(500, (int) ($options['per_integration_limit'] ?? 100)));
        $result = [
            'ok' => true,
            'checked_integrations' => 0,
            'processed_orders' => 0,
            'updated_orders' => 0,
            'skipped_orders' => 0,
            'failed_orders' => 0,
            'errors' => [],
        ];

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

            $baseUrl = trim((string) ($integration['base_url'] ?? ''));
            $apiKey = $this->integrations->getApiKeyDecrypted($integrationId);
            $timeout = max(1, min(120, (int) ($integration['timeout_seconds'] ?? 10)));
            if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') {
                continue;
            }

            $result['checked_integrations'] = (int) $result['checked_integrations'] + 1;
            $watchedStatuses = $this->resolveWatchedStatusCodes($integration);
            $orders = $this->findCandidateOrders($integrationId, $watchedStatuses, $perIntegrationLimit);

            foreach ($orders as $order) {
                $result['processed_orders'] = (int) $result['processed_orders'] + 1;
                $sourceOrderId = trim((string) ($order['source_order_id'] ?? ''));
                if ($sourceOrderId === '') {
                    $result['skipped_orders'] = (int) $result['skipped_orders'] + 1;
                    continue;
                }

                try {
                    $updated = $this->syncSingleOrderPayment(
                        $integrationId,
                        $baseUrl,
                        $apiKey,
                        $timeout,
                        $order
                    );
                    if ($updated) {
                        $result['updated_orders'] = (int) $result['updated_orders'] + 1;
                    } else {
                        $result['skipped_orders'] = (int) $result['skipped_orders'] + 1;
                    }
                } catch (Throwable $exception) {
                    $result['failed_orders'] = (int) $result['failed_orders'] + 1;
                    $errors = is_array($result['errors']) ? $result['errors'] : [];
                    if (count($errors) < 20) {
                        $errors[] = [
                            'integration_id' => $integrationId,
                            'order_id' => (int) ($order['id'] ?? 0),
                            'source_order_id' => $sourceOrderId,
                            'error' => $exception->getMessage(),
                        ];
                    }
                    $result['errors'] = $errors;
                }
            }
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $integration
     * @return array<int, string>
     */
    private function resolveWatchedStatusCodes(array $integration): array
    {
        $rawCodes = $integration['payment_sync_status_codes'] ?? [];
        if (!is_array($rawCodes)) {
            return [];
        }

        $result = [];
        $seen = [];
        foreach ($rawCodes as $rawCode) {
            $code = strtolower(trim((string) $rawCode));
            if ($code === '' || isset($seen[$code])) {
                continue;
            }
            $seen[$code] = true;
            $result[] = $code;
        }

        return $result;
    }

    /**
     * @param array<int, string> $watchedStatuses
     * @return array<int, array<string, mixed>>
     */
    private function findCandidateOrders(int $integrationId, array $watchedStatuses, int $limit): array
    {
        $where = [
            'source = :source',
            'integration_id = :integration_id',
            'source_order_id IS NOT NULL',
            'source_order_id <> ""',
            '(payment_status IS NULL OR payment_status <> :paid_status)',
        ];
        $params = [
            'source' => IntegrationSources::SHOPPRO,
            'integration_id' => $integrationId,
            'paid_status' => self::PAID_STATUS,
        ];

        $statusPlaceholders = [];
        $statusCodes = $watchedStatuses !== [] ? $watchedStatuses : self::DEFAULT_FINAL_STATUS_CODES;
        foreach ($statusCodes as $index => $statusCode) {
            $placeholder = ':status_' . $index;
            $statusPlaceholders[] = $placeholder;
            $params['status_' . $index] = strtolower($statusCode);
        }

        if ($watchedStatuses !== []) {
            $where[] = 'LOWER(COALESCE(external_status_id, "")) IN (' . implode(', ', $statusPlaceholders) . ')';
        } else {
            $where[] = 'LOWER(COALESCE(external_status_id, "")) NOT IN (' . implode(', ', $statusPlaceholders) . ')';
        }

        $sql = 'SELECT id, source_order_id, payment_status, total_paid, total_with_tax, currency, external_payment_type_id
            FROM orders
            WHERE ' . implode(' AND ', $where) . '
            ORDER BY source_updated_at DESC, id DESC
            LIMIT :limit';
        $stmt = $this->pdo->prepare($sql);
        foreach ($params as $key => $value) {
            if (is_int($value)) {
                $stmt->bindValue(':' . $key, $value, PDO::PARAM_INT);
                continue;
            }
            $stmt->bindValue(':' . $key, $value);
        }
        $stmt->bindValue(':limit', max(1, min(1000, $limit)), PDO::PARAM_INT);
        $stmt->execute();

        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return is_array($rows) ? $rows : [];
    }

    /**
     * @param array<string, mixed> $order
     */
    private function syncSingleOrderPayment(
        int $integrationId,
        string $baseUrl,
        string $apiKey,
        int $timeout,
        array $order
    ): bool {
        $sourceOrderId = trim((string) ($order['source_order_id'] ?? ''));
        if ($sourceOrderId === '') {
            return false;
        }

        $details = $this->apiClient->fetchOrderById($baseUrl, $apiKey, $timeout, $sourceOrderId);
        if (($details['ok'] ?? false) !== true || !is_array($details['order'] ?? null)) {
            throw new \RuntimeException((string) ($details['message'] ?? 'Blad pobierania szczegolow zamowienia.'));
        }

        $payload = (array) $details['order'];
        $isPaid = $this->resolvePaidFlag($payload);
        if ($isPaid === null) {
            return false;
        }

        $newPaymentStatus = $isPaid ? self::PAID_STATUS : self::UNPAID_STATUS;
        $existingTotalWithTax = $order['total_with_tax'] !== null ? (float) $order['total_with_tax'] : null;
        $newTotalPaid = $isPaid
            ? $this->resolvePaidAmount($payload, $existingTotalWithTax)
            : 0.0;
        $existingPaymentStatus = isset($order['payment_status']) ? (int) $order['payment_status'] : null;
        $existingTotalPaid = $order['total_paid'] !== null ? (float) $order['total_paid'] : null;
        $paymentMethod = StringHelper::nullableString((string) ($payload['payment_method'] ?? $order['external_payment_type_id'] ?? ''));
        $paymentDate = StringHelper::normalizeDateTime((string) ($payload['payment_date'] ?? ''));
        $sourceUpdatedAt = StringHelper::normalizeDateTime((string) ($payload['updated_at'] ?? $payload['date_updated'] ?? ''));

        if (
            $existingPaymentStatus === $newPaymentStatus
            && $this->floatsEqual($existingTotalPaid, $newTotalPaid)
            && $paymentMethod === StringHelper::nullableString((string) ($order['external_payment_type_id'] ?? ''))
        ) {
            return false;
        }

        $orderId = (int) ($order['id'] ?? 0);
        if ($orderId <= 0) {
            return false;
        }

        $this->pdo->beginTransaction();
        try {
            $this->updateOrderPaymentColumns($orderId, $newPaymentStatus, $newTotalPaid, $paymentMethod, $sourceUpdatedAt);
            $this->replaceOrderPaymentRow($orderId, $paymentMethod, $paymentDate, $newTotalPaid, (string) ($order['currency'] ?? 'PLN'), $isPaid);
            $this->pdo->commit();
        } catch (Throwable $exception) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $exception;
        }

        if ($existingPaymentStatus !== $newPaymentStatus) {
            try {
                $this->automation?->trigger('payment.status_changed', $orderId, [
                    'new_payment_status' => (string) $newPaymentStatus,
                    'old_payment_status' => $existingPaymentStatus !== null ? (string) $existingPaymentStatus : '',
                    'total_paid' => $newTotalPaid,
                    'payment_method' => $paymentMethod,
                ]);
            } catch (Throwable) {
            }
        }

        $summary = $isPaid
            ? 'shopPRO: zamowienie oznaczone jako oplacone'
            : 'shopPRO: zamowienie oznaczone jako nieoplacone';
        $this->orders->recordActivity(
            $orderId,
            'payment',
            $summary,
            [
                'integration_id' => $integrationId,
                'source_order_id' => $sourceOrderId,
                'old_payment_status' => $existingPaymentStatus,
                'new_payment_status' => $newPaymentStatus,
                'old_total_paid' => $existingTotalPaid,
                'new_total_paid' => $newTotalPaid,
            ],
            'sync',
            'shopPRO'
        );

        return true;
    }

    private function updateOrderPaymentColumns(
        int $orderId,
        int $paymentStatus,
        ?float $totalPaid,
        ?string $paymentMethod,
        ?string $sourceUpdatedAt
    ): void {
        $sql = 'UPDATE orders
            SET payment_status = :payment_status,
                total_paid = :total_paid,
                external_payment_type_id = :external_payment_type_id,
                fetched_at = NOW(),
                updated_at = NOW()';
        $params = [
            'id' => $orderId,
            'payment_status' => $paymentStatus,
            'total_paid' => $totalPaid,
            'external_payment_type_id' => $paymentMethod,
        ];
        if ($sourceUpdatedAt !== null) {
            $sql .= ', source_updated_at = :source_updated_at';
            $params['source_updated_at'] = $sourceUpdatedAt;
        }
        $sql .= ' WHERE id = :id';

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
    }

    private function replaceOrderPaymentRow(
        int $orderId,
        ?string $paymentMethod,
        ?string $paymentDate,
        ?float $amount,
        string $currency,
        bool $isPaid
    ): void {
        $deleteStmt = $this->pdo->prepare('DELETE FROM order_payments WHERE order_id = :order_id');
        $deleteStmt->execute(['order_id' => $orderId]);

        $insertStmt = $this->pdo->prepare(
            'INSERT INTO order_payments (
                order_id, source_payment_id, external_payment_id, payment_type_id, payment_date, amount, currency, comment, payload_json
            ) VALUES (
                :order_id, NULL, NULL, :payment_type_id, :payment_date, :amount, :currency, :comment, NULL
            )'
        );
        $insertStmt->execute([
            'order_id' => $orderId,
            'payment_type_id' => $paymentMethod ?? 'unknown',
            'payment_date' => $paymentDate,
            'amount' => $amount,
            'currency' => trim($currency) !== '' ? strtoupper($currency) : 'PLN',
            'comment' => $isPaid ? 'paid' : 'unpaid',
        ]);
    }

    private function resolvePaidFlag(array $payload): ?bool
    {
        $raw = $payload['paid'] ?? $payload['is_paid'] ?? null;
        if ($raw === null) {
            return null;
        }
        if (is_bool($raw)) {
            return $raw;
        }

        $value = strtolower(trim((string) $raw));
        if (in_array($value, ['1', 'true', 'yes', 'paid'], true)) {
            return true;
        }
        if (in_array($value, ['0', 'false', 'no', 'unpaid'], true)) {
            return false;
        }

        return null;
    }

    private function resolvePaidAmount(array $payload, ?float $fallbackGross): ?float
    {
        $value = $payload['total_paid'] ?? null;
        if ($value !== null && is_numeric((string) $value)) {
            return (float) $value;
        }

        $grossCandidates = [
            $payload['total_gross'] ?? null,
            $payload['total_with_tax'] ?? null,
            $payload['summary']['total'] ?? null,
            $payload['summary'] ?? null,
        ];
        foreach ($grossCandidates as $candidate) {
            if ($candidate !== null && is_numeric((string) $candidate)) {
                return (float) $candidate;
            }
        }

        return $fallbackGross;
    }

    private function floatsEqual(?float $left, ?float $right): bool
    {
        if ($left === null && $right === null) {
            return true;
        }
        if ($left === null || $right === null) {
            return false;
        }

        return abs($left - $right) < 0.00001;
    }
}
