<?php

namespace classes;

use Throwable;

class invoice_period_flag_service
{
    private const SOURCE_MANUAL = 'manual';
    private const SOURCE_AUTOMATIC = 'automatic';
    private const STATUS_ACTIVE = 'active';
    private const VALID_STATUSES = ['active', 'resolved', 'ignored', 'false_positive'];
    private const VALID_TARGETS = [
        'collected_order_invoice',
        'order',
        'order_field',
        'order_item',
        'order_item_field',
        'customer',
        'xlvask_usage_log',
    ];
    private const ORDER_FIELDS = ['customer', 'reference', 'po', 'notes'];
    private const ORDER_ITEM_FIELDS = ['notes', 'quantity', 'reference', 'price'];
    private const WASH_CERTIFICATE_PRODUCT_ID = 41;
    private array $economicCustomerDiscountCache = [];
    private array $userDisplayNameCache = [];
    private array $orderItemsPreviewCache = [];
    private ?array $manualFlagsInstanceCache = null;

    public function __construct()
    {
        invoice_period_flag_schema_bootstrap::ensureTables();
        price_overrides_schema_bootstrap::ensureColumns();
        department_customer_price_overrides_schema_bootstrap::ensureTables();
    }

    public function createManualFlag(array $payload, int $userId): array
    {
        global $db;

        $targetType = trim((string)($payload['target_type'] ?? ''));
        $targetId = (int)($payload['target_id'] ?? 0);
        $field = $this->normalizeField($targetType, $payload['field'] ?? null);
        $reason = trim((string)($payload['reason'] ?? ''));

        if (!in_array($targetType, self::VALID_TARGETS, true)) {
            throw new \InvalidArgumentException('Invalid flag target type.');
        }
        if ($targetId < 1) {
            throw new \InvalidArgumentException('Flag target id is required.');
        }
        if ($reason === '') {
            throw new \InvalidArgumentException('Manual flag reason is required.');
        }

        $context = $this->resolveTargetContext($targetType, $targetId, $field);
        $contextJson = $this->jsonSql($context);

        $sql = sprintf(
            "INSERT INTO invoice_period_flags
                (source, severity, status, target_type, target_id, field, customer_number, order_id, order_item_id,
                 invoice_collection_id, xlvask_usage_log_id, reason, context_json, created_by)
             VALUES
                ('%s', 'red', 'active', '%s', %d, %s, %s, %s, %s, %s, %s, '%s', %s, %s)",
            self::SOURCE_MANUAL,
            $db->escape_string($targetType),
            $targetId,
            $this->nullableStringSql($field),
            $this->nullableIntSql($context['customer_number'] ?? null),
            $this->nullableIntSql($context['order_id'] ?? null),
            $this->nullableIntSql($context['order_item_id'] ?? null),
            $this->nullableIntSql($context['invoice_collection_id'] ?? null),
            $this->nullableIntSql($context['xlvask_usage_log_id'] ?? null),
            $db->escape_string($reason),
            $contextJson,
            $this->nullableIntSql($userId > 0 ? $userId : null)
        );
        $db->query($sql);

        return $this->getStoredFlag((int)$db->insert_id());
    }

    public function updateManualFlagStatus(int $id, string $status, ?string $reason, int $userId): array
    {
        global $db;

        $status = trim($status);
        if (!in_array($status, self::VALID_STATUSES, true) || $status === self::STATUS_ACTIVE) {
            throw new \InvalidArgumentException('Invalid manual flag status.');
        }
        if ($id < 1) {
            throw new \InvalidArgumentException('Flag id is required.');
        }

        $sql = sprintf(
            "UPDATE invoice_period_flags
             SET status = '%s',
                 status_reason = %s,
                 status_changed_by = %s,
                 status_changed_at = NOW()
             WHERE id = %d AND source = '%s'",
            $db->escape_string($status),
            $this->nullableStringSql($reason),
            $this->nullableIntSql($userId > 0 ? $userId : null),
            $id,
            self::SOURCE_MANUAL
        );
        $db->query($sql);

        return $this->getStoredFlag($id);
    }

    public function updateAutomaticFlagStatus(array $payload, int $userId): array
    {
        global $db;

        $fingerprint = trim((string)($payload['fingerprint'] ?? ''));
        $status = trim((string)($payload['status'] ?? ''));
        $targetType = trim((string)($payload['target_type'] ?? ''));
        $targetId = (int)($payload['target_id'] ?? 0);
        $field = $this->normalizeField($targetType, $payload['field'] ?? null);
        $definitionKey = trim((string)($payload['definition_key'] ?? ''));
        $reason = isset($payload['reason']) ? trim((string)$payload['reason']) : null;

        if ($fingerprint === '') {
            throw new \InvalidArgumentException('Automatic flag fingerprint is required.');
        }
        if (!in_array($status, self::VALID_STATUSES, true) || $status === self::STATUS_ACTIVE) {
            throw new \InvalidArgumentException('Invalid automatic flag status.');
        }
        if (!in_array($targetType, self::VALID_TARGETS, true)) {
            throw new \InvalidArgumentException('Invalid automatic flag target type.');
        }
        if ($targetId < 1) {
            throw new \InvalidArgumentException('Automatic flag target id is required.');
        }

        $context = $this->resolveTargetContext($targetType, $targetId, $field);
        $contextJson = $this->jsonSql($context);

        $sql = sprintf(
            "INSERT INTO invoice_period_flags
                (source, severity, status, target_type, target_id, field, customer_number, order_id, order_item_id,
                 invoice_collection_id, xlvask_usage_log_id, definition_key, fingerprint, status_reason, context_json,
                 created_by, status_changed_by, status_changed_at)
             VALUES
                ('%s', 'yellow', '%s', '%s', %d, %s, %s, %s, %s, %s, %s, %s, '%s', %s, %s, %s, %s, NOW())
             ON DUPLICATE KEY UPDATE
                 status = VALUES(status),
                 target_type = VALUES(target_type),
                 target_id = VALUES(target_id),
                 field = VALUES(field),
                 customer_number = VALUES(customer_number),
                 order_id = VALUES(order_id),
                 order_item_id = VALUES(order_item_id),
                 invoice_collection_id = VALUES(invoice_collection_id),
                 xlvask_usage_log_id = VALUES(xlvask_usage_log_id),
                 definition_key = VALUES(definition_key),
                 status_reason = VALUES(status_reason),
                 context_json = VALUES(context_json),
                 status_changed_by = VALUES(status_changed_by),
                 status_changed_at = NOW()",
            self::SOURCE_AUTOMATIC,
            $db->escape_string($status),
            $db->escape_string($targetType),
            $targetId,
            $this->nullableStringSql($field),
            $this->nullableIntSql($context['customer_number'] ?? null),
            $this->nullableIntSql($context['order_id'] ?? null),
            $this->nullableIntSql($context['order_item_id'] ?? null),
            $this->nullableIntSql($context['invoice_collection_id'] ?? null),
            $this->nullableIntSql($context['xlvask_usage_log_id'] ?? null),
            $this->nullableStringSql($definitionKey !== '' ? $definitionKey : null),
            $db->escape_string($fingerprint),
            $this->nullableStringSql($reason),
            $contextJson,
            $this->nullableIntSql($userId > 0 ? $userId : null),
            $this->nullableIntSql($userId > 0 ? $userId : null)
        );
        $db->query($sql);

        return $this->getStoredAutomaticFlag($fingerprint);
    }

    /**
     * @param array<string,array<int,array<string,mixed>>> $types
     * @param int[]|null $onlyCustomerNumbers
     * @return array<string,array<int,array<string,mixed>>>
     */
    public function applyFlagsToPeriodTypes(array $types, string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers = null): array
    {
        global $response;
        $context = $this->buildPeriodContext($types, $dateFrom, $dateTo, $onlyCustomerNumbers);
        $manualFlags = $this->getManualFlagsForPeriod($context, $dateFrom, $dateTo, $onlyCustomerNumbers);
        $automaticFlags = $this->getAutomaticFlagsForPeriod($dateFrom, $dateTo, $onlyCustomerNumbers);
        $automaticFlags = $this->filterSuppressedAutomaticFlags($automaticFlags);
        $allFlags = array_merge($manualFlags, $automaticFlags);

        $types = $this->ensureFlagOnlyCustomers($types, $allFlags);
        $flagsByCustomerNumber = [];
        foreach ($allFlags as $flag) {
            $customerNumber = (int)($flag['customer_number'] ?? 0);
            if ($customerNumber < 1) {
                continue;
            }
            $flagsByCustomerNumber[$customerNumber][] = $flag;
        }

        foreach ($types as $typeName => $customers) {
            foreach ($customers as $index => $customer) {
                $customerNumber = (int)($customer['customer_number'] ?? 0);
                $flags = $this->flagsForCustomerCard(
                    $customer,
                    $flagsByCustomerNumber[$customerNumber] ?? [],
                    (string)$typeName
                );
                usort($flags, [$this, 'sortFlags']);
                $types[$typeName][$index]['flags'] = array_values($flags);
                $types[$typeName][$index]['flag_counts'] = $this->countFlags($flags);
                $types[$typeName][$index]['status_indicator'] = $this->statusIndicatorForCustomer(
                    $types[$typeName][$index],
                    $flags
                );
            }
        }

        return $types;
    }

    private function flagsForCustomerCard(array $customer, array $flags, string $typeName = ''): array
    {
        $transactionIds = [];
        $invoiceCollectionIds = [];
        foreach (($customer['transactions'] ?? []) as $transaction) {
            $orderId = (int)($transaction['id'] ?? 0);
            if ($orderId > 0) {
                $transactionIds[$orderId] = true;
            }

            $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
            if ($invoiceCollectionId > 0) {
                $invoiceCollectionIds[$invoiceCollectionId] = true;
            }
        }

        return array_values(array_filter($flags, function (array $flag) use ($customer, $transactionIds, $invoiceCollectionIds, $typeName): bool {
            return $this->flagBelongsToCustomerCard($customer, $flag, $transactionIds, $invoiceCollectionIds, $typeName);
        }));
    }

