<?php

namespace classes;

class economic_v2_revenue_statistics_service
{
    private const DEFAULT_PAGE_SIZE = 1000;
    private const DEFAULT_MAX_PAGES = 10;
    private const EPSILON = 0.00001;

    private economic $economic;

    /** @var array<int, array{customer_number: int, name: ?string, barred: ?bool, status: string}> */
    private array $customer_cache = [];

    public function __construct(?economic $economic = null)
    {
        $this->economic = $economic ?? new economic();
    }

    public function getBookedRevenueStatistics(array $filters = []): array
    {
        $normalized_filters = $this->normalizeFilters($filters);
        $warnings = [];

        $booked_invoices = $this->fetchBookedInvoices($normalized_filters, $warnings);
        $invoice_ids = [];
        foreach ($booked_invoices as $invoice) {
            $invoice_id = (int)($invoice->bookedInvoiceNumber ?? 0);
            if ($invoice_id > 0) {
                $invoice_ids[] = $invoice_id;
            }
        }

        $invoice_lines_map = [];
        if (!empty($invoice_ids)) {
            try {
                $invoice_lines_map = $this->economic->invoices->booked->get_invoice_lines($invoice_ids);
            } catch (\Throwable $e) {
                $warnings[] = 'Unable to fetch booked invoice lines in bulk: ' . $e->getMessage();
            }
        }

        $summary = [
            'invoice_count' => 0,
            'line_count' => 0,
            'net_amount' => 0.0,
            'vat_amount' => 0.0,
            'gross_amount' => 0.0,
            'average_invoice_net_amount' => 0.0,
        ];

        $customers = [];
        $departments = [];
        $currencies = [];
        $seen_customers = [];

        $customer_filter_map = array_fill_keys($normalized_filters['customer_numbers'], true);
        $department_filter_map = array_fill_keys($normalized_filters['department_numbers'], true);
        $has_customer_filter = !empty($customer_filter_map);
        $has_department_filter = !empty($department_filter_map);

        foreach ($booked_invoices as $invoice) {
            $invoice_id = (int)($invoice->bookedInvoiceNumber ?? 0);
            if ($invoice_id <= 0) {
                continue;
            }

            $customer_number = (int)($invoice->customer->customerNumber ?? 0);
            if ($has_customer_filter && !isset($customer_filter_map[$customer_number])) {
                continue;
            }

            $customer_snapshot = $this->resolveCustomerSnapshot($customer_number, $warnings);
            if (!$this->passesBarredFilter($customer_snapshot['barred'], $normalized_filters['barred'])) {
                continue;
            }

            $invoice_currency = strtoupper((string)($invoice->currency ?? ''));
            if ($normalized_filters['currency'] !== null && $invoice_currency !== $normalized_filters['currency']) {
                continue;
            }

            $invoice_lines = $invoice_lines_map[$invoice_id] ?? [];
            $line_reduction = $this->reduceInvoiceLines(
                $invoice_id,
                $invoice_lines,
                $departments,
                $department_filter_map,
                $has_department_filter
            );

            if ($has_department_filter && !$line_reduction['has_matching_departments']) {
                continue;
            }

            $invoice_net = $has_department_filter
                ? (float)$line_reduction['net_amount']
                : (float)($invoice->netAmount ?? $invoice->net_amount ?? $line_reduction['net_amount']);
            $invoice_vat = $has_department_filter
                ? (float)$line_reduction['vat_amount']
                : (float)($invoice->vatAmount ?? $invoice->vat_amount ?? $line_reduction['vat_amount']);
            $invoice_gross = $has_department_filter
                ? (float)$line_reduction['gross_amount']
                : (float)($invoice->grossAmount ?? $invoice->gross_amount ?? ($invoice_net + $invoice_vat));

            if (
                $has_department_filter &&
                abs($invoice_net) < self::EPSILON &&
                abs($invoice_vat) < self::EPSILON &&
                abs($invoice_gross) < self::EPSILON
            ) {
                continue;
            }

            $summary['invoice_count']++;
            $summary['line_count'] += (int)$line_reduction['line_count'];
            $summary['net_amount'] += $invoice_net;
            $summary['vat_amount'] += $invoice_vat;
            $summary['gross_amount'] += $invoice_gross;

            if (!isset($seen_customers[$customer_number])) {
                $seen_customers[$customer_number] = true;
            }

            if (!isset($customers[$customer_number])) {
                $customers[$customer_number] = [
                    'customer_number' => $customer_number,
                    'customer_name' => $customer_snapshot['name'],
                    'barred' => $customer_snapshot['barred'],
                    'invoice_count' => 0,
                    'net_amount' => 0.0,
                    'vat_amount' => 0.0,
                    'gross_amount' => 0.0,
                ];
            }
            $customers[$customer_number]['invoice_count']++;
            $customers[$customer_number]['net_amount'] += $invoice_net;
            $customers[$customer_number]['vat_amount'] += $invoice_vat;
            $customers[$customer_number]['gross_amount'] += $invoice_gross;

            $currency_key = $invoice_currency !== '' ? $invoice_currency : 'UNKNOWN';
            if (!isset($currencies[$currency_key])) {
                $currencies[$currency_key] = [
                    'currency' => $currency_key,
                    'invoice_count' => 0,
                    'net_amount' => 0.0,
                    'vat_amount' => 0.0,
                    'gross_amount' => 0.0,
                ];
            }
            $currencies[$currency_key]['invoice_count']++;
            $currencies[$currency_key]['net_amount'] += $invoice_net;
            $currencies[$currency_key]['vat_amount'] += $invoice_vat;
            $currencies[$currency_key]['gross_amount'] += $invoice_gross;
        }

        $summary['unique_customers'] = count($seen_customers);
        if ($summary['invoice_count'] > 0) {
            $summary['average_invoice_net_amount'] = $summary['net_amount'] / $summary['invoice_count'];
        }

        $customer_rows = array_values($customers);
        usort($customer_rows, static function (array $a, array $b): int {
            return $b['invoice_count'] <=> $a['invoice_count'];
        });

        $department_rows = [];
        foreach ($departments as $department_key => $row) {
            $department_rows[] = [
                'department_key' => $department_key,
                'department_number' => is_numeric((string)$department_key) ? (int)$department_key : null,
                'invoice_count' => count($row['invoice_ids']),
                'line_count' => $row['line_count'],
                'net_amount' => $row['net_amount'],
                'vat_amount' => $row['vat_amount'],
                'gross_amount' => $row['gross_amount'],
            ];
        }
        usort($department_rows, static function (array $a, array $b): int {
            return abs((float)$b['net_amount']) <=> abs((float)$a['net_amount']);
        });

        $currency_rows = array_values($currencies);
        usort($currency_rows, static function (array $a, array $b): int {
            return $b['invoice_count'] <=> $a['invoice_count'];
        });

        return [
            'filters' => [
                'dateFrom' => $normalized_filters['dateFrom'],
                'dateTo' => $normalized_filters['dateTo'],
                'customer_numbers' => array_values($normalized_filters['customer_numbers']),
                'department_numbers' => array_values($normalized_filters['department_numbers']),
                'currency' => $normalized_filters['currency'],
                'barred' => $normalized_filters['barred'],
                'max_pages' => $normalized_filters['max_pages'],
            ],
            'summary' => $this->roundNumericValues($summary),
            'customers' => $this->roundRows($customer_rows),
            'departments' => $this->roundRows($department_rows),
            'currencies' => $this->roundRows($currency_rows),
            'warnings' => array_values(array_unique($warnings)),
        ];
    }

