<?php

namespace classes;

use DateInterval;
use DatePeriod;
use DateTime;
use Exception;
use objects\departments_o;
use objects\orders_o;
use objects\products_o;
use objects\users_o;

class economic_v2_distribution_service
{
    private const SYSTEM_ORDER_DEPARTMENT_ID = 10;
    private const BOOKED_DEPARTMENT_75 = 75;
    private const BOOKED_INVOICE_PAGE_SIZE = 1000;
    private const BOOKED_INVOICE_MAX_PAGES = 10;
    private const EPSILON = 0.00001;
    private const FIXED_PRICING_SYSTEM_ORDER_REFERENCE = 'Fast pris aftale';
    private const WASH_SUBSCRIPTION_SYSTEM_ORDER_REFERENCE = 'Vaskeabonnementer';

    private economic_v2_versioning_service $versioning;
    private ?economic $economic = null;
    private array $department_name_cache = [];
    private array $department_excluded_cache = [];
    private array $customer_name_cache = [];
    private array $customer_inclusion_cache = [];
    private array $product_cache = [];
    private array $product_department_price_cache = [];
    private array $discount_resolution_cache = [];
    private array $orders_in_range_cache = [];
    private array $order_items_by_order_ids_cache = [];
    private array $booked_invoices_cache = [];
    private array $booked_invoice_lines_cache = [];
    private array $vehicle_subscription_versions_in_range_cache = [];
    private array $version_table_has_rows_cache = [];
    private bool $best_effort_backfill_attempted = false;

    public function __construct(?economic_v2_versioning_service $versioning = null, ?economic $economic = null)
    {
        department_customer_price_overrides_schema_bootstrap::ensureTables();
        $this->versioning = $versioning ?? new economic_v2_versioning_service();
        $this->economic = $economic;
    }

    public function getAllDistributions(string $date_from, string $date_to): array
    {
        $this->ensureVersionHistoryAvailable([
            'fixed_pricing',
            'vehicle_subscriptions',
            'discount_overrides',
        ]);

        $fixed_pricing = $this->getFixedPricingDistribution($date_from, $date_to);
        $wash_subscriptions = $this->getWashSubscriptionsDistribution($date_from, $date_to);

        return [
            'fixed_pricing' => $fixed_pricing,
            'wash_subscriptions' => $wash_subscriptions,
            'customer_prices' => $this->getCustomerPricesDistribution($date_from, $date_to),
            'booked_department_75' => $this->buildBookedDepartment75Distribution(
                $date_from,
                $date_to,
                $fixed_pricing,
                $wash_subscriptions
            ),
        ];
    }

    public function getBookedDepartment75Distribution(string $date_from, string $date_to): array
    {
        $this->ensureVersionHistoryAvailable([
            'fixed_pricing',
            'vehicle_subscriptions',
        ]);

        return $this->buildBookedDepartment75Distribution(
            $date_from,
            $date_to,
            $this->getFixedPricingDistribution($date_from, $date_to),
            $this->getWashSubscriptionsDistribution($date_from, $date_to)
        );
    }

    public function getFixedPricingDistribution(string $date_from, string $date_to): array
    {
        $this->ensureVersionHistoryAvailable(['fixed_pricing']);

        [$from_ts, $to_ts] = $this->buildDateRange($date_from, $date_to);
        $orders = $this->fetchOrdersInRange($from_ts, $to_ts);
        $order_items = $this->fetchOrderItemsByOrderIds(array_map(static fn($o) => (int)$o['id'], $orders));
        $collected = $this->collectFixedPricingData($orders, $order_items);
        if (empty($collected['groups'])) {
            $fallback = $this->collectFixedPricingData($orders, $order_items, true);
            if (!empty($fallback['groups'])) {
                $fallback['warnings'][] = 'System order fallback used for fixed pricing (department 10).';
                $collected = $fallback;
            }
        }

        $groups = $collected['groups'];
        $customer_transactions = $collected['customer_transactions'];
        $warnings = $collected['warnings'];

        $customers = [];
        $collective = [
            'total_fixed_price' => 0.0,
            'total_original_price' => 0.0,
            'total_department_totals' => [],
            'total_department_totals_relative' => [],
        ];

        foreach ($groups as $group) {
            $customer_number = (int)$group['customer_number'];
            if (!isset($customers[$customer_number])) {
                $customers[$customer_number] = $this->buildCustomerEnvelope($customer_number, $customer_transactions[$customer_number] ?? []);
                $customers[$customer_number]['meta']['fixed_pricing'] = [
                    'price' => 0.0,
                    'original_price' => 0.0,
                    'department_totals' => [],
                    'department_totals_relative' => [],
                    'version_groups' => [],
                ];
            }

            $group_original = (float)$group['original_price'];
            $group_price = (float)$group['price'];
            $relative_department_totals = [];
            $group_total_department_amount = array_sum($group['department_totals']);

            foreach ($group['department_totals'] as $department_id => $department_amount) {
                $department_id = (int)$department_id;
                $relative_amount = 0.0;
                if ($group_total_department_amount > 0.0) {
                    $relative_amount = ((float)$department_amount / $group_total_department_amount) * $group_price;
                }
                $relative_department_totals[$department_id] = $relative_amount;

                if (!isset($customers[$customer_number]['meta']['fixed_pricing']['department_totals'][$department_id])) {
                    $customers[$customer_number]['meta']['fixed_pricing']['department_totals'][$department_id] = 0.0;
                }
                if (!isset($customers[$customer_number]['meta']['fixed_pricing']['department_totals_relative'][$department_id])) {
                    $customers[$customer_number]['meta']['fixed_pricing']['department_totals_relative'][$department_id] = 0.0;
                }
                $customers[$customer_number]['meta']['fixed_pricing']['department_totals'][$department_id] += (float)$department_amount;
                $customers[$customer_number]['meta']['fixed_pricing']['department_totals_relative'][$department_id] += (float)$relative_amount;

                if (!isset($collective['total_department_totals'][$department_id])) {
                    $collective['total_department_totals'][$department_id] = 0.0;
                }
                if (!isset($collective['total_department_totals_relative'][$department_id])) {
                    $collective['total_department_totals_relative'][$department_id] = 0.0;
                }
                $collective['total_department_totals'][$department_id] += (float)$department_amount;
                $collective['total_department_totals_relative'][$department_id] += (float)$relative_amount;
            }

            if ($group_total_department_amount <= 0.0) {
                $warnings[] = 'Fixed pricing group has no transaction basis for customer ' . $customer_number . ' in ' . $group['month'];
            }

            $customers[$customer_number]['meta']['fixed_pricing']['price'] += $group_price;
            $customers[$customer_number]['meta']['fixed_pricing']['original_price'] += $group_original;
            $customers[$customer_number]['meta']['fixed_pricing']['version_groups'][] = [
                'version_id' => (int)$group['version_id'],
                'month' => (string)$group['month'],
                'price' => round($group_price, 5),
                'description' => (string)$group['description'],
                'source' => (string)$group['source'],
                'confidence' => round((float)$group['confidence'], 5),
                'inferred' => (bool)$group['inferred'],
                'effective_from' => (string)$group['effective_from'],
                'effective_to' => $group['effective_to'] !== null ? (string)$group['effective_to'] : null,
                'original_price' => round($group_original, 5),
                'department_totals' => $this->roundMap($group['department_totals']),
                'department_totals_relative' => $this->roundMap($relative_department_totals),
                'order_ids' => array_values(array_unique(array_map('intval', $group['order_ids']))),
            ];

            $collective['total_fixed_price'] += $group_price;
            $collective['total_original_price'] += $group_original;
        }

        $customers = array_values(array_map(function ($customer) {
            if (isset($customer['meta']['fixed_pricing'])) {
                $customer['meta']['fixed_pricing']['price'] = round((float)$customer['meta']['fixed_pricing']['price'], 5);
                $customer['meta']['fixed_pricing']['original_price'] = round((float)$customer['meta']['fixed_pricing']['original_price'], 5);
                $customer['meta']['fixed_pricing']['department_totals'] = $this->roundMap($customer['meta']['fixed_pricing']['department_totals']);
                $customer['meta']['fixed_pricing']['department_totals_relative'] = $this->roundMap($customer['meta']['fixed_pricing']['department_totals_relative']);
            }
            return $customer;
        }, $customers));

        return [
            'customers' => $customers,
            'collective_results' => [
                'total_fixed_price' => round((float)$collective['total_fixed_price'], 5),
                'total_original_price' => round((float)$collective['total_original_price'], 5),
                'total_department_totals' => $this->roundMap($collective['total_department_totals']),
                'total_department_totals_relative' => $this->roundMap($collective['total_department_totals_relative']),
                'total_department_totals_parsed' => $this->parseDepartmentMap($collective['total_department_totals']),
                'total_department_totals_relative_parsed' => $this->parseDepartmentMap($collective['total_department_totals_relative']),
            ],
            'warnings' => array_values(array_unique($warnings)),
        ];
    }

