<?php

namespace routes;

use classes\authentication;
use classes\economic;
use classes\economic_transfer_queue;
use classes\economic_v2_distribution_service;
use classes\economic_v2_versioning_service;
use classes\invoice_period_flag_service;
use classes\invoicing_period_utils;
use classes\slack;
use Exception;
use objects\collected_order_invoices_o;
use objects\customer_vehicles_o;
use objects\logs_o;
use objects\order_items_o;
use objects\orders_o;
use objects\products_o;
use objects\users_o;
use traits\route_t;

class InvoicingPeriodRoute
{
    use route_t;

    /**
     * Cache department metadata to avoid repeated object loads in large loops.
     * @var array<int, string>
     */
    private static array $departmentNameCache = [];

    /**
     * Cache whether a department is excluded from invoicing.
     * @var array<int, bool>
     */
    private static array $departmentExcludedFromInvoicingCache = [];

    private static ?bool $collectedOrderInvoicesHasDeletedAtColumn = null;

    /**
     * Local-only booked status caches used by the period response.
     * The period endpoint must not call e-conomic for each order.
     * @var array<int, bool>
     */
    private static array $periodOrderBookedCache = [];
    private static array $periodInvoiceCollectionBookedCache = [];

    /**
     * @throws Exception
     */
    private static function getDepartmentNameCached(int $departmentId): string
    {
        if (!isset(self::$departmentNameCache[$departmentId])) {
            $departmentName = (new \objects\departments_o())->select($departmentId)->name->value();
            self::$departmentNameCache[$departmentId] = !empty($departmentName) ? $departmentName : 'Unknown Department (' . $departmentId . ')';
        }
        return self::$departmentNameCache[$departmentId];
    }

    /**
     * @throws Exception
     */
    private static function isDepartmentExcludedFromInvoicingCached(int $departmentId): bool
    {
        if (!array_key_exists($departmentId, self::$departmentExcludedFromInvoicingCache)) {
            self::$departmentExcludedFromInvoicingCache[$departmentId] = (new \objects\departments_o())
                ->select($departmentId)
                ->isExcludedFromInvoicing();
        }
        return self::$departmentExcludedFromInvoicingCache[$departmentId];
    }

    private static function getLocalCustomerName(int $customerNumber): string
    {
        $names = (new users_o())->getCustomerNames([$customerNumber], false);
        return (string)($names[$customerNumber] ?? 'Unknown Customer');
    }

    private static function getEconomicFallbackDepartmentId(): int
    {
        $department_id = (new economic())->getDefaultDistributionDepartmentId();
        return $department_id > 0 ? $department_id : economic::DEFAULT_DISTRIBUTION_DEPARTMENT_ID;
    }

    /**
     * Slack summaries are expensive on request latency, so they are opt-in.
     * Enable with query param `sendSlackSummary=1` or env `INVOICING_PERIOD_SEND_SLACK_SUMMARY=true`.
     */
    private static function shouldSendSlackSummary(): bool
    {
        $requestOverride = $_GET['sendSlackSummary'] ?? null;
        if ($requestOverride !== null) {
            return in_array(strtolower((string)$requestOverride), ['1', 'true', 'yes'], true);
        }

        $envFlag = getenv('INVOICING_PERIOD_SEND_SLACK_SUMMARY');
        if ($envFlag === false) {
            return false;
        }

        return in_array(strtolower((string)$envFlag), ['1', 'true', 'yes'], true);
    }

    /**
     * @return array{dateFrom:string,dateTo:string}
     */
    private function requireAndNormalizeDateRange(): array
    {
        global $response;

        self::requireParameters([
            'dateFrom',
            'dateTo',
        ]);

        $dateFrom = (string)$this->getParameter('dateFrom');
        $dateTo = (string)$this->getParameter('dateTo');

        try {
            return invoicing_period_utils::normalizeDateRange($dateFrom, $dateTo);
        } catch (\InvalidArgumentException $e) {
            $response->error($e->getMessage(), 400);
        }
    }

    /**
     * @return int[]|null
     */
    private function getOptionalCustomerNumbersParameter(): ?array
    {
        if (!self::isParametersSet(['customerNumbers'])) {
            return null;
        }

        return self::normalizeCustomerNumbers(self::getParameter('customerNumbers'));
    }

    /**
     * @return int[]
     */
    private static function normalizeCustomerNumbers(mixed $customerNumbers): array
    {
        if ($customerNumbers === null || $customerNumbers === '') {
            return [];
        }

        $rawValues = is_array($customerNumbers)
            ? $customerNumbers
            : explode(',', (string)$customerNumbers);

        $normalized = [];
        foreach ($rawValues as $value) {
            $parsed = (int)trim((string)$value);
            if ($parsed < 1) {
                continue;
            }
            $normalized[$parsed] = $parsed;
        }

        return array_values($normalized);
    }

    /**
     * @param int[]|null $onlyCustomerNumbers
     * @return int[]
     */
    private static function filterCustomerNumbers(array $customerNumbers, ?array $onlyCustomerNumbers = null): array
    {
        $customerNumbers = self::normalizeCustomerNumbers($customerNumbers);
        if ($onlyCustomerNumbers === null) {
            return $customerNumbers;
        }

        $allowed = array_fill_keys(self::normalizeCustomerNumbers($onlyCustomerNumbers), true);
        if (empty($allowed)) {
            return [];
        }

        return array_values(array_filter($customerNumbers, static function (int $customerNumber) use ($allowed): bool {
            return isset($allowed[$customerNumber]);
        }));
    }

    /**
     * @param array<int,array<string,mixed>> $customers
     * @return array<int,array<string,mixed>>
     */
    private static function indexCustomersByNumber(array $customers): array
    {
        $customersByNumber = [];
        foreach ($customers as $customer) {
            $customerNumber = (int)($customer['customer_number'] ?? 0);
            if ($customerNumber > 0) {
                $customersByNumber[$customerNumber] = $customer;
            }
        }
        return $customersByNumber;
    }

    /**
     * Response cache TTL (seconds) for v2 distribution endpoints.
     * Set `INVOICING_PERIOD_DISTRIBUTION_V2_CACHE_TTL` to override.
     */
    private function getDistributionV2CacheTtl(): int
    {
        $raw = getenv('INVOICING_PERIOD_DISTRIBUTION_V2_CACHE_TTL');
        if ($raw === false || trim((string)$raw) === '') {
            return 300;
        }

        return max(0, (int)$raw);
    }

    private function getDistributionV2CacheKey(string $scope, string $dateFrom, string $dateTo): string
    {
        return 'invoicing_period:distribution:v2:' . $scope . ':' . md5($dateFrom . '|' . $dateTo);
    }

    /**
     * Best-effort Redis cache wrapper for v2 distribution payloads.
     * Falls back to direct computation when Redis is unavailable or TTL is disabled.
     *
     * @param callable():array $resolver
     * @return array
     */
    private function withCachedDistributionV2(string $scope, string $dateFrom, string $dateTo, callable $resolver): array
    {
        $cacheTtl = $this->getDistributionV2CacheTtl();
        if ($cacheTtl <= 0 || !defined('redis')) {
            return (array)$resolver();
        }

        $cacheKey = $this->getDistributionV2CacheKey($scope, $dateFrom, $dateTo);

        try {
            $cached = redis->get($cacheKey);
            if (is_string($cached) && $cached !== '') {
                $decoded = json_decode($cached, true);
                if (is_array($decoded)) {
                    return $decoded;
                }
            }
        } catch (\Throwable $e) {
            // Best-effort cache read.
        }

        $result = (array)$resolver();

        try {
            $encoded = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if (is_string($encoded)) {
                redis->setEx($cacheKey, $encoded, $cacheTtl);
            }
        } catch (\Throwable $e) {
            // Best-effort cache write.
        }

        return $result;
    }


    /**
     * @param array $collective_results
     * @return array
     * @throws Exception
     */
    private static function parseTheDepartmentIdsToDepartmentNames(array $collective_results): array
    {
        foreach ( $collective_results['total_department_totals'] as $department_id => $amount ) {
            $collective_results['total_department_totals_parsed'][self::getDepartmentNameCached((int)$department_id)] = $amount;
        }
        foreach ( $collective_results['total_department_totals_relative'] as $department_id => $amount ) {
            $collective_results['total_department_totals_relative_parsed'][self::getDepartmentNameCached((int)$department_id)] = $amount;
        }
        return $collective_results;
    }

    private static function jsonFragment(mixed $value): string
    {
        $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return is_string($json) ? $json : 'null';
    }

    private static function streamInvoicingPeriodResponse(array $period): void
    {
        global $response;

        header('Content-Type: application/json; charset=utf-8');
        http_response_code(200);

        echo '{"success":true,"data":{';
        echo '"dateFrom":' . self::jsonFragment($period['dateFrom'] ?? null);
        echo ',"dateTo":' . self::jsonFragment($period['dateTo'] ?? null);
        echo ',"types":{';

        $types = is_array($period['types'] ?? null) ? $period['types'] : [];
        $firstType = true;
        foreach ($types as $typeName => $customers) {
            if (!$firstType) {
                echo ',';
            }
            $firstType = false;
            echo self::jsonFragment((string)$typeName) . ':[';

            $firstCustomer = true;
            foreach ((array)$customers as $customer) {
                if (!$firstCustomer) {
                    echo ',';
                }
                $firstCustomer = false;
                echo self::jsonFragment($customer);
            }
            echo ']';
        }

        echo '}';
        foreach ($period as $key => $value) {
            if (in_array((string)$key, ['dateFrom', 'dateTo', 'types'], true)) {
                continue;
            }
            echo ',' . self::jsonFragment((string)$key) . ':' . self::jsonFragment($value);
        }
        echo '}';
        echo ',"meta":' . self::jsonFragment($response->get_meta());
        echo ',"includes":' . self::jsonFragment($response->get_includes());
        echo '}';
        exit;
    }

    private static function periodTypeNames(): array
    {
        return [
            'all',
            'vehicle_subscriptions',
            'fixed_pricing',
            'tank_cleaning',
            'special_arrangements',
            'invoice_per_order',
            'possible_duplicates',
        ];
    }

    private static function getPeriodPaginationOptionsFromRequest(): ?array
    {
        global $response;

        $paginationKeys = [
            'periodView',
            'page',
            'limit',
            'search',
            'includeRequiresAction',
            'includeBooked',
        ];

        $isPaginatedRequest = false;
        foreach ($paginationKeys as $key) {
            if ($response->isRequestParameterSet($key)) {
                $isPaginatedRequest = true;
                break;
            }
        }

        if (!$isPaginatedRequest) {
            return null;
        }

        return self::normalizePeriodPaginationOptions($response->getAllRequestParameters());
    }

    private static function normalizePeriodPaginationOptions(array $parameters): array
    {
        $allowedViews = array_fill_keys(self::periodTypeNames(), true);
        $periodView = trim((string)($parameters['periodView'] ?? 'all'));
        if ($periodView === '' || !isset($allowedViews[$periodView])) {
            $periodView = 'all';
        }

        $page = (int)($parameters['page'] ?? 1);
        if ($page < 1) {
            $page = 1;
        }

        $limitParameter = strtolower(trim((string)($parameters['limit'] ?? '100')));
        if ($limitParameter === 'all') {
            $limit = 'all';
        } else {
            $limit = (int)$limitParameter;
            if ($limit < 1) {
                $limit = 100;
            }
            $limit = min(500, $limit);
        }

        $allowedFlagTabs = ['all' => true, 'red' => true, 'yellow' => true, 'none' => true, 'filters' => true];
        $flagTab = trim((string)($parameters['flagTab'] ?? 'all'));
        if ($flagTab === '' || !isset($allowedFlagTabs[$flagTab])) {
            $flagTab = 'all';
        }

        return [
            'periodView' => $periodView,
            'page' => $page,
            'limit' => $limit,
            'search' => trim((string)($parameters['search'] ?? '')),
            'flagTab' => $flagTab,
            'includeRequiresAction' => self::parsePeriodBooleanOption(
                $parameters['includeRequiresAction'] ?? null,
                true
            ),
            'includeBooked' => self::parsePeriodBooleanOption($parameters['includeBooked'] ?? null, true),
        ];
    }

    private static function parsePeriodBooleanOption(mixed $value, bool $default): bool
    {
        if ($value === null || $value === '') {
            return $default;
        }

        if (is_bool($value)) {
            return $value;
        }

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

        return $default;
    }

    private static function applyPeriodPagination(array $period, array $options): array
    {
        $types = is_array($period['types'] ?? null) ? $period['types'] : [];
        $types = self::ensurePeriodTypeKeys($types);
        $types = self::enrichPeriodCustomerMetaFromTypes($types);
        $types = self::filterPeriodTypesBySearch($types, (string)($options['search'] ?? ''));
        $types = self::filterPeriodTypesByVisibility(
            $types,
            (bool)($options['includeRequiresAction'] ?? true),
            (bool)($options['includeBooked'] ?? true)
        );

        $periodView = (string)($options['periodView'] ?? 'all');
        if (!array_key_exists($periodView, $types)) {
            $periodView = 'all';
        }

        $typeCounts = self::summarizePeriodTypes($types);

        if (!empty($options['flagTab'])) {
            $types = self::filterPeriodTypesByFlagTab($types, (string)$options['flagTab']);
        }

        $total = count($types[$periodView] ?? []);
        $limit = $options['limit'] ?? 100;
        $isAllLimit = $limit === 'all';
        $perPage = $isAllLimit ? 'all' : max(1, min(500, (int)$limit));
        $totalPages = $isAllLimit || $total === 0 ? 1 : (int)ceil($total / $perPage);
        $page = $isAllLimit ? 1 : max(1, (int)($options['page'] ?? 1));
        $page = min($page, $totalPages);

        $pagedTypes = array_fill_keys(array_keys($types), []);
        if ($isAllLimit) {
            $pagedTypes[$periodView] = array_values($types[$periodView] ?? []);
        } else {
            $offset = ($page - 1) * $perPage;
            $pagedTypes[$periodView] = array_slice($types[$periodView], $offset, $perPage);
        }

        $period['types'] = $pagedTypes;
        $period['type_counts'] = $typeCounts;
        $period['type_totals'] = self::summarizePeriodTypeTotals($types);

        return [
            'period' => $period,
            'pagination' => [
                'page' => $page,
                'per_page' => $perPage,
                'total' => $total,
                'total_pages' => $totalPages,
                'search' => (string)($options['search'] ?? ''),
                'filters' => [
                    'includeRequiresAction' => (bool)($options['includeRequiresAction'] ?? true),
                    'includeBooked' => (bool)($options['includeBooked'] ?? true),
                ],
                'order' => [
                    'field' => 'customer_name',
                    'direction' => 'asc',
                ],
            ],
        ];
    }

