<?php
declare(strict_types=1);

namespace App\Modules\Orders;

use App\Core\Support\StringHelper;
use PDO;
use Throwable;

final class OrdersRepository
{
    private static ?bool $supportsMappedMedia = null;

    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * @param array<string, mixed> $filters
     * @return array{items:array<int, array<string, mixed>>, total:int, page:int, per_page:int, error:string}
     */
    public function paginate(array $filters): array
    {
        $page = max(1, (int) ($filters['page'] ?? 1));
        $perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20)));
        $offset = ($page - 1) * $perPage;
        $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
        $effectiveOrderedAtSql = $this->effectiveOrderedAtSql('o');

        ['where' => $where, 'params' => $params] = $this->buildPaginateFilters($filters, $effectiveStatusSql, $effectiveOrderedAtSql);
        $whereSql = $where === [] ? '' : (' WHERE ' . implode(' AND ', $where));

        $sort = (string) ($filters['sort'] ?? 'ordered_at');
        $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
        $sortColumn = match ($sort) {
            'source_order_id' => 'o.source_order_id',
            'external_order_id' => 'o.external_order_id',
            'external_status_id' => 'o.external_status_id',
            'payment_status' => 'o.payment_status',
            'total_with_tax' => 'o.total_with_tax',
            'total_paid' => 'o.total_paid',
            'source_updated_at' => 'o.source_updated_at',
            'fetched_at' => 'o.fetched_at',
            'id' => 'o.id',
            default => $effectiveOrderedAtSql,
        };

        try {
            $countSql = 'SELECT COUNT(*) FROM orders o '
                . 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" '
                . 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code'
                . $whereSql;
            $countStmt = $this->pdo->prepare($countSql);
            $countStmt->execute($params);
            $total = (int) $countStmt->fetchColumn();

            $listSql = $this->buildListSql($effectiveStatusSql, $effectiveOrderedAtSql, $whereSql, $sortColumn, $sortDir);
            $stmt = $this->pdo->prepare($listSql);
            foreach ($params as $key => $value) {
                $stmt->bindValue(':' . $key, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
            }
            $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
            $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
            $stmt->execute();

            $rows = $stmt->fetchAll();
            if (!is_array($rows)) {
                $rows = [];
            }
            $itemPreviewsByOrderId = $this->loadOrderItemsPreviews(array_map(
                static fn (array $row): int => (int) ($row['id'] ?? 0),
                $rows
            ));

            return [
                'items' => array_map(fn (array $row) => $this->transformOrderRow($row, $itemPreviewsByOrderId), $rows),
                'total' => $total,
                'page' => $page,
                'per_page' => $perPage,
                'error' => '',
            ];
        } catch (Throwable $exception) {
            return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage, 'error' => $exception->getMessage()];
        }
    }

    /**
     * @param array<string, mixed> $filters
     * @return array{where:array<int,string>,params:array<string,mixed>}
     */
    private function buildPaginateFilters(array $filters, string $effectiveStatusSql, string $effectiveOrderedAtSql): array
    {
        $where = [];
        $params = [];

        $search = trim((string) ($filters['search'] ?? ''));
        if ($search !== '') {
            $where[] = '(o.source_order_id LIKE :s1 OR o.external_order_id LIKE :s2 OR o.customer_login LIKE :s3 OR a.name LIKE :s4 OR a.email LIKE :s5)';
            $searchVal = '%' . $search . '%';
            $params['s1'] = $searchVal;
            $params['s2'] = $searchVal;
            $params['s3'] = $searchVal;
            $params['s4'] = $searchVal;
            $params['s5'] = $searchVal;
        }

        $source = trim((string) ($filters['source'] ?? ''));
        if ($source !== '') {
            $where[] = 'o.source = :source';
            $params['source'] = $source;
        }

        $status = trim((string) ($filters['status'] ?? ''));
        if ($status !== '') {
            $where[] = $effectiveStatusSql . ' = :status';
            $params['status'] = $status;
        }

        $paymentStatus = trim((string) ($filters['payment_status'] ?? ''));
        if ($paymentStatus !== '' && ctype_digit($paymentStatus)) {
            $where[] = 'o.payment_status = :payment_status';
            $params['payment_status'] = (int) $paymentStatus;
        }

        $dateFrom = trim((string) ($filters['date_from'] ?? ''));
        if ($dateFrom !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) === 1) {
            $where[] = $effectiveOrderedAtSql . ' >= :date_from';
            $params['date_from'] = $dateFrom . ' 00:00:00';
        }

        $dateTo = trim((string) ($filters['date_to'] ?? ''));
        if ($dateTo !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo) === 1) {
            $where[] = $effectiveOrderedAtSql . ' <= :date_to';
            $params['date_to'] = $dateTo . ' 23:59:59';
        }

        return ['where' => $where, 'params' => $params];
    }

    private function buildListSql(string $effectiveStatusSql, string $effectiveOrderedAtSql, string $whereSql, string $sortColumn, string $sortDir): string
    {
        return 'SELECT
                    o.id,
                    o.internal_order_number,
                    o.source,
                    o.source_order_id,
                    o.external_order_id,
                    o.external_status_id,
                    ' . $effectiveStatusSql . ' AS effective_status_id,
                    o.payment_status,
                    o.currency,
                    o.total_with_tax,
                    o.total_paid,
                    o.ordered_at,
                    o.source_created_at,
                    o.source_updated_at,
                    o.fetched_at,
                    ' . $effectiveOrderedAtSql . ' AS effective_ordered_at,
                    o.is_invoice,
                    o.is_canceled_by_buyer,
                    a.name AS buyer_name,
                    a.email AS buyer_email,
                    a.city AS buyer_city,
                    o.external_carrier_id,
                    o.external_payment_type_id,
                    COALESCE(oi_agg.items_count, 0) AS items_count,
                    COALESCE(oi_agg.items_qty, 0) AS items_qty,
                    COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
                    COALESCE(od_agg.documents_count, 0) AS documents_count,
                    ig.name AS integration_name
                FROM orders o
                LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"
                LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
                LEFT JOIN integrations ig ON ig.id = o.integration_id
                LEFT JOIN (
                    SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty
                    FROM order_items GROUP BY order_id
                ) oi_agg ON oi_agg.order_id = o.id
                LEFT JOIN (
                    SELECT order_id, COUNT(*) AS shipments_count
                    FROM order_shipments GROUP BY order_id
                ) sh_agg ON sh_agg.order_id = o.id
                LEFT JOIN (
                    SELECT order_id, COUNT(*) AS documents_count
                    FROM order_documents GROUP BY order_id
                ) od_agg ON od_agg.order_id = o.id'
            . $whereSql
            . ' ORDER BY ' . $sortColumn . ' ' . $sortDir
            . ' LIMIT :limit OFFSET :offset';
    }

    /**
     * @param array<string, mixed> $row
     * @param array<int, array<int, array{name:string,quantity:float,media_url:string}>> $itemPreviewsByOrderId
     * @return array<string, mixed>
     */
    private function transformOrderRow(array $row, array $itemPreviewsByOrderId): array
    {
        $orderId = (int) ($row['id'] ?? 0);

        return [
            'id' => $orderId,
            'internal_order_number' => (string) ($row['internal_order_number'] ?? ''),
            'source' => (string) ($row['source'] ?? ''),
            'source_order_id' => (string) ($row['source_order_id'] ?? ''),
            'external_order_id' => (string) ($row['external_order_id'] ?? ''),
            'external_status_id' => (string) ($row['external_status_id'] ?? ''),
            'effective_status_id' => (string) ($row['effective_status_id'] ?? ''),
            'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null,
            'currency' => (string) ($row['currency'] ?? ''),
            'total_with_tax' => $row['total_with_tax'] !== null ? (float) $row['total_with_tax'] : null,
            'total_paid' => $row['total_paid'] !== null ? (float) $row['total_paid'] : null,
            'ordered_at' => (string) ($row['effective_ordered_at'] ?? ''),
            'source_created_at' => (string) ($row['source_created_at'] ?? ''),
            'source_updated_at' => (string) ($row['source_updated_at'] ?? ''),
            'fetched_at' => (string) ($row['fetched_at'] ?? ''),
            'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1,
            'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1,
            'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''),
            'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''),
            'buyer_name' => (string) ($row['buyer_name'] ?? ''),
            'buyer_email' => (string) ($row['buyer_email'] ?? ''),
            'buyer_city' => (string) ($row['buyer_city'] ?? ''),
            'items_count' => (int) ($row['items_count'] ?? 0),
            'items_qty' => (float) ($row['items_qty'] ?? 0),
            'shipments_count' => (int) ($row['shipments_count'] ?? 0),
            'documents_count' => (int) ($row['documents_count'] ?? 0),
            'integration_name' => (string) ($row['integration_name'] ?? ''),
            'items_preview' => (array) ($itemPreviewsByOrderId[$orderId] ?? []),
        ];
    }

    /**
     * @return array<string, string>
     */
    public function statusOptions(): array
    {
        try {
            $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
            $rows = $this->pdo->query(
                'SELECT DISTINCT ' . $effectiveStatusSql . ' AS effective_status_id
                FROM orders o
                LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
                WHERE ' . $effectiveStatusSql . ' IS NOT NULL
                  AND ' . $effectiveStatusSql . ' <> ""
                ORDER BY effective_status_id ASC'
            )->fetchAll(PDO::FETCH_COLUMN);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows)) {
            return [];
        }

        $options = [];
        foreach ($rows as $row) {
            $value = trim((string) $row);
            if ($value === '') {
                continue;
            }
            $options[$value] = $value;
        }

        return $options;
    }

    /**
     * @return array<string, string>
     */
    public function sourceOptions(): array
    {
        try {
            $rows = $this->pdo->query('SELECT DISTINCT source FROM orders WHERE source IS NOT NULL AND source <> "" ORDER BY source ASC')->fetchAll(PDO::FETCH_COLUMN);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows)) {
            return [];
        }

        $options = [];
        foreach ($rows as $row) {
            $value = trim((string) $row);
            if ($value === '') {
                continue;
            }
            $options[$value] = $value;
        }

        return $options;
    }

    /**
     * @return array{all:int, paid:int, shipped:int}
     */
    public function quickStats(): array
    {
        try {
            $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
            $row = $this->pdo->query('SELECT
                    COUNT(*) AS all_count,
                    SUM(CASE WHEN payment_status = 2 THEN 1 ELSE 0 END) AS paid_count,
                    SUM(CASE WHEN ' . $effectiveStatusSql . ' IN ("shipped", "delivered", "returned") THEN 1 ELSE 0 END) AS shipped_count
                FROM orders o
                LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [
                'all' => 0,
                'paid' => 0,
                'shipped' => 0,
            ];
        }

        if (!is_array($row)) {
            return [
                'all' => 0,
                'paid' => 0,
                'shipped' => 0,
            ];
        }

        return [
            'all' => (int) ($row['all_count'] ?? 0),
            'paid' => (int) ($row['paid_count'] ?? 0),
            'shipped' => (int) ($row['shipped_count'] ?? 0),
        ];
    }

    /**
     * @return array<string, int>
     */
    public function statusCounts(): array
    {
        try {
            $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
            $rows = $this->pdo->query(
                'SELECT ' . $effectiveStatusSql . ' AS effective_status_id, COUNT(*) AS cnt
                FROM orders o
                LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
                GROUP BY effective_status_id'
            )->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows)) {
            return [];
        }

        $result = [];
        foreach ($rows as $row) {
            $key = trim((string) ($row['effective_status_id'] ?? ''));
            if ($key === '') {
                $key = '_empty';
            }
            $result[$key] = (int) ($row['cnt'] ?? 0);
        }

        return $result;
    }

    /**
     * @return array<int, array{name:string,color_hex:string,items:array<int, array{code:string,name:string}>}>
     */
    public function statusPanelConfig(): array
    {
        try {
            $sql = 'SELECT
                    g.id AS group_id,
                    g.name AS group_name,
                    g.color_hex AS group_color_hex,
                    g.sort_order AS group_sort_order,
                    s.code AS status_code,
                    s.name AS status_name,
                    s.sort_order AS status_sort_order
                FROM order_status_groups g
                LEFT JOIN order_statuses s ON s.group_id = g.id AND s.is_active = 1
                WHERE g.is_active = 1
                ORDER BY g.sort_order ASC, g.id ASC, s.sort_order ASC, s.id ASC';
            $rows = $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows) || $rows === []) {
            return [];
        }

        $groupMap = [];
        foreach ($rows as $row) {
            $groupId = (int) ($row['group_id'] ?? 0);
            if ($groupId <= 0) {
                continue;
            }

            if (!isset($groupMap[$groupId])) {
                $groupMap[$groupId] = [
                    'name' => trim((string) ($row['group_name'] ?? '')),
                    'color_hex' => StringHelper::normalizeColorHex((string) ($row['group_color_hex'] ?? '#64748b')),
                    'items' => [],
                ];
            }

            $statusCode = trim((string) ($row['status_code'] ?? ''));
            if ($statusCode === '') {
                continue;
            }

            $groupMap[$groupId]['items'][] = [
                'code' => $statusCode,
                'name' => trim((string) ($row['status_name'] ?? $statusCode)),
            ];
        }

        return array_values($groupMap);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function findDetails(int $orderId): ?array
    {
        if ($orderId <= 0) {
            return null;
        }

        try {
            $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
            $orderStmt = $this->pdo->prepare(
                'SELECT o.*, ' . $effectiveStatusSql . ' AS effective_status_id,
                    ig.name AS integration_name
                FROM orders o
                LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
                LEFT JOIN integrations ig ON ig.id = o.integration_id
                WHERE o.id = :id
                LIMIT 1'
            );
            $orderStmt->execute(['id' => $orderId]);
            $order = $orderStmt->fetch(PDO::FETCH_ASSOC);
            if (!is_array($order)) {
                return null;
            }

            return [
                'order' => $order,
                'addresses' => $this->loadOrderAddresses($orderId),
                'items' => $this->loadOrderItems($orderId),
                'payments' => $this->loadOrderPayments($orderId),
                'shipments' => $this->loadOrderShipments($orderId),
                'documents' => $this->loadOrderDocuments($orderId),
                'notes' => $this->loadOrderNotes($orderId),
                'status_history' => $this->loadOrderStatusHistory($orderId),
                'activity_log' => $this->loadActivityLog($orderId),
            ];
        } catch (Throwable) {
            return null;
        }
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderAddresses(int $orderId): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM order_addresses WHERE order_id = :order_id ORDER BY FIELD(address_type, "customer", "invoice", "delivery"), id ASC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderItems(int $orderId): array
    {
        $itemsMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source');
        $stmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url
            FROM order_items oi
            INNER JOIN orders o ON o.id = oi.order_id
            WHERE oi.order_id = :order_id
            ORDER BY oi.sort_order ASC, oi.id ASC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (!is_array($rows)) {
            return [];
        }

        return array_map(static function (array $row): array {
            $resolvedMediaUrl = trim((string) ($row['resolved_media_url'] ?? ''));
            if ($resolvedMediaUrl !== '') {
                $row['media_url'] = $resolvedMediaUrl;
            }
            unset($row['resolved_media_url']);

            return $row;
        }, $rows);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderPayments(int $orderId): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM order_payments WHERE order_id = :order_id ORDER BY payment_date ASC, id ASC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderShipments(int $orderId): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM order_shipments WHERE order_id = :order_id ORDER BY posted_at ASC, id ASC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderDocuments(int $orderId): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM order_documents WHERE order_id = :order_id ORDER BY source_created_at ASC, id ASC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderNotes(int $orderId): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id ORDER BY created_at_external DESC, id DESC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadOrderStatusHistory(int $orderId): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM order_status_history WHERE order_id = :order_id ORDER BY changed_at DESC, id DESC');
        $stmt->execute(['order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    }

    /**
     * @param array<int, int> $orderIds
     * @return array<int, array<int, array{name:string, quantity:float, media_url:string}>>
     */
    private function loadOrderItemsPreviews(array $orderIds): array
    {
        $cleanIds = [];
        foreach ($orderIds as $id) {
            $orderId = max(0, (int) $id);
            if ($orderId <= 0 || in_array($orderId, $cleanIds, true)) {
                continue;
            }
            $cleanIds[] = $orderId;
        }
        if ($cleanIds === []) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($cleanIds), '?'));
        try {
            $resolvedMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source');
            $sql = 'SELECT oi.order_id, oi.original_name, oi.quantity, ' . $resolvedMediaSql . ' AS media_url, oi.sort_order, oi.id
                FROM order_items oi
                INNER JOIN orders o ON o.id = oi.order_id
                WHERE oi.order_id IN (' . $placeholders . ')
                ORDER BY oi.order_id ASC, oi.sort_order ASC, oi.id ASC';
            $stmt = $this->pdo->prepare($sql);
            foreach ($cleanIds as $index => $orderId) {
                $stmt->bindValue($index + 1, $orderId, PDO::PARAM_INT);
            }
            $stmt->execute();
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [];
        }
        if (!is_array($rows)) {
            return [];
        }

        $result = [];
        foreach ($rows as $row) {
            $orderId = (int) ($row['order_id'] ?? 0);
            if ($orderId <= 0) {
                continue;
            }
            if (!isset($result[$orderId])) {
                $result[$orderId] = [];
            }
            if (count($result[$orderId]) >= 4) {
                continue;
            }
            $result[$orderId][] = [
                'name' => trim((string) ($row['original_name'] ?? '')),
                'quantity' => (float) ($row['quantity'] ?? 0),
                'media_url' => trim((string) ($row['media_url'] ?? '')),
            ];
        }

        return $result;
    }

    private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
    {
        return 'CASE
            WHEN ' . $orderAlias . '.source = "allegro"
             AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
             AND ' . $mappingAlias . '.orderpro_status_code <> ""
            THEN ' . $mappingAlias . '.orderpro_status_code
            ELSE ' . $orderAlias . '.external_status_id
        END';
    }

    private function effectiveOrderedAtSql(string $orderAlias): string
    {
        return 'COALESCE('
            . $orderAlias . '.ordered_at, '
            . $orderAlias . '.source_created_at, '
            . $orderAlias . '.source_updated_at, '
            . $orderAlias . '.fetched_at'
            . ')';
    }

    private function resolvedMediaUrlSql(string $itemAlias, string $sourceAlias = '"allegro"'): string
    {
        if (!$this->canResolveMappedMedia()) {
            return 'COALESCE(NULLIF(TRIM(' . $itemAlias . '.media_url), ""), "")';
        }

        return 'COALESCE(
            NULLIF(TRIM(' . $itemAlias . '.media_url), ""),
            (
                SELECT NULLIF(TRIM(pi.storage_path), "")
                FROM product_channel_map pcm
                INNER JOIN sales_channels sc ON sc.id = pcm.channel_id
                INNER JOIN product_images pi ON pi.product_id = pcm.product_id
                WHERE LOWER(sc.code) = LOWER(' . $sourceAlias . ')
                  AND (
                      pcm.external_product_id = ' . $itemAlias . '.external_item_id
                      OR pcm.external_product_id = ' . $itemAlias . '.source_product_id
                  )
                ORDER BY pi.is_main DESC, pi.sort_order ASC, pi.id ASC
                LIMIT 1
            ),
            ""
        )';
    }

    private function canResolveMappedMedia(): bool
    {
        if (self::$supportsMappedMedia !== null) {
            return self::$supportsMappedMedia;
        }

        try {
            $requiredColumns = [
                ['table' => 'product_channel_map', 'column' => 'product_id'],
                ['table' => 'product_channel_map', 'column' => 'channel_id'],
                ['table' => 'product_channel_map', 'column' => 'external_product_id'],
                ['table' => 'sales_channels', 'column' => 'id'],
                ['table' => 'sales_channels', 'column' => 'code'],
                ['table' => 'product_images', 'column' => 'id'],
                ['table' => 'product_images', 'column' => 'product_id'],
                ['table' => 'product_images', 'column' => 'storage_path'],
                ['table' => 'product_images', 'column' => 'sort_order'],
                ['table' => 'product_images', 'column' => 'is_main'],
            ];

            $pairsSql = [];
            $params = [];
            foreach ($requiredColumns as $index => $required) {
                $tableParam = ':table_' . $index;
                $columnParam = ':column_' . $index;
                $pairsSql[] = '(TABLE_NAME = ' . $tableParam . ' AND COLUMN_NAME = ' . $columnParam . ')';
                $params['table_' . $index] = $required['table'];
                $params['column_' . $index] = $required['column'];
            }

            $sql = 'SELECT COUNT(*) AS cnt
                FROM information_schema.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                  AND (' . implode(' OR ', $pairsSql) . ')';
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            $count = (int) $stmt->fetchColumn();

            self::$supportsMappedMedia = ($count === count($requiredColumns));
        } catch (Throwable) {
            self::$supportsMappedMedia = false;
        }

        return self::$supportsMappedMedia;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadActivityLog(int $orderId): array
    {
        try {
            $stmt = $this->pdo->prepare(
                'SELECT * FROM order_activity_log
                WHERE order_id = :order_id
                ORDER BY created_at DESC, id DESC'
            );
            $stmt->execute(['order_id' => $orderId]);
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

            return is_array($rows) ? $rows : [];
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @param array<string, mixed>|null $details
     */
    public function recordActivity(
        int $orderId,
        string $eventType,
        string $summary,
        ?array $details = null,
        string $actorType = 'system',
        ?string $actorName = null
    ): void {
        $stmt = $this->pdo->prepare(
            'INSERT INTO order_activity_log
                (order_id, event_type, summary, details_json, actor_type, actor_name, created_at)
            VALUES
                (:order_id, :event_type, :summary, :details_json, :actor_type, :actor_name, NOW())'
        );
        $stmt->execute([
            'order_id' => $orderId,
            'event_type' => $eventType,
            'summary' => $summary,
            'details_json' => $details !== null ? json_encode($details, JSON_UNESCAPED_UNICODE) : null,
            'actor_type' => $actorType,
            'actor_name' => $actorName,
        ]);
    }

    /**
     * @param array<string, mixed> $details
     */
    public function shouldSkipDuplicateImportActivity(int $orderId, array $details): bool
    {
        if ($orderId <= 0 || !empty($details['created'])) {
            return false;
        }

        $sourceOrderId = trim((string) ($details['source_order_id'] ?? ''));
        $sourceUpdatedAt = trim((string) ($details['source_updated_at'] ?? ''));
        $trigger = trim((string) ($details['trigger'] ?? ''));

        if ($sourceOrderId === '' || $sourceUpdatedAt === '' || $trigger === '') {
            return false;
        }

        try {
            $stmt = $this->pdo->prepare(
                'SELECT details_json
                FROM order_activity_log
                WHERE order_id = :order_id
                  AND event_type = :event_type
                ORDER BY created_at DESC, id DESC
                LIMIT 1'
            );
            $stmt->execute([
                'order_id' => $orderId,
                'event_type' => 'import',
            ]);
            $lastDetailsJson = $stmt->fetchColumn();
        } catch (Throwable) {
            return false;
        }

        if (!is_string($lastDetailsJson) || trim($lastDetailsJson) === '') {
            return false;
        }

        $lastDetails = json_decode($lastDetailsJson, true);
        if (!is_array($lastDetails) || !empty($lastDetails['created'])) {
            return false;
        }

        $lastSourceOrderId = trim((string) ($lastDetails['source_order_id'] ?? ''));
        $lastSourceUpdatedAt = trim((string) ($lastDetails['source_updated_at'] ?? ''));
        $lastTrigger = trim((string) ($lastDetails['trigger'] ?? ''));

        return $lastSourceOrderId === $sourceOrderId
            && $lastSourceUpdatedAt === $sourceUpdatedAt
            && $lastTrigger === $trigger;
    }

    public function recordStatusChange(
        int $orderId,
        ?string $fromStatus,
        string $toStatus,
        string $changeSource = 'manual',
        ?string $comment = null,
        string $actorType = 'system',
        ?string $actorName = null
    ): void {
        $stmt = $this->pdo->prepare(
            'INSERT INTO order_status_history
                (order_id, from_status_id, to_status_id, changed_at, change_source, comment)
            VALUES
                (:order_id, :from_status_id, :to_status_id, NOW(), :change_source, :comment)'
        );
        $stmt->execute([
            'order_id' => $orderId,
            'from_status_id' => $fromStatus,
            'to_status_id' => $toStatus,
            'change_source' => $changeSource,
            'comment' => $comment,
        ]);

        $fromLabel = $fromStatus !== null ? $this->resolveStatusName($fromStatus) : '-';
        $toLabel = $this->resolveStatusName($toStatus);
        $summary = 'Zmiana statusu: ' . $fromLabel . ' → ' . $toLabel;

        $this->recordActivity($orderId, 'status_change', $summary, [
            'from_status' => $fromStatus,
            'to_status' => $toStatus,
            'change_source' => $changeSource,
            'comment' => $comment,
        ], $actorType, $actorName);
    }

    /**
     * @param array<string, mixed> $data Keys: payment_type_id, amount, payment_date, comment, currency
     * @return array{id:int, payment_status:int, total_paid:float}|null
     */
    /**
     * @return array{source:string, integration_id:int, source_order_id:string}|null
     */
    public function findOrderSourceInfo(int $orderId): ?array
    {
        if ($orderId <= 0) {
            return null;
        }
        $stmt = $this->pdo->prepare('SELECT source, integration_id, source_order_id FROM orders WHERE id = :id LIMIT 1');
        $stmt->execute(['id' => $orderId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    /**
     * @param array<string, mixed> $data Keys: payment_type_id, amount, payment_date, comment, currency
     * @return array{id:int, payment_status:int, total_paid:float}|null
     */
    public function addPayment(int $orderId, array $data): ?array
    {
        if ($orderId <= 0) {
            return null;
        }

        $stmt = $this->pdo->prepare('SELECT id, total_with_tax, currency FROM orders WHERE id = :id LIMIT 1');
        $stmt->execute(['id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!is_array($order)) {
            return null;
        }

        $amount = round((float) ($data['amount'] ?? 0), 2);
        $paymentTypeId = trim((string) ($data['payment_type_id'] ?? ''));
        $paymentDate = trim((string) ($data['payment_date'] ?? ''));
        $comment = trim((string) ($data['comment'] ?? ''));
        $currency = trim((string) ($data['currency'] ?? $order['currency'] ?? 'PLN'));

        if ($amount <= 0 || $paymentTypeId === '') {
            return null;
        }

        $sourcePaymentId = 'manual_' . $orderId . '_' . time();

        $insert = $this->pdo->prepare(
            'INSERT INTO order_payments (order_id, source_payment_id, payment_type_id, payment_date, amount, currency, comment, created_at, updated_at)
            VALUES (:order_id, :source_payment_id, :payment_type_id, :payment_date, :amount, :currency, :comment, NOW(), NOW())'
        );
        $insert->execute([
            'order_id' => $orderId,
            'source_payment_id' => $sourcePaymentId,
            'payment_type_id' => $paymentTypeId,
            'payment_date' => $paymentDate !== '' ? $paymentDate : date('Y-m-d H:i:s'),
            'amount' => $amount,
            'currency' => $currency,
            'comment' => $comment !== '' ? $comment : null,
        ]);
        $paymentId = (int) $this->pdo->lastInsertId();

        $sumStmt = $this->pdo->prepare('SELECT COALESCE(SUM(amount), 0) FROM order_payments WHERE order_id = :order_id');
        $sumStmt->execute(['order_id' => $orderId]);
        $totalPaid = round((float) $sumStmt->fetchColumn(), 2);

        $totalWithTax = $order['total_with_tax'] !== null ? (float) $order['total_with_tax'] : null;
        $paymentStatus = 0;
        if ($totalPaid > 0 && $totalWithTax !== null && $totalPaid >= $totalWithTax) {
            $paymentStatus = 2;
        } elseif ($totalPaid > 0) {
            $paymentStatus = 1;
        }

        $update = $this->pdo->prepare('UPDATE orders SET payment_status = :payment_status, total_paid = :total_paid, updated_at = NOW() WHERE id = :id');
        $update->execute([
            'payment_status' => $paymentStatus,
            'total_paid' => $totalPaid,
            'id' => $orderId,
        ]);

        return [
            'id' => $paymentId,
            'payment_status' => $paymentStatus,
            'total_paid' => $totalPaid,
        ];
    }

    public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool
    {
        try {
            $stmt = $this->pdo->prepare('SELECT external_status_id FROM orders WHERE id = :id LIMIT 1');
            $stmt->execute(['id' => $orderId]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!is_array($row)) {
                return false;
            }

            $oldStatus = trim((string) ($row['external_status_id'] ?? ''));

            $update = $this->pdo->prepare('UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id');
            $update->execute(['status' => $newStatusCode, 'id' => $orderId]);

            $this->recordStatusChange(
                $orderId,
                $oldStatus !== '' ? $oldStatus : null,
                $newStatusCode,
                'manual',
                null,
                $actorType,
                $actorName
            );

            return true;
        } catch (Throwable) {
            return false;
        }
    }

    private function resolveStatusName(string $code): string
    {
        $normalized = strtolower(trim($code));
        if ($normalized === '') {
            return $code;
        }

        try {
            $stmt = $this->pdo->prepare('SELECT name FROM order_statuses WHERE LOWER(code) = :code LIMIT 1');
            $stmt->execute(['code' => $normalized]);
            $name = $stmt->fetchColumn();
            if (is_string($name) && trim($name) !== '') {
                return trim($name);
            }
        } catch (Throwable) {
        }

        return $code;
    }

}