    public function getWashSubscriptionsDistribution(string $date_from, string $date_to): array
    {
        $this->ensureVersionHistoryAvailable(['vehicle_subscriptions']);

        [$from_ts, $to_ts] = $this->buildDateRange($date_from, $date_to);
        $orders = $this->fetchOrdersInRange($from_ts, $to_ts);
        $order_items = $this->fetchOrderItemsByOrderIds(array_map(static fn($o) => (int)$o['id'], $orders));
        $months = $this->listMonthKeys($from_ts, $to_ts);
        $collected = $this->collectWashSubscriptionData($orders, $order_items);
        if (empty($collected['groups'])) {
            $fallback = $this->collectWashSubscriptionData($orders, $order_items, true);
            if (!empty($fallback['groups'])) {
                $fallback['warnings'][] = 'System order fallback used for wash subscriptions (department 10).';
                $collected = $fallback;
            }
        }

        $groups = $collected['groups'];
        $customer_transactions = $collected['customer_transactions'];
        $customer_department_month_map = $collected['customer_department_month_map'];
        $warnings = $collected['warnings'];

        $version_rows = $this->fetchVehicleSubscriptionVersionRows($from_ts, $to_ts);
        foreach ($version_rows as $row) {
            $customer_number = (int)$row['customer_number'];
            if (!$this->shouldIncludeCustomerNumber($customer_number)) {
                continue;
            }
            $reg = (string)$row['reg'];
            $version_id = (int)$row['id'];
            $vehicle_type = (int)$row['vehicle_type'];
            $monthly_price = $this->getSubscriptionMonthlyPrice($vehicle_type);
            if ($monthly_price <= 0.0) {
                continue;
            }

            foreach ($months as $month_key) {
                $month_start = $month_key . '-01 00:00:00';
                $month_end = date('Y-m-t 23:59:59', strtotime($month_start));
                if (!$this->versionOverlaps($row, $month_start, $month_end)) {
                    continue;
                }

                $group_key = $customer_number . '|' . $reg . '|' . $version_id . '|' . $month_key;
                if (isset($groups[$group_key])) {
                    continue;
                }

                $fallback_distribution = $this->buildSubscriptionFallbackDistribution(
                    $customer_number,
                    $month_key,
                    $monthly_price,
                    $customer_department_month_map
                );
                $groups[$group_key] = [
                    'customer_number' => $customer_number,
                    'reg' => $reg,
                    'vehicle_type' => $vehicle_type,
                    'version_id' => $version_id,
                    'month' => $month_key,
                    'monthly_price' => $monthly_price,
                    'source' => (string)($row['source'] ?? 'unknown'),
                    'confidence' => (float)($row['confidence'] ?? 0),
                    'inferred' => (bool)($row['inferred'] ?? false),
                    'distribution' => $fallback_distribution,
                    'order_ids' => [],
                    'fallback' => true,
                ];
                $warnings[] = 'Fallback allocation used for subscription ' . $reg . ' customer ' . $customer_number . ' in ' . $month_key;
            }
        }

        $customers = [];
        $collective = [
            'total_subscription_price' => 0.0,
            'subscription_price_department_distribution' => [],
        ];

        foreach ($groups as $group) {
            $customer_number = (int)$group['customer_number'];
            if (!isset($customers[$customer_number])) {
                $customers[$customer_number] = $this->buildCustomerEnvelope($customer_number, $customer_transactions[$customer_number] ?? []);
                $customers[$customer_number]['meta']['subscription'] = [
                    'subscription_total' => 0.0,
                    'subscription_price_department_distribution' => [],
                    'version_groups' => [],
                ];
            }

            $allocation = $this->normalizeSubscriptionGroupAllocation($group['distribution'], (float)$group['monthly_price']);
            foreach ($allocation as $department_id => $amount) {
                if (!isset($customers[$customer_number]['meta']['subscription']['subscription_price_department_distribution'][$department_id])) {
                    $customers[$customer_number]['meta']['subscription']['subscription_price_department_distribution'][$department_id] = 0.0;
                }
                $customers[$customer_number]['meta']['subscription']['subscription_price_department_distribution'][$department_id] += $amount;

                if (!isset($collective['subscription_price_department_distribution'][$department_id])) {
                    $collective['subscription_price_department_distribution'][$department_id] = 0.0;
                }
                $collective['subscription_price_department_distribution'][$department_id] += $amount;
            }

            $customers[$customer_number]['meta']['subscription']['subscription_total'] += (float)$group['monthly_price'];
            $customers[$customer_number]['meta']['subscription']['version_groups'][] = [
                'version_id' => (int)$group['version_id'],
                'month' => (string)$group['month'],
                'reg' => (string)$group['reg'],
                'vehicle_type' => (int)$group['vehicle_type'],
                'monthly_price' => round((float)$group['monthly_price'], 5),
                'source' => (string)$group['source'],
                'confidence' => round((float)$group['confidence'], 5),
                'inferred' => (bool)$group['inferred'],
                'fallback' => (bool)$group['fallback'],
                'department_distribution' => $this->roundMap($allocation),
                'order_ids' => array_values(array_unique(array_map('intval', $group['order_ids']))),
            ];

            $collective['total_subscription_price'] += (float)$group['monthly_price'];
        }

        $customers = array_values(array_map(function ($customer) {
            if (isset($customer['meta']['subscription'])) {
                $customer['meta']['subscription']['subscription_total'] = round((float)$customer['meta']['subscription']['subscription_total'], 5);
                $customer['meta']['subscription']['subscription_price_department_distribution'] = $this->roundMap(
                    $customer['meta']['subscription']['subscription_price_department_distribution']
                );
            }
            return $customer;
        }, $customers));

        return [
            'customers' => $customers,
            'collective_results' => [
                'total_subscription_price' => round((float)$collective['total_subscription_price'], 5),
                'subscription_price_department_distribution' => $this->roundMap($collective['subscription_price_department_distribution']),
                'subscription_price_department_distribution_parsed' => $this->parseDepartmentMap($collective['subscription_price_department_distribution']),
            ],
            'warnings' => array_values(array_unique($warnings)),
        ];
    }