    private function flagBelongsToCustomerCard(
        array $customer,
        array $flag,
        array $transactionIds,
        array $invoiceCollectionIds,
        string $typeName = ''
    ): bool {
        if ((string)($flag['status'] ?? self::STATUS_ACTIVE) !== self::STATUS_ACTIVE) {
            return false;
        }

        $customerNumber = (int)($customer['customer_number'] ?? 0);
        $targetType = (string)($flag['target_type'] ?? '');
        if ($targetType === 'customer') {
            return (int)($flag['customer_number'] ?? $flag['target_id'] ?? 0) === $customerNumber;
        }

        if ($targetType === 'xlvask_usage_log') {
            return $typeName === 'all' && (int)($flag['customer_number'] ?? 0) === $customerNumber;
        }

        if (in_array($targetType, ['order', 'order_field', 'order_item', 'order_item_field'], true)) {
            $orderId = (int)($flag['order_id'] ?? $flag['context']['order_id'] ?? 0);
            if ($orderId < 1 && in_array($targetType, ['order', 'order_field'], true)) {
                $orderId = (int)($flag['target_id'] ?? 0);
            }
            return $orderId > 0 && isset($transactionIds[$orderId]);
        }

        if ($targetType === 'collected_order_invoice') {
            $invoiceCollectionId = (int)(
                $flag['invoice_collection_id']
                ?? $flag['context']['invoice_collection_id']
                ?? $flag['target_id']
                ?? 0
            );
            return $invoiceCollectionId > 0 && isset($invoiceCollectionIds[$invoiceCollectionId]);
        }

        return false;
    }

    private function getStoredFlag(int $id): array
    {
        global $db;

        $result = $db->query("SELECT * FROM invoice_period_flags WHERE id = {$id} LIMIT 1");
        $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : null;
        if (!$row) {
            throw new \RuntimeException('Flag not found.');
        }

        return $this->formatStoredFlag($row);
    }

    private function getStoredAutomaticFlag(string $fingerprint): array
    {
        global $db;

        $fingerprint = $db->escape_string($fingerprint);
        $result = $db->query(
            "SELECT * FROM invoice_period_flags
             WHERE source = '" . self::SOURCE_AUTOMATIC . "' AND fingerprint = '{$fingerprint}'
             LIMIT 1"
        );
        $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : null;
        if (!$row) {
            throw new \RuntimeException('Automatic flag decision not found.');
        }

        return $this->formatStoredFlag($row);
    }

    public function warmManualFlagsCache(): void
    {
        $flags = $this->fetchActiveManualFlagsFromDb();

        try {
            (new redis())->cache_invoice_period_manual_flags($flags);
        } catch (Throwable) {
        }
    }

    private function fetchActiveManualFlagsFromDb(): array
    {
        global $db;

        $result = $db->query(
            "SELECT * FROM invoice_period_flags
             WHERE source = '" . self::SOURCE_MANUAL . "'
               AND status = '" . self::STATUS_ACTIVE . "'
             ORDER BY created_at ASC, id ASC"
        );

        $flags = [];
        if ($result) {
            while ($row = $result->fetch_assoc()) {
                $flags[] = $this->formatStoredFlag($row);
            }
        }

        return $flags;
    }

    private function formatStoredFlag(array $row): array
    {
        $context = [];
        if (!empty($row['context_json'])) {
            $decoded = json_decode((string)$row['context_json'], true);
            $context = is_array($decoded) ? $decoded : [];
        }

        return [
            'id' => (int)$row['id'],
            'source' => (string)$row['source'],
            'severity' => (string)$row['severity'],
            'status' => (string)$row['status'],
            'target_type' => (string)$row['target_type'],
            'target_id' => (int)$row['target_id'],
            'field' => $row['field'],
            'customer_number' => $row['customer_number'] === null ? null : (int)$row['customer_number'],
            'order_id' => $row['order_id'] === null ? null : (int)$row['order_id'],
            'order_item_id' => $row['order_item_id'] === null ? null : (int)$row['order_item_id'],
            'invoice_collection_id' => $row['invoice_collection_id'] === null ? null : (int)$row['invoice_collection_id'],
            'xlvask_usage_log_id' => $row['xlvask_usage_log_id'] === null ? null : (int)$row['xlvask_usage_log_id'],
            'definition_key' => $row['definition_key'],
            'fingerprint' => $row['fingerprint'],
            'reason' => $row['reason'],
            'status_reason' => $row['status_reason'],
            'context' => $context,
            'created_by' => $row['created_by'] === null ? null : (int)$row['created_by'],
            'created_by_name' => $this->getUserDisplayName($row['created_by'] === null ? null : (int)$row['created_by']),
            'status_changed_by' => $row['status_changed_by'] === null ? null : (int)$row['status_changed_by'],
            'status_changed_at' => $row['status_changed_at'],
            'created_at' => $row['created_at'],
            'updated_at' => $row['updated_at'],
            'message' => (string)($row['reason'] ?? ''),
        ];
    }

    private function getCachedManualFlags(): array
    {
        try {
            $flags = (new redis())->get_invoice_period_manual_flags();
        } catch (Throwable) {
            $flags = null;
        }

        if (!is_array($flags)) {
            // Cache miss — read from the database and refresh Redis without hiding active flags.
            $flags = $this->fetchActiveManualFlagsFromDb();
            try {
                (new redis())->cache_invoice_period_manual_flags($flags);
            } catch (Throwable) {
            }
        }

        return array_values(array_filter($flags, static function ($flag): bool {
            return is_array($flag);
        }));
    }

    private function buildPeriodContext(array $types, string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers): array
    {
        $customerNumbers = [];
        $orderIds = [];
        $invoiceCollectionIds = [];
        $orderToCustomer = [];
        $invoiceCollectionToCustomer = [];

        foreach ($types as $customers) {
            foreach ($customers as $customer) {
                $customerNumber = (int)($customer['customer_number'] ?? 0);
                if ($customerNumber > 0) {
                    $customerNumbers[$customerNumber] = true;
                }
                foreach (($customer['transactions'] ?? []) as $transaction) {
                    $orderId = (int)($transaction['id'] ?? 0);
                    if ($orderId > 0) {
                        $orderIds[$orderId] = true;
                        $orderToCustomer[$orderId] = $customerNumber;
                    }
                    $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
                    if ($invoiceCollectionId > 0) {
                        $invoiceCollectionIds[$invoiceCollectionId] = true;
                        $invoiceCollectionToCustomer[$invoiceCollectionId] = $customerNumber;
                    }
                }
            }
        }

        $orderItemToOrder = $this->getOrderItemToOrderMap(array_keys($orderIds));
        $xlvaskPeriodRows = $this->getXlVaskPeriodRows($dateFrom, $dateTo, $onlyCustomerNumbers);

        return [
            'customer_numbers' => array_keys($customerNumbers),
            'order_ids' => array_keys($orderIds),
            'invoice_collection_ids' => array_keys($invoiceCollectionIds),
            'order_to_customer' => $orderToCustomer,
            'invoice_collection_to_customer' => $invoiceCollectionToCustomer,
            'order_item_to_order' => $orderItemToOrder,
            'xlvask_period_rows' => $xlvaskPeriodRows,
        ];
    }

    private function getManualFlagsForPeriod(array $context, string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers): array
    {
        if ($this->manualFlagsInstanceCache === null) {
            $this->manualFlagsInstanceCache = $this->getCachedManualFlags();
        }
        $cachedFlags = $this->manualFlagsInstanceCache;
        if (empty($cachedFlags)) {
            return [];
        }

        $allowedCustomerNumbers = $onlyCustomerNumbers !== null
            ? array_fill_keys(array_map('intval', $onlyCustomerNumbers), true)
            : null;
        $periodCustomerNumbers = array_fill_keys(array_map('intval', $context['customer_numbers']), true);
        $orderToCustomer = $context['order_to_customer'];
        $invoiceCollectionToCustomer = $context['invoice_collection_to_customer'];
        $orderItemToOrder = $context['order_item_to_order'];
        $xlvaskRows = [];
        foreach ($context['xlvask_period_rows'] as $row) {
            $xlvaskRows[(int)$row['id']] = $row;
        }

        $flags = [];
        foreach ($cachedFlags as $row) {
            $targetType = (string)$row['target_type'];
            $targetId = (int)$row['target_id'];
            $customerNumber = null;

            if ($targetType === 'customer') {
                if (!isset($periodCustomerNumbers[$targetId])) {
                    continue;
                }
                $customerNumber = $targetId;
            } elseif ($targetType === 'order' || $targetType === 'order_field') {
                if (!isset($orderToCustomer[$targetId])) {
                    continue;
                }
                $customerNumber = (int)$orderToCustomer[$targetId];
            } elseif ($targetType === 'order_item' || $targetType === 'order_item_field') {
                $orderId = (int)($orderItemToOrder[$targetId] ?? 0);
                if ($orderId < 1 || !isset($orderToCustomer[$orderId])) {
                    continue;
                }
                $customerNumber = (int)$orderToCustomer[$orderId];
            } elseif ($targetType === 'collected_order_invoice') {
                if (!isset($invoiceCollectionToCustomer[$targetId])) {
                    continue;
                }
                $customerNumber = (int)$invoiceCollectionToCustomer[$targetId];
            } elseif ($targetType === 'xlvask_usage_log') {
                if (!isset($xlvaskRows[$targetId])) {
                    continue;
                }
                $customerNumber = (int)($xlvaskRows[$targetId]['customer_number'] ?? 0);
            }

            if ($customerNumber === null || $customerNumber < 1) {
                continue;
            }
            if ($allowedCustomerNumbers !== null && !isset($allowedCustomerNumbers[$customerNumber])) {
                continue;
            }

            $flag = $row;
            $flag['customer_number'] = $customerNumber;
            $flag['message'] = (string)$flag['reason'];
            $flags[] = $flag;
        }

        return $flags;
    }