    private static function filterPeriodTypesByFlagTab(array $types, string $flagTab): array
    {
        if (in_array($flagTab, ['all', 'filters', ''], true)) {
            return $types;
        }

        foreach ($types as $viewName => $entries) {
            $types[$viewName] = array_values(array_filter(
                is_array($entries) ? $entries : [],
                static function (array $customer) use ($flagTab): bool {
                    $tab = 'none';
                    $flagCounts = self::getActivePeriodFlagCounts($customer);
                    if ($flagCounts['manual'] > 0) {
                        $tab = 'red';
                    } elseif ($flagCounts['automatic'] > 0) {
                        $tab = 'yellow';
                    }

                    return $tab === $flagTab;
                }
            ));
        }

        return $types;
    }

    private static function ensurePeriodTypeKeys(array $types): array
    {
        foreach (self::periodTypeNames() as $typeName) {
            if (!array_key_exists($typeName, $types) || !is_array($types[$typeName])) {
                $types[$typeName] = [];
            }
        }

        return $types;
    }

    private static function enrichPeriodCustomerMetaFromTypes(array $types): array
    {
        $metaByCustomerNumber = [];
        foreach (['fixed_pricing', 'vehicle_subscriptions'] as $typeName) {
            foreach (($types[$typeName] ?? []) as $customer) {
                $customerNumber = (int)($customer['customer_number'] ?? 0);
                if ($customerNumber < 1) {
                    continue;
                }
                $meta = is_array($customer['meta'] ?? null) ? $customer['meta'] : [];
                if ($meta === []) {
                    continue;
                }
                $metaByCustomerNumber[$customerNumber] = array_merge(
                    $metaByCustomerNumber[$customerNumber] ?? [],
                    $meta
                );
            }
        }

        if ($metaByCustomerNumber === []) {
            return $types;
        }

        foreach ($types as $typeName => $customers) {
            foreach ($customers as $index => $customer) {
                $customerNumber = (int)($customer['customer_number'] ?? 0);
                if ($customerNumber < 1 || !isset($metaByCustomerNumber[$customerNumber])) {
                    continue;
                }

                $types[$typeName][$index]['meta'] = array_merge(
                    is_array($customer['meta'] ?? null) ? $customer['meta'] : [],
                    $metaByCustomerNumber[$customerNumber]
                );
            }
        }

        return $types;
    }

    private static function filterPeriodTypesBySearch(array $types, string $search): array
    {
        $search = self::normalizePeriodSearchTerm($search);
        if ($search === '') {
            return $types;
        }

        foreach ($types as $typeName => $customers) {
            $types[$typeName] = array_values(array_filter(
                is_array($customers) ? $customers : [],
                static fn(array $customer): bool => self::periodCustomerMatchesSearch($customer, $search)
            ));
        }

        return $types;
    }

    private static function filterPeriodTypesByVisibility(
        array $types,
        bool $includeRequiresAction,
        bool $includeBooked
    ): array {
        foreach ($types as $typeName => $customers) {
            $types[$typeName] = array_values(array_filter(
                is_array($customers) ? $customers : [],
                static function (array $customer) use ($includeRequiresAction, $includeBooked): bool {
                    if (!$includeRequiresAction && (bool)($customer['requires_action'] ?? false)) {
                        return false;
                    }

                    if (
                        !$includeBooked
                        && !((bool)($customer['requires_action'] ?? false))
                        && self::areAllPeriodCustomerTransactionsBooked($customer)
                    ) {
                        return false;
                    }

                    return true;
                }
            ));
        }

        return $types;
    }

    private static function periodCustomerMatchesSearch(array $customer, string $search): bool
    {
        $values = [
            $customer['customer_number'] ?? '',
            $customer['customer_name'] ?? '',
        ];

        foreach (($customer['transactions'] ?? []) as $transaction) {
            if (!is_array($transaction)) {
                continue;
            }
            foreach (['id', 'reference', 'po', 'notes', 'reg_1', 'reg_2', 'reg_3'] as $field) {
                $values[] = $transaction[$field] ?? '';
            }
        }

        foreach ($values as $value) {
            if (str_contains(self::normalizePeriodSearchTerm((string)$value), $search)) {
                return true;
            }
        }

        return false;
    }

    private static function normalizePeriodSearchTerm(string $value): string
    {
        return mb_strtolower(trim($value), 'UTF-8');
    }

    private static function areAllPeriodCustomerTransactionsBooked(array $customer): bool
    {
        $transactions = is_array($customer['transactions'] ?? null) ? $customer['transactions'] : [];
        foreach ($transactions as $transaction) {
            if (!is_array($transaction) || (bool)($transaction['booked'] ?? false) !== true) {
                return false;
            }
        }

        return true;
    }

    private static function summarizePeriodTypes(array $types): array
    {
        $counts = [];
        foreach ($types as $typeName => $customers) {
            $counts[$typeName] = self::summarizePeriodType(is_array($customers) ? $customers : []);
        }

        return $counts;
    }

    private static function summarizePeriodTypeTotals(array $types): array
    {
        $totals = [];
        foreach ($types as $typeName => $customers) {
            $totals[$typeName] = self::summarizePeriodTypeTotalsForCustomers(
                is_array($customers) ? $customers : []
            );
        }

        return $totals;
    }

    private static function summarizePeriodTypeTotalsForCustomers(array $customers): array
    {
        $total = 0.0;
        $booked = 0.0;

        foreach ($customers as $customer) {
            if (!is_array($customer)) {
                continue;
            }

            $total += self::getPeriodCustomerTotalAmount($customer);
            $booked += self::sumPeriodCustomerTransactions($customer, true);
        }

        return [
            'total' => $total,
            'booked' => $booked,
            'not_booked' => $total - $booked,
        ];
    }

    private static function getPeriodCustomerTotalAmount(array $customer): float
    {
        $fixedPrice = $customer['meta']['fixed_pricing']['price'] ?? null;
        if ($fixedPrice !== null && $fixedPrice !== '') {
            return (float)$fixedPrice;
        }

        return self::sumPeriodCustomerTransactions($customer, false);
    }

    private static function sumPeriodCustomerTransactions(array $customer, bool $bookedOnly): float
    {
        $total = 0.0;
        $transactions = is_array($customer['transactions'] ?? null) ? $customer['transactions'] : [];
        foreach ($transactions as $transaction) {
            if (!is_array($transaction)) {
                continue;
            }
            if ((bool)($transaction['excluded'] ?? false)) {
                continue;
            }
            if ($bookedOnly && (bool)($transaction['booked'] ?? false) !== true) {
                continue;
            }

            $total += (float)($transaction['amount'] ?? $transaction['net_amount'] ?? 0);
        }

        return $total;
    }

    private static function summarizePeriodType(array $customers): array
    {
        $requiresAction = 0;
        $draft = 0;
        $manualFlags = 0;
        $automaticFlags = 0;

        foreach ($customers as $customer) {
            if ((bool)($customer['requires_action'] ?? false)) {
                $requiresAction++;
            }
            if (($customer['draft']['is_action_blocked'] ?? false) === true) {
                $draft++;
            }

            $flagCounts = self::getActivePeriodFlagCounts($customer);
            if ($flagCounts['manual'] > 0) {
                $manualFlags++;
            }
            if ($flagCounts['automatic'] > 0) {
                $automaticFlags++;
            }
        }

        return [
            'requires_action' => $requiresAction,
            'draft' => $draft,
            'manual_flags' => $manualFlags,
            'automatic_flags' => $automaticFlags,
            'completed' => max(0, count($customers) - $requiresAction - $draft),
            'total' => count($customers),
        ];
    }

    private static function getActivePeriodFlagCounts(array $customer): array
    {
        $manual = 0;
        $automatic = 0;
        if (is_array($customer['flags'] ?? null)) {
            foreach ($customer['flags'] as $flag) {
                if (!is_array($flag) || (string)($flag['status'] ?? 'active') !== 'active') {
                    continue;
                }
                if (($flag['source'] ?? null) === 'manual') {
                    $manual++;
                } elseif (($flag['source'] ?? null) === 'automatic') {
                    $automatic++;
                }
            }

            return [
                'manual' => $manual,
                'automatic' => $automatic,
                'total' => $manual + $automatic,
            ];
        }

        return [
            'manual' => (int)($customer['flag_counts']['manual'] ?? 0),
            'automatic' => (int)($customer['flag_counts']['automatic'] ?? 0),
            'total' => (int)($customer['flag_counts']['total'] ?? 0),
        ];
    }

    public function run(): void
    {
        $this->get('/superuser/invoicing/period', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the request was successful
            if ($user) {
                $dateRange = $this->requireAndNormalizeDateRange();
                $dateFrom = $dateRange['dateFrom'];
                $dateTo = $dateRange['dateTo'];
                $customerNumbers = $this->getOptionalCustomerNumbersParameter();
                // Add date from and date to to the response meta
                $response->add_meta('date_from', $dateFrom);
                $response->add_meta('date_to', $dateTo);
                if ($customerNumbers !== null) {
                    $response->add_meta('customer_numbers', $customerNumbers);
                }
                $paginationOptions = self::getPeriodPaginationOptionsFromRequest();
                $includeInvoicePeriodFlags = $this->hasPermission('list_invoice_period_flags');
                // Get the invoicing period for the user
                $period = self::getInvoicingPeriod($dateFrom, $dateTo, $customerNumbers, $includeInvoicePeriodFlags);
                if ($paginationOptions !== null) {
                    $paginated = self::applyPeriodPagination($period, $paginationOptions);
                    $period = $paginated['period'];
                    $response->add_meta('pagination', $paginated['pagination']);
                }
                self::streamInvoicingPeriodResponse($period);
            } else {
                // Log the incident
                (new logs_o())->add('vehicles', 'global', 1, 0, 'LIST_OWN_VEHICLES', 'No user found, or invalid session');
                // Return an error
                $response->error('Invalid session', 400);
            }
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
                'list_invoice_period_flags' => 'List invoice period flags in the period response',
            ]
        );