    public function getCustomerPricesDistribution(string $date_from, string $date_to): array
    {
        $this->ensureVersionHistoryAvailable(['discount_overrides']);

        [$from_ts, $to_ts] = $this->buildDateRange($date_from, $date_to);
        $orders = $this->fetchOrdersInRange($from_ts, $to_ts);
        $order_items = $this->fetchOrderItemsByOrderIds(array_map(static fn($o) => (int)$o['id'], $orders));

        $customers = [];
        $collective = [
            'total_discount_amount' => 0.0,
            'department_discount_totals' => [],
        ];

        foreach ($orders as $order) {
            $order_id = (int)$order['id'];
            $customer_number = (int)$order['customer_id'];
            if (!$this->shouldIncludeCustomerNumber($customer_number)) {
                continue;
            }
            $department_id = (int)$order['department_id'];
            $created_at = (string)$order['created_at'];

            $order_is_included = $this->isOrderEligible($order);
            if (!$order_is_included) {
                continue;
            }

            $order_discount_total = 0.0;
            foreach (($order_items[$order_id] ?? []) as $item) {
                $product_id = (int)($item['product_id'] ?? 0);
                $quantity = (float)($item['quantity'] ?? 0);
                if ($product_id <= 0 || $quantity <= 0) {
                    continue;
                }

                $base_price = (float)$this->getProductDepartmentPrice($product_id, $department_id);
                if ($base_price <= 0) {
                    continue;
                }

                $discount_row = $this->resolveDiscountForProduct($customer_number, $product_id, $created_at, $department_id);
                if ($discount_row === null) {
                    continue;
                }
                if (array_key_exists('fixed_price', $discount_row) && $discount_row['fixed_price'] !== null) {
                    $fixed_price = (float)$discount_row['fixed_price'];
                    $order_discount_total += (($base_price - $fixed_price) * $quantity);
                    continue;
                }

                $discount_percentage = (float)($discount_row['discount'] ?? 0);
                if ($discount_percentage <= 0) {
                    continue;
                }

                $discount_amount = ($base_price * $quantity) * ($discount_percentage / 100);
                $order_discount_total += $discount_amount;
            }

            if (!isset($customers[$customer_number])) {
                $customers[$customer_number] = $this->buildCustomerEnvelope($customer_number, []);
                $customers[$customer_number]['meta']['customer_prices'] = [
                    'discount_total' => 0.0,
                    'department_discount_totals' => [],
                ];
            }

            $customers[$customer_number]['transactions'][] = $this->buildTransactionObject($order_id, $created_at, $department_id, $order_discount_total, $order_is_included);
            $customers[$customer_number]['meta']['customer_prices']['discount_total'] += $order_discount_total;
            if (!isset($customers[$customer_number]['meta']['customer_prices']['department_discount_totals'][$department_id])) {
                $customers[$customer_number]['meta']['customer_prices']['department_discount_totals'][$department_id] = 0.0;
            }
            $customers[$customer_number]['meta']['customer_prices']['department_discount_totals'][$department_id] += $order_discount_total;

            $collective['total_discount_amount'] += $order_discount_total;
            if (!isset($collective['department_discount_totals'][$department_id])) {
                $collective['department_discount_totals'][$department_id] = 0.0;
            }
            $collective['department_discount_totals'][$department_id] += $order_discount_total;
        }

        $customers = array_values(array_map(function ($customer) {
            if (isset($customer['meta']['customer_prices'])) {
                $customer['meta']['customer_prices']['discount_total'] = round((float)$customer['meta']['customer_prices']['discount_total'], 5);
                $customer['meta']['customer_prices']['department_discount_totals'] = $this->roundMap(
                    $customer['meta']['customer_prices']['department_discount_totals']
                );
            }
            return $customer;
        }, $customers));

        return [
            'customers' => $customers,
            'collective_results' => [
                'total_discount_amount' => round((float)$collective['total_discount_amount'], 5),
                'department_discount_totals' => $this->roundMap($collective['department_discount_totals']),
                'department_discount_totals_parsed' => $this->parseDepartmentMap($collective['department_discount_totals']),
            ],
            'warnings' => [],
        ];
    }

    private function buildBookedDepartment75Distribution(
        string $date_from,
        string $date_to,
        array $fixed_pricing,
        array $wash_subscriptions
    ): array {
        $weight_index = $this->buildBookedDepartment75WeightIndex($fixed_pricing, $wash_subscriptions);
        $transaction_map = $this->buildBookedDepartment75TransactionMap($fixed_pricing, $wash_subscriptions);
        $warnings = [];
        $groups = $this->collectBookedDepartment75Groups($date_from, $date_to, $warnings);

        $customers = [];
        $collective = [
            'booked_net_amount' => 0.0,
            'distributed_net_amount' => 0.0,
            'undistributed_net_amount' => 0.0,
            'department_distribution' => [],
        ];

        foreach ($groups as $group) {
            $customer_number = (int)$group['customer_number'];
            if (!isset($customers[$customer_number])) {
                $customers[$customer_number] = $this->buildBookedDepartment75CustomerEnvelope(
                    $customer_number,
                    $transaction_map[$customer_number] ?? []
                );
                $customers[$customer_number]['meta']['booked_department_75'] = [
                    'booked_net_amount' => 0.0,
                    'distributed_net_amount' => 0.0,
                    'undistributed_net_amount' => 0.0,
                    'department_distribution' => [],
                    'booked_groups' => [],
                ];
            }

            $redistributed = $this->redistributeBookedDepartment75Group($group, $weight_index, $warnings);
            $distributed_amount = array_sum($redistributed['department_distribution']);
            $booked_amount = (float)$group['booked_net_amount'];
            $invoice_ids = array_values(array_unique(array_map('intval', $group['invoice_ids'] ?? [])));
            sort($invoice_ids);

            $customers[$customer_number]['meta']['booked_department_75']['booked_net_amount'] += $booked_amount;
            $customers[$customer_number]['meta']['booked_department_75']['distributed_net_amount'] += $distributed_amount;
            $customers[$customer_number]['meta']['booked_department_75']['undistributed_net_amount'] += (float)$redistributed['undistributed_net_amount'];
            foreach ($redistributed['department_distribution'] as $department_id => $amount) {
                $department_id = (int)$department_id;
                if (!isset($customers[$customer_number]['meta']['booked_department_75']['department_distribution'][$department_id])) {
                    $customers[$customer_number]['meta']['booked_department_75']['department_distribution'][$department_id] = 0.0;
                }
                $customers[$customer_number]['meta']['booked_department_75']['department_distribution'][$department_id] += (float)$amount;

                if (!isset($collective['department_distribution'][$department_id])) {
                    $collective['department_distribution'][$department_id] = 0.0;
                }
                $collective['department_distribution'][$department_id] += (float)$amount;
            }

            $customers[$customer_number]['meta']['booked_department_75']['booked_groups'][] = [
                'month' => (string)$group['month'],
                'source_category' => (string)$group['source_category'],
                'invoice_ids' => $invoice_ids,
                'booked_net_amount' => round($booked_amount, 5),
                'department_distribution' => $this->roundMap($redistributed['department_distribution']),
                'undistributed_net_amount' => round((float)$redistributed['undistributed_net_amount'], 5),
            ];

            $collective['booked_net_amount'] += $booked_amount;
            $collective['distributed_net_amount'] += $distributed_amount;
            $collective['undistributed_net_amount'] += (float)$redistributed['undistributed_net_amount'];
        }

        $customers = array_values(array_map(function (array $customer): array {
            if (!isset($customer['meta']['booked_department_75'])) {
                return $customer;
            }

            $customer['meta']['booked_department_75']['booked_net_amount'] = round(
                (float)$customer['meta']['booked_department_75']['booked_net_amount'],
                5
            );
            $customer['meta']['booked_department_75']['distributed_net_amount'] = round(
                (float)$customer['meta']['booked_department_75']['distributed_net_amount'],
                5
            );
            $customer['meta']['booked_department_75']['undistributed_net_amount'] = round(
                (float)$customer['meta']['booked_department_75']['undistributed_net_amount'],
                5
            );
            $customer['meta']['booked_department_75']['department_distribution'] = $this->roundMap(
                $customer['meta']['booked_department_75']['department_distribution']
            );
            usort($customer['meta']['booked_department_75']['booked_groups'], static function (array $left, array $right): int {
                $month_compare = strcmp((string)$left['month'], (string)$right['month']);
                if ($month_compare !== 0) {
                    return $month_compare;
                }

                return strcmp((string)$left['source_category'], (string)$right['source_category']);
            });

            return $customer;
        }, $customers));
        usort($customers, static fn(array $left, array $right): int => ((int)$left['customer_number']) <=> ((int)$right['customer_number']));

        return [
            'customers' => $customers,
            'collective_results' => [
                'booked_net_amount' => round((float)$collective['booked_net_amount'], 5),
                'distributed_net_amount' => round((float)$collective['distributed_net_amount'], 5),
                'undistributed_net_amount' => round((float)$collective['undistributed_net_amount'], 5),
                'department_distribution' => $this->roundMap($collective['department_distribution']),
                'department_distribution_parsed' => $this->parseDepartmentMap($collective['department_distribution']),
            ],
            'warnings' => array_values(array_unique($warnings)),
        ];
    }

