<?php

namespace classes;

require_once WD . '/classes/xlvask_usage_logs_schema_bootstrap.php';

use Exception;
use helpers\xlvask_usage_log;
use objects\order_items_o;
use objects\orders_o;
use objects\xlvask_usage_logs_o;

class xlvask_automation_service
{
    private const ACTION_ATTACH = 'attach_order';
    private const ACTION_CREATE = 'create_order';
    private const ACTION_NONE = 'none';

    private const STATUS_SUGGESTED = 'suggested';
    private const STATUS_AUTO_ACCEPTED = 'auto_accepted';
    private const STATUS_ACCEPTED = 'accepted';
    private const STATUS_DENIED = 'denied';
    private const STATUS_FAILED = 'failed';
    private const STATUS_NONE = 'none';

    private const SOURCE_DETERMINISTIC = 'deterministic';
    private const SOURCE_FUZZY = 'fuzzy';
    private const SOURCE_HISTORY = 'history';
    private const SOURCE_OPENAI = 'openai';

    private const AUTOMATION_CASHIER_ID = 2285;
    private const MIN_SUGGESTION_CONFIDENCE = 0.70;
    private const AUTO_ATTACH_CONFIDENCE = 0.92;
    private const AUTO_CREATE_CONFIDENCE = 0.97;
    private const CREATE_MIN_AGE_HOURS = 6.0;
    private const OPENAI_CACHE_VERSION = 1;

    public function __construct()
    {
        xlvask_usage_logs_schema_bootstrap::ensureTables();
    }

    public function evaluateUsageLogById(int $usageLogId, ?int $actorId = null, bool $allowExecute = true): array
    {
        $row = $this->loadUsageLogRow($usageLogId);
        if ($row === null) {
            return $this->emptyAutomation('XL Vask-vasken blev ikke fundet.');
        }

        return $this->evaluateUsageLogRow($row, $actorId, $allowExecute);
    }

    public function evaluateUsageLogRow(array $row, ?int $actorId = null, bool $allowExecute = true): array
    {
        $usageLogId = (int)($row['id'] ?? 0);
        if ($usageLogId < 1) {
            return $this->emptyAutomation('XL Vask-vasken mangler et gyldigt id.');
        }

        try {
            $log = $this->usageLogFromRow($row);
            $guard = $this->guardReason($log);
            $existing = $this->latestTerminalSuggestion($usageLogId);
            if ($guard !== null) {
                if ($existing !== null && in_array((string)$existing['status'], [
                    self::STATUS_AUTO_ACCEPTED,
                    self::STATUS_ACCEPTED,
                    self::STATUS_DENIED,
                ], true)) {
                    return $this->formatSuggestion($existing);
                }

                return $this->emptyAutomation($guard);
            }

            if ($existing !== null) {
                if ((string)$existing['status'] === self::STATUS_SUGGESTED) {
                    $context = $this->buildContext($usageLogId, $log);
                    $contextGuard = $this->contextGuardReason($context);
                    if ($contextGuard !== null) {
                        return $this->emptyAutomation($contextGuard);
                    }

                    $freshSuggestion = $this->buildSuggestionForContext($context);
                    if ($freshSuggestion !== null) {
                        $suggestionId = $this->persistSuggestion($context, $freshSuggestion, $actorId);
                        $existing = $this->loadSuggestion($suggestionId) ?? $existing;
                    }

                    if ($allowExecute && $this->shouldAutoExecute($existing, $context)) {
                        return $this->executeSuggestion($existing, $context, $actorId, true);
                    }
                }

                return $this->formatSuggestion($existing);
            }

            $context = $this->buildContext($usageLogId, $log);
            $contextGuard = $this->contextGuardReason($context);
            if ($contextGuard !== null) {
                return $this->emptyAutomation($contextGuard);
            }

            if ($this->hasDeniedFeedback($context['signature_hash'], self::ACTION_ATTACH)
                && $this->hasDeniedFeedback($context['signature_hash'], self::ACTION_CREATE)) {
                return $this->emptyAutomation('Tidligere afvist for samme køretøjsmønster.');
            }

            $suggestion = $this->buildSuggestionForContext($context);

            if ($suggestion === null || $suggestion['confidence'] < self::MIN_SUGGESTION_CONFIDENCE) {
                return $this->emptyAutomation('Ingen sikker automatiseringshandling fundet.');
            }

            if ($this->hasDeniedFeedback($context['signature_hash'], $suggestion['action'])) {
                return $this->emptyAutomation('Tidligere afvist for samme køretøjsmønster.');
            }

            $suggestionId = $this->persistSuggestion($context, $suggestion, $actorId);
            $suggestionRow = $this->loadSuggestion($suggestionId);
            if ($suggestionRow === null) {
                return $this->emptyAutomation('Forslaget kunne ikke gemmes.');
            }

            if ($allowExecute && $this->shouldAutoExecute($suggestionRow, $context)) {
                return $this->executeSuggestion($suggestionRow, $context, $actorId, true);
            }

            return $this->formatSuggestion($suggestionRow);
        } catch (Exception $e) {
            return [
                ...$this->emptyAutomation('Automatiseringen kunne ikke evaluere vasken.'),
                'status' => self::STATUS_FAILED,
                'error' => $e->getMessage(),
            ];
        }
    }

    public function acceptUsageLogById(int $usageLogId, ?int $actorId = null, ?int $suggestionId = null, ?string $reason = null): array
    {
        $row = $this->loadUsageLogRow($usageLogId);
        if ($row === null) {
            return $this->emptyAutomation('XL Vask-vasken blev ikke fundet.');
        }

        $log = $this->usageLogFromRow($row);
        $guard = $this->guardReason($log);
        if ($guard !== null) {
            return $this->emptyAutomation($guard);
        }

        $context = $this->buildContext($usageLogId, $log);
        $contextGuard = $this->contextGuardReason($context);
        if ($contextGuard !== null) {
            return $this->emptyAutomation($contextGuard);
        }

        $suggestion = $suggestionId !== null ? $this->loadSuggestion($suggestionId) : $this->latestActionableSuggestion($usageLogId);
        if ($suggestion === null) {
            $this->evaluateUsageLogRow($row, $actorId, false);
            $suggestion = $this->latestActionableSuggestion($usageLogId);
        }

        if ($suggestion === null) {
            return $this->emptyAutomation('Der er intet forslag at acceptere.');
        }

        $result = $this->executeSuggestion($suggestion, $context, $actorId, false);
        $this->persistFeedback($context, (string)$suggestion['action'], 'accepted', (int)($result['matched_order_id'] ?? $result['created_order_id'] ?? 0), $actorId, $reason);

        return $result;
    }