        $this->post('/superuser/invoicing/period/flags', function () {
            global $response;
            $this->requirePermission('add_invoice_period_flag');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }

            try {
                $flag = (new invoice_period_flag_service())->createManualFlag(
                    $this->getParametersAsArray(),
                    (int)$user->id
                );
                $response->success($flag);
            } catch (\InvalidArgumentException $e) {
                $response->error($e->getMessage(), 400);
            } catch (\Throwable $e) {
                $response->error($e->getMessage(), 500);
            }
        },
            [
                'add_invoice_period_flag' => 'Add a manual invoice period flag',
            ]
        );

        $this->patch('/superuser/invoicing/period/flags/{id}/status', function () {
            global $response;
            $this->requirePermission('update_invoice_period_flag_status');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }

            $id = (int)($this->fromRoute('id') ?? 0);
            try {
                $flag = (new invoice_period_flag_service())->updateManualFlagStatus(
                    $id,
                    (string)$this->getParameter('status'),
                    $this->isParametersSet(['reason']) ? (string)$this->getParameter('reason') : null,
                    (int)$user->id
                );
                $response->success($flag);
            } catch (\InvalidArgumentException $e) {
                $response->error($e->getMessage(), 400);
            } catch (\Throwable $e) {
                $response->error($e->getMessage(), 500);
            }
        },
            [
                'update_invoice_period_flag_status' => 'Update a manual invoice period flag status',
            ]
        );

        $this->post('/superuser/invoicing/period/flags/automatic/status', function () {
            global $response;
            $this->requirePermission('update_invoice_period_flag_status');
            $user = (new authentication())->get_user();
            if (!$user) {
                $response->error('Invalid session', 400);
            }

            try {
                $flag = (new invoice_period_flag_service())->updateAutomaticFlagStatus(
                    $this->getParametersAsArray(),
                    (int)$user->id
                );
                $response->success($flag);
            } catch (\InvalidArgumentException $e) {
                $response->error($e->getMessage(), 400);
            } catch (\Throwable $e) {
                $response->error($e->getMessage(), 500);
            }
        },
            [
                'update_invoice_period_flag_status' => 'Update an automatic invoice period flag status',
            ]
        );


        $this->get('/superuser/invoicing/period/distribution/all', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            // Add date from and date to to the response meta
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);
            $result = [
                'subscriptions' => self::getTransactionsWithItemsNotIncludedInInvoices(self::getVehicleSubscriptions($dateFrom, $dateTo)),
                'fixed_pricing' => self::getOriginalPrice(self::getFixedPricing($dateFrom, $dateTo)),

            ];
            $response->success($result);
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/fixed-pricing', function () {
            // Require the user to be logged in
            global $response;
           $this->requirePermission('superuser_invoicing_period');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            // Add date from and date to to the response meta
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);
            $response->success([...self::getOriginalPrice(self::getFixedPricing($dateFrom, $dateTo))]);
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/wash-subscriptions', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            // Add date from and date to to the response meta
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);
            $response->success([...self::getTransactionsWithItemsNotIncludedInInvoices(self::getVehicleSubscriptions($dateFrom, $dateTo))]);
        },
            [
                'superuser_invoicing_period' => 'Get the invoicing period for superusers',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/v2/all', function () {
            global $response;
            $this->requirePermission('superuser_invoicing_period_distribution_v2');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);

            $response->success($this->withCachedDistributionV2('all', $dateFrom, $dateTo, static function () use ($dateFrom, $dateTo) {
                return (new economic_v2_distribution_service())->getAllDistributions($dateFrom, $dateTo);
            }));
        },
            [
                'superuser_invoicing_period_distribution_v2' => 'Get historical version-aware departmental distribution (all categories).',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/v2/fixed-pricing', function () {
            global $response;
            $this->requirePermission('superuser_invoicing_period_distribution_v2');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);

            $response->success($this->withCachedDistributionV2('fixed-pricing', $dateFrom, $dateTo, static function () use ($dateFrom, $dateTo) {
                return (new economic_v2_distribution_service())->getFixedPricingDistribution($dateFrom, $dateTo);
            }));
        },
            [
                'superuser_invoicing_period_distribution_v2' => 'Get historical version-aware fixed pricing distribution.',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/v2/wash-subscriptions', function () {
            global $response;
            $this->requirePermission('superuser_invoicing_period_distribution_v2');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);

            $response->success($this->withCachedDistributionV2('wash-subscriptions', $dateFrom, $dateTo, static function () use ($dateFrom, $dateTo) {
                return (new economic_v2_distribution_service())->getWashSubscriptionsDistribution($dateFrom, $dateTo);
            }));
        },
            [
                'superuser_invoicing_period_distribution_v2' => 'Get historical version-aware wash subscription distribution.',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/v2/customer-prices', function () {
            global $response;
            $this->requirePermission('superuser_invoicing_period_distribution_v2');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);

            $response->success($this->withCachedDistributionV2('customer-prices', $dateFrom, $dateTo, static function () use ($dateFrom, $dateTo) {
                return (new economic_v2_distribution_service())->getCustomerPricesDistribution($dateFrom, $dateTo);
            }));
        },
            [
                'superuser_invoicing_period_distribution_v2' => 'Get historical version-aware customer discount distribution.',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/v2/booked-department-75', function () {
            global $response;
            $this->requirePermission('superuser_invoicing_period_distribution_v2');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);

            $response->success($this->withCachedDistributionV2('booked-department-75', $dateFrom, $dateTo, static function () use ($dateFrom, $dateTo) {
                return (new economic_v2_distribution_service())->getBookedDepartment75Distribution($dateFrom, $dateTo);
            }));
        },
            [
                'superuser_invoicing_period_distribution_v2' => 'Get booked e-conomic department 75 redistribution based on actual booked net amounts.',
            ]
        );

        $this->get('/superuser/customers/pricing-history', function () {
            global $response;
            $this->requirePermission('superuser_customer_pricing_history_v2');
            self::requireParameters(['customer_number']);
            self::requireType((int)self::getParameter('customer_number'), self::type_int());
            $customer_number = (int)self::getParameter('customer_number');
            self::requireMinValue($customer_number, 1);
            self::requireMaxValue($customer_number, 999999999);

            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];

            $versioning = new economic_v2_versioning_service();
            $fixed_pricing = $versioning->listFixedPricingVersions($customer_number, $dateFrom, $dateTo);
            $vehicle_subscriptions = $versioning->listVehicleSubscriptionVersions($customer_number, $dateFrom, $dateTo);
            $discount_overrides = $versioning->listDiscountOverrideVersions($customer_number, $dateFrom, $dateTo);

            $timeline = [];
            foreach ($fixed_pricing as $row) {
                $timeline[] = [
                    'type' => 'fixed_pricing',
                    ...$row,
                ];
            }
            foreach ($vehicle_subscriptions as $row) {
                $timeline[] = [
                    'type' => 'vehicle_subscription',
                    ...$row,
                ];
            }
            foreach ($discount_overrides as $row) {
                $timeline[] = [
                    'type' => 'discount_override',
                    ...$row,
                ];
            }
            usort($timeline, static function ($a, $b) {
                $left = strtotime((string)($a['effective_from'] ?? '1970-01-01 00:00:00'));
                $right = strtotime((string)($b['effective_from'] ?? '1970-01-01 00:00:00'));
                if ($left === $right) {
                    return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
                }
                return $left <=> $right;
            });

            $response->add_meta('date_from', $dateFrom);
            $response->add_meta('date_to', $dateTo);
            $response->success([
                'customer_number' => $customer_number,
                'fixed_pricing' => $fixed_pricing,
                'vehicle_subscriptions' => $vehicle_subscriptions,
                'discount_overrides' => $discount_overrides,
                'timeline' => $timeline,
            ]);
        },
            [
                'superuser_customer_pricing_history_v2' => 'Get customer pricing/subscription/discount timeline with confidence and provenance.',
            ]
        );

        $this->get('/superuser/invoicing/period/distribution/wash-subscriptions/historical', function () {
            // Require the user to be logged in
            global $response;
            $this->requirePermission('superuser_invoicing_period');
            $dateRange = $this->requireAndNormalizeDateRange();
            $dateFrom = $dateRange['dateFrom'];
            $dateTo = $dateRange['dateTo'];
            // Get all orders in the date range that have:
            // - Department ID: 10
            // - Reference: Vaskeabonnementer
            $orders_o = new orders_o();
            $orders = $orders_o->getFieldsWhere([
                'department_id' => 10,
                'reference' => 'Vaskeabonnementer',
            ], ['id', 'created_at']);
            // Filter out the orders to only include those in the date range
            $orders = array_filter($orders, function ($order) use ($dateFrom, $dateTo) {
                $order_date = strtotime($order['created_at']);
                return $order_date >= strtotime($dateFrom) && $order_date <= strtotime($dateTo);
            });
            // Construct the order objects
            $orders = array_map(function ($order) {
                return (new orders_o())->select((int)$order['id']);
            }, $orders);
            // Filter out deleted orders
            $orders = array_filter($orders, function ($order) {
                return $order->deleted_at->value() === null;
            });
            // Define the warnings variable
            $warnings = [];
            // Define logs variable
            $logs = [];
            // Define the customer variable
            $unique_customers = [];
            foreach ( $orders as $order ) {
                $customer_id = (int)$order->customer_id->value();
                if (!in_array($customer_id, $unique_customers)) {
                    $unique_customers[] = $customer_id;
                } else {
                    $warnings[] = "Duplicate order for customer ID " . $customer_id . " in order ID " . $order->id;
                }
            }
            // Define the total subscription amount
            $total_subscription_amount = 0;
            // Define the collection of order items for all subscriptions
            $all_subscription_order_items = [];
            // Add up the total subscription amount and collect the order items
            /** @var orders_o $order */
            foreach ( $orders as $order ) {
                $customer_id = (int)$order->customer_id->value();
                $logs[] = "[{$order->id}] Processing order items for customer ID {$customer_id}";
                // Get the order items for the order
                $tmp_items = $order->getOrderItemObjects();
                /** @var order_items_o $item */
                foreach ( $tmp_items as $item ) {
                    $price = (int)$item->price->value();
                    $amount = (int)$item->quantity->value();
                    $total_subscription_amount += $price * $amount;
                    $all_subscription_order_items[] = $item;
                    $logs[] = "[{$order->created_at->value()}] Processing order item ID {$item->id}, price: {$price}, quantity: {$amount}, subtotal: +" . ($price * $amount) . " (total: {$total_subscription_amount}, items: " . count($all_subscription_order_items) . ")";
                }
            }
            unset($tmp_items, $item, $order, $customer_id, $price, $amount);
            // Define the total fixed pricing amount
            $total_fixed_pricing_amount = 0;
            $all_fixed_pricing_orders = [];
            $all_fixed_pricing_order_items = [];
            // Get all fixed pricing orders in the date range
            $fixed_pricing_orders = $orders_o->getFieldsWhere([
                'department_id' => 10,
                'reference' => 'Fast pris aftale',
            ], ['id', 'created_at']);
            // Filter out the orders to only include those in the date range
            $fixed_pricing_orders = array_filter($fixed_pricing_orders, function ($order) use ($dateFrom, $dateTo) {
                $order_date = strtotime($order['created_at']);
                $dateFrom = date('Y-m-d 00:00:00', strtotime($dateFrom));
                $dateToMonthEnd = date('Y-m-d 23:59:59', strtotime($dateTo));
                return $order_date >= strtotime($dateFrom) && $order_date <= strtotime($dateToMonthEnd);
            });
            // Construct the order objects
            $fixed_pricing_orders = array_map(function ($order) {
                return (new orders_o())->select((int)$order['id']);
            }, $fixed_pricing_orders);
            // Filter out deleted orders
            $fixed_pricing_orders = array_filter($fixed_pricing_orders, function ($order) {
                /** @var orders_o $order */
                return !$order->deleted_at->value();
            });
            $unique_customers_fixed_pricing = [];
            // Add up the total fixed pricing amount and collect the order items
            foreach ( $fixed_pricing_orders as $order ) {
                $customer_id = (int)$order->customer_id->value();
                $logs[] = "[{$order->id}] Processing fixed pricing order items for customer ID {$customer_id}";
                if (!in_array($customer_id, $unique_customers_fixed_pricing)) {
                    $unique_customers_fixed_pricing[] = $customer_id;
                } else {
                    $warnings[] = "Duplicate fixed pricing order for customer ID " . $customer_id . " in order ID " . $order->id;
                }
                $all_fixed_pricing_orders[] = $order;
                // Get the order items for the order
                $tmp_items = $order->getOrderItemObjects();
                /** @var order_items_o $item */
                foreach ( $tmp_items as $item ) {
                    $price = (int)$item->price->value();
                    $amount = (int)$item->quantity->value();
                    $total_fixed_pricing_amount += $price * $amount;
                    $all_fixed_pricing_order_items[] = $item;
                    $logs[] = "[{$order->created_at->value()}] Processing fixed pricing order item ID {$item->id}, price: {$price}, quantity: {$amount}, subtotal: +" . ($price * $amount) . " (total fixed pricing: {$total_fixed_pricing_amount}, items: " . count($all_fixed_pricing_order_items) . ")";
                }
            }

            // Get the total combined amount
            $total_combined_amount = $total_subscription_amount + $total_fixed_pricing_amount;

            // Return the orders
            $response->success([
                "hello" => "world",
                "orders" => count($orders),
                "unique_customers" => count($unique_customers),
                "warnings" => $warnings,
                "logs" => $logs,
                "total_subscription_amount" => $total_subscription_amount,
                "all_subscription_order_items" => count($all_subscription_order_items),
                "total_fixed_pricing_amount" => $total_fixed_pricing_amount,
                "all_fixed_pricing_order_items" => count($all_fixed_pricing_order_items),
                "total_combined_amount" => $total_combined_amount,
                "unique_customers_fixed_pricing" => count($unique_customers_fixed_pricing),
                "fixed_pricing_orders" => count($fixed_pricing_orders),
            ]);
        });
    }

    /**
     * Get the transactions with items, that are not included in invoices. (include_in_invoice = 0 - order_items)
     * Requirements:
     * - The transaction must not be deleted. (deleted_at is null - orders))
     * - The transaction contains at least one item that is not included in invoices. (include_in_invoice = 0 - order_items)
     * - The transaction must be within the specified date range. (created_at between dateFrom and dateTo - orders)
     * - The transaction item must not be deleted. (deleted_at is null - order_items)
     * Returns an array of customers with their transactions and items.
     * @param array $customersWithSubscriptions The customers with subscriptions.
     * @param array $dateRange The date range to filter the transactions. (dateFrom, dateTo)
     * @return array The customers with their transactions and items.
     * @throws Exception
     * @example
     * [
     *   [
     *     'customer_number' => 12345678,
     *     'customer_name' => 'Customer Name',
     *     'transactions' => [
     *       [
     *         'id' => 1,
     *         'date' => '2023-01-01',
     *         'amount' => 100.00,
     *         'booked' => true,
     *       ],
     *       [
     *         'id' => 2,
     *         'date' => '2023-01-02',
     *         'amount' => 200.00,
     *         'booked' => false,
     *       ],
     *     ],
     *     'requires_action' => false,
     *     'meta' => [
     *       'subscription' => [
     *         'id' => 1,
     *         'name' => 'Subscription Name',
     *         'price' => 100.00,
     *         'description' => 'Subscription Description',
     *       ],
     *     ],
     *   ],
     *   [
     *     'customer_number' => 87654321,
     *     'customer_name' => 'Another Customer',
     *     'transactions' => [
     *       [
     *         'id' => 3,
     *         'date' => '2023-01-03',
     *         'amount' => 300.00,
     *         'booked' => true,
     *       ],
     *     ],
     *     'requires_action' => true,
     *     'meta' => [
     *       'subscription' => [
     *         'id' => 2,
     *         'name' => 'Another Subscription',
     *         'price' => 200.00,
     *         'description' => 'Another Description',
     *       ],
     *     ],
     *   ],
     * ]
     * @see orders_o::getTransactionsWithItemsNotIncludedInInvoices
     * @see order_items_o::include_in_invoice
     * @see orders_o::deleted_at
     * @see order_items_o::deleted_at
     * @see orders_o::created_at
     * @see orders_o::customer_id
     * @see self::getVehicleSubscriptions()
     */
    private static function getTransactionsWithItemsNotIncludedInInvoices(array $customersWithSubscriptions): array
    {
        global $response;
        // Loop through each customer and get their transactions with items not included in invoices
        foreach ( $customersWithSubscriptions as &$customer ) {
            // Initialize the meta['subscription'] array if it doesn't exist
            if (!isset($customer['meta']['subscription'])) {
                $customer['meta']['subscription'] = [
                    'vehicles' => [], // Array of vehicle registration numbers
                    'subscription_total' => 0, // Total price of the subscriptions for the customer
                    'subscriptions' => [], // Array of subscriptions for the customer (['registration' => ['type' => (int), 'price' => (int), 'distributions' => <(int: departmentId)>[]])
                    'subscription_price_department_distribution' => [
                        // departmentId => price / (number of unique distributions)
                        // This is calculated after the subscriptions have been added
                        // This is used to distribute the subscription price across the transactions
                    ]
                ];
            }
            // Get the vehicles for the customer
            $vehicles_o = new customer_vehicles_o();
            // Get the vehicles for the customer (as an array of customer_vehicles_o objects)
            $customer_vehicles = array_map(function ($vehicle) {
                return (new customer_vehicles_o())->select((int)$vehicle['id']);
            }, $vehicles_o->getFieldsWhere([
                'customer_id' => (int)$customer['customer_number'],
                'wash_subscription' => 1, // Only get vehicles with a wash subscription
            ], ['id']));
            // Add the vehicle registration numbers to the meta['subscription']['vehicles'] array
            foreach ( $customer_vehicles as $vehicle ) {
                if ($vehicle instanceof customer_vehicles_o) {
                    $customer['meta']['subscription']['vehicles'][] = $vehicle->reg->value();
                } else {
                    $customer['meta']['subscription']['vehicles'][] = 'Unknown Vehicle';
                }
            }
            // Calculate the total price of the subscriptions for the customer
            foreach ( $customer_vehicles as $vehicle ) {
                if ($vehicle instanceof customer_vehicles_o) {
                    // Get the transaction ids covered by the subscription for the vehicle
                    // This is done to be able to distribute the subscription price across the transactions
                    // We only want to get the transactions that are in the customer's transactions array, (this is to avoid getting transactions that are outside the date range)
                    // We also want to make sure that the transactions are for the correct vehicle (reg_1 = vehicle reg), and that the transactions are not deleted (deleted_at is null)
                    $transaction_ids_covered_by_subscription = array_map(function ($transaction) {
                        return (int)$transaction;
                    }, $vehicle->getSubscriptionAppliedTransactionsFromList(array_map(
                        function ($transaction) {
                            return (int)$transaction['id'];
                        }, (new orders_o())->getFieldsWhereIn([
                        'id' => array_map(function ($transaction) {
                            return (int)$transaction['id'];
                        }, $customer['transactions']),
                        'reg_1' => $vehicle->reg->value(),
                    ], ['id'])
                    )));
                    // Get the subscription details for the vehicle
                    $subscription = [
                        'type' => (int)$vehicle->type->value(),
                        'price' => (int)(new products_o())->select((int)$vehicle->type->value())->getSubscriptionMonthlyPrice(),
                        // Get the unique department ids for the transactions covered by the subscription
                        'distributions' => array_unique(array_map(function ($transaction_id) {
                            // Get the department id for the transaction(s) covered by the subscription
                            $transaction = (new orders_o())->select((int)$transaction_id);
                            return (int)$transaction->department_id->value();
                        }, $transaction_ids_covered_by_subscription)),
                    ];
                    $customer['meta']['subscription']['subscriptions'][$vehicle->reg->value()] = $subscription;
                    $customer['meta']['subscription']['subscription_total'] += $subscription['price'];
                    // Add the subscription price per transaction to the meta['subscription']['subscription_price_department_distribution'] array
                    foreach ( $subscription['distributions'] as $department_id ) {
                        if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                            $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                        }
                        // Divide the subscription price by the number of unique distributions
                        $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $subscription['price'] / count($subscription['distributions']);
                    }
                    // If the vehicle has no transactions covered by the subscription, we need to run through the list of fallback options
                    if (count($transaction_ids_covered_by_subscription) === 0) {
                        // Run fallback options
                        self::attemptSubscriptionFallbacks($vehicle, $customer);
                    }
                }
            }
        }
        // Get the department distribution for all customers (sum of all customers)
        $collective_results = [
            'total_subscription_price' => 0,
            'subscription_price_department_distribution' => [
                // departmentId => price
            ],
        ];
        unset($customer); // Unset the reference to avoid issues
        foreach ( $customersWithSubscriptions as $customer ) {
            if (isset($customer['meta']['subscription'])) {
                $collective_results['total_subscription_price'] += $customer['meta']['subscription']['subscription_total'];
                foreach ( $customer['meta']['subscription']['subscription_price_department_distribution'] as $department_id => $price ) {
                    if (!isset($collective_results['subscription_price_department_distribution'][$department_id])) {
                        $collective_results['subscription_price_department_distribution'][$department_id] = 0;
                    }
                    $collective_results['subscription_price_department_distribution'][$department_id] += $price;
                }
                // Verify that the sum of the department distribution equals the total subscription price for the customer
                $sum_of_distribution = array_sum($customer['meta']['subscription']['subscription_price_department_distribution']);
                $difference = $customer['meta']['subscription']['subscription_total'] - $sum_of_distribution;
                if (abs($difference) > 0.01) {
                    // Debug info
                    $debug_info = [
                        'customer_number' => $customer['customer_number'],
                        'subscription_total' => $customer['meta']['subscription']['subscription_total'],
                        'sum_of_distribution' => $sum_of_distribution,
                        'difference' => $difference,
                    ];
                    // Send slack alert
                    $message = "Subscription price distribution mismatch for customer " . $customer['customer_number'] . " (" . self::getLocalCustomerName((int)$customer['customer_number']) . ")\n";
                    $message .= "Subscription total: " . $customer['meta']['subscription']['subscription_total'] . "\n";
                    $message .= "Distribution total: " . $sum_of_distribution . "\n";
                    $message .= "Difference: " . $difference . "\n";
                    $message .= "Debug info: " . print_r($debug_info, true);
                    // Describe what to check
                    (new slack())->send_message($message, 'Subscription Price Distribution Mismatch');
                    // Throw an error
                    throw new Exception('Subscription price distribution does not equal total subscription price for customer ' . $customer['customer_number'] . '. Difference: ' . $difference);
                }
            }
        }
        // Parse the department ids to department names
        $parsed_distribution = [];
        foreach ( $collective_results['subscription_price_department_distribution'] as $department_id => $price ) {
            $parsed_distribution[self::getDepartmentNameCached((int)$department_id)] = $price;
        }
        $collective_results['subscription_price_department_distribution_parsed'] = $parsed_distribution;
        // Include the collective results in the response
        $response->add_include('collective_subscription_results', $collective_results);
        if (self::shouldSendSlackSummary()) {
            // Send summary to slack
            $slack_message = "Subscription Price Distribution Summary:\n";
            $slack_message .= "Total Subscription Price: " . $collective_results['total_subscription_price'] . "\n";
            $slack_message .= "Department Distribution:\n";
            $tmp_total = 0;
            foreach ( $collective_results['subscription_price_department_distribution_parsed'] as $department_name => $price ) {
                $slack_message .= "- " . $department_name . ": " . $price . "\n";
                $tmp_total += $price;
            }
            $slack_message .= "Total Distribution: " . $tmp_total . "\n";
            (new slack())->send_message($slack_message, 'Subscription Price Distribution Summary');
        }
        return $customersWithSubscriptions;
    }

    /**
     * Attempt fallback options to find transactions covered by the subscription.
     * This is used when a vehicle has a subscription, but no transactions in the given date range.
     * This function processes the following fallback options:
     * 1. Attempt to divide the subscription price across the departments the customer has subscription transactions in.
     * 2. Attempt to get the last transaction the vehicle was washed in, and use that department.
     * 3. If no transactions are found, assign the subscription to the departments used in the most recent 10 transactions by the customer.
     * 4. If no departments are found, assign the subscription to a default department (e.g., department ID 1).
     * @param customer_vehicles_o $vehicle The vehicle object.
     * @param array $customer The customer object (by reference).
     * @retuns bool True if a fallback was applied, false otherwise.
     * @throws Exception If an error occurs while processing the fallbacks.
     */
    private static function attemptSubscriptionFallbacks(customer_vehicles_o $vehicle, array &$customer): bool
    {
        if (self::debug) echo "Attempting fallbacks for vehicle " . $vehicle->reg->value() . " of customer " . $customer['customer_number'] .  " - " . self::getLocalCustomerName((int)$customer['customer_number']) . "\n";
        // Run the fallback options in order
        $subscription_price = (int)(new products_o())->select((int)$vehicle->type->value())->getSubscriptionMonthlyPrice();
        if (self::divideSubscriptionAcrossCustomerDepartments($customer, $subscription_price)) {
            return true;
        }
        if (self::useLastTransactionDepartment($customer, $vehicle, $subscription_price)) {
            return true;
        }
        if (self::useRecentCustomerTransactions($customer, $subscription_price)) {
            return true;
        }
        if (self::useDefaultDepartment($customer, $subscription_price)) {
            return true;
        }
        // If no fallback was applied, return false
        if (self::debug) echo "All fallbacks failed for vehicle " . $vehicle->reg->value() . " of customer " . $customer['customer_number'] . "\n";
        return false; // No fallback applied
    }
    const debug = false;
    // 1. Attempt to divide the subscription price across the departments the customer has transactions in.
    private static function divideSubscriptionAcrossCustomerDepartments(array &$customer, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 1: Dividing subscription price across customer departments\n";
        // Get the unique department ids for the customer's transactions
        $department_ids = array_unique(array_map(/**
         * @throws Exception
         */ function ($transaction) {
            $transaction_obj = (new orders_o())->select((int)$transaction['id']);
            return (int)$transaction_obj->department_id->value();
        }, $customer['transactions']));
        // Remove department id 10 (automatic) from the list
        $department_ids = array_filter($department_ids, function ($department_id) {
            return $department_id !== 10;
        });
        if (count($department_ids) > 0) {
            foreach ( $department_ids as $department_id ) {
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                }
                // Divide the subscription price by the number of unique departments
                $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $subscription_price / count($department_ids);
            }
            // Debug:
            if (self::debug) echo "Fallback 1 applied: Divided subscription price across customer departments. Departments: " . implode(', ', $department_ids) . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 1 not applied: No departments found in customer's transactions\n";
        return false; // No departments found
    }
    // 2. Attempt to get the last transaction the vehicle was washed in, and use that department.

    /**
     * @throws Exception
     */
    private static function useLastTransactionDepartment(array &$customer, customer_vehicles_o $vehicle, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 2: Using last transactions departments\n";
        // Get the last transaction the vehicle was washed in
        $last_transaction_ids = $vehicle->getLastTransactions(10); // Get the last 10 transactions
        $distribution_department_ids = []; // Array to hold the department ids from the last transactions, together with their counts (1 = 10%, 2 = 20%, etc.)
        // Calculate the department distribution from the last transactions
        foreach ( $last_transaction_ids as $transaction_id ) {
            $transaction = (new orders_o())->select((int)$transaction_id);
            if ($transaction instanceof orders_o) {
                $department_id = (int)$transaction->department_id->value();
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($distribution_department_ids[$department_id])) {
                    $distribution_department_ids[$department_id] = 0;
                }
                $distribution_department_ids[$department_id]++;
            }
        }
        // If we have department ids from the last transactions, use them to distribute the subscription price
        if (count($distribution_department_ids) > 0) {
            $total_counts = array_sum($distribution_department_ids);
            foreach ( $distribution_department_ids as $department_id => $count ) {
                if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                }
                // Distribute the subscription price based on the count of the department in the last transactions
                $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += ($count / $total_counts) * $subscription_price;
            }
            // Debug:
            if (self::debug) echo "Fallback 2 applied: Used last transaction departments. Departments: " . implode(', ', array_keys($distribution_department_ids)) . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 2 not applied: No last transaction found for vehicle\n";
        return false; // No last transaction found
    }
    // 3. If no transactions are found, assign the subscription to the departments used in the most recent 10 transactions by the customer.
    private static function useRecentCustomerTransactions(array &$customer, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 3: Using recent customer transactions\n";
        // Get the last 10 transactions of the customer
        $recent_transaction_ids = array_slice(array_map(function ($transaction) {
            return (int)$transaction['id'];
        }, (new orders_o())->getFieldsWhere([
            'customer_id' => (int)$customer['customer_number'],
            'deleted_at' => null,
        ], ['id', 'created_at'])), 0, 10);
        $distribution_department_ids = []; // Array to hold the department ids from the recent transactions, together with their counts (1 = 10%, 2 = 20%, etc.)
        // Calculate the department distribution from the recent transactions
        foreach ( $recent_transaction_ids as $transaction_id ) {
            $transaction = (new orders_o())->select((int)$transaction_id);
            if ($transaction instanceof orders_o) {
                $department_id = (int)$transaction->department_id->value();
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($distribution_department_ids[$department_id])) {
                    $distribution_department_ids[$department_id] = 0;
                }
                $distribution_department_ids[$department_id]++;
            }
        }
        // If we have department ids from the recent transactions, use them to distribute the subscription price
        if (count($distribution_department_ids) > 0) {
            $total_counts = array_sum($distribution_department_ids);
            foreach ( $distribution_department_ids as $department_id => $count ) {
                // If the department id is 10 (automatic), skip it
                if ($department_id === 10) {
                    continue;
                }
                if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0;
                }
                // Distribute the subscription price based on the count of the department in the recent transactions
                $customer['meta']['subscription']['subscription_price_department_distribution'][$department_id] += ($count / $total_counts) * $subscription_price;
            }
            // Debug:
            if (self::debug) echo "Fallback 3 applied: Used recent customer transaction departments. Departments: " . implode(', ', array_keys($distribution_department_ids)) . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 3 not applied: No recent transactions found for customer\n";
        return false; // No recent transactions found
    }
    // 4. If no departments are found, assign the subscription to customer default department
    //    or fallback to e-conomic default distribution department.
    private static function useDefaultDepartment(array &$customer, int $subscription_price): bool
    {
        if (self::debug) echo "Fallback 4: Using default department\n";
        $customer_default_department_id = (int)(new users_o)->getUserByCustomerNumber((int)$customer['customer_number'])->getDefaultDepartment();
        $default_department_id = $customer_default_department_id > 0
            ? $customer_default_department_id
            : self::getEconomicFallbackDepartmentId();

        if ($default_department_id > 0) {
            if (!isset($customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id])) {
                $customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id] = 0;
            }
            $customer['meta']['subscription']['subscription_price_department_distribution'][$default_department_id] += $subscription_price;
            // Debug:
            if (self::debug) echo "Fallback 4 applied: Used default department ID " . $default_department_id . "\n";
            return true; // Fallback applied
        }
        if (self::debug) echo "Fallback 4 not applied: No default department found for customer\n";
        return false; // No default department found
    }

    /**
     * Get the original price for each customer, and transaction in the fixed pricing array.
     *
     * @param array $fixed_pricing The fixed pricing array.
     * @return array The fixed pricing array with the original price added.
     * @throws Exception
     */
    private static function getOriginalPrice(array $fixed_pricing): array
    {
        global $response;
        $order_items = new order_items_o();
        foreach ( $fixed_pricing as &$customer ) {
            if (isset($customer['meta']['fixed_pricing'])) {
                $original_price = 0;
                $department_totals = []; // Array to hold totals per department

                $eligible_transaction_department_ids = [];
                foreach ( $customer['transactions'] as $transaction ) {
                    $department_id = (int)($transaction['department_id'] ?? 0);
                    $excluded = (bool)($transaction['excluded'] ?? false);

                    // Skip excluded transactions and automatic department.
                    if ($excluded || $department_id === 10) {
                        continue;
                    }
                    $eligible_transaction_department_ids[(int)$transaction['id']] = $department_id;
                }

                if (!empty($eligible_transaction_department_ids)) {
                    $transaction_original_prices = [];
                    $product_cache = [];
                    $department_price_cache = [];
                    $discount_cache = [];
                    $user = (new users_o())->getUserByCustomerNumber((int)$customer['customer_number']);
                    $rows = $order_items->getFieldsWhere(
                        ['order_id' => array_keys($eligible_transaction_department_ids)],
                        ['order_id', 'product_id', 'price', 'quantity']
                    );

                    foreach ( $rows as $row ) {
                        $order_id = (int)$row['order_id'];
                        $department_id = (int)($eligible_transaction_department_ids[$order_id] ?? 0);
                        if ($department_id <= 0) {
                            continue;
                        }
                        $product_id = (int)$row['product_id'];
                        $price = (int)$row['price'];
                        $quantity = (int)$row['quantity'];

                        if ($price > 0 && $quantity > 0) {
                            $transaction_original_prices[$order_id] = (int)(($transaction_original_prices[$order_id] ?? 0) + ($price * $quantity));
                            continue;
                        }

                        if (!isset($product_cache[$product_id])) {
                            $product_cache[$product_id] = (new products_o())->select($product_id);
                        }
                        if (!isset($department_price_cache[$department_id][$product_id])) {
                            $department_price_cache[$department_id][$product_id] = $product_cache[$product_id]->getDepartmentPriceResolution($department_id);
                        }
                        if (!array_key_exists($product_id, $discount_cache)) {
                            $unit_price = (int)$department_price_cache[$department_id][$product_id]['price'];
                            if (!products_o::priceResolutionIsCustomMissing($department_price_cache[$department_id][$product_id])) {
                                $unit_price = $user->applyProductCustomerPricing($product_id, $unit_price, false, $department_id);
                            }
                            $discount_cache[$product_id] = $unit_price;
                        }
                        $post_discount = (int)$discount_cache[$product_id] * $quantity;
                        $transaction_original_prices[$order_id] = (int)(($transaction_original_prices[$order_id] ?? 0) + $post_discount);
                    }

                    foreach ( $eligible_transaction_department_ids as $transaction_id => $department_id ) {
                        $transaction_original_price = (int)($transaction_original_prices[$transaction_id] ?? 0);
                        $original_price += $transaction_original_price;
                        // Initialize the department total if it doesn't exist
                        if (!isset($department_totals[$department_id])) {
                            $department_totals[$department_id] = 0;
                        }
                        // Add the transaction amount to the department total
                        $department_totals[$department_id] += $transaction_original_price;
                    }
                } else {
                    $customer_default_department_id = (int)(new users_o())->getUserByCustomerNumber((int)$customer['customer_number'])->getDefaultDepartment();
                    $fallback_department_id = $customer_default_department_id > 0
                        ? $customer_default_department_id
                        : self::getEconomicFallbackDepartmentId();

                    if ($fallback_department_id > 0) {
                        $department_totals[$fallback_department_id] = (float)$customer['meta']['fixed_pricing']['price'];
                    }
                }

                $customer['meta']['fixed_pricing']['original_price'] = $original_price;
                $customer['meta']['fixed_pricing']['department_totals'] = $department_totals;
                // Take the relative price of the department totals in relation to the fixed price
                // This is done to see how much each department contributes to the fixed price
                $total = array_sum($department_totals);
                foreach ( $department_totals as $department_id => $amount ) {
                    if ($total > 0) {
                        $department_totals[$department_id] = ($amount / $total) * $customer['meta']['fixed_pricing']['price'];
                    } else {
                        $department_totals[$department_id] = 0;
                    }
                }
                $customer['meta']['fixed_pricing']['department_totals_relative'] = $department_totals;
            }
        }
        // Calculate the total original price for all customers
        $collective_results = [
            'total_fixed_price' => 0,
            'total_original_price' => 0,
            'total_department_totals' => [],
            'total_department_totals_relative' => [],
        ];
        unset($customer); // Unset the reference to avoid issues
        foreach ( $fixed_pricing as $customer ) {
            if (isset($customer['meta']['fixed_pricing'])) {
                $collective_results['total_fixed_price'] += $customer['meta']['fixed_pricing']['price'];
                $collective_results['total_original_price'] += $customer['meta']['fixed_pricing']['original_price'];
                // Sum the department totals
                foreach ( $customer['meta']['fixed_pricing']['department_totals'] as $department_id => $amount ) {
                    if (!isset($collective_results['total_department_totals'][$department_id])) {
                        $collective_results['total_department_totals'][$department_id] = 0;
                    }
                    $collective_results['total_department_totals'][$department_id] += $amount;
                }
                // Sum the relative department totals
                foreach ( $customer['meta']['fixed_pricing']['department_totals_relative'] as $department_id => $amount ) {
                    if (!isset($collective_results['total_department_totals_relative'][$department_id])) {
                        $collective_results['total_department_totals_relative'][$department_id] = 0;
                    }
                    $collective_results['total_department_totals_relative'][$department_id] += $amount;
                }
            }
        }
        // Parse the department ids to department names
        $collective_results = self::parseTheDepartmentIdsToDepartmentNames($collective_results);
        // Include the collective results in the response
        $response->add_include('collective_fixed_pricing_results', $collective_results);
        if (self::shouldSendSlackSummary()) {
            // Send slack message with the collective results
            $slack_message = "Fixed Pricing Invoicing Period Summary:\n";
            $slack_message .= "Total Fixed Price: " . number_format($collective_results['total_fixed_price'], 2) . " DKK\n";
            $slack_message .= "Total Original Price: " . number_format($collective_results['total_original_price'], 2) . " DKK\n";
            $slack_message .= "Department Totals:\n";
            $tmp_sum = 0;
            foreach ( $collective_results['total_department_totals_parsed'] as $department_name => $amount ) {
                $slack_message .= "- " . $department_name . ": " . number_format($amount, 2) . " DKK\n";
                $tmp_sum += $amount;
            }
            $slack_message .= "Total Department Totals: " . number_format($tmp_sum, 2) . " DKK\n";
            $slack_message .= "Relative Department Totals:\n";
            $tmp_sum = 0;
            foreach ( $collective_results['total_department_totals_relative_parsed'] as $department_name => $amount ) {
                $slack_message .= "- " . $department_name . ": " . number_format($amount, 2) . " DKK\n";
                $tmp_sum += $amount;
            }
            $slack_message .= "Total Relative Department Totals: " . number_format($tmp_sum, 2) . " DKK\n";
            (new slack())->send_message($slack_message, 'Fixed Pricing Invoicing Period Summary');
        }
        return $fixed_pricing;
    }

    /**
     * @throws Exception
     */
    private static function getInvoicingPeriod(
        string $dateFrom,
        string $dateTo,
        ?array $onlyCustomerNumbers = null,
        bool $includeInvoicePeriodFlags = false
    ): array
    {
        //$customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo)
        $onlyCustomerNumbers = $onlyCustomerNumbers !== null
            ? self::normalizeCustomerNumbers($onlyCustomerNumbers)
            : null;

        $customersWithTransactions = self::debugGetTime(function () use ($dateFrom, $dateTo, $onlyCustomerNumbers) {
            return self::getCustomersWithTransactions($dateFrom, $dateTo, $onlyCustomerNumbers);
        }, 'customers_with_transactions');
        $types = [];
        // Add the customers with transactions to the types array
        $types['all'] = $customersWithTransactions;
        $types['vehicle_subscriptions'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers) {
            return self::getVehicleSubscriptions($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers);
        }, 'vehicle_subscriptions');
        $types['fixed_pricing'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers) {
            return self::getFixedPricing($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers);
        }, 'fixed_pricing');
        $types['tank_cleaning'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers) {
            return self::getTankCleaning($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers);
        }, 'tank_cleaning');
        $types['special_arrangements'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers) {
            return self::getSpecialArrangements($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers);
        }, 'special_arrangements');
        $types['invoice_per_order'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers) {
            return self::getInvoicingPerOrder($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers);
        }, 'invoice_per_order');
        $types['possible_duplicates'] = self::debugGetTime(function () use ($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers) {
            return self::getPossibleDuplicates($dateFrom, $dateTo, $customersWithTransactions, $onlyCustomerNumbers);
        }, 'possible_duplicates');
        $queueOverlay = self::debugGetTime(function () use ($dateFrom, $dateTo) {
            return self::getActiveCollectedInvoiceQueueOverlay($dateFrom, $dateTo);
        }, 'active_collected_invoice_queue_overlay');
        $types = self::applyCollectedInvoiceQueueOverlayToPeriodTypes(
            $types,
            $queueOverlay['by_collection_id'] ?? [],
            $queueOverlay['by_customer_number'] ?? [],
        );
        $draftOverlay = self::debugGetTime(function () use ($types, $dateFrom, $dateTo) {
            return self::getValidCollectedInvoiceDraftOverlay($types, $dateFrom, $dateTo);
        }, 'valid_collected_invoice_draft_overlay');
        $types = self::applyCollectedInvoiceDraftOverlayToPeriodTypes(
            $types,
            $draftOverlay['by_collection_id'] ?? [],
            $draftOverlay['by_customer_number'] ?? [],
        );
        $invoiceCollectionMetadata = self::debugGetTime(function () use ($types) {
            return self::getPeriodInvoiceCollectionMetadata($types);
        }, 'invoice_collection_metadata');
        $types = self::applyPeriodInvoiceStateOverlayToTypes($types, $invoiceCollectionMetadata);
        if ($includeInvoicePeriodFlags) {
            $types = self::debugGetTime(function () use ($types, $dateFrom, $dateTo, $onlyCustomerNumbers) {
                return (new invoice_period_flag_service())->applyFlagsToPeriodTypes(
                    $types,
                    $dateFrom,
                    $dateTo,
                    $onlyCustomerNumbers
                );
            }, 'invoice_period_flags');
        }
        return [
            'dateFrom' => $dateFrom,
            'dateTo' => $dateTo,
            'types' => $types,
        ];
    }

    /**
     * Debugging function to measure the time taken by a function.
     *
     * @param callable $function The function to debug.
     * @return mixed The result of the function.
     */
    private static function debugGetTime(callable $function, string $debug_label = ''): mixed
    {
        global $response;
        $start_time = microtime(true) * 1000; // Start time in milliseconds
        $result = $function();
        $end_time = microtime(true) * 1000; // End time in milliseconds
        $execution_time = $end_time - $start_time;
        $response->add_include(
            'debug_invoicing_period_' . $debug_label,
            [
                'execution_time' => $execution_time,
            ]
        );
        return $result;
    }

    /**
     * @throws Exception
     */
    private static function getCustomersWithTransactions(string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers = null): array
    {
        $onlyCustomerNumbers = $onlyCustomerNumbers !== null
            ? self::normalizeCustomerNumbers($onlyCustomerNumbers)
            : null;
        if ($onlyCustomerNumbers !== null && empty($onlyCustomerNumbers)) {
            return [];
        }

        $customer_number_transactions = [];
        self::debugGetTime(function () use ($onlyCustomerNumbers, $dateFrom, $dateTo, &$customer_number_transactions) {
            $customer_number_transactions = (new orders_o())->getPeriodTransactionsForCustomersInDateRange(
                $onlyCustomerNumbers,
                $dateFrom,
                $dateTo
            );
        }, 'get_transactions_for_customers_in_date_range');

        $customer_numbers = [];
        $tmp = [];
        self::debugGetTime(function () use ($customer_number_transactions, &$customer_numbers) {
            foreach ( $customer_number_transactions as $customer_number => $transactions ) {
                $customer_number = (int)$customer_number;
                if (empty($customer_number)) {
                    continue;
                }
                if (isset($customer_numbers[$customer_number])) {
                    continue;
                }
                $firstTransaction = is_array($transactions) ? ($transactions[0] ?? []) : [];
                $userId = (int)($firstTransaction['user_id'] ?? 0);
                $customer_numbers[$customer_number] = $userId > 0 ? $userId : null;
            }
        }, 'process_customer_numbers');
        self::debugGetTime(static function (): void {
            // Net totals are resolved by getPeriodTransactionsForCustomersInDateRange().
        }, 'calculate_transaction_totals');
        // Get the customer names from the cache
        $customer_names = (new \objects\users_o())->getCustomerNames(array_keys($customer_numbers), false);
        // Process the customer numbers to ensure they are unique
        self::debugGetTime(function () use ($customer_numbers, $customer_number_transactions, &$tmp, $customer_names) {
            foreach ( $customer_numbers as $customer_number => $user_id ) {
                if (empty($customer_number)) {
                    // Skip if the customer number is empty
                    continue;
                }
                // Check if the customer number is already in the array
                if (isset($tmp[$customer_number])) {
                    // If the customer number is already in the array, skip it
                    continue;
                }
                // If the customer number is not in the array, add it
                // Construct the customer object with transactions
                $tmp[] = self::constructCustomerObject(
                    (int)$customer_number,
                    $customer_names[(int)$customer_number] ?? 'Unknown Customer',
                    $customer_number_transactions[(int)$customer_number] ?? [],
                    false,
                    (int)$user_id > 0 ? (int)$user_id : null,
                );
            }
        }, 'construct_customer_objects');
        return $tmp;
    }

    /**
     * @param int $customer_number
     * @param string $customer_name
     * @param orders_o[] $transactions
     * @param bool $requires_action
     * @return array
     * @throws Exception
     */
    protected static function constructCustomerObject(
        int    $customer_number,
        string $customer_name,
        array  $transactions = [],
        bool   $requires_action = false,
        ?int   $user_id = null,
        ?array $meta = null
    ): array
    {
        $user = null;
        if ($user_id === null) {
            $user = (new users_o())->getUserByCustomerNumber((int)$customer_number);
        }
        return [
            'id' => $user_id ?? ($user !== null && $user->exists() ? $user->id : null),
            'customer_number' => $customer_number,
            'customer_name' => $customer_name,
            'transactions' => $parsed_transactions = array_map(function ($transaction) {
                return self::constructTransactionObject($transaction);
            }, $transactions),
            'requires_action' => self::checkRequiresAction($parsed_transactions, $requires_action),
            'meta' => $meta ?? [],
            'queue' => self::getDefaultQueueSummary(),
            'draft' => self::getDefaultDraftSummary(),
            'invoice_collections' => [],
        ];
    }

    /**
     * @throws Exception
     */
    private static function constructTransactionObject(mixed $transaction): array
    {
        if (is_array($transaction)) {
            return self::constructTransactionObjectFromPeriodRow($transaction);
        }
        if (!$transaction instanceof orders_o) {
            throw new \InvalidArgumentException('Invalid period transaction row.');
        }

        $departmentId = (int)$transaction->department_id->value();
        $invoiceCollectionId = (int)$transaction->invoice_collection_id->value();
        $booked = self::isTransactionBookedFromLocalState($transaction);
        $completedAt = !empty($transaction->completed_at->value())
            ? (string)$transaction->completed_at->value()
            : null;
        return [
            'id' => $transaction->id,
            'date' => $transaction->created_at->value(),
            'amount' => $transaction->temporary_net_amount, // Use the temporary net amount set earlier
            'booked' => $booked,
            'department_id' => $departmentId,
            'customer_number' => (int)$transaction->customer_id->value(),
            'reference' => (string)$transaction->reference->value(),
            'po' => (string)$transaction->po->value(),
            'notes' => (string)$transaction->notes->value(),
            'reg_1' => (string)$transaction->reg_1->value(),
            'reg_2' => (string)$transaction->reg_2->value(),
            'reg_3' => (string)$transaction->reg_3->value(),
            'completed_at' => $completedAt,
            'excluded' => !$transaction->isIncludedInInvoicing(),
            'invoice_collection_id' => $invoiceCollectionId > 0 ? $invoiceCollectionId : null,
            'invoice_state' => self::periodOrderState($booked, $completedAt),
            'queue_status' => null,
            'queue_job_id' => null,
        ];
    }

    private static function constructTransactionObjectFromPeriodRow(array $transaction): array
    {
        $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
        return [
            'id' => (int)($transaction['id'] ?? $transaction['order_id'] ?? 0),
            'date' => (string)($transaction['date'] ?? $transaction['created_at'] ?? ''),
            'amount' => (float)($transaction['amount'] ?? $transaction['net_amount'] ?? 0),
            'booked' => (bool)($transaction['booked'] ?? false),
            'department_id' => (int)($transaction['department_id'] ?? 0),
            'customer_number' => (int)($transaction['customer_number'] ?? $transaction['customer_id'] ?? 0),
            'reference' => (string)($transaction['reference'] ?? $transaction['order_reference'] ?? ''),
            'po' => (string)($transaction['po'] ?? $transaction['order_po'] ?? ''),
            'notes' => (string)($transaction['notes'] ?? $transaction['order_notes'] ?? ''),
            'reg_1' => (string)($transaction['reg_1'] ?? ''),
            'reg_2' => (string)($transaction['reg_2'] ?? ''),
            'reg_3' => (string)($transaction['reg_3'] ?? ''),
            'completed_at' => !empty($transaction['completed_at']) ? (string)$transaction['completed_at'] : null,
            'excluded' => (bool)($transaction['excluded'] ?? ((int)($transaction['include_in_invoice_effective'] ?? 1) !== 1)),
            'invoice_collection_id' => $invoiceCollectionId > 0 ? $invoiceCollectionId : null,
            'invoice_state' => self::periodOrderState(
                (bool)($transaction['booked'] ?? false),
                !empty($transaction['completed_at']) ? (string)$transaction['completed_at'] : null
            ),
            'queue_status' => $transaction['queue_status'] ?? null,
            'queue_job_id' => $transaction['queue_job_id'] ?? null,
        ];
    }

    /**
     * Resolve booked state from local stored invoice metadata only.
     * Remote e-conomic invoice lookups are intentionally avoided here because this method runs for every
     * transaction in the period response.
     */
    private static function isTransactionBookedFromLocalState(orders_o $transaction): bool
    {
        global $db;

        $orderId = (int)$transaction->id;
        if ($orderId < 1) {
            return false;
        }
        if (array_key_exists($orderId, self::$periodOrderBookedCache)) {
            return self::$periodOrderBookedCache[$orderId];
        }

        $invoiceCollectionId = (int)$transaction->invoice_collection_id->value();
        if ($invoiceCollectionId > 0) {
            if (!array_key_exists($invoiceCollectionId, self::$periodInvoiceCollectionBookedCache)) {
                $result = $db->query("SELECT booked_invoice_id FROM collected_order_invoices WHERE id = {$invoiceCollectionId} LIMIT 1");
                $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : null;
                self::$periodInvoiceCollectionBookedCache[$invoiceCollectionId] = !empty($row['booked_invoice_id'] ?? null);
            }
            return self::$periodOrderBookedCache[$orderId] = self::$periodInvoiceCollectionBookedCache[$invoiceCollectionId];
        }

        $result = $db->query("SELECT invoice_id FROM economic_module_orders WHERE id = {$orderId} LIMIT 1");
        $row = $result && $result->num_rows > 0 ? $result->fetch_assoc() : null;
        return self::$periodOrderBookedCache[$orderId] = !empty($row['invoice_id'] ?? null);
    }

    private static function periodOrderState(bool $booked, ?string $completedAt): string
    {
        if ($booked) {
            return 'economic_booked';
        }

        return !empty($completedAt) ? 'closed' : 'open';
    }

    private static function checkRequiresAction(array $parsed_transactions, bool $requires_action): bool
    {
        // If requires_action is already set to true, return true
        if ($requires_action) {
            return true;
        }
        // Check if any transaction is not booked
        foreach ( $parsed_transactions as $transaction ) {
            if (!$transaction['booked'] && !$transaction['excluded']) {
                return true;
            }
        }
        // If all transactions are booked, return false
        return false;
    }

    private static function getDefaultQueueSummary(): array
    {
        return [
            'has_active_job' => false,
            'statuses' => [],
            'invoice_collection_ids' => [],
            'is_action_blocked' => false,
        ];
    }

    private static function getDefaultDraftSummary(): array
    {
        return [
            'has_valid_draft' => false,
            'invoice_collection_ids' => [],
            'is_action_blocked' => false,
        ];
    }

    private static function getActiveCollectedInvoiceQueueOverlay(string $dateFrom, string $dateTo): array
    {
        $overlay = [
            'by_collection_id' => [],
            'by_customer_number' => [],
        ];

        try {
            $queue = new economic_transfer_queue();
            $offset = 0;
            $limit = 250;

            do {
                $jobs = $queue->listJobs(
                    [
                        economic_transfer_queue::STATUS_QUEUED,
                        economic_transfer_queue::STATUS_PROCESSING,
                    ],
                    $limit,
                    $offset,
                    economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT
                );

                foreach ($jobs as $job) {
                    $normalizedJob = self::normalizeCollectedInvoiceQueueJob($job, $dateFrom, $dateTo);
                    if ($normalizedJob === null || empty($normalizedJob['is_period_relevant'])) {
                        continue;
                    }

                    $invoiceCollectionId = (int)($normalizedJob['invoice_collection_id'] ?? 0);
                    if ($invoiceCollectionId > 0 && !isset($overlay['by_collection_id'][$invoiceCollectionId])) {
                        $overlay['by_collection_id'][$invoiceCollectionId] = $normalizedJob;
                    }

                    $customerNumber = (int)($normalizedJob['customer_number'] ?? 0);
                    if ($customerNumber > 0) {
                        $overlay['by_customer_number'][$customerNumber] = $overlay['by_customer_number'][$customerNumber] ?? [];
                        $overlay['by_customer_number'][$customerNumber][] = $normalizedJob;
                    }
                }

                $offset += count($jobs);
            } while (count($jobs) === $limit);
        } catch (\Throwable) {
            return $overlay;
        }

        return $overlay;
    }

    private static function normalizeCollectedInvoiceQueueJob(array $job, string $dateFrom, string $dateTo): ?array
    {
        $invoiceCollectionId = (int)(
            $job['payload']['collected_invoice_id']
            ?? $job['collected_invoice_id']
            ?? $job['invoice_collection_id']
            ?? 0
        );

        if ($invoiceCollectionId < 1) {
            return null;
        }

        try {
            $invoiceCollection = (new collected_order_invoices_o())->select($invoiceCollectionId);
            if (!$invoiceCollection->exists()) {
                return null;
            }

            $customerNumber = (int)$invoiceCollection->customer_number->value();
            $closedAt = (string)$invoiceCollection->closed_at->value();
            $createdAt = (string)$invoiceCollection->created_at->value();

            return [
                'queue_job_id' => (int)($job['id'] ?? $job['queue_job_id'] ?? 0),
                'queue_status' => (string)($job['status'] ?? $job['queue_status'] ?? ''),
                'invoice_collection_id' => $invoiceCollectionId,
                'customer_number' => $customerNumber,
                'created_at' => $createdAt,
                'closed_at' => $closedAt,
                'is_period_relevant' => self::isInvoiceCollectionRelevantToPeriod($createdAt, $closedAt, $dateFrom, $dateTo),
            ];
        } catch (\Throwable) {
            return null;
        }
    }

    private static function isInvoiceCollectionRelevantToPeriod(
        ?string $createdAt,
        ?string $closedAt,
        string $dateFrom,
        string $dateTo
    ): bool {
        return self::isTimestampWithinPeriod($closedAt, $dateFrom, $dateTo)
            || self::isTimestampWithinPeriod($createdAt, $dateFrom, $dateTo);
    }

    private static function isTimestampWithinPeriod(?string $timestamp, string $dateFrom, string $dateTo): bool
    {
        if (empty($timestamp)) {
            return false;
        }

        $normalizedTimestamp = substr((string)$timestamp, 0, 10);
        return $normalizedTimestamp >= $dateFrom && $normalizedTimestamp <= $dateTo;
    }

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

        global $db;

        try {
            $result = $db->query("SHOW COLUMNS FROM `collected_order_invoices` LIKE 'deleted_at'");
            self::$collectedOrderInvoicesHasDeletedAtColumn = $result !== false && (int)$result->num_rows > 0;
        } catch (\Throwable) {
            self::$collectedOrderInvoicesHasDeletedAtColumn = false;
        }

        return self::$collectedOrderInvoicesHasDeletedAtColumn;
    }

    private static function applyCollectedInvoiceQueueOverlayToPeriodTypes(
        array $types,
        array $queueJobsByCollectionId,
        array $queueJobsByCustomerNumber
    ): array {
        foreach ($types as $type => $customers) {
            if (!is_array($customers)) {
                continue;
            }

            $types[$type] = array_map(function ($customer) use ($queueJobsByCollectionId, $queueJobsByCustomerNumber) {
                if (!is_array($customer)) {
                    return $customer;
                }

                return self::applyCollectedInvoiceQueueOverlayToCustomer(
                    $customer,
                    $queueJobsByCollectionId,
                    $queueJobsByCustomerNumber
                );
            }, $customers);
        }

        return $types;
    }

    private static function applyCollectedInvoiceQueueOverlayToCustomer(
        array $customer,
        array $queueJobsByCollectionId,
        array $queueJobsByCustomerNumber
    ): array {
        $customerNumber = (int)($customer['customer_number'] ?? 0);
        $activeCustomerJobs = array_values(array_filter(
            $queueJobsByCustomerNumber[$customerNumber] ?? [],
            static function ($job): bool {
                return !empty($job['is_period_relevant']);
            }
        ));

        $transactions = [];
        $actionableTransactionCount = 0;
        $queuedActionableTransactionCount = 0;

        foreach (($customer['transactions'] ?? []) as $transaction) {
            if (!is_array($transaction)) {
                continue;
            }

            $transaction['invoice_collection_id'] = isset($transaction['invoice_collection_id']) && (int)$transaction['invoice_collection_id'] > 0
                ? (int)$transaction['invoice_collection_id']
                : null;
            $transaction['queue_status'] = $transaction['queue_status'] ?? null;
            $transaction['queue_job_id'] = $transaction['queue_job_id'] ?? null;

            $isActionable = !($transaction['booked'] ?? false) && !($transaction['excluded'] ?? false);
            if ($isActionable) {
                $actionableTransactionCount++;
            }

            $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
            if ($invoiceCollectionId > 0 && isset($queueJobsByCollectionId[$invoiceCollectionId])) {
                $queueJob = $queueJobsByCollectionId[$invoiceCollectionId];
                $transaction['queue_status'] = $queueJob['queue_status'] ?? null;
                $transaction['queue_job_id'] = $queueJob['queue_job_id'] ?? null;

                if ($isActionable) {
                    $queuedActionableTransactionCount++;
                }
            }

            $transactions[] = $transaction;
        }

        $customerLevelQueueBlock = false;
        if ($actionableTransactionCount === 0 && self::customerSupportsCustomerLevelQueueBlocking($customer) && !empty($activeCustomerJobs)) {
            $customerLevelQueueBlock = true;
        }

        $isActionBlocked = false;
        if ($actionableTransactionCount > 0) {
            $isActionBlocked = $queuedActionableTransactionCount > 0
                && $queuedActionableTransactionCount === $actionableTransactionCount;
        } elseif ($customerLevelQueueBlock) {
            $isActionBlocked = true;
        }

        $statuses = [];
        $invoiceCollectionIds = [];
        foreach ($activeCustomerJobs as $job) {
            $status = (string)($job['queue_status'] ?? '');
            if ($status !== '' && !in_array($status, $statuses, true)) {
                $statuses[] = $status;
            }

            $invoiceCollectionId = (int)($job['invoice_collection_id'] ?? 0);
            if ($invoiceCollectionId > 0 && !in_array($invoiceCollectionId, $invoiceCollectionIds, true)) {
                $invoiceCollectionIds[] = $invoiceCollectionId;
            }
        }

        $customer['transactions'] = $transactions;
        $customer['queue'] = [
            'has_active_job' => !empty($activeCustomerJobs),
            'statuses' => $statuses,
            'invoice_collection_ids' => $invoiceCollectionIds,
            'is_action_blocked' => $isActionBlocked,
        ];

        if ($isActionBlocked) {
            $customer['requires_action'] = false;
        }

        return $customer;
    }

    private static function getValidCollectedInvoiceDraftOverlay(array $types, string $dateFrom, string $dateTo): array
    {
        $overlay = [
            'by_collection_id' => [],
            'by_customer_number' => [],
        ];

        $candidateInvoiceCollectionIds = [];
        $customerLevelCandidateNumbers = [];

        foreach ($types as $customers) {
            if (!is_array($customers)) {
                continue;
            }

            foreach ($customers as $customer) {
                if (!is_array($customer)) {
                    continue;
                }

                foreach (($customer['transactions'] ?? []) as $transaction) {
                    $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
                    if ($invoiceCollectionId > 0) {
                        $candidateInvoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
                    }
                }

                $customerNumber = (int)($customer['customer_number'] ?? 0);
                if ($customerNumber > 0 && self::customerSupportsCustomerLevelQueueBlocking($customer)) {
                    $customerLevelCandidateNumbers[$customerNumber] = $customerNumber;
                }
            }
        }

        if (empty($candidateInvoiceCollectionIds) && empty($customerLevelCandidateNumbers)) {
            return $overlay;
        }

        global $db;

        try {
            $whereCandidates = [];
            if (!empty($candidateInvoiceCollectionIds)) {
                $whereCandidates[] = 'id IN (' . implode(',', array_map('intval', array_values($candidateInvoiceCollectionIds))) . ')';
            }
            if (!empty($customerLevelCandidateNumbers)) {
                $dateFromEscaped = $db->escape_string($dateFrom);
                $dateToEscaped = $db->escape_string($dateTo);
                $whereCandidates[] = '(customer_number IN (' . implode(',', array_map('intval', array_values($customerLevelCandidateNumbers))) . ')
                    AND (
                        DATE(closed_at) BETWEEN \'' . $dateFromEscaped . '\' AND \'' . $dateToEscaped . '\'
                        OR DATE(created_at) BETWEEN \'' . $dateFromEscaped . '\' AND \'' . $dateToEscaped . '\'
                    ))';
            }

            if (!defined('\objects\ECONOMIC_PROCESSOR')) {
                class_exists(collected_order_invoices_o::class);
            }
            $processor = defined('\objects\ECONOMIC_PROCESSOR')
                ? (int)constant('\objects\ECONOMIC_PROCESSOR')
                : 1;
            $deletedAtFilter = self::collectedOrderInvoicesHasDeletedAtColumn()
                ? 'deleted_at IS NULL
                    AND '
                : '';
            $sql = "SELECT id, customer_number, created_at, closed_at
                FROM collected_order_invoices
                WHERE {$deletedAtFilter}processor = $processor
                    AND external_id IS NOT NULL
                    AND external_id <> ''
                    AND booked_invoice_id IS NULL
                    AND error_message IS NULL
                    AND (" . implode(' OR ', $whereCandidates) . ")";

            $result = $db->query($sql);
            if (!$result) {
                return $overlay;
            }

            while ($row = $result->fetch_assoc()) {
                $invoiceCollectionId = (int)($row['id'] ?? 0);
                $customerNumber = (int)($row['customer_number'] ?? 0);
                if ($invoiceCollectionId < 1 || $customerNumber < 1) {
                    continue;
                }

                $normalizedDraft = [
                    'invoice_collection_id' => $invoiceCollectionId,
                    'customer_number' => $customerNumber,
                    'created_at' => (string)($row['created_at'] ?? ''),
                    'closed_at' => (string)($row['closed_at'] ?? ''),
                    'is_period_relevant' => self::isInvoiceCollectionRelevantToPeriod(
                        (string)($row['created_at'] ?? ''),
                        (string)($row['closed_at'] ?? ''),
                        $dateFrom,
                        $dateTo
                    ),
                ];

                $overlay['by_collection_id'][$invoiceCollectionId] = $normalizedDraft;
                $overlay['by_customer_number'][$customerNumber] = $overlay['by_customer_number'][$customerNumber] ?? [];
                $overlay['by_customer_number'][$customerNumber][] = $normalizedDraft;
            }
        } catch (\Throwable) {
            return $overlay;
        }

        return $overlay;
    }

    private static function applyCollectedInvoiceDraftOverlayToPeriodTypes(
        array $types,
        array $draftsByCollectionId,
        array $draftsByCustomerNumber
    ): array {
        foreach ($types as $type => $customers) {
            if (!is_array($customers)) {
                continue;
            }

            $types[$type] = array_map(function ($customer) use ($draftsByCollectionId, $draftsByCustomerNumber) {
                if (!is_array($customer)) {
                    return $customer;
                }

                return self::applyCollectedInvoiceDraftOverlayToCustomer(
                    $customer,
                    $draftsByCollectionId,
                    $draftsByCustomerNumber
                );
            }, $customers);
        }

        return $types;
    }

    private static function applyCollectedInvoiceDraftOverlayToCustomer(
        array $customer,
        array $draftsByCollectionId,
        array $draftsByCustomerNumber
    ): array {
        $customerNumber = (int)($customer['customer_number'] ?? 0);
        $activeCustomerDrafts = array_values(array_filter(
            $draftsByCustomerNumber[$customerNumber] ?? [],
            static function ($draft): bool {
                return !empty($draft['is_period_relevant']);
            }
        ));

        $transactions = [];
        $actionableTransactionCount = 0;
        $coveredActionableTransactionCount = 0;
        $queuedActionableTransactionCount = 0;
        $draftActionableTransactionCount = 0;
        $invoiceCollectionIds = [];

        foreach (($customer['transactions'] ?? []) as $transaction) {
            if (!is_array($transaction)) {
                continue;
            }

            $transaction['invoice_collection_id'] = isset($transaction['invoice_collection_id']) && (int)$transaction['invoice_collection_id'] > 0
                ? (int)$transaction['invoice_collection_id']
                : null;

            $isActionable = !($transaction['booked'] ?? false) && !($transaction['excluded'] ?? false);
            if (!$isActionable) {
                $transactions[] = $transaction;
                continue;
            }

            $actionableTransactionCount++;
            $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
            $isQueued = !empty($transaction['queue_status']);
            $isDraft = $invoiceCollectionId > 0 && isset($draftsByCollectionId[$invoiceCollectionId]);

            if ($isQueued || $isDraft) {
                $coveredActionableTransactionCount++;
            }
            if ($isQueued) {
                $queuedActionableTransactionCount++;
            }
            if ($isDraft) {
                $draftActionableTransactionCount++;
                $invoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
            }

            $transactions[] = $transaction;
        }

        foreach ($activeCustomerDrafts as $draft) {
            $invoiceCollectionId = (int)($draft['invoice_collection_id'] ?? 0);
            if ($invoiceCollectionId > 0) {
                $invoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
            }
        }

        $isDraftActionBlocked = false;
        $queue = is_array($customer['queue'] ?? null) ? $customer['queue'] : self::getDefaultQueueSummary();
        if ($actionableTransactionCount > 0 && $coveredActionableTransactionCount === $actionableTransactionCount) {
            if ($queuedActionableTransactionCount > 0) {
                $queue['is_action_blocked'] = true;
            } elseif ($draftActionableTransactionCount > 0) {
                $isDraftActionBlocked = true;
            }
        } elseif (
            $actionableTransactionCount === 0
            && self::customerSupportsCustomerLevelQueueBlocking($customer)
            && !empty($activeCustomerDrafts)
            && empty($queue['is_action_blocked'])
        ) {
            $isDraftActionBlocked = true;
        }

        $customer['transactions'] = $transactions;
        $customer['queue'] = $queue;
        $customer['draft'] = [
            'has_valid_draft' => !empty($invoiceCollectionIds),
            'invoice_collection_ids' => array_values($invoiceCollectionIds),
            'is_action_blocked' => $isDraftActionBlocked,
        ];

        if ($isDraftActionBlocked || !empty($queue['is_action_blocked'])) {
            $customer['requires_action'] = false;
        }

        return $customer;
    }

    /**
     * Fetch the collected-invoice rows referenced by the complete period payload in one local query.
     * No e-conomic calls are allowed from the period endpoint.
     *
     * @return array<int,array<string,mixed>> Rows keyed by collected invoice ID.
     */
    private static function getPeriodInvoiceCollectionMetadata(array $types): array
    {
        global $db;

        $invoiceCollectionIds = [];
        foreach ($types as $customers) {
            if (!is_array($customers)) {
                continue;
            }

            foreach ($customers as $customer) {
                if (!is_array($customer)) {
                    continue;
                }

                foreach (($customer['transactions'] ?? []) as $transaction) {
                    $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
                    if ($invoiceCollectionId > 0) {
                        $invoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
                    }
                }

                foreach (['queue', 'draft'] as $summaryKey) {
                    foreach (($customer[$summaryKey]['invoice_collection_ids'] ?? []) as $invoiceCollectionId) {
                        $invoiceCollectionId = (int)$invoiceCollectionId;
                        if ($invoiceCollectionId > 0) {
                            $invoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
                        }
                    }
                }
            }
        }

        if (empty($invoiceCollectionIds)) {
            return [];
        }

        $sql = 'SELECT id, customer_number, name, notes, processor, external_id, booked_invoice_id, '
            . 'po_number, error_message, closed_at, created_at, updated_at '
            . 'FROM collected_order_invoices '
            . 'WHERE id IN (' . implode(',', array_map('intval', array_values($invoiceCollectionIds))) . ') '
            . 'ORDER BY id';

        try {
            $result = $db->query($sql);
            if (!$result) {
                return [];
            }

            $metadata = [];
            while ($row = $result->fetch_assoc()) {
                $invoiceCollectionId = (int)($row['id'] ?? 0);
                if ($invoiceCollectionId < 1) {
                    continue;
                }

                $row['id'] = $invoiceCollectionId;
                $row['invoice_collection_id'] = $invoiceCollectionId;
                $row['customer_number'] = (int)($row['customer_number'] ?? 0);
                $row['processor'] = (int)($row['processor'] ?? 0);
                $row['booked_invoice_id'] = !empty($row['booked_invoice_id'])
                    ? (int)$row['booked_invoice_id']
                    : null;
                $row['state'] = self::periodInvoiceCollectionState($row);
                $metadata[$invoiceCollectionId] = $row;
            }

            return $metadata;
        } catch (\Throwable) {
            return [];
        }
    }

    private static function periodInvoiceCollectionState(array $invoiceCollection): string
    {
        if (!empty($invoiceCollection['booked_invoice_id'])) {
            return 'economic_booked';
        }

        if (!defined('\\objects\\ECONOMIC_PROCESSOR')) {
            class_exists(collected_order_invoices_o::class);
        }
        $economicProcessor = defined('\\objects\\ECONOMIC_PROCESSOR')
            ? (int)constant('\\objects\\ECONOMIC_PROCESSOR')
            : 1;
        if (
            (int)($invoiceCollection['processor'] ?? 0) === $economicProcessor
            && trim((string)($invoiceCollection['external_id'] ?? '')) !== ''
            && trim((string)($invoiceCollection['error_message'] ?? '')) === ''
        ) {
            return 'economic_draft';
        }

        return !empty($invoiceCollection['closed_at']) ? 'closed' : 'open';
    }

    private static function applyPeriodInvoiceStateOverlayToTypes(array $types, array $invoiceCollectionsById): array
    {
        foreach ($types as $type => $customers) {
            if (!is_array($customers)) {
                continue;
            }

            $types[$type] = array_map(static function ($customer) use ($invoiceCollectionsById) {
                if (!is_array($customer)) {
                    return $customer;
                }

                return self::applyPeriodInvoiceStateOverlayToCustomer($customer, $invoiceCollectionsById);
            }, $customers);
        }

        return $types;
    }

    private static function applyPeriodInvoiceStateOverlayToCustomer(array $customer, array $invoiceCollectionsById): array
    {
        $customerNumber = (int)($customer['customer_number'] ?? 0);
        $collectionStats = [];
        $invoiceCollectionIds = [];
        $transactions = [];

        foreach (($customer['transactions'] ?? []) as $transaction) {
            if (!is_array($transaction)) {
                continue;
            }

            $invoiceCollectionId = (int)($transaction['invoice_collection_id'] ?? 0);
            $invoiceCollection = $invoiceCollectionsById[$invoiceCollectionId] ?? null;
            $canUseCollection = is_array($invoiceCollection)
                && (int)($invoiceCollection['customer_number'] ?? 0) === $customerNumber;

            if ($canUseCollection) {
                $transaction['invoice_state'] = (string)$invoiceCollection['state'];
                $transaction['booked'] = $transaction['invoice_state'] === 'economic_booked';
                $invoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
                $collectionStats[$invoiceCollectionId] = $collectionStats[$invoiceCollectionId] ?? [
                    'order_ids' => [],
                    'total_net_amount' => 0.0,
                ];
                $orderId = (int)($transaction['id'] ?? 0);
                if ($orderId > 0) {
                    $collectionStats[$invoiceCollectionId]['order_ids'][$orderId] = $orderId;
                }
                $collectionStats[$invoiceCollectionId]['total_net_amount'] += (float)($transaction['amount'] ?? 0);
            } else {
                $transaction['invoice_state'] = (string)($transaction['invoice_state'] ?? self::periodOrderState(
                    (bool)($transaction['booked'] ?? false),
                    !empty($transaction['completed_at']) ? (string)$transaction['completed_at'] : null
                ));
            }

            $transactions[] = $transaction;
        }

        foreach (['queue', 'draft'] as $summaryKey) {
            foreach (($customer[$summaryKey]['invoice_collection_ids'] ?? []) as $invoiceCollectionId) {
                $invoiceCollectionId = (int)$invoiceCollectionId;
                if (
                    $invoiceCollectionId > 0
                    && isset($invoiceCollectionsById[$invoiceCollectionId])
                    && (int)($invoiceCollectionsById[$invoiceCollectionId]['customer_number'] ?? 0) === $customerNumber
                ) {
                    $invoiceCollectionIds[$invoiceCollectionId] = $invoiceCollectionId;
                }
            }
        }

        $invoiceCollections = [];
        foreach ($invoiceCollectionIds as $invoiceCollectionId) {
            $invoiceCollection = $invoiceCollectionsById[$invoiceCollectionId];
            $stats = $collectionStats[$invoiceCollectionId] ?? [
                'order_ids' => [],
                'total_net_amount' => 0.0,
            ];
            $orderIds = array_values($stats['order_ids']);
            $invoiceCollection['order_ids'] = $orderIds;
            $invoiceCollection['order_count'] = count($orderIds);
            $invoiceCollection['total_net_amount'] = (float)$stats['total_net_amount'];
            $invoiceCollections[] = $invoiceCollection;
        }

        $customer['transactions'] = $transactions;
        $customer['invoice_collections'] = $invoiceCollections;
        return $customer;
    }

    private static function customerSupportsCustomerLevelQueueBlocking(array $customer): bool
    {
        $meta = $customer['meta'] ?? [];

        return isset($meta['fixed_pricing'])
            || isset($meta['wash_subscription'])
            || !empty($meta['has_vehicle_subscription']);
    }

    /**
     * @throws Exception
     */
    private static function getVehicleSubscriptions(string $dateFrom, string $dateTo, array $customersWithTransactions = null, ?array $onlyCustomerNumbers = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo, $onlyCustomerNumbers);
        }
        // Since these are monthly subscriptions, we don't need to filter by transactions
        $customer_numbers = self::filterCustomerNumbers(
            (new \objects\users_o())->getCustomersWithVehicleSubscriptions(),
            $onlyCustomerNumbers
        );
        // Get all customers with vehicle subscriptions
        $subscriptions = [];
        $customer_names = (new \objects\users_o())->getCustomerNames(array_map('intval', $customer_numbers), false);
        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $customer = $customers_by_number[(int)$customer_number] ?? null;
            if ($customer !== null) {
                $customer['meta'] = array_merge($customer['meta'] ?? [], [
                    'has_vehicle_subscription' => true,
                ]);
                $subscriptions[] = $customer;
                continue;
            }

            $subscriptions[] = self::constructCustomerObject(
                (int)$customer_number,
                $customer_names[(int)$customer_number] ?? 'Unknown Customer',
                [],
                true,
                null,
                [
                    'has_vehicle_subscription' => true,
                ]
            );
        }
        return $subscriptions;
    }

    /**
     * Get the customer from the list of customers with transactions.
     *
     * @param int $customer_number The customer number to search for.
     * @param array $customersWithTransactions The list of customers with transactions.
     * @return array|null The customer object if found, null otherwise.
     */
    private static function getCustomerFromList(int $customer_number, array $customersWithTransactions): ?array
    {
        $direct = $customersWithTransactions[$customer_number] ?? null;
        if (is_array($direct) && (int)($direct['customer_number'] ?? 0) === $customer_number) {
            return $direct;
        }

        // Search for the customer in the list of customers with transactions
        foreach ( $customersWithTransactions as $customer ) {
            if ($customer['customer_number'] === $customer_number) {
                return $customer;
            }
        }
        // If the customer is not found, return null
        return null;
    }

    /**
     * @throws Exception
     */
    private static function getFixedPricing(string $dateFrom, string $dateTo, array $customersWithTransactions = null, ?array $onlyCustomerNumbers = null): array
    {
        // Get all customers with fixed pricing
        $customer_numbers = self::filterCustomerNumbers(
            (new \objects\users_o())->getCustomersWithFixedPricing(),
            $onlyCustomerNumbers
        );

        // If customersWithTransactions is not provided, only resolve transaction customers for fixed-pricing customers.
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo, $customer_numbers);
        }

        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);

        // Get the customers fixed pricing
        $tmp_fixed_pricing = array_map(function ($arr) {
            // Return the customer number and price
            return [
                'customer_number' => (int)$arr['customer_number'],
                'price' => (float)$arr['price'],
                'description' => (string)($arr['description'] ?? ''),
            ];
        }, (new \objects\customer_fixed_pricing_o())->getFieldsWhere(['customer_number' => $customer_numbers], ['customer_number', 'price', 'description']));

        $fixed_pricing_by_customer_number = [];
        foreach ( $tmp_fixed_pricing as $item ) {
            $fixed_pricing_by_customer_number[(int)$item['customer_number']] = $item;
        }

        $customer_names = (new \objects\users_o())->getCustomerNames(array_map('intval', $customer_numbers), false);

        // Get all customers with fixed pricing
        $fixed_pricing = [];
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $customer_number = (int)$customer_number;
            $fixed_pricing[] = $customers_by_number[$customer_number] ?? null;
            // Check if the last entry is null, if so, create a new customer object
            if (end($fixed_pricing) === null) {
                $fixed_pricing[count($fixed_pricing) - 1] = self::constructCustomerObject(
                    $customer_number,
                    $customer_names[$customer_number] ?? 'Unknown Customer',
                    [],
                    true,
                );
            }
            // Add the fixed pricing to the customer object
            $fixed_pricing[count($fixed_pricing) - 1]['meta']['fixed_pricing'] = $fixed_pricing_by_customer_number[$customer_number] ?? null;
        }
        return $fixed_pricing;
    }

    /**
     * Get an object from an array based on a callback function.
     *
     * @param array $array The array to search in.
     * @param callable $callback The callback function to use for searching.
     * @return mixed|null The found object or null if not found.
     */
    private static function getObjectFromArray(array $array, callable $callback): mixed
    {
        // Iterate through the array and apply the callback function to each element
        foreach ( $array as $item ) {
            // If the callback returns true, return the item
            if ($callback($item)) {
                return $item;
            }
        }
        // If no item matches the callback, return null
        return null;
    }

    /**
     * @throws Exception
     */
    private static function getTankCleaning(string $dateFrom, string $dateTo, array $customersWithTransactions = null, ?array $onlyCustomerNumbers = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo, $onlyCustomerNumbers);
        }
        // Filter out customers that do not have any transactions in the specified date range
        $customer_numbers = self::filterCustomerNumbers(
            (new \objects\users_o())->getCustomersWithTankCleaning(),
            $onlyCustomerNumbers
        );
        self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);
        // Get all customers with tank cleaning
        $tank_cleaning = [];
        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $customer = $customers_by_number[(int)$customer_number] ?? null;
            if ($customer !== null) {
                $tank_cleaning[] = $customer;
            }
            // Add the tank cleaning to the list if it has transactions
        }
        return $tank_cleaning;
    }

    /**
     * Filters the customer numbers based on whether they have transactions in the specified date range.
     *
     * @param array $customer_numbers The customer numbers to filter.
     * @param array $customersWithTransactions The customers with transactions in the specified date range.
     */
    private static function filterCustomersWithTransactions(array &$customer_numbers, array $customersWithTransactions): void
    {
        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);
        $customer_numbers = array_values(array_filter($customer_numbers, static function ($customer_number) use ($customers_by_number): bool {
            return isset($customers_by_number[(int)$customer_number]);
        }));
    }

    /**
     * @throws Exception
     */
    private static function getSpecialArrangements(string $dateFrom, string $dateTo, array $customersWithTransactions = null, ?array $onlyCustomerNumbers = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo, $onlyCustomerNumbers);
        }
        // Get all customers with tank cleaning
        $customer_numbers = self::filterCustomerNumbers(
            (new \objects\users_o())->getCustomersWithSpecialArrangements(),
            $onlyCustomerNumbers
        );
        // Filter out customers that do not have any transactions in the specified date range
        self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);
        $special_arrangements = [];
        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            $customer = $customers_by_number[(int)$customer_number] ?? null;
            if ($customer !== null) {
                $special_arrangements[] = $customer;
            }
        }
        return $special_arrangements;
    }

    /**
     * @throws Exception
     */
    private static function getInvoicingPerOrder(string $dateFrom, string $dateTo, array $customersWithTransactions = null, ?array $onlyCustomerNumbers = null): array
    {
        // Get all customers with the invoicing per order attribute
        $customer_numbers = self::filterCustomerNumbers(
            (new users_o())->getCustomerNumbersWithAttributes(['invoiceAllOrdersIndividually']),
            $onlyCustomerNumbers
        );
        // Filter out customers that do not have any transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo, $onlyCustomerNumbers);
        }
        self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions);

        $invoicing_per_order = [];
        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);
        /** @var int $customer_number */
        foreach ( $customer_numbers as $customer_number ) {
            // Add the invoicing per order to the list
            $customer = $customers_by_number[(int)$customer_number] ?? null;
            if ($customer !== null) {
                $invoicing_per_order[] = $customer;
            }
        }
        return $invoicing_per_order;
    }

    /**
     * @throws Exception
     */
    private static function getPossibleDuplicates(string $dateFrom, string $dateTo, array $customersWithTransactions = null, ?array $onlyCustomerNumbers = null): array
    {
        // If customersWithTransactions is not provided, get all customers with transactions in the specified date range
        if ($customersWithTransactions === null) {
            $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo, $onlyCustomerNumbers);
        }
        $allowedCustomerNumbers = $onlyCustomerNumbers !== null
            ? array_fill_keys(self::normalizeCustomerNumbers($onlyCustomerNumbers), true)
            : null;
        $ordersByRegistration = [];
        foreach ($customersWithTransactions as $customer) {
            foreach (($customer['transactions'] ?? []) as $transaction) {
                $transaction = self::constructTransactionObject($transaction);
                $registration = trim((string)($transaction['reg_1'] ?? ''));
                if ($registration === '') {
                    continue;
                }
                $customerNumber = (int)($transaction['customer_number'] ?? 0);
                if ($allowedCustomerNumbers !== null && !isset($allowedCustomerNumbers[$customerNumber])) {
                    continue;
                }
                $ordersByRegistration[$registration][] = [
                    'id' => (int)$transaction['id'],
                    'created_at' => (string)$transaction['date'],
                    'customer_number' => $customerNumber,
                    'object' => $transaction,
                ];
            }
        }
        $orders = invoicing_period_utils::filterPossibleDuplicates($ordersByRegistration, 86400);
        $tmp_customer_arr = [];
        // Remove duplicates from the customer numbers
        $possible_duplicates = [];
        $customer_names = [];
        foreach ($orders as $order) {
            $customer_names[(int)($order[0]['customer_number'] ?? 0)] = true;
        }
        $customer_names = (new \objects\users_o())->getCustomerNames(array_keys($customer_names), false);
        $customers_by_number = self::indexCustomersByNumber($customersWithTransactions);
        /** @var int $customer_number */
        foreach ( $orders as $order ) {
            // Get the customer number from the order
            $customer_number = (int)($order[0]['customer_number'] ?? 0);
            if ($allowedCustomerNumbers !== null && !isset($allowedCustomerNumbers[$customer_number])) {
                continue;
            }
            // Check if the customer number is already in the array
            if (isset($tmp_customer_arr[$customer_number])) {
                continue;
            }
            // Add the customer number to the array
            $tmp_customer_arr[$customer_number] = true;
            // Get the customer from the list of customers with transactions
            $customer = $customers_by_number[$customer_number] ?? null;
            // Add the customer to the possible duplicates array
            $possible_duplicates[] = self::constructCustomerObject(
                $customer_number,
                $customer_names[$customer_number] ?? 'Unknown Customer',
                array_map(function ($transaction) {
                    // Construct the transaction object from the order
                    return $transaction['object'];
                }, $order),
                false, // Requires action because there are possible duplicates
                $customer['id'] ?? null // Use the id from the customer object if available
            );
        }
        return $possible_duplicates;
    }
}