    private function buildBookedDepartment75WeightIndex(array $fixed_pricing, array $wash_subscriptions): array
    {
        $weights = [];

        foreach (($fixed_pricing['customers'] ?? []) as $customer) {
            $customer_number = (int)($customer['customer_number'] ?? 0);
            foreach ((array)($customer['meta']['fixed_pricing']['version_groups'] ?? []) as $group) {
                $this->accumulateBookedDepartment75Weights(
                    $weights,
                    $customer_number,
                    (string)($group['month'] ?? ''),
                    'fixed_pricing',
                    (array)($group['department_totals_relative'] ?? [])
                );
            }
        }

        foreach (($wash_subscriptions['customers'] ?? []) as $customer) {
            $customer_number = (int)($customer['customer_number'] ?? 0);
            foreach ((array)($customer['meta']['subscription']['version_groups'] ?? []) as $group) {
                $this->accumulateBookedDepartment75Weights(
                    $weights,
                    $customer_number,
                    (string)($group['month'] ?? ''),
                    'wash_subscriptions',
                    (array)($group['department_distribution'] ?? [])
                );
            }
        }

        return $weights;
    }

    private function accumulateBookedDepartment75Weights(
        array &$weights,
        int $customer_number,
        string $month_key,
        string $source_category,
        array $distribution
    ): void {
        if ($customer_number <= 0 || $month_key === '' || $source_category === '') {
            return;
        }

        foreach ($distribution as $department_id => $amount) {
            $department_id = (int)$department_id;
            if (!$this->isDepartmentEligible($department_id)) {
                continue;
            }

            if (!isset($weights[$customer_number][$month_key][$source_category][$department_id])) {
                $weights[$customer_number][$month_key][$source_category][$department_id] = 0.0;
            }
            $weights[$customer_number][$month_key][$source_category][$department_id] += (float)$amount;
        }
    }

    private function buildBookedDepartment75TransactionMap(array $fixed_pricing, array $wash_subscriptions): array
    {
        $transaction_map = [];
        foreach ([$fixed_pricing, $wash_subscriptions] as $response) {
            foreach (($response['customers'] ?? []) as $customer) {
                $customer_number = (int)($customer['customer_number'] ?? 0);
                if ($customer_number <= 0) {
                    continue;
                }

                foreach ((array)($customer['transactions'] ?? []) as $transaction) {
                    $transaction_id = (int)($transaction['id'] ?? 0);
                    if ($transaction_id > 0) {
                        $transaction_map[$customer_number][$transaction_id] = $transaction;
                        continue;
                    }

                    $transaction_map[$customer_number][] = $transaction;
                }
            }
        }

        return $transaction_map;
    }

    private function buildBookedDepartment75CustomerEnvelope(int $customer_number, array $transaction_map): array
    {
        try {
            return $this->buildCustomerEnvelope($customer_number, $transaction_map);
        } catch (\Throwable $e) {
            return [
                'id' => null,
                'customer_number' => $customer_number,
                'customer_name' => $this->customer_name_cache[$customer_number] ?? 'Unknown Customer',
                'transactions' => array_values($transaction_map),
                'requires_action' => false,
                'meta' => [],
            ];
        }
    }

    private function collectBookedDepartment75Groups(string $date_from, string $date_to, array &$warnings): array
    {
        $booked_invoices = $this->fetchBookedInvoicesInDateRange($date_from, $date_to, $warnings);
        $invoice_ids = [];
        foreach ($booked_invoices as $invoice_raw) {
            $invoice = $this->toArray($invoice_raw);
            $invoice_id = (int)($invoice['bookedInvoiceNumber'] ?? $invoice['booked_invoice_number'] ?? 0);
            if ($invoice_id > 0) {
                $invoice_ids[] = $invoice_id;
            }
        }

        $invoice_lines = $this->fetchBookedInvoiceLines($invoice_ids, $warnings);
        $groups = [];

        foreach ($booked_invoices as $invoice_raw) {
            $invoice = $this->toArray($invoice_raw);
            $invoice_id = (int)($invoice['bookedInvoiceNumber'] ?? $invoice['booked_invoice_number'] ?? 0);
            $customer_number = (int)($invoice['customer']['customerNumber'] ?? $invoice['customer']['customer_number'] ?? 0);
            $invoice_date = (string)($invoice['date'] ?? '');

            if ($invoice_id <= 0 || $customer_number <= 0 || $invoice_date === '') {
                continue;
            }
            if (!$this->shouldIncludeCustomerNumber($customer_number)) {
                continue;
            }

            $month_key = substr($invoice_date, 0, 7);
            foreach ($this->parseBookedDepartment75InvoiceLines($invoice_id, (array)($invoice_lines[$invoice_id] ?? []), $warnings) as $line) {
                if (abs((float)$line['booked_net_amount']) <= self::EPSILON) {
                    continue;
                }

                $source_category = (string)($line['source_category'] ?? 'unclassified');
                $group_key = $customer_number . '|' . $month_key . '|' . $source_category;
                if (!isset($groups[$group_key])) {
                    $groups[$group_key] = [
                        'customer_number' => $customer_number,
                        'month' => $month_key,
                        'source_category' => $source_category,
                        'invoice_ids' => [],
                        'booked_net_amount' => 0.0,
                    ];
                }

                $groups[$group_key]['invoice_ids'][$invoice_id] = true;
                $groups[$group_key]['booked_net_amount'] += (float)$line['booked_net_amount'];
            }
        }

        foreach ($groups as &$group) {
            $group['invoice_ids'] = array_values(array_map('intval', array_keys((array)$group['invoice_ids'])));
            sort($group['invoice_ids']);
        }
        unset($group);

        return $groups;
    }

    private function parseBookedDepartment75InvoiceLines(int $invoice_id, array $invoice_lines, array &$warnings): array
    {
        $parsed = [];
        $active_category = null;

        foreach ($invoice_lines as $line_raw) {
            $line = $this->normalizeBookedDepartment75Line($line_raw);
            $description = trim((string)($line['description'] ?? ''));
            if ($description !== '' && $this->isBookedDepartment75TransactionHeader($description)) {
                $active_category = null;
                continue;
            }

            $marker = $this->resolveBookedDepartment75Marker($description);
            if ($marker !== null) {
                $active_category = $marker;
                continue;
            }

            if (!(bool)($line['billable'] ?? false)) {
                continue;
            }

            $department_share = (float)(
                $line['department_distribution'][(string)self::BOOKED_DEPARTMENT_75]
                ?? $line['department_distribution'][self::BOOKED_DEPARTMENT_75]
                ?? 0.0
            );
            if (abs($department_share) <= self::EPSILON) {
                continue;
            }

            $booked_net_amount = (float)($line['line_net_amount'] ?? 0.0) * ($department_share / 100.0);
            $source_category = $active_category;
            if ($source_category === null) {
                $warnings[] = 'Unable to classify booked department 75 line on invoice '
                    . $invoice_id
                    . ' line '
                    . (int)($line['source_line_id'] ?? 0)
                    . '; amount remains undistributed.';
                $source_category = 'unclassified';
            }

            $parsed[] = [
                'source_category' => $source_category,
                'booked_net_amount' => $booked_net_amount,
            ];
        }

        return $parsed;
    }

    private function normalizeBookedDepartment75Line(mixed $raw_line): array
    {
        $line = $this->toArray($raw_line);
        $product_number = $line['product']['productNumber']
            ?? $line['productNumber']
            ?? $line['product']['product_number']
            ?? null;
        $quantity = isset($line['quantity']) ? (float)$line['quantity'] : 0.0;
        $unit_net_price = isset($line['unitNetPrice'])
            ? (float)$line['unitNetPrice']
            : (isset($line['unit_net_price']) ? (float)$line['unit_net_price'] : 0.0);
        $line_net_amount = isset($line['totalNetAmount'])
            ? (float)$line['totalNetAmount']
            : (isset($line['total_net_amount']) ? (float)$line['total_net_amount'] : $quantity * $unit_net_price);

        return [
            'description' => (string)($line['description'] ?? ''),
            'product_number' => $product_number !== null ? (string)$product_number : null,
            'quantity' => $quantity,
            'line_net_amount' => $line_net_amount,
            'billable' => ($product_number !== null) || abs($line_net_amount) > self::EPSILON || abs($quantity) > self::EPSILON,
            'source_line_id' => (int)($line['lineNumber'] ?? $line['line_number'] ?? $line['number'] ?? $line['userInterfaceNumber'] ?? 0),
            'department_distribution' => $this->extractBookedDepartment75Distribution($line),
        ];
    }