    private function normalizeFilters(array $filters): array
    {
        $date_from = (string)($filters['dateFrom'] ?? date('Y-m-01'));
        $date_to = (string)($filters['dateTo'] ?? date('Y-m-d'));

        $customer_numbers = $this->normalizeIntegerList($filters['customer_numbers'] ?? []);
        $department_numbers = $this->normalizeIntegerList($filters['department_numbers'] ?? []);

        $currency = isset($filters['currency']) && trim((string)$filters['currency']) !== ''
            ? strtoupper(trim((string)$filters['currency']))
            : null;

        $barred = strtolower(trim((string)($filters['barred'] ?? 'all')));
        if (!in_array($barred, ['all', 'barred', 'active'], true)) {
            $barred = 'all';
        }

        $max_pages = (int)($filters['max_pages'] ?? self::DEFAULT_MAX_PAGES);
        $max_pages = max(1, min(200, $max_pages));

        return [
            'dateFrom' => $date_from,
            'dateTo' => $date_to,
            'customer_numbers' => $customer_numbers,
            'department_numbers' => $department_numbers,
            'currency' => $currency,
            'barred' => $barred,
            'max_pages' => $max_pages,
        ];
    }

    private function fetchBookedInvoices(array $normalized_filters, array &$warnings): array
    {
        $filters = [
            '(date$gte:' . $normalized_filters['dateFrom'] . '$and:date$lte:' . $normalized_filters['dateTo'] . ')' => '',
        ];
        if ($normalized_filters['currency'] !== null) {
            $filters['currency'] = '$eq:' . $normalized_filters['currency'];
        }
        if (count($normalized_filters['customer_numbers']) === 1) {
            $filters['customer.customerNumber'] = '$eq:' . $normalized_filters['customer_numbers'][0];
        }

        $all = [];
        for ($page = 0; $page < $normalized_filters['max_pages']; $page++) {
            $response = $this->economic->invoices->booked->get(
                $filters,
                [
                    'skipPages' => $page,
                    'pageSize' => self::DEFAULT_PAGE_SIZE,
                ]
            );
            $collection = is_array($response->collection ?? null) ? $response->collection : [];
            $all = array_merge($all, $collection);

            if (count($collection) < self::DEFAULT_PAGE_SIZE) {
                break;
            }
            if ($page + 1 >= $normalized_filters['max_pages']) {
                $warnings[] = 'Reached pagination safety limit (max_pages=' . $normalized_filters['max_pages'] . ').';
            }
        }

        return $all;
    }