    public function denyUsageLogById(int $usageLogId, ?int $actorId = null, ?int $suggestionId = null, ?string $reason = null): array
    {
        $row = $this->loadUsageLogRow($usageLogId);
        if ($row === null) {
            return $this->emptyAutomation('XL Vask-vasken blev ikke fundet.');
        }

        $log = $this->usageLogFromRow($row);
        $context = $this->buildContext($usageLogId, $log);
        $suggestion = $suggestionId !== null ? $this->loadSuggestion($suggestionId) : $this->latestActionableSuggestion($usageLogId);

        if ($suggestion === null) {
            return $this->emptyAutomation('Der er intet forslag at afvise.');
        }

        $this->updateSuggestionStatus((int)$suggestion['id'], self::STATUS_DENIED, $actorId);
        $this->persistFeedback($context, (string)$suggestion['action'], 'denied', (int)($suggestion['matched_order_id'] ?? 0), $actorId, $reason);

        return $this->formatSuggestion($this->loadSuggestion((int)$suggestion['id']) ?? $suggestion);
    }

    public function runPending(?string $dateFrom = null, ?string $dateTo = null, array $ids = [], int $limit = 100, ?int $actorId = null): array
    {
        $rows = $ids !== [] ? $this->loadUsageLogRowsByIds($ids) : $this->loadPendingRows($dateFrom, $dateTo, $limit);
        $results = [];
        foreach ($rows as $row) {
            $results[] = $this->evaluateUsageLogRow($row, $actorId, true);
        }

        return [
            'processed' => count($results),
            'results' => $results,
        ];
    }

    public static function normalizeRegistrationForAutomation(string $registration): string
    {
        return strtoupper(preg_replace('/[^A-Z0-9]/i', '', $registration) ?? '');
    }

    public static function itemSignaturePartsForAutomation(array $items): array
    {
        $parts = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }

            $parts[] = implode(':', [
                (int)($item['product_id'] ?? 0),
                (int)($item['quantity'] ?? 0),
                (int)($item['price'] ?? 0),
            ]);
        }

        sort($parts, SORT_STRING);
        return $parts;
    }

    public static function scoreItemMatchForAutomation(array $usageItems, array $orderItems): array
    {
        $usageSignature = self::itemSignaturePartsForAutomation($usageItems);
        $orderSignature = self::itemSignaturePartsForAutomation($orderItems);
        $usageTotal = self::itemsTotalForAutomation($usageItems);
        $orderTotal = self::itemsTotalForAutomation($orderItems);

        if ($usageSignature === $orderSignature && $usageTotal === $orderTotal) {
            return [
                'confidence' => 0.95,
                'source' => self::SOURCE_DETERMINISTIC,
                'reason' => 'Produkterne og prisen matcher en ordre fra samme dag.',
            ];
        }

        $usagePrimary = (int)($usageItems[0]['product_id'] ?? 0);
        $orderPrimary = (int)($orderItems[0]['product_id'] ?? 0);
        if ($usagePrimary < 1 || $usagePrimary !== $orderPrimary) {
            return ['confidence' => 0.0, 'source' => self::SOURCE_DETERMINISTIC, 'reason' => ''];
        }

        $overlap = self::productOverlapForAutomation($usageItems, $orderItems);
        $totalDiff = abs($usageTotal - $orderTotal);
        if ($overlap >= 0.70 && $totalDiff <= 50) {
            return [
                'confidence' => 0.93,
                'source' => self::SOURCE_FUZZY,
                'reason' => 'Samme primære produkt og relaterede tilføjelser matcher en ordre fra samme dag.',
            ];
        }

        if ($overlap >= 0.50 && $totalDiff <= 150) {
            return [
                'confidence' => 0.80,
                'source' => self::SOURCE_FUZZY,
                'reason' => 'Vasken ligner en ordre fra samme dag, men kræver manuel godkendelse.',
            ];
        }

        $matchableUsageItems = self::matchableUsageItemsForAutomation($usageItems);
        $matchableOverlap = self::productOverlapForAutomation($matchableUsageItems, $orderItems);
        if (
            $matchableUsageItems !== []
            && $matchableOverlap >= 0.95
            && self::orderHasAdditionsBeyondUsage($matchableUsageItems, $orderItems)
        ) {
            return [
                'confidence' => 0.88,
                'source' => self::SOURCE_FUZZY,
                'reason' => 'Ordren indeholder XL Vask-produkterne samt ekstra ydelser fra samme dag.',
            ];
        }

        return ['confidence' => 0.0, 'source' => self::SOURCE_DETERMINISTIC, 'reason' => ''];
    }

    public static function itemsTotalForAutomation(array $items): int
    {
        return array_reduce($items, fn(int $total, array $item): int => $total + ((int)($item['price'] ?? 0) * (int)($item['quantity'] ?? 0)), 0);
    }

    public static function isExactItemMatchForAutomation(array $usageItems, array $orderItems): bool
    {
        return self::itemSignaturePartsForAutomation($usageItems) === self::itemSignaturePartsForAutomation($orderItems)
            && self::itemsTotalForAutomation($usageItems) === self::itemsTotalForAutomation($orderItems);
    }

    public static function productOverlapForAutomation(array $usageItems, array $orderItems): float
    {
        $usageBag = self::productBagForAutomation($usageItems);
        $orderBag = self::productBagForAutomation($orderItems);
        $usageTotal = array_sum($usageBag);
        if ($usageTotal <= 0) {
            return 0.0;
        }

        $overlap = 0;
        foreach ($usageBag as $productId => $quantity) {
            $overlap += min($quantity, $orderBag[$productId] ?? 0);
        }

        return $overlap / $usageTotal;
    }

    public static function productBagForAutomation(array $items): array
    {
        $bag = [];
        foreach ($items as $item) {
            $productId = (int)($item['product_id'] ?? 0);
            if ($productId < 1) {
                continue;
            }
            $bag[$productId] = ($bag[$productId] ?? 0) + max(1, (int)($item['quantity'] ?? 1));
        }

        return $bag;
    }

    private static function matchableUsageItemsForAutomation(array $items): array
    {
        $positiveItems = array_values(array_filter($items, static function (array $item): bool {
            return (int)($item['product_id'] ?? 0) > 0
                && (int)($item['quantity'] ?? 0) > 0
                && (int)($item['price'] ?? 0) > 0;
        }));

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

        return array_values(array_filter($items, static function (array $item): bool {
            return (int)($item['product_id'] ?? 0) > 0
                && (int)($item['quantity'] ?? 0) > 0;
        }));
    }

    private static function orderHasAdditionsBeyondUsage(array $usageItems, array $orderItems): bool
    {
        $usageBag = self::productBagForAutomation($usageItems);
        foreach (self::productBagForAutomation($orderItems) as $productId => $quantity) {
            if ($quantity > ($usageBag[$productId] ?? 0)) {
                return true;
            }
        }

        return false;
    }

    public static function normalizeUsageLogRowForAutomation(array $row): array
    {
        unset($row['id']);

        $washItems = $row['WashItems'] ?? [];
        if (is_string($washItems)) {
            $decoded = json_decode($washItems, true);
            $row['WashItems'] = is_array($decoded) ? $decoded : [];
        } elseif (!is_array($washItems)) {
            $row['WashItems'] = [];
        }

        return $row;
    }

    public static function openAiCacheKeyForAutomation(
        string $schemaName,
        string $prompt,
        array $payload,
        array $schema,
        float $temperature
    ): string {
        $input = [
            'version' => self::OPENAI_CACHE_VERSION,
            'schema_name' => $schemaName,
            'prompt' => $prompt,
            'payload' => $payload,
            'schema' => $schema,
            'temperature' => round($temperature, 4),
        ];

        return hash('sha256', self::stableJsonForAutomation($input));
    }

    public static function stableJsonForAutomation(mixed $value): string
    {
        $encoded = json_encode(
            self::normalizeForStableJson($value),
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
        );

        if ($encoded === false) {
            throw new Exception('Kunne ikke opbygge en stabil cache-nÃ¸gle for XL Vask-automatisering.');
        }

        return $encoded;
    }

    private static function normalizeForStableJson(mixed $value): mixed
    {
        if (!is_array($value)) {
            return $value;
        }

        $normalized = array_map(fn(mixed $item): mixed => self::normalizeForStableJson($item), $value);
        $isList = $normalized === [] || array_keys($normalized) === range(0, count($normalized) - 1);
        if (!$isList) {
            ksort($normalized, SORT_STRING);
        }

        return $normalized;
    }

    private function buildDeterministicSuggestion(array $context): ?array
    {
        $best = null;
        foreach ($context['candidate_orders'] as $candidate) {
            $score = $this->scoreOrderMatch($context['items'], $candidate['order_items']);
            if ($score['confidence'] < self::MIN_SUGGESTION_CONFIDENCE) {
                continue;
            }

            $candidateSuggestion = [
                'action' => self::ACTION_ATTACH,
                'confidence' => $score['confidence'],
                'source' => $score['source'],
                'matched_order_id' => (int)$candidate['id'],
                'created_order_id' => null,
                'candidate_order' => $candidate,
                'proposed_order' => $context['proposed_order'],
                'reason' => $score['reason'] . ' Ordre #' . (int)$candidate['id'] . '.',
            ];

            if ($best === null || $candidateSuggestion['confidence'] > $best['confidence']) {
                $best = $candidateSuggestion;
            }
        }

        if ($best !== null && $this->hasAcceptedFeedback($context['signature_hash'], self::ACTION_ATTACH)) {
            $best['confidence'] = max($best['confidence'], 0.96);
            $best['source'] = self::SOURCE_HISTORY;
            $best['reason'] = 'Tidligere godkendt mønster for køretøjet matcher ordre #' . (int)$best['matched_order_id'] . '.';
        }

        if ($best !== null) {
            return $best;
        }

        if ($context['age_hours'] >= self::CREATE_MIN_AGE_HOURS) {
            $history = $this->findMatchingHistoricalOrder($context);
            if ($history !== null || $this->hasAcceptedFeedback($context['signature_hash'], self::ACTION_CREATE)) {
                return [
                    'action' => self::ACTION_CREATE,
                    'confidence' => 0.98,
                    'source' => self::SOURCE_HISTORY,
                    'matched_order_id' => null,
                    'created_order_id' => null,
                    'candidate_order' => $history,
                    'proposed_order' => $context['proposed_order'],
                    'reason' => 'Vasken er over 6 timer gammel og matcher et tidligere godkendt køretøjsmønster.',
                ];
            }
        }

        return null;
    }

    private function buildSuggestionForContext(array $context): ?array
    {
        $suggestion = $this->buildDeterministicSuggestion($context);

        if (
            ($suggestion === null || $suggestion['confidence'] < self::MIN_SUGGESTION_CONFIDENCE)
            && $this->isOpenAiEnabled()
        ) {
            $suggestion = $this->buildOpenAiSuggestion($context) ?? $suggestion;
        }

        if ($suggestion === null || $suggestion['confidence'] < self::MIN_SUGGESTION_CONFIDENCE) {
            return null;
        }

        if ($this->hasDeniedFeedback($context['signature_hash'], $suggestion['action'])) {
            return null;
        }

        return $suggestion;
    }

    private function buildOpenAiSuggestion(array $context): ?array
    {
        try {
            $schemaName = 'xlvask_automation';
            $prompt = 'Vurder om en XL Vask-vask skal tilknyttes en eksisterende ordre, oprettes som ordre eller ikke behandles. Returner kun JSON efter skemaet.';
            $temperature = 0.1;
            $schema = [
                'type' => 'object',
                'properties' => [
                    'action' => ['type' => 'string', 'enum' => [self::ACTION_ATTACH, self::ACTION_CREATE, self::ACTION_NONE]],
                    'confidence' => ['type' => 'number'],
                    'reason_da' => ['type' => 'string'],
                    'candidate_order_id' => ['type' => ['integer', 'null']],
                    'proposed_order_items' => [
                        'type' => 'array',
                        'items' => [
                            'type' => 'object',
                            'properties' => [
                                'product_id' => ['type' => 'integer'],
                                'quantity' => ['type' => 'integer'],
                                'price' => ['type' => 'integer'],
                            ],
                            'required' => ['product_id', 'quantity', 'price'],
                            'additionalProperties' => false,
                        ],
                    ],
                    'risk_flags' => ['type' => 'array', 'items' => ['type' => 'string']],
                ],
                'required' => ['action', 'confidence', 'reason_da', 'candidate_order_id', 'proposed_order_items', 'risk_flags'],
                'additionalProperties' => false,
            ];

            $creationAllowed = $context['age_hours'] >= self::CREATE_MIN_AGE_HOURS;
            $payload = [
                'usage_log' => [
                    'wash_id' => $context['wash_id'],
                    'registration' => $context['signature']['registration'],
                    'customer_number' => $context['signature']['customer_number'],
                    'department_id' => $context['signature']['department_id'],
                    'lane' => $context['signature']['lane'],
                    'created_at' => $context['proposed_order']['created_at'] ?? null,
                    'total_net_amount' => $context['total'],
                    'items' => $this->compactItems($context['items']),
                    'creation_allowed' => $creationAllowed,
                    'age_bucket' => $creationAllowed ? 'older_than_6_hours' : 'newer_than_6_hours',
                ],
                'candidate_orders' => array_map(fn(array $candidate): array => [
                    'id' => (int)$candidate['id'],
                    'created_at' => $candidate['created_at'] ?? null,
                    'total_net_amount' => (int)($candidate['total_net_amount'] ?? 0),
                    'items' => $this->compactItems($candidate['order_items'] ?? []),
                ], $context['candidate_orders']),
            ];

            $cacheKey = self::openAiCacheKeyForAutomation($schemaName, $prompt, $payload, $schema, $temperature);
            $result = $this->loadOpenAiCacheResult($cacheKey);
            if ($result === null) {
                $openai = new openai();
                $result = $openai->jsonTask($schemaName, $prompt, $payload, $schema, $temperature);
                $this->persistOpenAiCacheResult($cacheKey, $schemaName, $payload, $schema, $prompt, $temperature, $result);
            }

            $action = (string)($result['action'] ?? self::ACTION_NONE);
            $confidence = (float)($result['confidence'] ?? 0);
            if (!in_array($action, [self::ACTION_ATTACH, self::ACTION_CREATE], true) || $confidence < self::MIN_SUGGESTION_CONFIDENCE) {
                return null;
            }

            if ($action === self::ACTION_CREATE && $context['age_hours'] < self::CREATE_MIN_AGE_HOURS) {
                return null;
            }

            $candidate = null;
            $candidateOrderId = (int)($result['candidate_order_id'] ?? 0);
            if ($action === self::ACTION_ATTACH) {
                foreach ($context['candidate_orders'] as $candidateOrder) {
                    if ((int)$candidateOrder['id'] === $candidateOrderId) {
                        $candidate = $candidateOrder;
                        break;
                    }
                }
                if ($candidate === null) {
                    return null;
                }
            }

            return [
                'action' => $action,
                'confidence' => min(1.0, max(0.0, $confidence)),
                'source' => self::SOURCE_OPENAI,
                'matched_order_id' => $candidateOrderId > 0 ? $candidateOrderId : null,
                'created_order_id' => null,
                'candidate_order' => $candidate,
                'proposed_order' => $context['proposed_order'],
                'reason' => (string)($result['reason_da'] ?? 'OpenAI foreslår handlingen ud fra tilgængelige ordredata.'),
            ];
        } catch (Exception) {
            return null;
        }
    }

    private function shouldAutoExecute(array $suggestion, array $context): bool
    {
        $confidence = (float)$suggestion['confidence'];
        $action = (string)$suggestion['action'];
        $xlvask = new xlvask();

        if ($action === self::ACTION_ATTACH) {
            return $xlvask->config->automatic_order_attachment_enabled->isTrue()
                && $confidence >= self::AUTO_ATTACH_CONFIDENCE
                && $this->isExactAttachSuggestionForContext($suggestion, $context);
        }

        if ($action === self::ACTION_CREATE) {
            return false;
        }

        return false;
    }

    private function isExactAttachSuggestionForContext(array $suggestion, array $context): bool
    {
        if ((string)($suggestion['action'] ?? '') !== self::ACTION_ATTACH) {
            return false;
        }

        $matchedOrderId = (int)($suggestion['matched_order_id'] ?? 0);
        $candidateOrder = $this->candidateOrderFromSuggestion($suggestion);
        if ($matchedOrderId < 1 || !is_array($candidateOrder) || (int)($candidateOrder['id'] ?? 0) !== $matchedOrderId) {
            return false;
        }

        $usageItems = $context['items'] ?? [];
        $orderItems = $candidateOrder['order_items'] ?? [];
        return is_array($usageItems)
            && is_array($orderItems)
            && self::isExactItemMatchForAutomation($usageItems, $orderItems);
    }

    private function candidateOrderFromSuggestion(array $suggestion): ?array
    {
        $candidateOrder = $suggestion['candidate_order'] ?? null;
        if (is_array($candidateOrder)) {
            return $candidateOrder;
        }

        $candidateOrderJson = $suggestion['candidate_order_json'] ?? null;
        if (!is_string($candidateOrderJson) || trim($candidateOrderJson) === '') {
            return null;
        }

        $decoded = json_decode($candidateOrderJson, true);
        return is_array($decoded) ? $decoded : null;
    }

    private function executeSuggestion(array $suggestion, array $context, ?int $actorId, bool $automatic): array
    {
        try {
            $action = (string)$suggestion['action'];
            if ($action === self::ACTION_ATTACH) {
                $orderId = (int)$suggestion['matched_order_id'];
                if ($orderId < 1) {
                    throw new Exception('Forslaget mangler en ordre at tilknytte.');
                }

                if ((new orders_o())->selectByWashId($context['wash_id']) !== null) {
                    throw new Exception('Vasken er allerede tilknyttet en ordre.');
                }

                $order = (new orders_o())->select($orderId);
                $order->wash_id->set($context['wash_id']);
                $this->updateSuggestionExecution((int)$suggestion['id'], $automatic ? self::STATUS_AUTO_ACCEPTED : self::STATUS_ACCEPTED, $actorId, $orderId, null);
            } elseif ($action === self::ACTION_CREATE) {
                if ($context['age_hours'] < self::CREATE_MIN_AGE_HOURS) {
                    throw new Exception('Vasken er ikke gammel nok til automatisk ordreoprettelse.');
                }

                if ((new orders_o())->selectByWashId($context['wash_id']) !== null) {
                    throw new Exception('Vasken er allerede tilknyttet en ordre.');
                }

                $order = $this->createOrderFromContext($context);
                $this->updateSuggestionExecution((int)$suggestion['id'], $automatic ? self::STATUS_AUTO_ACCEPTED : self::STATUS_ACCEPTED, $actorId, null, (int)$order->id);
            } else {
                throw new Exception('Ukendt automatiseringshandling.');
            }

            $latest = $this->loadSuggestion((int)$suggestion['id']) ?? $suggestion;
            if ($automatic) {
                $this->persistFeedback($context, $action, 'accepted', (int)($latest['matched_order_id'] ?? $latest['created_order_id'] ?? 0), $actorId, $this->automaticFeedbackReason($suggestion, $context));
            }

            return $this->formatSuggestion($latest);
        } catch (Exception $e) {
            $this->updateSuggestionFailure((int)$suggestion['id'], $e->getMessage(), $actorId);
            return [
                ...$this->formatSuggestion($this->loadSuggestion((int)$suggestion['id']) ?? $suggestion),
                'status' => self::STATUS_FAILED,
                'error' => $e->getMessage(),
            ];
        }
    }

    private function automaticFeedbackReason(array $suggestion, array $context): string
    {
        if ($this->isExactAttachSuggestionForContext($suggestion, $context)) {
            return 'Automatisk accepteret: Prisoverensstemmelse.';
        }

        return 'Automatisk accepteret.';
    }

    private function createOrderFromContext(array $context): orders_o
    {
        $orderData = $context['proposed_order'];
        $items = $context['items'];

        $order = new orders_o();
        $order->add(
            (int)$orderData['customer_id'],
            self::AUTOMATION_CASHIER_ID,
            (string)($orderData['reference'] ?? ''),
            (string)($orderData['notes'] ?? ''),
            (int)$orderData['department_id'],
            (string)($orderData['reg_1'] ?? ''),
            (string)($orderData['reg_2'] ?? ''),
            (string)($orderData['reg_3'] ?? '')
        );
        $order->wash_id->set($context['wash_id']);
        if (isset($orderData['lane'])) {
            $order->lane->set((int)$orderData['lane']);
        }
        if (!empty($orderData['created_at'])) {
            $order->created_at->set((string)$orderData['created_at']);
        }

        $firstItemId = null;
        foreach ($items as $item) {
            $orderItem = new order_items_o();
            $orderItem->add(
                (int)$order->id,
                (int)$item['product_id'],
                (string)($item['reference'] ?? ''),
                (string)($item['notes'] ?? ''),
                self::AUTOMATION_CASHIER_ID,
                (int)$item['price'],
                (int)$item['quantity'],
                $firstItemId
            );
            if ($firstItemId === null) {
                $firstItemId = (int)$orderItem->id;
            }
        }

        $order->objectChanged();
        return $order;
    }

    private function buildContext(int $usageLogId, xlvask_usage_log $log): array
    {
        $simulated = (new orders_o())->simulateOrderFromXLVask($log, true);
        $proposedOrder = $simulated['order'] ?? [];
        $items = $simulated['order_items'] ?? [];
        $signature = $this->buildSignature($log, $proposedOrder, $items);
        $signatureJson = json_encode($signature, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($signatureJson === false) {
            throw new Exception('Kunne ikke opbygge signatur for XL Vask-vasken.');
        }

        return [
            'usage_log_id' => $usageLogId,
            'wash_id' => (string)$log->WashId,
            'log' => $log,
            'proposed_order' => $proposedOrder,
            'items' => $items,
            'total' => $this->itemsTotal($items),
            'signature' => $signature,
            'signature_json' => $signatureJson,
            'signature_hash' => hash('sha256', $signatureJson),
            'age_hours' => max(0.0, (time() - strtotime((string)$log->StartTime)) / 3600),
            'candidate_orders' => $this->findSameDayCandidateOrders($log, $proposedOrder),
        ];
    }

    private function contextGuardReason(array $context): ?string
    {
        if ((int)($context['proposed_order']['customer_id'] ?? 0) < 1) {
            return 'Vasken mangler en gyldig kundemapping.';
        }

        if ((int)($context['proposed_order']['department_id'] ?? 0) < 1) {
            return 'Vasken mangler en gyldig afdelingsmapping.';
        }

        if (!is_array($context['items'] ?? null) || count($context['items']) < 1) {
            return 'Vasken mangler gyldige produkter.';
        }

        foreach ($context['items'] as $item) {
            if (!is_array($item) || (int)($item['product_id'] ?? 0) < 1) {
                return 'Vasken mangler gyldige produkter.';
            }
        }

        return null;
    }

    private function buildSignature(xlvask_usage_log $log, array $proposedOrder, array $items): array
    {
        return [
            'registration' => $this->normalizeRegistration((string)$log->RegistrationNumber),
            'customer_number' => (int)$log->CustomerId,
            'department_id' => (int)($proposedOrder['department_id'] ?? 0),
            'lane' => (int)($proposedOrder['lane'] ?? 0),
            'primary_product_id' => (int)($items[0]['product_id'] ?? 0),
            'items' => $this->itemSignatureParts($items),
            'total_net_amount' => $this->itemsTotal($items),
        ];
    }

    private function scoreOrderMatch(array $usageItems, array $orderItems): array
    {
        return self::scoreItemMatchForAutomation($usageItems, $orderItems);
    }

    private function findSameDayCandidateOrders(xlvask_usage_log $log, array $proposedOrder): array
    {
        global $db;

        $registration = $db->escape_string($this->normalizeRegistration((string)$log->RegistrationNumber));
        $rawRegistration = $db->escape_string(trim((string)$log->RegistrationNumber));
        $customerNumber = (int)$log->CustomerId;
        $departmentId = (int)($proposedOrder['department_id'] ?? 0);
        $date = date('Y-m-d', strtotime((string)$log->StartTime));
        $from = $db->escape_string($date . ' 00:00:00');
        $to = $db->escape_string($date . ' 23:59:59');

        if ($registration === '' || $customerNumber < 1 || $departmentId < 1) {
            return [];
        }

        $sql = "SELECT *
                FROM orders
                WHERE deleted_at IS NULL
                  AND customer_id = {$customerNumber}
                  AND department_id = {$departmentId}
                  AND cashier_id <> " . self::AUTOMATION_CASHIER_ID . "
                  AND created_at BETWEEN '{$from}' AND '{$to}'
                  AND (wash_id IS NULL OR wash_id = '')
                  AND (
                    REPLACE(UPPER(reg_1), ' ', '') IN ('{$registration}', '{$rawRegistration}')
                    OR REPLACE(UPPER(reg_2), ' ', '') IN ('{$registration}', '{$rawRegistration}')
                    OR REPLACE(UPPER(reg_3), ' ', '') IN ('{$registration}', '{$rawRegistration}')
                  )
                ORDER BY ABS(TIMESTAMPDIFF(SECOND, created_at, '" . $db->escape_string(date('Y-m-d H:i:s', strtotime((string)$log->StartTime))) . "')) ASC
                LIMIT 20";

        $rows = $db->fetch_all($db->query($sql));
        return array_map(function (array $row): array {
            $orderItems = (new orders_o())->getOrderItems((int)$row['id']);
            return [
                ...$row,
                'id' => (int)$row['id'],
                'total_net_amount' => (int)($row['total_net_amount'] ?? $this->itemsTotal($orderItems)),
                'order_items' => $orderItems,
            ];
        }, $rows);
    }

    private function findMatchingHistoricalOrder(array $context): ?array
    {
        global $db;

        $signature = $context['signature'];
        $registration = $db->escape_string((string)$signature['registration']);
        $customerNumber = (int)$signature['customer_number'];
        $departmentId = (int)$signature['department_id'];
        $createdBefore = $db->escape_string((string)($context['proposed_order']['created_at'] ?? date('Y-m-d H:i:s')));

        if ($registration === '' || $customerNumber < 1 || $departmentId < 1) {
            return null;
        }

        $sql = "SELECT *
                FROM orders
                WHERE deleted_at IS NULL
                  AND customer_id = {$customerNumber}
                  AND department_id = {$departmentId}
                  AND created_at < '{$createdBefore}'
                  AND (
                    REPLACE(UPPER(reg_1), ' ', '') = '{$registration}'
                    OR REPLACE(UPPER(reg_2), ' ', '') = '{$registration}'
                    OR REPLACE(UPPER(reg_3), ' ', '') = '{$registration}'
                  )
                ORDER BY created_at DESC
                LIMIT 10";

        foreach ($db->fetch_all($db->query($sql)) as $row) {
            $orderItems = (new orders_o())->getOrderItems((int)$row['id']);
            if ($this->itemSignatureParts($orderItems) === $signature['items']) {
                return [
                    ...$row,
                    'id' => (int)$row['id'],
                    'order_items' => $orderItems,
                ];
            }
        }

        return null;
    }

    private function guardReason(xlvask_usage_log $log): ?string
    {
        if (!empty($log->ignored_at)) {
            return 'Vasken er ignoreret.';
        }

        if (!$log->isCompleted()) {
            return 'Vasken er ikke afsluttet.';
        }

        if (!$log->hasBillableCustomer()) {
            return 'Vasken mangler en fakturerbar kunde.';
        }

        if ((new orders_o())->selectByWashId($log->WashId) !== null) {
            return 'Vasken er allerede tilknyttet en ordre.';
        }

        return null;
    }

    private function persistSuggestion(array $context, array $suggestion, ?int $actorId): int
    {
        global $db;

        $existing = $this->latestActionableSuggestion((int)$context['usage_log_id']);
        if ($existing !== null) {
            $this->updateSuggestionProposal((int)$existing['id'], $context, $suggestion, $actorId);
            return (int)$existing['id'];
        }

        $fields = [
            'usage_log_id' => (int)$context['usage_log_id'],
            'wash_id' => (string)$context['wash_id'],
            'signature_hash' => (string)$context['signature_hash'],
            'signature_json' => (string)$context['signature_json'],
            'action' => (string)$suggestion['action'],
            'status' => self::STATUS_SUGGESTED,
            'confidence' => (float)$suggestion['confidence'],
            'source' => (string)$suggestion['source'],
            'matched_order_id' => $suggestion['matched_order_id'] === null ? null : (int)$suggestion['matched_order_id'],
            'created_order_id' => null,
            'proposed_order_json' => json_encode($suggestion['proposed_order'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'candidate_order_json' => json_encode($suggestion['candidate_order'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'reason' => (string)$suggestion['reason'],
            'created_by' => $actorId,
        ];

        $columns = [];
        $values = [];
        foreach ($fields as $column => $value) {
            $columns[] = "`{$column}`";
            if ($value === null) {
                $values[] = 'NULL';
            } elseif (is_int($value) || is_float($value)) {
                $values[] = (string)$value;
            } else {
                $values[] = "'" . $db->escape_string((string)$value) . "'";
            }
        }

        $db->query('INSERT INTO xlvask_automation_suggestions (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ')');
        return (int)$db->insert_id();
    }

    private function updateSuggestionProposal(int $suggestionId, array $context, array $suggestion, ?int $actorId): void
    {
        global $db;

        $fields = [
            'signature_hash' => (string)$context['signature_hash'],
            'signature_json' => (string)$context['signature_json'],
            'action' => (string)$suggestion['action'],
            'confidence' => (float)$suggestion['confidence'],
            'source' => (string)$suggestion['source'],
            'matched_order_id' => $suggestion['matched_order_id'] === null ? null : (int)$suggestion['matched_order_id'],
            'created_order_id' => null,
            'proposed_order_json' => json_encode($suggestion['proposed_order'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'candidate_order_json' => json_encode($suggestion['candidate_order'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'reason' => (string)$suggestion['reason'],
        ];

        if ($actorId !== null) {
            $fields['created_by'] = $actorId;
        }

        $assignments = [];
        foreach ($fields as $column => $value) {
            if ($value === null) {
                $sqlValue = 'NULL';
            } elseif (is_int($value) || is_float($value)) {
                $sqlValue = (string)$value;
            } else {
                $sqlValue = "'" . $db->escape_string((string)$value) . "'";
            }
            $assignments[] = "`{$column}` = {$sqlValue}";
        }

        $db->query(
            'UPDATE xlvask_automation_suggestions SET ' . implode(', ', $assignments) .
            " WHERE id = {$suggestionId} AND status = '" . self::STATUS_SUGGESTED . "'"
        );
    }

    private function persistFeedback(array $context, string $action, string $decision, int $orderId = 0, ?int $actorId = null, ?string $reason = null): void
    {
        global $db;

        $values = [
            'usage_log_id' => (int)$context['usage_log_id'],
            'wash_id' => (string)$context['wash_id'],
            'signature_hash' => (string)$context['signature_hash'],
            'signature_json' => (string)$context['signature_json'],
            'action' => $action,
            'decision' => $decision,
            'order_id' => $orderId > 0 ? $orderId : null,
            'reason' => $reason,
            'created_by' => $actorId,
        ];

        $columns = [];
        $sqlValues = [];
        foreach ($values as $column => $value) {
            $columns[] = "`{$column}`";
            if ($value === null) {
                $sqlValues[] = 'NULL';
            } elseif (is_int($value)) {
                $sqlValues[] = (string)$value;
            } else {
                $sqlValues[] = "'" . $db->escape_string((string)$value) . "'";
            }
        }

        $db->query('INSERT INTO xlvask_automation_feedback (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $sqlValues) . ')');
    }

    private function loadOpenAiCacheResult(string $cacheKey): ?array
    {
        global $db;

        $cacheKey = $db->escape_string($cacheKey);
        $result = $db->query(
            "SELECT result_json FROM xlvask_automation_openai_cache
             WHERE cache_key = '{$cacheKey}'
             LIMIT 1"
        );
        if ($result === false || $result->num_rows < 1) {
            return null;
        }

        $row = $db->fetch_assoc($result);
        $decoded = json_decode((string)($row['result_json'] ?? ''), true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
            return null;
        }

        $db->query(
            "UPDATE xlvask_automation_openai_cache
             SET hits = hits + 1, last_hit_at = NOW()
             WHERE cache_key = '{$cacheKey}'"
        );

        return $decoded;
    }

    private function persistOpenAiCacheResult(
        string $cacheKey,
        string $schemaName,
        array $payload,
        array $schema,
        string $prompt,
        float $temperature,
        array $result
    ): void {
        global $db;

        $input = [
            'version' => self::OPENAI_CACHE_VERSION,
            'schema_name' => $schemaName,
            'prompt' => $prompt,
            'payload' => $payload,
            'schema' => $schema,
            'temperature' => round($temperature, 4),
        ];

        $inputJson = self::stableJsonForAutomation($input);
        $resultJson = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
        if ($resultJson === false) {
            return;
        }

        $cacheKey = $db->escape_string($cacheKey);
        $schemaName = $db->escape_string($schemaName);
        $inputJson = $db->escape_string($inputJson);
        $resultJson = $db->escape_string($resultJson);

        $db->query(
            "INSERT INTO xlvask_automation_openai_cache
                (cache_key, schema_name, input_json, result_json)
             VALUES
                ('{$cacheKey}', '{$schemaName}', '{$inputJson}', '{$resultJson}')
             ON DUPLICATE KEY UPDATE
                result_json = VALUES(result_json),
                input_json = VALUES(input_json),
                updated_at = NOW()"
        );
    }

    private function hasAcceptedFeedback(string $signatureHash, string $action): bool
    {
        return $this->hasFeedbackDecision($signatureHash, $action, 'accepted');
    }

    private function hasDeniedFeedback(string $signatureHash, string $action): bool
    {
        return $this->hasFeedbackDecision($signatureHash, $action, 'denied');
    }

    private function hasFeedbackDecision(string $signatureHash, string $action, string $decision): bool
    {
        global $db;
        $signatureHash = $db->escape_string($signatureHash);
        $action = $db->escape_string($action);
        $decision = $db->escape_string($decision);
        $result = $db->query(
            "SELECT id FROM xlvask_automation_feedback
             WHERE signature_hash = '{$signatureHash}' AND action = '{$action}' AND decision = '{$decision}'
             ORDER BY id DESC LIMIT 1"
        );
        return $result !== false && $result->num_rows > 0;
    }

    private function latestTerminalSuggestion(int $usageLogId): ?array
    {
        return $this->latestSuggestionWhere($usageLogId, [
            self::STATUS_SUGGESTED,
            self::STATUS_AUTO_ACCEPTED,
            self::STATUS_ACCEPTED,
            self::STATUS_DENIED,
        ]);
    }

    private function latestActionableSuggestion(int $usageLogId): ?array
    {
        return $this->latestSuggestionWhere($usageLogId, [self::STATUS_SUGGESTED]);
    }

    private function latestSuggestionWhere(int $usageLogId, array $statuses): ?array
    {
        global $db;
        $statusSql = implode(',', array_map(fn(string $status): string => "'" . $db->escape_string($status) . "'", $statuses));
        $result = $db->query(
            "SELECT * FROM xlvask_automation_suggestions
             WHERE usage_log_id = {$usageLogId} AND status IN ({$statusSql})
             ORDER BY id DESC LIMIT 1"
        );
        if ($result === false || $result->num_rows < 1) {
            return null;
        }

        return $db->fetch_assoc($result);
    }

    private function loadSuggestion(int $suggestionId): ?array
    {
        global $db;
        $result = $db->query("SELECT * FROM xlvask_automation_suggestions WHERE id = {$suggestionId} LIMIT 1");
        if ($result === false || $result->num_rows < 1) {
            return null;
        }
        return $db->fetch_assoc($result);
    }

    private function updateSuggestionStatus(int $suggestionId, string $status, ?int $actorId): void
    {
        global $db;
        $status = $db->escape_string($status);
        $actorSql = $actorId === null ? 'NULL' : (string)(int)$actorId;
        $db->query(
            "UPDATE xlvask_automation_suggestions
             SET status = '{$status}', decided_by = {$actorSql}, decided_at = NOW()
             WHERE id = {$suggestionId}"
        );
    }

    private function updateSuggestionExecution(int $suggestionId, string $status, ?int $actorId, ?int $matchedOrderId, ?int $createdOrderId): void
    {
        global $db;
        $status = $db->escape_string($status);
        $actorSql = $actorId === null ? 'NULL' : (string)(int)$actorId;
        $matchedSql = $matchedOrderId === null ? 'matched_order_id' : (string)(int)$matchedOrderId;
        $createdSql = $createdOrderId === null ? 'created_order_id' : (string)(int)$createdOrderId;
        $db->query(
            "UPDATE xlvask_automation_suggestions
             SET status = '{$status}',
                 decided_by = {$actorSql},
                 decided_at = NOW(),
                 executed_at = NOW(),
                 matched_order_id = {$matchedSql},
                 created_order_id = {$createdSql}
             WHERE id = {$suggestionId}"
        );
    }

    private function updateSuggestionFailure(int $suggestionId, string $message, ?int $actorId): void
    {
        global $db;
        $actorSql = $actorId === null ? 'NULL' : (string)(int)$actorId;
        $message = $db->escape_string($message);
        $db->query(
            "UPDATE xlvask_automation_suggestions
             SET status = '" . self::STATUS_FAILED . "',
                 reason = CONCAT(COALESCE(reason, ''), ' Fejl: {$message}'),
                 decided_by = {$actorSql},
                 decided_at = NOW()
             WHERE id = {$suggestionId}"
        );
    }

    private function loadUsageLogRow(int $usageLogId): ?array
    {
        global $db;
        (new xlvask_usage_logs_o())->structure();
        $result = $db->query("SELECT * FROM xlvask_usage_logs WHERE id = {$usageLogId} LIMIT 1");
        if ($result === false || $result->num_rows < 1) {
            return null;
        }

        return $db->fetch_assoc($result);
    }

    private function loadUsageLogRowsByIds(array $ids): array
    {
        global $db;
        $ids = array_values(array_filter(array_map('intval', $ids), fn(int $id): bool => $id > 0));
        if ($ids === []) {
            return [];
        }

        (new xlvask_usage_logs_o())->structure();
        $result = $db->query('SELECT * FROM xlvask_usage_logs WHERE id IN (' . implode(',', $ids) . ')');
        return $db->fetch_all($result);
    }

    private function loadPendingRows(?string $dateFrom, ?string $dateTo, int $limit): array
    {
        global $db;
        (new xlvask_usage_logs_o())->structure();
        $startTimeExpression = "STR_TO_DATE(REPLACE(SUBSTRING(StartTime, 1, 19), 'T', ' '), '%Y-%m-%d %H:%i:%s')";
        $where = [
            'FinishStatus = 1',
            '(ignored_at IS NULL OR ignored_at = "")',
        ];

        if ($dateFrom !== null && strtotime($dateFrom) !== false) {
            $where[] = "{$startTimeExpression} >= '" . $db->escape_string(date('Y-m-d 00:00:00', strtotime($dateFrom))) . "'";
        } else {
            $where[] = "{$startTimeExpression} >= '" . $db->escape_string(date('Y-m-d H:i:s', strtotime('-7 days'))) . "'";
        }

        if ($dateTo !== null && strtotime($dateTo) !== false) {
            $where[] = "{$startTimeExpression} <= '" . $db->escape_string(date('Y-m-d 23:59:59', strtotime($dateTo))) . "'";
        }

        $limit = max(1, min(500, $limit));
        $result = $db->query('SELECT * FROM xlvask_usage_logs WHERE ' . implode(' AND ', $where) . " ORDER BY StartTime DESC LIMIT {$limit}");
        return $db->fetch_all($result);
    }

    private function usageLogFromRow(array $row): xlvask_usage_log
    {
        $row = self::normalizeUsageLogRowForAutomation($row);

        $xlvask = new xlvask();
        return $xlvask->new($xlvask->helpers->xlvask_usage_log)->setProperties($row);
    }

    private function formatSuggestion(array $row): array
    {
        $status = (string)($row['status'] ?? self::STATUS_NONE);
        return [
            'id' => isset($row['id']) ? (int)$row['id'] : null,
            'status' => $status,
            'action' => (string)($row['action'] ?? self::ACTION_NONE),
            'confidence' => isset($row['confidence']) ? (float)$row['confidence'] : 0.0,
            'source' => (string)($row['source'] ?? ''),
            'reason' => (string)($row['reason'] ?? ''),
            'matched_order_id' => isset($row['matched_order_id']) && $row['matched_order_id'] !== null ? (int)$row['matched_order_id'] : null,
            'created_order_id' => isset($row['created_order_id']) && $row['created_order_id'] !== null ? (int)$row['created_order_id'] : null,
            'candidate_order' => $this->decodeJsonField($row['candidate_order_json'] ?? null),
            'proposed_order' => $this->decodeJsonField($row['proposed_order_json'] ?? null),
            'can_accept' => $status === self::STATUS_SUGGESTED,
            'can_deny' => $status === self::STATUS_SUGGESTED,
        ];
    }

    private function emptyAutomation(string $reason = ''): array
    {
        return [
            'id' => null,
            'status' => self::STATUS_NONE,
            'action' => self::ACTION_NONE,
            'confidence' => 0.0,
            'source' => '',
            'reason' => $reason,
            'matched_order_id' => null,
            'created_order_id' => null,
            'candidate_order' => null,
            'proposed_order' => null,
            'can_accept' => false,
            'can_deny' => false,
        ];
    }

    private function decodeJsonField(?string $value): mixed
    {
        if ($value === null || $value === '') {
            return null;
        }

        $decoded = json_decode($value, true);
        return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
    }

    private function itemSignatureParts(array $items): array
    {
        return self::itemSignaturePartsForAutomation($items);
    }

    private function compactItems(array $items): array
    {
        return array_map(fn(array $item): array => [
            'product_id' => (int)($item['product_id'] ?? 0),
            'product_name' => (string)($item['product']['name'] ?? $item['product_name'] ?? ''),
            'quantity' => (int)($item['quantity'] ?? 0),
            'price' => (int)($item['price'] ?? 0),
        ], $items);
    }

    private function itemsTotal(array $items): int
    {
        return array_reduce($items, fn(int $total, array $item): int => $total + ((int)($item['price'] ?? 0) * (int)($item['quantity'] ?? 0)), 0);
    }

    private function productOverlap(array $usageItems, array $orderItems): float
    {
        $usageBag = $this->productBag($usageItems);
        $orderBag = $this->productBag($orderItems);
        $usageTotal = array_sum($usageBag);
        if ($usageTotal <= 0) {
            return 0.0;
        }

        $overlap = 0;
        foreach ($usageBag as $productId => $quantity) {
            $overlap += min($quantity, $orderBag[$productId] ?? 0);
        }

        return $overlap / $usageTotal;
    }

    private function productBag(array $items): array
    {
        $bag = [];
        foreach ($items as $item) {
            $productId = (int)($item['product_id'] ?? 0);
            if ($productId < 1) {
                continue;
            }
            $bag[$productId] = ($bag[$productId] ?? 0) + max(1, (int)($item['quantity'] ?? 1));
        }

        return $bag;
    }

    private function normalizeRegistration(string $registration): string
    {
        return self::normalizeRegistrationForAutomation($registration);
    }

    private function isOpenAiEnabled(): bool
    {
        try {
            $xlvask = new xlvask();
            if (!$xlvask->config->openai_integration_enabled->isTrue()) {
                return false;
            }

            $openai = new openai();
            return $openai->config->enabled->isTrue();
        } catch (Exception) {
            return false;
        }
    }
}