    private function extractBookedDepartment75Distribution(array $line): array
    {
        $distribution = [];
        $departmental_distribution = $line['departmentalDistribution'] ?? $line['departmental_distribution'] ?? null;
        if (is_array($departmental_distribution)) {
            $distributions = $departmental_distribution['distributions'] ?? null;
            if (is_array($distributions)) {
                foreach ($distributions as $entry_raw) {
                    $entry = $this->toArray($entry_raw);
                    $department_number = $entry['department']['departmentNumber']
                        ?? $entry['department']['department_number']
                        ?? null;
                    if ($department_number === null) {
                        continue;
                    }

                    $distribution[(string)$department_number] = (float)($entry['percentage'] ?? 0.0);
                }
            }

            if (empty($distribution)) {
                $fallback_number = $departmental_distribution['departmentalDistributionNumber']
                    ?? $departmental_distribution['departmental_distribution_number']
                    ?? null;
                if ($fallback_number !== null) {
                    $distribution[(string)$fallback_number] = 100.0;
                }
            }
        }

        if (empty($distribution)) {
            $department_number = $line['departmentNumber'] ?? $line['department_number'] ?? null;
            if ($department_number !== null) {
                $distribution[(string)$department_number] = 100.0;
            }
        }

        return $distribution;
    }

    private function resolveBookedDepartment75Marker(string $description): ?string
    {
        $normalized = strtolower(trim(preg_replace('/\s+/', ' ', $description)));

        return match ($normalized) {
            '# fast pris aftale', 'fast pris aftale' => 'fixed_pricing',
            '# vaskeabonnementer', 'vaskeabonnementer' => 'wash_subscriptions',
            default => null,
        };
    }

    private function isBookedDepartment75TransactionHeader(string $description): bool
    {
        return str_starts_with($description, '[') && str_ends_with($description, ']');
    }

    private function redistributeBookedDepartment75Group(array $group, array $weight_index, array &$warnings): array
    {
        $booked_amount = (float)($group['booked_net_amount'] ?? 0.0);
        if (abs($booked_amount) <= self::EPSILON) {
            return [
                'department_distribution' => [],
                'undistributed_net_amount' => 0.0,
            ];
        }

        $customer_number = (int)($group['customer_number'] ?? 0);
        $month_key = (string)($group['month'] ?? '');
        $source_category = (string)($group['source_category'] ?? 'unclassified');
        $invoice_ids = implode(', ', array_map('intval', (array)($group['invoice_ids'] ?? [])));

        if ($source_category === 'unclassified') {
            $warnings[] = 'Booked department 75 amount for customer '
                . $customer_number
                . ' in '
                . $month_key
                . ' on invoice(s) '
                . $invoice_ids
                . ' could not be classified and remains undistributed.';

            return [
                'department_distribution' => [],
                'undistributed_net_amount' => $booked_amount,
            ];
        }

        $weights = $weight_index[$customer_number][$month_key][$source_category] ?? [];
        $eligible_weights = [];
        foreach ($weights as $department_id => $amount) {
            $department_id = (int)$department_id;
            if (!$this->isDepartmentEligible($department_id)) {
                continue;
            }
            $eligible_weights[$department_id] = (float)$amount;
        }

        $weight_total = array_sum($eligible_weights);
        if (abs($weight_total) <= self::EPSILON) {
            $fallback_department_id = $this->getFallbackDistributionDepartmentId();
            $warnings[] = 'Booked department 75 '
                . $source_category
                . ' amount for customer '
                . $customer_number
                . ' in '
                . $month_key
                . ' on invoice(s) '
                . $invoice_ids
                . ' has no redistribution basis and was assigned to fallback department '
                . $fallback_department_id
                . '.';

            return [
                'department_distribution' => [
                    $fallback_department_id => $booked_amount,
                ],
                'undistributed_net_amount' => 0.0,
            ];
        }

        $distribution = [];
        foreach ($eligible_weights as $department_id => $weight) {
            $distribution[$department_id] = $booked_amount * ($weight / $weight_total);
        }

        return [
            'department_distribution' => $distribution,
            'undistributed_net_amount' => 0.0,
        ];
    }

    protected function fetchBookedInvoicesInDateRange(string $date_from, string $date_to, array &$warnings): array
    {
        $cache_key = $date_from . '|' . $date_to;
        if (isset($this->booked_invoices_cache[$cache_key])) {
            return $this->booked_invoices_cache[$cache_key];
        }

        $filters = [
            '(date$gte:' . $date_from . '$and:date$lte:' . $date_to . ')' => '',
        ];
        $all = [];

        for ($page = 0; $page < self::BOOKED_INVOICE_MAX_PAGES; $page++) {
            try {
                $response = $this->getEconomicClient()->invoices->booked->get(
                    $filters,
                    [
                        'skipPages' => $page,
                        'pageSize' => self::BOOKED_INVOICE_PAGE_SIZE,
                    ]
                );
            } catch (\Throwable $e) {
                $warnings[] = 'Failed to fetch booked e-conomic invoices for department 75 distribution: ' . $e->getMessage();
                break;
            }

            $collection = is_array($response->collection ?? null) ? $response->collection : [];
            $all = array_merge($all, $collection);

            if (count($collection) < self::BOOKED_INVOICE_PAGE_SIZE) {
                break;
            }

            if ($page + 1 >= self::BOOKED_INVOICE_MAX_PAGES) {
                $warnings[] = 'Reached pagination safety limit (max_pages='
                    . self::BOOKED_INVOICE_MAX_PAGES
                    . ') while fetching booked department 75 invoices.';
            }
        }

        $this->booked_invoices_cache[$cache_key] = $all;
        return $this->booked_invoices_cache[$cache_key];
    }

    protected function fetchBookedInvoiceLines(array $invoice_ids, array &$warnings): array
    {
        $invoice_ids = array_values(array_unique(array_filter(array_map('intval', $invoice_ids), static fn(int $id): bool => $id > 0)));
        sort($invoice_ids);
        if (empty($invoice_ids)) {
            return [];
        }

        $cache_key = implode(',', $invoice_ids);
        if (isset($this->booked_invoice_lines_cache[$cache_key])) {
            return $this->booked_invoice_lines_cache[$cache_key];
        }

        try {
            $this->booked_invoice_lines_cache[$cache_key] = $this->getEconomicClient()->invoices->booked->get_invoice_lines($invoice_ids);
        } catch (\Throwable $e) {
            $warnings[] = 'Failed to fetch booked invoice lines for department 75 distribution: ' . $e->getMessage();
            $this->booked_invoice_lines_cache[$cache_key] = [];
        }

        return $this->booked_invoice_lines_cache[$cache_key];
    }

    protected function ensureVersionHistoryAvailable(array $areas): void
    {
        if ($this->best_effort_backfill_attempted) {
            return;
        }

        foreach ($areas as $area) {
            $table = $this->getVersionTableForArea($area);
            if ($table === null || $this->versionTableHasRows($table)) {
                continue;
            }

            $this->best_effort_backfill_attempted = true;
            $this->versioning->runBestEffortBackfill();
            $this->version_table_has_rows_cache = [];
            return;
        }
    }

    protected function getVersionTableForArea(string $area): ?string
    {
        return match ($area) {
            'fixed_pricing' => 'customer_fixed_pricing_versions',
            'vehicle_subscriptions' => 'customer_vehicle_subscription_versions',
            'discount_overrides' => 'customer_discount_override_versions',
            default => null,
        };
    }

    protected function versionTableHasRows(string $table): bool
    {
        if (array_key_exists($table, $this->version_table_has_rows_cache)) {
            return $this->version_table_has_rows_cache[$table];
        }

        global $db;
        $allowed_tables = [
            'customer_fixed_pricing_versions' => true,
            'customer_vehicle_subscription_versions' => true,
            'customer_discount_override_versions' => true,
        ];
        if (!isset($allowed_tables[$table])) {
            return $this->version_table_has_rows_cache[$table] = true;
        }

        $result = $db->query("SELECT 1 FROM $table LIMIT 1");
        if (!$result) {
            return $this->version_table_has_rows_cache[$table] = false;
        }

        return $this->version_table_has_rows_cache[$table] = $result->num_rows > 0;
    }