    private function getAutomaticFlagsForPeriod(string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers): array
    {
        try {
            $flags = (new redis())->get_invoice_period_automatic_flags($dateFrom, $dateTo);
        } catch (Throwable) {
            $flags = null;
        }

        if (!is_array($flags)) {
            $flags = $this->calculateAutomaticFlagsForPeriod($dateFrom, $dateTo);
            $this->cacheAutomaticFlagsForPeriod($dateFrom, $dateTo, $flags);
        }

        if ($onlyCustomerNumbers === null) {
            return $flags;
        }

        $allowed = array_fill_keys(array_map('intval', $onlyCustomerNumbers), true);
        return array_values(array_filter($flags, static function (array $flag) use ($allowed): bool {
            return isset($allowed[(int)($flag['customer_number'] ?? 0)]);
        }));
    }

    public function warmAutomaticFlagsForPeriod(string $dateFrom, string $dateTo): void
    {
        [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo);
        $this->cacheAutomaticFlagsForPeriod(
            $dateFrom,
            $dateTo,
            $this->calculateAutomaticFlagsForPeriod($dateFrom, $dateTo)
        );
    }

    private function calculateAutomaticFlagsForPeriod(string $dateFrom, string $dateTo): array
    {
        [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo);
        $rows = $this->getPeriodOrderItemRows($dateFrom, $dateTo, null);
        $attributes = $this->getCustomerAttributes(null);

        return array_merge(
            $this->detectCustomerRuleViolations($rows, $attributes),
            $this->detectPriceMismatches($rows),
            $this->detectAbnormalQuantities($rows, $dateFrom, $dateTo),
            $this->detectVehicleTypeMismatches($rows, $dateFrom),
            $this->detectMissingXlVaskLinks($dateFrom, $dateTo, null)
        );
    }

    private function cacheAutomaticFlagsForPeriod(string $dateFrom, string $dateTo, array $flags): void
    {
        try {
            (new redis())->cache_invoice_period_automatic_flags($dateFrom, $dateTo, $flags);
        } catch (Throwable) {
        }
    }

    private function filterSuppressedAutomaticFlags(array $flags): array
    {
        global $db;

        $fingerprints = array_values(array_unique(array_filter(array_map(
            static fn(array $flag): string => (string)($flag['fingerprint'] ?? ''),
            $flags
        ))));

        if (empty($fingerprints)) {
            return $flags;
        }

        $in = implode(',', array_map(static function (string $fingerprint) use ($db): string {
            return "'" . $db->escape_string($fingerprint) . "'";
        }, $fingerprints));

        $suppressed = [];
        $result = $db->query(
            "SELECT fingerprint FROM invoice_period_flags
             WHERE source = '" . self::SOURCE_AUTOMATIC . "'
               AND status IN ('resolved', 'ignored', 'false_positive')
               AND fingerprint IN ({$in})"
        );
        if ($result) {
            while ($row = $result->fetch_assoc()) {
                $suppressed[(string)$row['fingerprint']] = true;
            }
        }

        return array_values(array_filter($flags, static function (array $flag) use ($suppressed): bool {
            return !isset($suppressed[(string)($flag['fingerprint'] ?? '')]);
        }));
    }

    private function getPeriodOrderItemRows(string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers): array
    {
        [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo);
        try {
            $rows = (new redis())->get_invoice_period_order_item_rows($dateFrom, $dateTo);
        } catch (Throwable) {
            $rows = null;
        }

        if (!is_array($rows)) {
            $rows = $this->fetchOrderItemRowsFromDb($dateFrom, $dateTo);
            try {
                (new redis())->cache_invoice_period_order_item_rows($dateFrom, $dateTo, $rows);
            } catch (Throwable) {
            }
        }

        $this->seedOrderItemsPreviewCacheFromRows($rows);

        if ($onlyCustomerNumbers === null) {
            return $rows;
        }

        $allowed = array_fill_keys(array_map('intval', $onlyCustomerNumbers), true);
        return array_values(array_filter($rows, static function (array $row) use ($allowed): bool {
            return isset($allowed[(int)($row['customer_number'] ?? 0)]);
        }));
    }

    public function warmOrderItemRowsForPeriod(string $dateFrom, string $dateTo): void
    {
        [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo);
        $rows = $this->fetchOrderItemRowsFromDb($dateFrom, $dateTo);
        try {
            (new redis())->cache_invoice_period_order_item_rows($dateFrom, $dateTo, $rows);
        } catch (Throwable) {
        }
    }

    private function normalizePeriodDateRange(string $dateFrom, string $dateTo): array
    {
        return [
            $this->normalizePeriodDate($dateFrom, true),
            $this->normalizePeriodDate($dateTo, false),
        ];
    }

    private function normalizePeriodDate(string $date, bool $startOfDay): string
    {
        $timestamp = strtotime($date);
        if ($timestamp === false) {
            return $date;
        }

        return date($startOfDay ? 'Y-m-d 00:00:00' : 'Y-m-d 23:59:59', $timestamp);
    }

    private function fetchOrderItemRowsFromDb(string $dateFrom, string $dateTo): array
    {
        global $db;

        $escapedDateFrom = $db->escape_string($dateFrom);
        $escapedDateTo = $db->escape_string($dateTo);

        $sql = "
            SELECT
                o.id AS order_id,
                o.customer_id AS customer_number,
                u.id AS user_id,
                u.display_name AS customer_name,
                o.reference AS order_reference,
                o.po AS order_po,
                o.notes AS order_notes,
                o.department_id,
                d.custom_pricing_only AS department_custom_pricing_only,
                o.reg_1,
                o.invoice_collection_id,
                o.wash_id,
                o.safety_seal,
                o.created_at AS order_created_at,
                oi.id AS order_item_id,
                oi.product_id,
                oi.reference AS item_reference,
                oi.notes AS item_notes,
                oi.price AS item_price,
                oi.quantity AS item_quantity,
                oi.related_item_id,
                oi.include_in_invoice AS item_include_in_invoice,
                p.name AS product_name,
                p.price AS product_base_price,
                p.category AS product_category,
                p.apply_category_discount,
                p.is_wash,
                p.subscription_allowed,
                p.max_quantity_per_order,
                c.name AS category_name,
                pdp.price AS department_price,
                CASE
                    WHEN d.custom_pricing_only = 1 THEN department_product_discount.percentage
                    ELSE product_discount.percentage
                END AS product_discount_percentage,
                CASE
                    WHEN d.custom_pricing_only = 1 THEN department_product_discount.fixed_price
                    ELSE product_discount.fixed_price
                END AS product_fixed_price,
                CASE
                    WHEN d.custom_pricing_only = 1 THEN GREATEST(
                        COALESCE(department_category_discount.percentage, 0),
                        COALESCE(department_global_discount.percentage, 0)
                    )
                    ELSE category_discount.percentage
                END AS category_discount_percentage
            FROM orders o
            LEFT JOIN (
                SELECT customer_number, MIN(id) AS id, MAX(display_name) AS display_name
                FROM users
                WHERE customer_number IS NOT NULL AND customer_number <> 0
                GROUP BY customer_number
            ) u ON u.customer_number = o.customer_id
            LEFT JOIN order_items oi ON oi.order_id = o.id AND (oi.deleted_at IS NULL OR oi.deleted_at = '')
            LEFT JOIN departments d ON d.id = o.department_id
            LEFT JOIN products p ON p.id = oi.product_id
            LEFT JOIN categories c ON c.id = p.category
            LEFT JOIN product_department_prices pdp ON pdp.department_id = o.department_id AND pdp.product_id = p.id
            LEFT JOIN (
                SELECT discount_user.customer_number, po.product_or_category_id, MAX(po.percentage) AS percentage, MAX(po.fixed_price) AS fixed_price
                FROM price_overrides po
                INNER JOIN users discount_user ON discount_user.id = po.user_id
                WHERE po.is_category = 0
                GROUP BY discount_user.customer_number, po.product_or_category_id
            ) product_discount
                ON product_discount.customer_number = o.customer_id
               AND product_discount.product_or_category_id = p.id
            LEFT JOIN (
                SELECT discount_user.customer_number, po.product_or_category_id, MAX(po.percentage) AS percentage
                FROM price_overrides po
                INNER JOIN users discount_user ON discount_user.id = po.user_id
                WHERE po.is_category = 1
                GROUP BY discount_user.customer_number, po.product_or_category_id
            ) category_discount
                ON category_discount.customer_number = o.customer_id
               AND category_discount.product_or_category_id = p.category
            LEFT JOIN (
                SELECT department_id, user_id, product_or_category_id, MAX(percentage) AS percentage, MAX(fixed_price) AS fixed_price
                FROM department_customer_price_overrides
                WHERE is_category = 0
                GROUP BY department_id, user_id, product_or_category_id
            ) department_product_discount
                ON department_product_discount.department_id = o.department_id
               AND department_product_discount.user_id = u.id
               AND department_product_discount.product_or_category_id = p.id
            LEFT JOIN (
                SELECT department_id, user_id, product_or_category_id, MAX(percentage) AS percentage
                FROM department_customer_price_overrides
                WHERE is_category = 1 AND product_or_category_id <> 'global'
                GROUP BY department_id, user_id, product_or_category_id
            ) department_category_discount
                ON department_category_discount.department_id = o.department_id
               AND department_category_discount.user_id = u.id
               AND department_category_discount.product_or_category_id = p.category
            LEFT JOIN (
                SELECT department_id, user_id, MAX(percentage) AS percentage
                FROM department_customer_price_overrides
                WHERE is_category = 1 AND product_or_category_id = 'global'
                GROUP BY department_id, user_id
            ) department_global_discount
                ON department_global_discount.department_id = o.department_id
               AND department_global_discount.user_id = u.id
            WHERE o.created_at BETWEEN '{$escapedDateFrom}' AND '{$escapedDateTo}'
              AND o.deleted_at IS NULL
            ORDER BY o.customer_id, o.id, oi.id";

        $result = $db->query($sql);
        $rows = $result ? $db->fetch_all($result) : [];
        $certificateAttachmentOrderIds = $this->getWashCertificateAttachmentOrderIds(array_column($rows, 'order_id'));

        foreach ($rows as &$row) {
            $orderId = (int)($row['order_id'] ?? 0);
            $row['has_wash_certificate_attachment'] = isset($certificateAttachmentOrderIds[$orderId]) ? 1 : 0;
        }
        unset($row);
        $this->seedOrderItemsPreviewCacheFromRows($rows);

        return $rows;
    }