    /**
     * @param array<int|string, mixed> $department_totals
     * @param array<int, bool> $department_filter_map
     * @return array{net_amount:float,vat_amount:float,gross_amount:float,line_count:int,has_matching_departments:bool}
     */
    private function reduceInvoiceLines(
        int $invoice_id,
        array $invoice_lines,
        array &$department_totals,
        array $department_filter_map,
        bool $has_department_filter
    ): array {
        $invoice_net = 0.0;
        $invoice_vat = 0.0;
        $invoice_gross = 0.0;
        $line_count = 0;
        $has_matching_departments = false;

        foreach ($invoice_lines as $line_raw) {
            $line = $this->toArray($line_raw);

            $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);
            $line_vat_amount = isset($line['vatAmount'])
                ? (float)$line['vatAmount']
                : (isset($line['vat_amount']) ? (float)$line['vat_amount'] : $line_net_amount * ((float)($line['vatRate'] ?? 0.0) / 100));
            $line_gross_amount = $line_net_amount + $line_vat_amount;

            $is_billable = abs($line_net_amount) > self::EPSILON || abs($quantity) > self::EPSILON;
            if (!$is_billable) {
                continue;
            }

            $distribution = $this->extractDepartmentDistribution($line);
            $matching_percentage_total = 0.0;
            foreach ($distribution as $department_key => $percentage) {
                if ($has_department_filter && !isset($department_filter_map[(int)$department_key])) {
                    continue;
                }
                $matching_percentage_total += (float)$percentage;
                $has_matching_departments = true;

                if (!isset($department_totals[$department_key])) {
                    $department_totals[$department_key] = [
                        'invoice_ids' => [],
                        'line_count' => 0,
                        'net_amount' => 0.0,
                        'vat_amount' => 0.0,
                        'gross_amount' => 0.0,
                    ];
                }

                $ratio = ((float)$percentage / 100.0);
                $department_totals[$department_key]['invoice_ids'][$invoice_id] = true;
                $department_totals[$department_key]['line_count']++;
                $department_totals[$department_key]['net_amount'] += $line_net_amount * $ratio;
                $department_totals[$department_key]['vat_amount'] += $line_vat_amount * $ratio;
                $department_totals[$department_key]['gross_amount'] += $line_gross_amount * $ratio;
            }

            if ($has_department_filter && $matching_percentage_total <= self::EPSILON) {
                continue;
            }

            $factor = $has_department_filter ? ($matching_percentage_total / 100.0) : 1.0;
            $invoice_net += $line_net_amount * $factor;
            $invoice_vat += $line_vat_amount * $factor;
            $invoice_gross += $line_gross_amount * $factor;
            $line_count++;
        }