    protected function collectFixedPricingData(array $orders, array $order_items_by_order_id, bool $system_order_fallback = false): array
    {
        $groups = [];
        $customer_transactions = [];
        $warnings = [];

        foreach ($orders as $order) {
            $order_id = (int)$order['id'];
            $customer_number = (int)$order['customer_id'];
            if (!$this->shouldIncludeCustomerNumber($customer_number)) {
                continue;
            }
            $department_id = (int)$order['department_id'];
            $created_at = (string)$order['created_at'];

            if ($system_order_fallback) {
                if (!$this->isSystemOrderCandidate($order, self::FIXED_PRICING_SYSTEM_ORDER_REFERENCE)) {
                    continue;
                }
            } elseif (!$this->isOrderEligible($order)) {
                continue;
            }
            $distribution_department_id = $system_order_fallback
                ? $this->getFallbackDistributionDepartmentId()
                : $department_id;

            $fixed_version = $this->versioning->resolveFixedPricingVersionAt($customer_number, $created_at);
            if ($fixed_version === null) {
                continue;
            }

            $month_key = substr($created_at, 0, 7);
            $group_key = $customer_number . '|' . (int)$fixed_version['id'] . '|' . $month_key;
            if (!isset($groups[$group_key])) {
                $groups[$group_key] = [
                    'customer_number' => $customer_number,
                    'version_id' => (int)$fixed_version['id'],
                    'month' => $month_key,
                    'price' => (float)($fixed_version['price'] ?? 0),
                    'description' => (string)($fixed_version['description'] ?? ''),
                    'source' => (string)($fixed_version['source'] ?? 'unknown'),
                    'confidence' => (float)($fixed_version['confidence'] ?? 0),
                    'inferred' => (bool)($fixed_version['inferred'] ?? false),
                    'effective_from' => (string)($fixed_version['effective_from'] ?? ''),
                    'effective_to' => $fixed_version['effective_to'] ?? null,
                    'original_price' => 0.0,
                    'department_totals' => [],
                    'order_ids' => [],
                ];
            }

            $order_original_price = $this->calculateOrderOriginalPrice(
                $order_items_by_order_id[$order_id] ?? [],
                $customer_number,
                $distribution_department_id,
                $created_at
            );
            $groups[$group_key]['original_price'] += $order_original_price;
            if (!isset($groups[$group_key]['department_totals'][$distribution_department_id])) {
                $groups[$group_key]['department_totals'][$distribution_department_id] = 0.0;
            }
            $groups[$group_key]['department_totals'][$distribution_department_id] += $order_original_price;
            $groups[$group_key]['order_ids'][] = $order_id;

            if (!isset($customer_transactions[$customer_number][$order_id])) {
                $customer_transactions[$customer_number][$order_id] = $this->buildTransactionObject(
                    $order_id,
                    $created_at,
                    $department_id,
                    null,
                    $this->isOrderEligible($order)
                );
            }
        }

        return [
            'groups' => $groups,
            'customer_transactions' => $customer_transactions,
            'warnings' => $warnings,
        ];
    }

    protected function collectWashSubscriptionData(array $orders, array $order_items_by_order_id, bool $system_order_fallback = false): array
    {
        $groups = [];
        $customer_transactions = [];
        $customer_department_month_map = [];
        $warnings = [];

        foreach ($orders as $order) {
            $order_id = (int)$order['id'];
            $customer_number = (int)$order['customer_id'];
            if (!$this->shouldIncludeCustomerNumber($customer_number)) {
                continue;
            }
            $department_id = (int)$order['department_id'];
            $created_at = (string)$order['created_at'];

            if ($system_order_fallback) {
                if (!$this->isSystemOrderCandidate($order, self::WASH_SUBSCRIPTION_SYSTEM_ORDER_REFERENCE)) {
                    continue;
                }
            } elseif (!$this->isOrderEligible($order)) {
                continue;
            }
            $distribution_department_id = $system_order_fallback
                ? $this->getFallbackDistributionDepartmentId()
                : $department_id;

            $month_key = substr($created_at, 0, 7);
            if (!isset($customer_department_month_map[$customer_number][$month_key][$distribution_department_id])) {
                $customer_department_month_map[$customer_number][$month_key][$distribution_department_id] = 0;
            }
            $customer_department_month_map[$customer_number][$month_key][$distribution_department_id]++;

            $candidates = $this->buildWashSubscriptionCandidates(
                $order,
                $order_items_by_order_id[$order_id] ?? [],
                $system_order_fallback
            );
            if (empty($candidates)) {
                continue;
            }

            $active_subscriptions = $this->versioning->resolveVehicleSubscriptionVersionsAt($customer_number, $created_at);
            $matched_order = false;
            foreach ($candidates as $candidate) {
                $matching_version = $this->findMatchingSubscriptionVersion(
                    $active_subscriptions,
                    (string)$candidate['reg'],
                    (int)$candidate['vehicle_type']
                );
                if ($matching_version === null) {
                    continue;
                }

                $monthly_price = $this->getSubscriptionMonthlyPrice((int)$matching_version['vehicle_type']);
                if ($monthly_price <= 0.0) {
                    $warnings[] = 'Subscription type ' . (int)$matching_version['vehicle_type'] . ' has no monthly price for customer ' . $customer_number;
                    continue;
                }

                $group_key = $customer_number . '|' . (string)$matching_version['reg'] . '|' . (int)$matching_version['id'] . '|' . $month_key;
                if (!isset($groups[$group_key])) {
                    $groups[$group_key] = [
                        'customer_number' => $customer_number,
                        'reg' => (string)$matching_version['reg'],
                        'vehicle_type' => (int)$matching_version['vehicle_type'],
                        'version_id' => (int)$matching_version['id'],
                        'month' => $month_key,
                        'monthly_price' => $monthly_price,
                        'source' => (string)($matching_version['source'] ?? 'unknown'),
                        'confidence' => (float)($matching_version['confidence'] ?? 0),
                        'inferred' => (bool)($matching_version['inferred'] ?? false),
                        'distribution' => [],
                        'order_ids' => [],
                        'fallback' => false,
                    ];
                }

                if (!isset($groups[$group_key]['distribution'][$distribution_department_id])) {
                    $groups[$group_key]['distribution'][$distribution_department_id] = 0;
                }
                $groups[$group_key]['distribution'][$distribution_department_id]++;
                $groups[$group_key]['order_ids'][] = $order_id;
                $matched_order = true;
            }

            if ($matched_order && !isset($customer_transactions[$customer_number][$order_id])) {
                $customer_transactions[$customer_number][$order_id] = $this->buildTransactionObject(
                    $order_id,
                    $created_at,
                    $department_id,
                    null,
                    $this->isOrderEligible($order)
                );
            }
        }

        return [
            'groups' => $groups,
            'customer_transactions' => $customer_transactions,
            'customer_department_month_map' => $customer_department_month_map,
            'warnings' => $warnings,
        ];
    }

    protected function buildDateRange(string $date_from, string $date_to): array
    {
        $from = date('Y-m-d 00:00:00', strtotime($date_from));
        $to = date('Y-m-d 23:59:59', strtotime($date_to));
        return [$from, $to];
    }

    protected function fetchOrdersInRange(string $from_ts, string $to_ts): array
    {
        $cache_key = $from_ts . '|' . $to_ts;
        if (isset($this->orders_in_range_cache[$cache_key])) {
            return $this->orders_in_range_cache[$cache_key];
        }

        global $db;
        $from = $db->escape_string($from_ts);
        $to = $db->escape_string($to_ts);
        $sql = "SELECT id, customer_id, department_id, created_at, include_in_invoice, reg_1, reference
                FROM orders
                WHERE deleted_at IS NULL
                  AND created_at >= '$from'
                  AND created_at <= '$to'";
        $result = $db->query($sql);
        if (!$result) {
            $this->orders_in_range_cache[$cache_key] = [];
            return $this->orders_in_range_cache[$cache_key];
        }
        $this->orders_in_range_cache[$cache_key] = $db->fetch_all($result);
        return $this->orders_in_range_cache[$cache_key];
    }