    private function getWashCertificateAttachmentOrderIds(array $orderIds): array
    {
        global $db;

        $orderIds = array_values(array_unique(array_filter(
            array_map('intval', $orderIds),
            static fn(int $orderId): bool => $orderId > 0
        )));
        if (empty($orderIds) || !$this->tableExists('object_attachments')) {
            return [];
        }

        $objectTypes = [];
        foreach (['orders', '`orders`'] as $type) {
            $objectTypes[] = "'" . $db->escape_string($type) . "'";
        }
        $in = implode(',', $orderIds);
        $result = $db->query(
            "SELECT object_id, content
             FROM object_attachments
             WHERE object_type IN (" . implode(',', $objectTypes) . ")
               AND object_id IN ({$in})
               AND deleted_at IS NULL"
        );

        $attached = [];
        if (!$result) {
            return $attached;
        }

        while ($row = $result->fetch_assoc()) {
            $content = json_decode((string)($row['content'] ?? ''), true);
            $other = is_array($content) ? ($content['other'] ?? null) : null;
            if (is_string($other) && strtolower(trim($other)) === 'wash_certificate') {
                $attached[(int)$row['object_id']] = true;
            }
        }

        return $attached;
    }

    private function getCustomerAttributes(?array $onlyCustomerNumbers): array
    {
        global $db;

        customer_rule_product_restriction_schema_bootstrap::ensureSchema();

        $customerFilter = $this->customerFilterSql('u.customer_number', $onlyCustomerNumbers);
        $result = $db->query(
            "SELECT u.customer_number, ca.attribute, cp.product_id
             FROM customer_attributes ca
             JOIN users u ON u.id = ca.user_id
             LEFT JOIN customer_rule_product_restrictions r ON r.attribute = ca.attribute
             LEFT JOIN customer_rule_product_collections c ON c.attribute = r.attribute
             LEFT JOIN customer_rule_product_collection_products cp ON cp.collection_id = c.id
             WHERE 1=1 {$customerFilter}"
        );

        $attributes = [];
        if (!$result) {
            return $attributes;
        }

        while ($row = $result->fetch_assoc()) {
            $customerNumber = (int)$row['customer_number'];
            $attribute = (string)$row['attribute'];
            $attributes[$customerNumber][$attribute] = true;
            if ($row['product_id'] !== null && in_array($attribute, customer_rule_product_restriction_service::PRODUCT_IMPACT_ATTRIBUTES, true)) {
                $attributes[$customerNumber]['__disabled_products'][(int)$row['product_id']][$attribute] = true;
            }
        }

        return $attributes;
    }

    private function detectCustomerRuleViolations(array $rows, array $attributes): array
    {
        $flags = [];
        $orders = [];
        $collectionOrders = [];
        $matchingRules = static fn(int $customerNumber, int $productId): array =>
            array_keys($attributes[$customerNumber]['__disabled_products'][$productId] ?? []);

        foreach ($rows as $row) {
            $customerNumber = (int)$row['customer_number'];
            $orderId = (int)$row['order_id'];
            if ($orderId > 0 && !isset($orders[$orderId])) {
                $orders[$orderId] = $row;
            }
            $invoiceCollectionId = (int)($row['invoice_collection_id'] ?? 0);
            if ($invoiceCollectionId > 0) {
                $collectionOrders[$customerNumber][$invoiceCollectionId][$orderId] = true;
            }

            if (!$this->hasAttribute($attributes, $customerNumber, 'restrictAdditionalServices')
                && !$this->hasAttribute($attributes, $customerNumber, 'restrictTankCleaning')
                && !$this->hasAttribute($attributes, $customerNumber, 'restrictSpotFree')
                && !$this->hasAttribute($attributes, $customerNumber, 'restrictInteriorCleaning')
                && !$this->hasAttribute($attributes, $customerNumber, 'exemptFromAdministrationFee')
                && !$this->hasAttribute($attributes, $customerNumber, 'onlyTankCleaning')) {
                continue;
            }

            if ((int)($row['order_item_id'] ?? 0) < 1) {
                continue;
            }

            $productRuleDefinitions = [
                'restrictAdditionalServices' => 'customer_rule_restrict_addon_services',
                'restrictTankCleaning' => 'customer_rule_restrict_tank_cleaning',
                'onlyTankCleaning' => 'customer_rule_only_tank_cleaning',
                'restrictSpotFree' => 'customer_rule_restrict_spot_free',
                'restrictInteriorCleaning' => 'customer_rule_restrict_interior_cleaning',
            ];
            foreach ($matchingRules($customerNumber, (int)($row['product_id'] ?? 0)) as $attribute) {
                if (isset($productRuleDefinitions[$attribute])) {
                    $flags[] = $this->automaticFlag(
                        $productRuleDefinitions[$attribute],
                        'order_item',
                        (int)$row['order_item_id'],
                        null,
                        $row,
                        ['product' => $this->productLabel($row)],
                        $this->orderItemContext($row)
                    );
                }
            }

            if ($this->hasAttribute($attributes, $customerNumber, 'exemptFromAdministrationFee')
                && $this->rowMatchesProductTerms($row, ['administration fee', 'administrationsgebyr', 'administration'])) {
                $flags[] = $this->automaticFlag(
                    'customer_rule_exempt_from_administration_fees',
                    'order_item',
                    (int)$row['order_item_id'],
                    null,
                    $row,
                    ['product' => $this->productLabel($row)],
                    $this->orderItemContext($row)
                );
            }
        }

        foreach ($orders as $orderId => $row) {
            $customerNumber = (int)$row['customer_number'];
            if ($this->hasAttribute($attributes, $customerNumber, 'requiresReferenceNumber')
                && trim((string)($row['order_reference'] ?? '')) === '') {
                $context = $this->orderContext($row);
                $context['order_items'] = $this->getOrderItemsForPreview((int)$orderId);
                $flags[] = $this->automaticFlag(
                    'customer_rule_requires_reference',
                    'order_field',
                    $orderId,
                    'reference',
                    $row,
                    [],
                    $context
                );
            }
            if ($this->hasAttribute($attributes, $customerNumber, 'usePONumbers')
                && trim((string)($row['order_po'] ?? '')) === '') {
                $context = $this->orderContext($row);
                $context['order_items'] = $this->getOrderItemsForPreview((int)$orderId);
                $flags[] = $this->automaticFlag(
                    'customer_rule_requires_po_number',
                    'order_field',
                    $orderId,
                    'po',
                    $row,
                    [],
                    $context
                );
            }
        }

        foreach ($collectionOrders as $customerNumber => $collections) {
            if (!$this->hasAttribute($attributes, (int)$customerNumber, 'invoiceAllOrdersIndividually')) {
                continue;
            }
            foreach ($collections as $invoiceCollectionId => $orderSet) {
                if (count($orderSet) <= 1) {
                    continue;
                }
                $row = $orders[(int)array_key_first($orderSet)] ?? ['customer_number' => $customerNumber, 'invoice_collection_id' => $invoiceCollectionId];
                $flags[] = $this->automaticFlag(
                    'customer_rule_invoice_all_orders_individually',
                    'collected_order_invoice',
                    (int)$invoiceCollectionId,
                    null,
                    $row,
                    ['count' => count($orderSet)],
                    $this->invoiceCollectionContext($row)
                );
            }
        }

        return $this->dedupeAutomaticFlags($flags);
    }

    private function detectPriceMismatches(array $rows): array
    {
        $this->preloadEconomicCustomerDiscounts($rows);

        $flags = [];
        foreach ($rows as $row) {
            $orderItemId = (int)($row['order_item_id'] ?? 0);
            if ($orderItemId < 1 || !$this->isIncludedOrderItem($row)) {
                continue;
            }

            $expected = $this->calculateExpectedPrice($row);
            $actual = (int)($row['item_price'] ?? 0);
            if ($actual === $expected) {
                continue;
            }

            $context = $this->orderItemContext($row);
            $context['actual_price'] = $actual;
            $context['expected_price'] = $expected;
            $context['expected_price_breakdown'] = $this->priceBreakdown($row, $expected);
            $context['order_items'] = $this->getOrderItemsForPreview((int)$row['order_id']);

            $flags[] = $this->automaticFlag(
                'price_mismatch',
                'order_item_field',
                $orderItemId,
                'price',
                $row,
                [
                    'product' => $this->productLabel($row),
                    'expected' => 'expected',
                    'actual_price' => $actual,
                    'expected_price' => $expected,
                ],
                $context
            );
        }

        return $flags;
    }