        return [
            'net_amount' => $invoice_net,
            'vat_amount' => $invoice_vat,
            'gross_amount' => $invoice_gross,
            'line_count' => $line_count,
            'has_matching_departments' => $has_matching_departments,
        ];
    }

    /**
     * @return array<int|string, float>
     */
    private function extractDepartmentDistribution(array $line): array
    {
        $distribution = [];
        $departmental_distribution = $line['departmentalDistribution'] ?? $line['departmental_distribution'] ?? null;
        if (!is_array($departmental_distribution)) {
            return ['unassigned' => 100.0];
        }

        $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[(int)$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[(int)$fallback_number] = 100.0;
            }
        }

        if (empty($distribution)) {
            $distribution['unassigned'] = 100.0;
        }

        return $distribution;
    }

    /**
     * @return array{customer_number: int, name: ?string, barred: ?bool, status: string}
     */
    private function resolveCustomerSnapshot(int $customer_number, array &$warnings): array
    {
        if (isset($this->customer_cache[$customer_number])) {
            return $this->customer_cache[$customer_number];
        }

        $snapshot = [
            'customer_number' => $customer_number,
            'name' => null,
            'barred' => null,
            'status' => 'unknown',
        ];

        if ($customer_number <= 0) {
            $this->customer_cache[$customer_number] = $snapshot;
            return $snapshot;
        }

        try {
            $raw = $this->economic->customers->customers->get($customer_number);
            if (isset($raw->customerNumber)) {
                $snapshot['name'] = isset($raw->name) ? (string)$raw->name : null;
                $snapshot['barred'] = isset($raw->barred) ? (bool)$raw->barred : null;
                $snapshot['status'] = 'resolved';
            } else {
                $warnings[] = 'Unable to resolve e-conomic customer ' . $customer_number . ' while evaluating barred filter.';
            }
        } catch (\Throwable $e) {
            $warnings[] = 'Failed to fetch e-conomic customer ' . $customer_number . ': ' . $e->getMessage();
        }

        $this->customer_cache[$customer_number] = $snapshot;
        return $snapshot;
    }

    private function passesBarredFilter(?bool $barred, string $mode): bool
    {
        return match ($mode) {
            'barred' => $barred === true,
            'active' => $barred !== true,
            default => true,
        };
    }

    private 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 normalizeIntegerList(mixed $raw): array
    {
        $values = [];
        if (is_array($raw)) {
            $values = $raw;
        } elseif (is_string($raw)) {
            $values = explode(',', $raw);
        } elseif (is_numeric($raw)) {
            $values = [$raw];
        }

        $normalized = [];
        foreach ($values as $value) {
            $int_value = (int)$value;
            if ($int_value > 0) {
                $normalized[$int_value] = true;
            }
        }

        return array_map('intval', array_keys($normalized));
    }

    private function roundRows(array $rows): array
    {
        $result = [];
        foreach ($rows as $row) {
            $result[] = $this->roundNumericValues($row);
        }
        return $result;
    }

    private function roundNumericValues(array $data): array
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = $this->roundNumericValues($value);
                continue;
            }
            if (is_float($value)) {
                $data[$key] = round($value, 5);
            }
        }
        return $data;
    }
}