    protected function fetchOrderItemsByOrderIds(array $order_ids): array
    {
        global $db;
        $order_ids = array_values(array_unique(array_filter(array_map('intval', $order_ids), static fn($id) => $id > 0)));
        sort($order_ids);
        if (empty($order_ids)) {
            return [];
        }

        $cache_key = implode(',', $order_ids);
        if (isset($this->order_items_by_order_ids_cache[$cache_key])) {
            return $this->order_items_by_order_ids_cache[$cache_key];
        }

        $sql = "SELECT order_id, product_id, price, quantity, reference
                FROM order_items
                WHERE deleted_at IS NULL
                  AND order_id IN (" . implode(',', $order_ids) . ")";
        $result = $db->query($sql);
        if (!$result) {
            $this->order_items_by_order_ids_cache[$cache_key] = [];
            return $this->order_items_by_order_ids_cache[$cache_key];
        }
        $rows = $db->fetch_all($result);
        $grouped = [];
        foreach ($rows as $row) {
            $order_id = (int)$row['order_id'];
            if (!isset($grouped[$order_id])) {
                $grouped[$order_id] = [];
            }
            $grouped[$order_id][] = $row;
        }
        $this->order_items_by_order_ids_cache[$cache_key] = $grouped;
        return $this->order_items_by_order_ids_cache[$cache_key];
    }

    protected function fetchVehicleSubscriptionVersionRows(string $from_ts, string $to_ts): array
    {
        $cache_key = $from_ts . '|' . $to_ts;
        if (isset($this->vehicle_subscription_versions_in_range_cache[$cache_key])) {
            return $this->vehicle_subscription_versions_in_range_cache[$cache_key];
        }

        global $db;
        $from = $db->escape_string($from_ts);
        $to = $db->escape_string($to_ts);
        $sql = "SELECT *
                FROM customer_vehicle_subscription_versions
                WHERE wash_subscription = 1
                  AND effective_from <= '$to'
                  AND (effective_to IS NULL OR effective_to >= '$from')";
        $result = $db->query($sql);
        if (!$result) {
            $this->vehicle_subscription_versions_in_range_cache[$cache_key] = [];
            return $this->vehicle_subscription_versions_in_range_cache[$cache_key];
        }
        $this->vehicle_subscription_versions_in_range_cache[$cache_key] = $db->fetch_all($result);
        return $this->vehicle_subscription_versions_in_range_cache[$cache_key];
    }

    private function versionOverlaps(array $version_row, string $from_ts, string $to_ts): bool
    {
        $version_from = (string)$version_row['effective_from'];
        $version_to = $version_row['effective_to'] !== null ? (string)$version_row['effective_to'] : null;
        if ($version_from > $to_ts) {
            return false;
        }
        if ($version_to !== null && $version_to < $from_ts) {
            return false;
        }
        return true;
    }

    protected function buildWashSubscriptionCandidates(array $order, array $order_items, bool $system_order_fallback = false): array
    {
        if (!$system_order_fallback) {
            $reg = trim((string)($order['reg_1'] ?? ''));
            if ($reg === '') {
                return [];
            }

            return [[
                'reg' => $reg,
                'vehicle_type' => 0,
            ]];
        }

        $candidates = [];
        foreach ($order_items as $item) {
            $reg = trim((string)($item['reference'] ?? ''));
            if ($reg === '') {
                continue;
            }

            $vehicle_type = (int)($item['product_id'] ?? 0);
            $candidate_key = strtoupper($reg) . '|' . $vehicle_type;
            if (isset($candidates[$candidate_key])) {
                continue;
            }

            $candidates[$candidate_key] = [
                'reg' => $reg,
                'vehicle_type' => $vehicle_type,
            ];
        }

        return array_values($candidates);
    }

    protected function findMatchingSubscriptionVersion(array $active_subscriptions, string $reg, int $vehicle_type = 0): ?array
    {
        $fallback_match = null;
        foreach ($active_subscriptions as $candidate) {
            if (strcasecmp((string)$candidate['reg'], $reg) !== 0) {
                continue;
            }

            if ($vehicle_type > 0 && (int)($candidate['vehicle_type'] ?? 0) === $vehicle_type) {
                return $candidate;
            }

            if ($fallback_match === null) {
                $fallback_match = $candidate;
            }
        }

        return $fallback_match;
    }

    protected function isSystemOrderCandidate(array $order, string $reference): bool
    {
        return (int)($order['department_id'] ?? 0) === self::SYSTEM_ORDER_DEPARTMENT_ID
            && strcasecmp(trim((string)($order['reference'] ?? '')), $reference) === 0;
    }

    private function buildSubscriptionFallbackDistribution(
        int $customer_number,
        string $month_key,
        float $monthly_price,
        array $customer_department_month_map
    ): array {
        $distribution = [];
        $department_counts = $customer_department_month_map[$customer_number][$month_key] ?? [];
        if (!empty($department_counts)) {
            $department_counts = $this->normalizeFallbackDepartmentCounts($department_counts);
        }
        if (!empty($department_counts)) {
            $total = (float)array_sum($department_counts);
            foreach ($department_counts as $department_id => $count) {
                $distribution[(int)$department_id] = $monthly_price * ((float)$count / max($total, 1.0));
            }
            return $distribution;
        }

        $customer_default_department_id = $this->getCustomerDefaultDepartmentId($customer_number);
        $default_department = $customer_default_department_id !== null && $customer_default_department_id > 0
            ? $customer_default_department_id
            : $this->getFallbackDistributionDepartmentId();

        $distribution[$default_department] = $monthly_price;
        return $distribution;
    }

    /**
     * @param array<int|string,int|float> $department_counts
     * @return array<int,int|float>
     */
    private function normalizeFallbackDepartmentCounts(array $department_counts): array
    {
        $normalized = [];
        foreach ($department_counts as $department_id => $count) {
            $department_id = (int)$department_id;
            if ($department_id === self::SYSTEM_ORDER_DEPARTMENT_ID || $department_id <= 0) {
                $department_id = $this->getFallbackDistributionDepartmentId();
            }
            if (!isset($normalized[$department_id])) {
                $normalized[$department_id] = 0;
            }
            $normalized[$department_id] += $count;
        }

        return $normalized;
    }

    protected function getCustomerDefaultDepartmentId(int $customer_number): ?int
    {
        try {
            $default = (new users_o())->getUserByCustomerNumber($customer_number)->getDefaultDepartment();
            return !empty($default) ? (int)$default : null;
        } catch (Exception $e) {
            return null;
        }
    }

    protected function getFallbackDistributionDepartmentId(): int
    {
        try {
            $department_id = (new economic())->getDefaultDistributionDepartmentId();
        } catch (\Throwable $e) {
            $department_id = economic::DEFAULT_DISTRIBUTION_DEPARTMENT_ID;
        }

        if ($department_id <= 0 || $department_id === self::SYSTEM_ORDER_DEPARTMENT_ID) {
            return economic::DEFAULT_DISTRIBUTION_DEPARTMENT_ID;
        }

        return $department_id;
    }

    private function normalizeSubscriptionGroupAllocation(array $distribution, float $monthly_price): array
    {
        if (empty($distribution)) {
            return [];
        }

        $has_fractional = false;
        foreach ($distribution as $v) {
            if (abs((float)$v - round((float)$v)) > 0.00001) {
                $has_fractional = true;
                break;
            }
        }

        if (!$has_fractional) {
            $departments = array_keys($distribution);
            $count = count($departments);
            if ($count === 0) {
                return [];
            }
            $per_department = $monthly_price / $count;
            $out = [];
            foreach ($departments as $department_id) {
                $out[(int)$department_id] = $per_department;
            }
            return $out;
        }

        return array_map(static fn($amount) => (float)$amount, $distribution);
    }

    protected function calculateOrderOriginalPrice(array $order_items, int $customer_number, int $department_id, string $timestamp): float
    {
        $total = 0.0;
        foreach ($order_items as $item) {
            $product_id = (int)($item['product_id'] ?? 0);
            $quantity = (float)($item['quantity'] ?? 0);
            if ($product_id <= 0 || $quantity <= 0) {
                continue;
            }

            $explicit_price = (float)($item['price'] ?? 0);
            if ($explicit_price > 0) {
                $line_price = $explicit_price * $quantity;
            } else {
                $line_price = ((float)$this->getProductDepartmentPrice($product_id, $department_id)) * $quantity;
            }

            $discount_row = $this->resolveDiscountForProduct($customer_number, $product_id, $timestamp, $department_id);
            if ($discount_row !== null && array_key_exists('fixed_price', $discount_row) && $discount_row['fixed_price'] !== null) {
                $line_price = ((float)$discount_row['fixed_price']) * $quantity;
                $total += $line_price;
                continue;
            }

            $discount_percentage = (float)($discount_row['discount'] ?? 0);
            if ($discount_percentage > 0) {
                $line_price *= (1 - ($discount_percentage / 100));
            }
            $total += $line_price;
        }
        return $total;
    }