    private function detectAbnormalQuantities(array $rows, string $dateFrom, string $dateTo): array
    {
        $flags = [];
        $primaryByOrderProduct = [];
        $orders = [];
        $washCertificateByOrder = [];
        $hasWashCertificateAttachmentByOrder = [];
        $fixedPricingGroups = [];
        $subscriptionGroups = [];

        foreach ($rows as $row) {
            $orderId = (int)$row['order_id'];
            $orderItemId = (int)($row['order_item_id'] ?? 0);
            if ($orderId > 0 && !isset($orders[$orderId])) {
                $orders[$orderId] = $row;
            }
            if ($orderId > 0 && $this->rowHasWashCertificateAttachment($row)) {
                $hasWashCertificateAttachmentByOrder[$orderId] = true;
            }
            if ($orderItemId < 1) {
                continue;
            }

            if ($this->isPrimaryVehicleItem($row)) {
                $primaryByOrderProduct[$orderId][(int)$row['product_id']][$orderItemId] ??= $row;
            }

            $limit = (int)($row['max_quantity_per_order'] ?? 0);
            if ($limit > 0 && (int)($row['item_quantity'] ?? 0) > $limit) {
                $flags[] = $this->automaticFlag(
                    'quantity_exceeds_product_limit',
                    'order_item_field',
                    $orderItemId,
                    'quantity',
                    $row,
                    [
                        'product' => $this->productLabel($row),
                        'quantity' => (int)$row['item_quantity'],
                        'limit' => $limit,
                    ],
                    $this->orderItemContext($row) + ['quantity_limit' => $limit]
                );
            }

            if ($this->isWashCertificateProduct($row)) {
                $washCertificateByOrder[$orderId][] = $row;
                if (!$this->rowHasWashCertificateAttachment($row)) {
                    $context = $this->orderItemContext($row);
                    $context['order_items'] = $this->getOrderItemsForPreview((int)$row['order_id']);
                    $flags[] = $this->automaticFlag(
                        'wash_certificate_item_without_certificate',
                        'order_item',
                        $orderItemId,
                        null,
                        $row,
                        ['product' => $this->productLabel($row)],
                        $context
                    );
                }
            }

            $monthKey = date('Y-m', strtotime((string)$row['order_created_at']));
            if ($this->rowMatchesProductTerms($row, ['fixed pricing', 'fastpris', 'fixed price'])) {
                $fixedPricingGroups[(int)$row['customer_number']][$monthKey][] = $row;
            }
            if ($this->rowMatchesProductTerms($row, ['subscription', 'abonnement', 'vaskeabonnement'])) {
                $reg = strtoupper(trim((string)($row['reg_1'] ?? '')));
                $subscriptionGroups[(int)$row['customer_number']][$reg][$monthKey][(int)$row['product_id']][] = $row;
            }
        }

        foreach ($primaryByOrderProduct as $orderId => $products) {
            foreach ($products as $productId => $items) {
                if (count($items) <= 1) {
                    continue;
                }
                $items = array_values($items);
                $row = $items[0];
                $context = $this->orderContext($row);
                $context['order_items'] = $this->getOrderItemsForPreview((int)$row['order_id']);
                $flags[] = $this->automaticFlag(
                    'multiple_identical_primary_vehicle_items',
                    'order',
                    (int)$orderId,
                    null,
                    $row,
                    ['product' => $this->productLabel($row), 'count' => count($items)],
                    $context
                );
            }
        }

        foreach ($orders as $orderId => $row) {
            if (isset($hasWashCertificateAttachmentByOrder[$orderId]) && empty($washCertificateByOrder[$orderId])) {
                $context = $this->orderContext($row);
                $context['order_items'] = $this->getOrderItemsForPreview((int)$orderId);
                $flags[] = $this->automaticFlag(
                    'wash_certificate_attached_without_item',
                    'order',
                    (int)$orderId,
                    null,
                    $row,
                    [],
                    $context
                );
            }
        }

        foreach ($fixedPricingGroups as $customerGroups) {
            foreach ($customerGroups as $items) {
                if (count($items) <= 1) {
                    continue;
                }
                $row = $items[0];
                $flags[] = $this->automaticFlag(
                    'multiple_fixed_pricing_items_same_month',
                    'order_item',
                    (int)$row['order_item_id'],
                    null,
                    $row,
                    ['count' => count($items)],
                    $this->orderItemContext($row)
                );
            }
        }

        foreach ($subscriptionGroups as $customerGroups) {
            foreach ($customerGroups as $regGroups) {
                foreach ($regGroups as $monthGroups) {
                    foreach ($monthGroups as $items) {
                        if (count($items) <= 1) {
                            continue;
                        }
                        $row = $items[0];
                        $flags[] = $this->automaticFlag(
                            'duplicate_vehicle_subscription_charge_same_month',
                            'order_item',
                            (int)$row['order_item_id'],
                            null,
                            $row,
                            ['product' => $this->productLabel($row), 'count' => count($items)],
                            $this->orderItemContext($row)
                        );
                    }
                }
            }
        }

        return $this->dedupeAutomaticFlags($flags);
    }

    private function detectVehicleTypeMismatches(array $rows, string $dateFrom): array
    {
        global $db;

        $flags = [];
        $primaryRows = array_values(array_filter($rows, fn(array $row): bool => $this->isPrimaryVehicleItem($row)));
        if (empty($primaryRows)) {
            return [];
        }

        $vehicleTypeByCustomerReg = $this->getVehicleSubscriptionTypeMap($primaryRows);
        foreach ($primaryRows as $row) {
            $reg = strtoupper(trim((string)($row['reg_1'] ?? '')));
            $key = (int)$row['customer_number'] . '|' . $reg;
            $expectedProductId = (int)($vehicleTypeByCustomerReg[$key]['product_id'] ?? 0);
            $expectedProductName = (string)($vehicleTypeByCustomerReg[$key]['product_name'] ?? '');
            if ($expectedProductId > 0
                && !$this->primaryVehicleProductsMatch(
                    (int)$row['product_id'],
                    $this->productLabel($row),
                    $expectedProductId,
                    $expectedProductName
                )) {
                $flags[] = $this->automaticFlag(
                    'vehicle_subscription_type_mismatch',
                    'order_item',
                    (int)$row['order_item_id'],
                    null,
                    $row,
                    [
                        'product' => $this->productLabel($row),
                        'expected_product' => $expectedProductName !== '' ? $expectedProductName : (string)$expectedProductId,
                    ],
                    $this->orderItemContext($row) + ['expected_product_id' => $expectedProductId]
                );
            }
        }

        $history = $this->getPrimaryProductHistory($dateFrom, array_column($primaryRows, 'reg_1'));
        foreach ($primaryRows as $row) {
            $reg = strtoupper(trim((string)($row['reg_1'] ?? '')));
            if ($reg === '' || !isset($history[$reg])) {
                continue;
            }
            $expectedProductId = (int)$history[$reg]['product_id'];
            if ($this->primaryVehicleProductsMatch(
                (int)$row['product_id'],
                $this->productLabel($row),
                $expectedProductId,
                (string)$history[$reg]['product_name']
            )) {
                continue;
            }
            $flags[] = $this->automaticFlag(
                'historical_primary_product_mismatch',
                'order_item',
                (int)$row['order_item_id'],
                null,
                $row,
                [
                    'product' => $this->productLabel($row),
                    'expected_product' => (string)$history[$reg]['product_name'],
                ],
                $this->orderItemContext($row) + [
                    'expected_product_id' => $expectedProductId,
                    'expected_product_name' => $history[$reg]['product_name'],
                    'history_count' => (int)$history[$reg]['count'],
                ]
            );
        }

        return $this->dedupeAutomaticFlags($flags);
    }

    private function detectMissingXlVaskLinks(string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers): array
    {
        $flags = [];
        foreach ($this->getXlVaskPeriodRows($dateFrom, $dateTo, $onlyCustomerNumbers, true) as $row) {
            $flags[] = $this->automaticFlag(
                'xlvask_missing_order_link',
                'xlvask_usage_log',
                (int)$row['id'],
                null,
                [
                    'customer_number' => (int)$row['customer_number'],
                    'customer_name' => (string)($row['customer_name'] ?? ''),
                    'order_id' => null,
                    'order_item_id' => null,
                    'invoice_collection_id' => null,
                    'xlvask_usage_log_id' => (int)$row['id'],
                ],
                [
                    'wash_id' => (string)$row['wash_id'],
                    'registration_number' => (string)($row['registration_number'] ?? ''),
                ],
                [
                    'customer_number' => (int)$row['customer_number'],
                    'customer_name' => (string)($row['customer_name'] ?? ''),
                    'xlvask_usage_log_id' => (int)$row['id'],
                    'wash_id' => (string)$row['wash_id'],
                    'registration_number' => (string)($row['registration_number'] ?? ''),
                    'start_time' => (string)($row['start_time'] ?? ''),
                ]
            );
        }

        return $flags;
    }

    private function getXlVaskPeriodRows(string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers, bool $onlyMissingLinks = false): array
    {
        global $db;

        if (!$this->tableExists('xlvask_usage_logs')) {
            return [];
        }

        $dateFrom = $db->escape_string($dateFrom);
        $dateTo = $db->escape_string($dateTo);
        $customerFilter = $this->customerFilterSql('CAST(x.CustomerId AS UNSIGNED)', $onlyCustomerNumbers);
        $missingFilter = $onlyMissingLinks
            ? "AND linked_order.id IS NULL"
            : '';

        $sql = "
            SELECT
                x.id,
                x.WashId AS wash_id,
                CAST(x.CustomerId AS UNSIGNED) AS customer_number,
                COALESCE(u.display_name, x.Customer) AS customer_name,
                x.RegistrationNumber AS registration_number,
                x.StartTime AS start_time
            FROM xlvask_usage_logs x
            LEFT JOIN users u ON u.customer_number = CAST(x.CustomerId AS UNSIGNED)
            LEFT JOIN orders linked_order
                ON linked_order.wash_id = x.WashId
               AND linked_order.deleted_at IS NULL
               AND linked_order.created_at BETWEEN '{$dateFrom}' AND '{$dateTo}'
            WHERE STR_TO_DATE(REPLACE(SUBSTRING(x.StartTime, 1, 19), 'T', ' '), '%Y-%m-%d %H:%i:%s')
                  BETWEEN '{$dateFrom}' AND '{$dateTo}'
              AND COALESCE(x.ignored_at, '') = ''
              AND COALESCE(x.FinishStatus, '') = '1'
              AND CAST(x.CustomerId AS UNSIGNED) > 0
              {$customerFilter}
              {$missingFilter}";

        try {
            $result = $db->query($sql);
            return $result ? $db->fetch_all($result) : [];
        } catch (Throwable) {
            return [];
        }
    }

    private function automaticFlag(
        string $definitionKey,
        string $targetType,
        int $targetId,
        ?string $field,
        array $row,
        array $messageParams,
        array $context
    ): array {
        $customerNumber = (int)($row['customer_number'] ?? $context['customer_number'] ?? 0);
        $orderId = isset($context['order_id']) ? (int)$context['order_id'] : (isset($row['order_id']) ? (int)$row['order_id'] : null);
        $orderItemId = isset($context['order_item_id']) ? (int)$context['order_item_id'] : (isset($row['order_item_id']) ? (int)$row['order_item_id'] : null);
        $invoiceCollectionId = isset($context['invoice_collection_id']) ? (int)$context['invoice_collection_id'] : (isset($row['invoice_collection_id']) ? (int)$row['invoice_collection_id'] : null);
        $xlvaskUsageLogId = isset($context['xlvask_usage_log_id']) ? (int)$context['xlvask_usage_log_id'] : null;

        $fingerprint = sha1(json_encode([
            $definitionKey,
            $targetType,
            $targetId,
            $field,
            $messageParams['actual_price'] ?? null,
            $messageParams['expected_price'] ?? null,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

        $message = $this->automaticMessage($definitionKey, $messageParams);

        return [
            'id' => 'auto:' . $fingerprint,
            'source' => self::SOURCE_AUTOMATIC,
            'severity' => 'yellow',
            'status' => self::STATUS_ACTIVE,
            'target_type' => $targetType,
            'target_id' => $targetId,
            'field' => $field,
            'customer_number' => $customerNumber,
            'customer_name' => (string)($row['customer_name'] ?? $context['customer_name'] ?? ''),
            'order_id' => $orderId,
            'order_item_id' => $orderItemId,
            'invoice_collection_id' => $invoiceCollectionId,
            'xlvask_usage_log_id' => $xlvaskUsageLogId,
            'definition_key' => $definitionKey,
            'fingerprint' => $fingerprint,
            'reason' => null,
            'message_key' => 'invoice_period.flags.automatic.' . $definitionKey,
            'message_params' => $messageParams,
            'message' => $message,
            'message_parts' => $this->messageParts($definitionKey, $messageParams),
            'context' => $context,
        ];
    }

    private function automaticMessage(string $definitionKey, array $params): string
    {
        $product = (string)($params['product'] ?? 'Item');
        $expectedProduct = (string)($params['expected_product'] ?? 'expected product');
        return match ($definitionKey) {
            'price_mismatch' => "{$product} product price differs from expected.",
            'customer_rule_restrict_addon_services' => "{$product} violates restricted addon services.",
            'customer_rule_restrict_tank_cleaning' => "{$product} violates restricted tank cleaning.",
            'customer_rule_restrict_spot_free' => "{$product} violates restricted Spot Free.",
            'customer_rule_restrict_interior_cleaning' => "{$product} violates restricted interior wash.",
            'customer_rule_exempt_from_administration_fees' => "{$product} is an administration fee for an exempt customer.",
            'customer_rule_only_tank_cleaning' => "{$product} violates the only tank cleaning rule.",
            'customer_rule_requires_reference' => "Order is missing a required reference.",
            'customer_rule_requires_po_number' => "Order is missing a required PO number.",
            'customer_rule_invoice_all_orders_individually' => "Invoice collection contains multiple orders for a customer requiring individual invoices.",
            'quantity_exceeds_product_limit' => "{$product} quantity exceeds the product limit.",
            'multiple_identical_primary_vehicle_items' => "Order contains multiple identical primary vehicle items.",
            'wash_certificate_item_without_certificate' => "Wash certificate item is present without a wash certificate.",
            'wash_certificate_attached_without_item' => "Wash certificate is attached without a wash certificate item.",
            'multiple_fixed_pricing_items_same_month' => "Multiple fixed pricing items exist in the same month.",
            'duplicate_vehicle_subscription_charge_same_month' => "Duplicate vehicle subscription charges exist in the same month.",
            'vehicle_subscription_type_mismatch' => "{$product} does not match the vehicle subscription type {$expectedProduct}.",
            'historical_primary_product_mismatch' => "{$product} differs from the registration number's usual product {$expectedProduct}.",
            'xlvask_missing_order_link' => "XL Vask wash is neither ignored nor linked to an order in the selected period.",
            default => "Automatically detected invoice-period issue.",
        };
    }

    private function messageParts(string $definitionKey, array $params): array
    {
        return match ($definitionKey) {
            'price_mismatch' => [
                ['type' => 'order_item', 'text' => (string)($params['product'] ?? 'Item')],
                ['type' => 'text', 'text' => ' product price differs from '],
                ['type' => 'expected_price', 'text' => 'expected'],
                ['type' => 'text', 'text' => '.'],
            ],
            'multiple_identical_primary_vehicle_items' => [
                ['type' => 'order', 'text' => 'Order'],
                ['type' => 'text', 'text' => ' contains multiple identical primary vehicle items.'],
            ],
            'wash_certificate_item_without_certificate' => [
                ['type' => 'order_item', 'text' => 'Wash certificate item'],
                ['type' => 'text', 'text' => ' is present without a wash certificate.'],
            ],
            'wash_certificate_attached_without_item' => [
                ['type' => 'order', 'text' => 'Wash certificate'],
                ['type' => 'text', 'text' => ' is attached without a wash certificate item.'],
            ],
            'xlvask_missing_order_link' => [
                ['type' => 'xlvask_usage_log', 'text' => 'XL Vask wash'],
                ['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
            ],
            default => [],
        };
    }

    private function resolveTargetContext(string $targetType, int $targetId, ?string $field): array
    {
        global $db;

        if ($targetType === 'customer') {
            return ['customer_number' => $targetId];
        }

        if ($targetType === 'order' || $targetType === 'order_field') {
            $result = $db->query("SELECT id, customer_id, invoice_collection_id, department_id FROM orders WHERE id = {$targetId} LIMIT 1");
            $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : [];
            return [
                'customer_number' => isset($row['customer_id']) ? (int)$row['customer_id'] : null,
                'order_id' => $targetId,
                'invoice_collection_id' => isset($row['invoice_collection_id']) ? (int)$row['invoice_collection_id'] : null,
                'department_id' => isset($row['department_id']) ? (int)$row['department_id'] : null,
                'field' => $field,
            ];
        }

        if ($targetType === 'order_item' || $targetType === 'order_item_field') {
            $result = $db->query(
                "SELECT oi.id, oi.order_id, o.customer_id, o.invoice_collection_id, o.department_id
                 FROM order_items oi
                 JOIN orders o ON o.id = oi.order_id
                 WHERE oi.id = {$targetId}
                 LIMIT 1"
            );
            $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : [];
            return [
                'customer_number' => isset($row['customer_id']) ? (int)$row['customer_id'] : null,
                'order_id' => isset($row['order_id']) ? (int)$row['order_id'] : null,
                'order_item_id' => $targetId,
                'invoice_collection_id' => isset($row['invoice_collection_id']) ? (int)$row['invoice_collection_id'] : null,
                'department_id' => isset($row['department_id']) ? (int)$row['department_id'] : null,
                'field' => $field,
            ];
        }

        if ($targetType === 'collected_order_invoice') {
            $result = $db->query("SELECT id, customer_number FROM collected_order_invoices WHERE id = {$targetId} LIMIT 1");
            $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : [];
            return [
                'customer_number' => isset($row['customer_number']) ? (int)$row['customer_number'] : null,
                'invoice_collection_id' => $targetId,
            ];
        }

        if ($targetType === 'xlvask_usage_log') {
            $result = $db->query("SELECT id, CustomerId, WashId, RegistrationNumber, StartTime FROM xlvask_usage_logs WHERE id = {$targetId} LIMIT 1");
            $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : [];
            return [
                'customer_number' => isset($row['CustomerId']) ? (int)$row['CustomerId'] : null,
                'xlvask_usage_log_id' => $targetId,
                'wash_id' => (string)($row['WashId'] ?? ''),
                'registration_number' => (string)($row['RegistrationNumber'] ?? ''),
                'start_time' => (string)($row['StartTime'] ?? ''),
            ];
        }

        return [];
    }

    private function normalizeField(string $targetType, mixed $field): ?string
    {
        $field = trim((string)($field ?? ''));
        if ($field === '') {
            return null;
        }
        if ($targetType === 'order_field' && !in_array($field, self::ORDER_FIELDS, true)) {
            throw new \InvalidArgumentException('Invalid order flag field.');
        }
        if ($targetType === 'order_item_field' && !in_array($field, self::ORDER_ITEM_FIELDS, true)) {
            throw new \InvalidArgumentException('Invalid order item flag field.');
        }
        return $field;
    }

    private function statusIndicatorForCustomer(array $customer, array $flags): string
    {
        $counts = $this->countFlags($flags);
        if ($counts['manual'] > 0) {
            return 'flag_red';
        }
        if ($counts['automatic'] > 0) {
            return 'flag_yellow';
        }
        if (($customer['draft']['is_action_blocked'] ?? false) === true) {
            return 'circle_yellow';
        }
        return ($customer['requires_action'] ?? false) ? 'circle_red' : 'circle_green';
    }

    private function countFlags(array $flags): array
    {
        $manual = 0;
        $automatic = 0;
        foreach ($flags as $flag) {
            if (($flag['status'] ?? self::STATUS_ACTIVE) !== self::STATUS_ACTIVE) {
                continue;
            }
            if (($flag['source'] ?? '') === self::SOURCE_MANUAL) {
                $manual++;
            } elseif (($flag['source'] ?? '') === self::SOURCE_AUTOMATIC) {
                $automatic++;
            }
        }
        return [
            'manual' => $manual,
            'automatic' => $automatic,
            'total' => $manual + $automatic,
        ];
    }

    private function sortFlags(array $a, array $b): int
    {
        $sourceOrder = [self::SOURCE_MANUAL => 0, self::SOURCE_AUTOMATIC => 1];
        $sourceCompare = ($sourceOrder[$a['source'] ?? ''] ?? 99) <=> ($sourceOrder[$b['source'] ?? ''] ?? 99);
        if ($sourceCompare !== 0) {
            return $sourceCompare;
        }
        return strcmp((string)($a['created_at'] ?? $a['fingerprint'] ?? ''), (string)($b['created_at'] ?? $b['fingerprint'] ?? ''));
    }

    private function ensureFlagOnlyCustomers(array $types, array $flags): array
    {
        if (!isset($types['all']) || !is_array($types['all'])) {
            $types['all'] = [];
        }

        $existing = [];
        foreach ($types['all'] as $customer) {
            $customerNumber = (int)($customer['customer_number'] ?? 0);
            if ($customerNumber > 0) {
                $existing[$customerNumber] = true;
            }
        }

        foreach ($flags as $flag) {
            $customerNumber = (int)($flag['customer_number'] ?? 0);
            if ($customerNumber < 1 || isset($existing[$customerNumber])) {
                continue;
            }
            $types['all'][] = [
                'id' => null,
                'customer_number' => $customerNumber,
                'customer_name' => (string)($flag['customer_name'] ?? $this->getCustomerName($customerNumber)),
                'transactions' => [],
                'requires_action' => false,
                'meta' => ['flag_only' => true],
                'queue' => ['has_active_job' => false, 'statuses' => [], 'invoice_collection_ids' => [], 'is_action_blocked' => false],
                'draft' => ['has_valid_draft' => false, 'invoice_collection_ids' => [], 'is_action_blocked' => false],
            ];
            $existing[$customerNumber] = true;
        }

        return $types;
    }

    private function getCustomerName(int $customerNumber): string
    {
        global $db;
        $result = $db->query("SELECT display_name FROM users WHERE customer_number = {$customerNumber} LIMIT 1");
        if ($result && $result->num_rows > 0) {
            $row = $result->fetch_assoc();
            $displayName = trim((string)($row['display_name'] ?? ''));
            return $displayName !== '' ? $displayName : '#' . $customerNumber;
        }
        return '#' . $customerNumber;
    }

    private function getUserDisplayName(?int $userId): ?string
    {
        global $db;
        if ($userId === null || $userId < 1) {
            return null;
        }
        if (array_key_exists($userId, $this->userDisplayNameCache)) {
            return $this->userDisplayNameCache[$userId];
        }

        $result = $db->query("SELECT display_name FROM users WHERE id = {$userId} LIMIT 1");
        if (!$result || $result->num_rows === 0) {
            $this->userDisplayNameCache[$userId] = null;
            return null;
        }

        $row = $result->fetch_assoc();
        $displayName = trim((string)($row['display_name'] ?? ''));
        $this->userDisplayNameCache[$userId] = $displayName === '' ? null : $displayName;
        return $this->userDisplayNameCache[$userId];
    }

    private function getOrderItemToOrderMap(array $orderIds): array
    {
        global $db;
        $orderIds = array_values(array_filter(array_map('intval', $orderIds)));
        if (empty($orderIds)) {
            return [];
        }
        $in = implode(',', $orderIds);
        $result = $db->query("SELECT id, order_id FROM order_items WHERE order_id IN ({$in})");
        $map = [];
        if ($result) {
            while ($row = $result->fetch_assoc()) {
                $map[(int)$row['id']] = (int)$row['order_id'];
            }
        }
        return $map;
    }

    private function getVehicleSubscriptionTypeMap(array $primaryRows): array
    {
        global $db;
        $pairs = [];
        foreach ($primaryRows as $row) {
            $customerNumber = (int)$row['customer_number'];
            $reg = strtoupper(trim((string)($row['reg_1'] ?? '')));
            if ($customerNumber > 0 && $reg !== '') {
                $pairs[$customerNumber . '|' . $reg] = [$customerNumber, $reg];
            }
        }
        if (empty($pairs)) {
            return [];
        }

        $customerNumbers = implode(',', array_unique(array_map(static fn($pair): int => (int)$pair[0], $pairs)));
        $deletedFilter = $this->columnExists('customer_vehicles', 'deleted_at')
            ? "AND cv.deleted_at IS NULL"
            : "";
        $result = $db->query(
            "SELECT cv.customer_id, UPPER(TRIM(cv.reg)) AS reg, cv.type AS product_id, p.name AS product_name
             FROM customer_vehicles cv
             LEFT JOIN products p ON p.id = cv.type
             WHERE cv.customer_id IN ({$customerNumbers})
               AND cv.wash_subscription = 1
               {$deletedFilter}"
        );
        $map = [];
        if ($result) {
            while ($row = $result->fetch_assoc()) {
                $key = (int)$row['customer_id'] . '|' . strtoupper(trim((string)$row['reg']));
                if (isset($pairs[$key])) {
                    $map[$key] = [
                        'product_id' => (int)$row['product_id'],
                        'product_name' => (string)($row['product_name'] ?? ''),
                    ];
                }
            }
        }
        return $map;
    }

    private function getPrimaryProductHistory(string $dateFrom, array $registrationNumbers): array
    {
        global $db;

        $registrations = [];
        foreach ($registrationNumbers as $registrationNumber) {
            $registrationNumber = preg_replace('/[^A-Z0-9]/', '', strtoupper(trim((string)$registrationNumber)));
            $registrationNumber = is_string($registrationNumber) ? $registrationNumber : '';
            if ($registrationNumber !== '') {
                $registrations[$registrationNumber] = true;
            }
        }
        if (empty($registrations)) {
            return [];
        }

        $dateFrom = $db->escape_string($dateFrom);
        $historyStart = $db->escape_string(date('Y-m-d H:i:s', strtotime($dateFrom . ' -18 months')));
        $registrationFilter = implode(',', array_map(static function (string $registrationNumber) use ($db): string {
            return "'" . $db->escape_string($registrationNumber) . "'";
        }, array_keys($registrations)));
        $result = $db->query(
            "SELECT UPPER(TRIM(o.reg_1)) AS reg, oi.product_id, p.name AS product_name, COUNT(*) AS usage_count
             FROM orders o
             JOIN order_items oi ON oi.order_id = o.id
             JOIN products p ON p.id = oi.product_id
             WHERE o.created_at >= '{$historyStart}'
               AND o.created_at < '{$dateFrom}'
               AND o.deleted_at IS NULL
               AND (oi.deleted_at IS NULL OR oi.deleted_at = '')
               AND p.is_wash = 1
               AND COALESCE(oi.related_item_id, 0) = 0
               AND COALESCE(o.reg_1, '') <> ''
               AND o.reg_1 IN ({$registrationFilter})
             GROUP BY UPPER(TRIM(o.reg_1)), oi.product_id, p.name
             ORDER BY reg, usage_count DESC, oi.product_id ASC"
        );

        $history = [];
        $seenCounts = [];
        if ($result) {
            while ($row = $result->fetch_assoc()) {
                $reg = (string)$row['reg'];
                $item = [
                    'product_id' => (int)$row['product_id'],
                    'product_name' => (string)($row['product_name'] ?? ''),
                    'count' => (int)$row['usage_count'],
                ];

                if (!isset($seenCounts[$reg])) {
                    $seenCounts[$reg] = 1;
                    if ($item['count'] >= 3) {
                        $history[$reg] = $item;
                    }
                    continue;
                }

                if ($seenCounts[$reg] === 1) {
                    $seenCounts[$reg] = 2;
                    if (isset($history[$reg]) && $item['count'] >= (int)$history[$reg]['count']) {
                        unset($history[$reg]);
                    }
                }
            }
        }
        return $history;
    }

    private function primaryVehicleProductsMatch(
        int $currentProductId,
        string $currentProductName,
        int $expectedProductId,
        string $expectedProductName
    ): bool {
        if ($expectedProductId > 0 && $currentProductId === $expectedProductId) {
            return true;
        }

        $currentVehicleType = $this->normalizePrimaryVehicleProductName($currentProductName);
        $expectedVehicleType = $this->normalizePrimaryVehicleProductName($expectedProductName);
        if ($currentVehicleType === '' || $expectedVehicleType === '') {
            return false;
        }
        return $currentVehicleType === $expectedVehicleType;
    }

    private function normalizePrimaryVehicleProductName(string $productName): string
    {
        $normalized = strtolower(strtr($productName, [
            'Æ' => 'ae',
            'Ø' => 'oe',
            'Å' => 'aa',
            'æ' => 'ae',
            'ø' => 'oe',
            'å' => 'aa',
        ]));
        $normalized = (string)preg_replace('/[^a-z0-9]+/', ' ', $normalized);
        $tokens = array_values(array_filter(
            explode(' ', trim($normalized)),
            static fn(string $token): bool => $token !== ''
                && !in_array($token, [
                    'indvendig',
                    'indv',
                    'interior',
                    'internal',
                    'vask',
                    'wash',
                ], true)
        ));

        return implode(' ', $tokens);
    }

    private function getOrderItemsForPreview(int $orderId): array
    {
        global $db;
        if ($orderId < 1) {
            return [];
        }
        if (array_key_exists($orderId, $this->orderItemsPreviewCache)) {
            return $this->orderItemsPreviewCache[$orderId];
        }

        $result = $db->query(
            "SELECT oi.id, oi.product_id, oi.price, oi.quantity, p.name AS product_name
             FROM order_items oi
             LEFT JOIN products p ON p.id = oi.product_id
             WHERE oi.order_id = {$orderId}
               AND (oi.deleted_at IS NULL OR oi.deleted_at = '')
             ORDER BY oi.related_item_id IS NOT NULL, oi.id"
        );
        $rows = $result ? $db->fetch_all($result) : [];
        $this->orderItemsPreviewCache[$orderId] = array_map(static function (array $row): array {
            return [
                'id' => (int)($row['id'] ?? 0),
                'product_id' => (int)($row['product_id'] ?? 0),
                'product_name' => (string)($row['product_name'] ?? ''),
                'quantity' => (int)($row['quantity'] ?? 0),
                'price' => (int)($row['price'] ?? 0),
            ];
        }, $rows);

        return $this->orderItemsPreviewCache[$orderId];
    }

    private function seedOrderItemsPreviewCacheFromRows(array $rows): void
    {
        $grouped = [];
        foreach ($rows as $row) {
            $orderId = (int)($row['order_id'] ?? 0);
            if ($orderId < 1) {
                continue;
            }
            $grouped[$orderId] ??= [];

            $orderItemId = (int)($row['order_item_id'] ?? 0);
            if ($orderItemId < 1) {
                continue;
            }

            $grouped[$orderId][] = [
                'id' => $orderItemId,
                'product_id' => (int)($row['product_id'] ?? 0),
                'product_name' => (string)($row['product_name'] ?? ''),
                'quantity' => (int)($row['item_quantity'] ?? 0),
                'price' => (int)($row['item_price'] ?? 0),
                '_related_sort' => (int)($row['related_item_id'] ?? 0) > 0 ? 1 : 0,
            ];
        }

        foreach ($grouped as $orderId => $items) {
            usort($items, static function (array $a, array $b): int {
                return ((int)$a['_related_sort'] <=> (int)$b['_related_sort'])
                    ?: ((int)$a['id'] <=> (int)$b['id']);
            });
            $this->orderItemsPreviewCache[(int)$orderId] = array_map(static function (array $item): array {
                unset($item['_related_sort']);
                return $item;
            }, $items);
        }
    }

    private function preloadEconomicCustomerDiscounts(array $rows): void
    {
        $customerUserIds = [];
        foreach ($rows as $row) {
            if ((int)($row['apply_category_discount'] ?? 0) !== 1) {
                continue;
            }

            $customerNumber = (int)($row['customer_number'] ?? 0);
            $userId = (int)($row['user_id'] ?? 0);
            if ($customerNumber < 1 || $userId < 1 || array_key_exists($customerNumber, $this->economicCustomerDiscountCache)) {
                continue;
            }

            $customerUserIds[$customerNumber] = $userId;
        }

        foreach ($customerUserIds as $customerNumber => $userId) {
            $discount = $this->getCachedEconomicCustomerDiscount($userId);
            if ($discount === null) {
                $discount = $this->loadEconomicCustomerDiscount((int)$customerNumber, $userId);
            }

            $this->economicCustomerDiscountCache[(int)$customerNumber] = $discount;
        }
    }

    private function getCachedEconomicCustomerDiscount(int $userId): ?int
    {
        if ($userId < 1 || !defined('redis')) {
            return null;
        }

        try {
            $cachedDiscount = constant('redis')->get_economic_customer_discount_percentage($userId);
            return $cachedDiscount === null ? null : (int)$cachedDiscount;
        } catch (Throwable $e) {
            return null;
        }
    }

    private function loadEconomicCustomerDiscount(int $customerNumber, int $userId): int
    {
        if ($customerNumber < 1 || $userId < 1 || !defined('redis')) {
            return 0;
        }

        try {
            $discount = (int)(new \customers\economicCustomers())->getCustomerDiscountPercentage($customerNumber);
            constant('redis')->cache_economic_customer_discount_percentage($userId, $discount);
            return $discount;
        } catch (Throwable $e) {
            return 0;
        }
    }

    private function calculateExpectedPrice(array $row): int
    {
        $customMissingPrice = $this->isCustomMissingDepartmentPrice($row);
        if ($customMissingPrice) {
            return \objects\products_o::CUSTOM_PRICING_MISSING_PRICE;
        }

        $fixedPrice = $this->rowProductFixedPrice($row);
        if ($fixedPrice !== null) {
            return $fixedPrice;
        }

        $base = $row['department_price'] !== null
            ? (int)$row['department_price']
            : (int)($row['product_base_price'] ?? 0);
        $discount = $this->discountBreakdown($row)['applied_discount_percentage'];
        return (int)round($base * (1 - ($discount / 100)));
    }

    private function priceBreakdown(array $row, int $expected): array
    {
        $departmentPrice = $row['department_price'] !== null ? (int)$row['department_price'] : null;
        $customMissingPrice = $this->isCustomMissingDepartmentPrice($row);
        $base = $departmentPrice ?? ($customMissingPrice ? \objects\products_o::CUSTOM_PRICING_MISSING_PRICE : (int)($row['product_base_price'] ?? 0));
        $discount = $this->discountBreakdown($row);
        if ($customMissingPrice) {
            $discount['applied_discount_percentage'] = 0;
        }

        return [
            'product_price' => $customMissingPrice ? \objects\products_o::CUSTOM_PRICING_MISSING_PRICE : (int)($row['product_base_price'] ?? 0),
            'department_price' => $departmentPrice,
            'effective_base_price' => $base,
            'product_fixed_price' => $this->rowProductFixedPrice($row),
            'product_discount_percentage' => $discount['product_discount_percentage'],
            'category_discount_percentage' => $discount['category_discount_percentage'],
            'economic_customer_discount_percentage' => $discount['economic_customer_discount_percentage'],
            'applied_discount_percentage' => $discount['applied_discount_percentage'],
            'expected_price' => $expected,
        ];
    }

    private function isCustomMissingDepartmentPrice(array $row): bool
    {
        return $row['department_price'] === null && (bool)(int)($row['department_custom_pricing_only'] ?? 0);
    }

    private function discountBreakdown(array $row): array
    {
        $productDiscount = (int)($row['product_discount_percentage'] ?? 0);
        $categoryApplied = (int)($row['apply_category_discount'] ?? 0) === 1;
        $categoryDiscount = $categoryApplied ? (int)($row['category_discount_percentage'] ?? 0) : 0;
        $economicDiscount = $categoryApplied ? $this->economicCustomerDiscountPercentage($row) : 0;
        $appliedDiscount = $this->rowProductFixedPrice($row) !== null
            ? 0
            : max($productDiscount, $categoryDiscount, $economicDiscount);

        return [
            'product_discount_percentage' => $productDiscount,
            'category_discount_percentage' => $categoryDiscount,
            'economic_customer_discount_percentage' => $economicDiscount,
            'applied_discount_percentage' => $appliedDiscount,
        ];
    }

    private function rowProductFixedPrice(array $row): ?int
    {
        return array_key_exists('product_fixed_price', $row) && $row['product_fixed_price'] !== null
            ? (int)$row['product_fixed_price']
            : null;
    }

    private function economicCustomerDiscountPercentage(array $row): int
    {
        $customerNumber = (int)($row['customer_number'] ?? 0);
        if ($customerNumber < 1) {
            return 0;
        }
        if (array_key_exists($customerNumber, $this->economicCustomerDiscountCache)) {
            return $this->economicCustomerDiscountCache[$customerNumber];
        }

        $discount = 0;
        $userId = (int)($row['user_id'] ?? 0);
        if ($userId > 0) {
            $discount = $this->getCachedEconomicCustomerDiscount($userId) ?? 0;
        }

        $this->economicCustomerDiscountCache[$customerNumber] = $discount;
        return $discount;
    }

    private function orderContext(array $row): array
    {
        return [
            'customer_number' => (int)($row['customer_number'] ?? 0),
            'customer_name' => (string)($row['customer_name'] ?? ''),
            'order_id' => (int)($row['order_id'] ?? 0),
            'invoice_collection_id' => (int)($row['invoice_collection_id'] ?? 0) ?: null,
            'department_id' => (int)($row['department_id'] ?? 0) ?: null,
            'reg_1' => (string)($row['reg_1'] ?? ''),
        ];
    }

    private function orderItemContext(array $row): array
    {
        return $this->orderContext($row) + [
            'order_item_id' => (int)($row['order_item_id'] ?? 0),
            'product_id' => (int)($row['product_id'] ?? 0),
            'product_name' => $this->productLabel($row),
        ];
    }

    private function invoiceCollectionContext(array $row): array
    {
        return [
            'customer_number' => (int)($row['customer_number'] ?? 0),
            'customer_name' => (string)($row['customer_name'] ?? ''),
            'invoice_collection_id' => (int)($row['invoice_collection_id'] ?? 0),
        ];
    }

    private function productLabel(array $row): string
    {
        $name = trim((string)($row['product_name'] ?? ''));
        return $name !== '' ? $name : 'Item #' . (int)($row['product_id'] ?? 0);
    }

    private function hasAttribute(array $attributes, int $customerNumber, string $attribute): bool
    {
        return isset($attributes[$customerNumber][$attribute]);
    }

    private function rowMatchesProductTerms(array $row, array $terms): bool
    {
        $haystack = strtolower(trim(
            (string)($row['product_name'] ?? '') . ' ' .
            (string)($row['category_name'] ?? '')
        ));
        foreach ($terms as $term) {
            if ($term !== '' && str_contains($haystack, strtolower($term))) {
                return true;
            }
        }
        return false;
    }

    private function isIncludedOrderItem(array $row): bool
    {
        $value = $row['item_include_in_invoice'] ?? 1;
        return $value === null || $value === '' || (int)$value === 1;
    }

    private function isPrimaryVehicleItem(array $row): bool
    {
        return (int)($row['order_item_id'] ?? 0) > 0
            && (int)($row['is_wash'] ?? 0) === 1
            && (int)($row['related_item_id'] ?? 0) === 0;
    }

    private function isWashCertificateProduct(array $row): bool
    {
        return (int)($row['product_id'] ?? 0) === self::WASH_CERTIFICATE_PRODUCT_ID
            || $this->rowMatchesProductTerms($row, ['wash certificate', 'vaskecertifikat']);
    }

    private function rowHasWashCertificateAttachment(array $row): bool
    {
        return (int)($row['has_wash_certificate_attachment'] ?? 0) === 1;
    }

    private function dedupeAutomaticFlags(array $flags): array
    {
        $deduped = [];
        foreach ($flags as $flag) {
            $deduped[(string)$flag['fingerprint']] = $flag;
        }
        return array_values($deduped);
    }

    private function customerFilterSql(string $column, ?array $onlyCustomerNumbers): string
    {
        if ($onlyCustomerNumbers === null) {
            return '';
        }
        $numbers = array_values(array_filter(array_map('intval', $onlyCustomerNumbers), static fn(int $value): bool => $value > 0));
        if (empty($numbers)) {
            return ' AND 1=0';
        }
        return ' AND ' . $column . ' IN (' . implode(',', array_unique($numbers)) . ')';
    }

    private function nullableIntSql(mixed $value): string
    {
        if ($value === null || $value === '') {
            return 'NULL';
        }
        return (string)(int)$value;
    }

    private function nullableStringSql(?string $value): string
    {
        global $db;
        if ($value === null || trim($value) === '') {
            return 'NULL';
        }
        return "'" . $db->escape_string($value) . "'";
    }

    private function jsonSql(array $value): string
    {
        global $db;
        $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            return 'NULL';
        }
        return "'" . $db->escape_string($json) . "'";
    }

    private function tableExists(string $table): bool
    {
        global $db;
        $table = str_replace(['\\', "'", '`'], ['\\\\', "\\'", ''], $table);
        $result = $db->query("SHOW TABLES LIKE '{$table}'");
        return $result !== false && is_object($result) && property_exists($result, 'num_rows') && (int)$result->num_rows > 0;
    }

    private function columnExists(string $table, string $column): bool
    {
        global $db;
        $table = str_replace(['\\', "'", '`'], ['\\\\', "\\'", ''], $table);
        $column = str_replace(['\\', "'", '`'], ['\\\\', "\\'", ''], $column);
        $result = $db->query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'");
        return $result !== false && is_object($result) && property_exists($result, 'num_rows') && (int)$result->num_rows > 0;
    }
}