    protected function resolveDiscountForProduct(int $customer_number, int $product_id, string $timestamp, ?int $department_id = null): ?array
    {
        $cache_key = $customer_number . '|' . $product_id . '|' . (int)($department_id ?? 0) . '|' . substr($timestamp, 0, 19);
        if (array_key_exists($cache_key, $this->discount_resolution_cache)) {
            return $this->discount_resolution_cache[$cache_key];
        }

        $scopedDepartmentId = $department_id !== null && (new \objects\departments_o())->isCustomPricingOnly((int)$department_id)
            ? (int)$department_id
            : null;

        $direct = $this->versioning->resolveDiscountOverrideAt($customer_number, false, (string)$product_id, $timestamp, $scopedDepartmentId);
        if ($direct !== null && (
            (array_key_exists('fixed_price', $direct) && $direct['fixed_price'] !== null)
            || (int)($direct['discount'] ?? 0) > 0
        )) {
            return $this->discount_resolution_cache[$cache_key] = $direct;
        }

        $product = $this->getProduct($product_id);
        if ($product !== null) {
            $category = (string)$product->category->value();
            if ($category !== '') {
                $category_discount = $this->versioning->resolveDiscountOverrideAt($customer_number, true, $category, $timestamp, $scopedDepartmentId);
                if ($category_discount !== null && (int)($category_discount['discount'] ?? 0) > 0) {
                    return $this->discount_resolution_cache[$cache_key] = $category_discount;
                }
            }
        }

        if ($scopedDepartmentId !== null) {
            $global_discount = $this->versioning->resolveDiscountOverrideAt($customer_number, true, 'global', $timestamp, $scopedDepartmentId);
            if ($global_discount !== null && (int)($global_discount['discount'] ?? 0) > 0) {
                return $this->discount_resolution_cache[$cache_key] = $global_discount;
            }
        }

        return $this->discount_resolution_cache[$cache_key] = null;
    }

    protected function getProductDepartmentPrice(int $product_id, int $department_id): float
    {
        if (!isset($this->product_department_price_cache[$department_id][$product_id])) {
            $product = $this->getProduct($product_id);
            if ($product === null) {
                $this->product_department_price_cache[$department_id][$product_id] = 0.0;
            } else {
                $this->product_department_price_cache[$department_id][$product_id] = (float)$product->getDepartmentPrice($department_id);
            }
        }
        return (float)$this->product_department_price_cache[$department_id][$product_id];
    }

    protected function getSubscriptionMonthlyPrice(int $vehicle_type): float
    {
        $product = $this->getProduct($vehicle_type);
        if ($product === null) {
            return 0.0;
        }
        return (float)$product->getSubscriptionMonthlyPrice();
    }

    private function getEconomicClient(): economic
    {
        if ($this->economic === null) {
            $this->economic = new economic();
        }

        return $this->economic;
    }

    protected function toArray(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }
        if (!is_object($value)) {
            return [];
        }

        return json_decode(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true) ?: [];
    }

    private function getProduct(int $product_id): ?products_o
    {
        if (!isset($this->product_cache[$product_id])) {
            $product = new products_o();
            $product->select($product_id);
            if (!$product->exists()) {
                $this->product_cache[$product_id] = null;
            } else {
                $this->product_cache[$product_id] = $product;
            }
        }
        return $this->product_cache[$product_id];
    }

    protected function buildCustomerEnvelope(int $customer_number, array $transaction_map): array
    {
        return [
            'id' => (new users_o())->getUserByCustomerNumber($customer_number)->id,
            'customer_number' => $customer_number,
            'customer_name' => $this->getCustomerName($customer_number),
            'transactions' => array_values($transaction_map),
            'requires_action' => false,
            'meta' => [],
        ];
    }

    protected function buildTransactionObject(int $order_id, string $created_at, int $department_id, ?float $amount = null, ?bool $included = null): array
    {
        $order = (new orders_o())->select($order_id);
        return [
            'id' => $order_id,
            'date' => $created_at,
            'amount' => round((float)($amount ?? (float)$order->getNetAmount()), 5),
            'booked' => $order->isBooked(true),
            'department_id' => $department_id,
            'excluded' => !($included ?? $this->isDepartmentEligible($department_id)),
        ];
    }

    protected function isOrderEligible(array $order): bool
    {
        $department_id = (int)($order['department_id'] ?? 0);
        if ($department_id === self::SYSTEM_ORDER_DEPARTMENT_ID || $department_id <= 0) {
            return false;
        }

        $override = orders_o::normalizeNullableBooleanValue($order['include_in_invoice'] ?? null);
        if ($override !== null) {
            return $override;
        }

        return $this->isDepartmentEligible($department_id);
    }

    private function getCustomerName(int $customer_number): string
    {
        if (!isset($this->customer_name_cache[$customer_number])) {
            $this->customer_name_cache[$customer_number] = (new users_o())->getCustomerName($customer_number) ?? 'Unknown Customer';
        }
        return (string)$this->customer_name_cache[$customer_number];
    }

    protected function shouldIncludeCustomerNumber(int $customer_number): bool
    {
        if ($customer_number <= 0) {
            return false;
        }
        if (array_key_exists($customer_number, $this->customer_inclusion_cache)) {
            return (bool)$this->customer_inclusion_cache[$customer_number];
        }

        $rows = (new users_o())->getFieldsWhere(['customer_number' => $customer_number], ['id']);
        if (empty($rows)) {
            $this->customer_inclusion_cache[$customer_number] = false;
            return false;
        }

        try {
            $this->getCustomerName($customer_number);
        } catch (\Throwable $e) {
            $this->customer_inclusion_cache[$customer_number] = false;
            return false;
        }

        $this->customer_inclusion_cache[$customer_number] = true;
        return true;
    }

    protected function isDepartmentEligible(int $department_id): bool
    {
        if ($department_id === self::SYSTEM_ORDER_DEPARTMENT_ID || $department_id <= 0) {
            return false;
        }
        if (!array_key_exists($department_id, $this->department_excluded_cache)) {
            try {
                $this->department_excluded_cache[$department_id] = (new departments_o())->select($department_id)->isExcludedFromInvoicing();
            } catch (Exception $e) {
                $this->department_excluded_cache[$department_id] = false;
            }
        }
        return !$this->department_excluded_cache[$department_id];
    }

    protected function parseDepartmentMap(array $department_map): array
    {
        $parsed = [];
        foreach ($department_map as $department_id => $amount) {
            $parsed[$this->getDepartmentName((int)$department_id)] = round((float)$amount, 5);
        }
        return $parsed;
    }

    private function getDepartmentName(int $department_id): string
    {
        if (!isset($this->department_name_cache[$department_id])) {
            try {
                $name = (new departments_o())->select($department_id)->name->value();
                $this->department_name_cache[$department_id] = !empty($name)
                    ? (string)$name
                    : 'Unknown Department (' . $department_id . ')';
            } catch (Exception $e) {
                $this->department_name_cache[$department_id] = 'Unknown Department (' . $department_id . ')';
            }
        }
        return (string)$this->department_name_cache[$department_id];
    }

    private function roundMap(array $map): array
    {
        $out = [];
        foreach ($map as $key => $value) {
            $out[(string)$key] = round((float)$value, 5);
        }
        return $out;
    }

    private function listMonthKeys(string $from_ts, string $to_ts): array
    {
        $start = new DateTime(date('Y-m-01 00:00:00', strtotime($from_ts)));
        $end = new DateTime(date('Y-m-01 00:00:00', strtotime($to_ts)));
        $end->modify('+1 month');

        $period = new DatePeriod($start, new DateInterval('P1M'), $end);
        $months = [];
        foreach ($period as $dt) {
            $months[] = $dt->format('Y-m');
        }
        return $months;
    }
}
