<?php

namespace classes;

use helpers\economic_invoice_booked;
use objects\collected_order_invoices_o;
use objects\order_items_o;
use objects\orders_o;
use objects\products_o;

class economic_v2_line_normalizer
{
    public static function normalizeInternalCollectedInvoice(collected_order_invoices_o $invoice): array
    {
        $lines = [];
        $warnings = [];

        foreach ($invoice->getOrders() as $order_row) {
            $order = (new orders_o())->select((int)$order_row['id']);
            $department_id = (int)$order->department_id->value();
            $order_items = (new order_items_o())->getAllItemsAsArray(
                (int)$order->id,
                ['id', 'product_id', 'reference', 'notes', 'price', 'quantity', 'include_in_invoice']
            );

            foreach ($order_items as $row) {
                if (!(bool)($row['include_in_invoice'] ?? false)) {
                    continue;
                }

                $product = (new products_o())->getProductById((int)$row['product_id']);
                if (!$product->exists()) {
                    $warnings[] = 'Missing product for internal order item id ' . (int)$row['id'];
                    continue;
                }

                $product_number = $product->economic_product_id->value();
                $quantity = (float)$row['quantity'];
                $unit_net_price = (float)$row['price'];
                $line_net_amount = $quantity * $unit_net_price;
                $department_distribution = [$department_id => 100.0];

                $line = [
                    'index' => count($lines),
                    'source' => 'internal',
                    'source_order_id' => (int)$order->id,
                    'source_line_id' => (int)$row['id'],
                    'line_type' => self::detectLineType($product_number, $unit_net_price, true),
                    'billable' => true,
                    'product_number' => $product_number !== null ? (string)$product_number : null,
                    'product_id' => (int)$row['product_id'],
                    'description' => (string)$product->name->value(),
                    'reference' => (string)($row['reference'] ?? ''),
                    'quantity' => $quantity,
                    'unit_net_price' => $unit_net_price,
                    'line_net_amount' => $line_net_amount,
                    'department_distribution' => $department_distribution,
                ];
                $line['match_key'] = self::buildMatchKey($line);
                $lines[] = $line;
            }
        }

        return self::wrap('internal', $lines, $warnings);
    }

    public static function normalizeDraftInvoice(object|array|null $draft_invoice): array
    {
        if ($draft_invoice === null) {
            return self::wrap('draft', [], ['Draft invoice missing']);
        }

        $data = self::toArray($draft_invoice);
        $raw_lines = is_array($data['lines'] ?? null) ? $data['lines'] : [];
        $lines = [];

        foreach ($raw_lines as $raw_line) {
            $line = self::normalizeEconomicLine($raw_line, 'draft');
            $line['index'] = count($lines);
            $line['source_line_id'] = isset($raw_line['lineNumber']) ? (int)$raw_line['lineNumber'] : (int)($raw_line['line_number'] ?? count($lines) + 1);
            $line['match_key'] = self::buildMatchKey($line);
            $lines[] = $line;
        }

        $wrapped = self::wrap('draft', $lines, []);
        if (isset($data['netAmount'])) {
            $wrapped['totals']['net_total'] = (float)$data['netAmount'];
        } elseif (isset($data['net_amount'])) {
            $wrapped['totals']['net_total'] = (float)$data['net_amount'];
        }
        $wrapped['totals']['difference_from_line_sum'] = round(
            (float)$wrapped['totals']['net_total'] - (float)$wrapped['totals']['line_net_total'],
            5
        );

        return $wrapped;
    }

    public static function normalizeBookedInvoice(object|array|null $booked_invoice): array
    {
        if ($booked_invoice === null) {
            return self::wrap('booked', [], ['Booked invoice missing']);
        }

        if ($booked_invoice instanceof economic_invoice_booked) {
            $data = $booked_invoice->toArray();
        } else {
            $data = self::toArray($booked_invoice);
        }

        $raw_lines = is_array($data['lines'] ?? null) ? $data['lines'] : [];
        $lines = [];
        foreach ($raw_lines as $raw_line) {
            $line = self::normalizeEconomicLine($raw_line, 'booked');
            $line['index'] = count($lines);
            $line['source_line_id'] = (int)($raw_line['lineNumber'] ?? $raw_line['line_number'] ?? count($lines) + 1);
            $line['match_key'] = self::buildMatchKey($line);
            $lines[] = $line;
        }

        $wrapped = self::wrap('booked', $lines, []);
        if (isset($data['netAmount'])) {
            $wrapped['totals']['net_total'] = (float)$data['netAmount'];
        } elseif (isset($data['net_amount'])) {
            $wrapped['totals']['net_total'] = (float)$data['net_amount'];
        }
        $wrapped['totals']['difference_from_line_sum'] = round(
            (float)$wrapped['totals']['net_total'] - (float)$wrapped['totals']['line_net_total'],
            5
        );

        return $wrapped;
    }

    private static function normalizeEconomicLine(array $raw_line, string $source): array
    {
        $line = self::toArray($raw_line);

        $product_number = $line['product']['productNumber']
            ?? $line['product']['product_number']
            ?? null;
        $description = (string)($line['description'] ?? '');
        $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);

        $department_distribution = self::extractDepartmentDistribution($line);
        $billable = ($product_number !== null) || abs($line_net_amount) > 0.00001 || abs($quantity) > 0.00001;

        return [
            'source' => $source,
            'source_order_id' => null,
            'line_type' => self::detectLineType($product_number, $unit_net_price, $billable),
            'billable' => $billable,
            'product_number' => $product_number !== null ? (string)$product_number : null,
            'product_id' => null,
            'description' => $description,
            'reference' => '',
            'quantity' => $quantity,
            'unit_net_price' => $unit_net_price,
            'line_net_amount' => $line_net_amount,
            'department_distribution' => $department_distribution,
        ];
    }

    private static function extractDepartmentDistribution(array $line): array
    {
        $distribution = [];
        $dd = $line['departmentalDistribution'] ?? $line['departmental_distribution'] ?? null;
        if (is_array($dd)) {
            $distributions = $dd['distributions'] ?? null;
            if (is_array($distributions)) {
                foreach ($distributions as $entry) {
                    $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) && isset($dd['departmentalDistributionNumber'])) {
                $distribution[(string)$dd['departmentalDistributionNumber']] = 100.0;
            } elseif (empty($distribution) && isset($dd['departmental_distribution_number'])) {
                $distribution[(string)$dd['departmental_distribution_number']] = 100.0;
            }
        }

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

        return $distribution;
    }

    private static function wrap(string $source, array $lines, array $warnings): array
    {
        $net_total = 0.0;
        $line_net_total = 0.0;
        $billable_count = 0;
        $departments = [];

        foreach ($lines as $line) {
            $line_net_total += (float)$line['line_net_amount'];
            if (!(bool)$line['billable']) {
                continue;
            }
            $billable_count++;
            $line_amount = (float)$line['line_net_amount'];
            $net_total += $line_amount;

            foreach ($line['department_distribution'] as $department_key => $percentage) {
                if (!isset($departments[$department_key])) {
                    $departments[$department_key] = 0.0;
                }
                $departments[$department_key] += $line_amount * ((float)$percentage / 100);
            }
        }

        return [
            'source' => $source,
            'totals' => [
                'net_total' => round($net_total, 5),
                'line_net_total' => round($line_net_total, 5),
                'line_count' => count($lines),
                'billable_line_count' => $billable_count,
            ],
            'departments' => self::roundMap($departments),
            'lines' => $lines,
            'warnings' => $warnings,
        ];
    }

    private static function buildMatchKey(array $line): string
    {
        if (!empty($line['product_number'])) {
            return 'product:' . strtolower(trim((string)$line['product_number'])) .
                '|ref:' . strtolower(trim((string)($line['reference'] ?? '')));
        }

        return 'text:' . self::normalizeText((string)$line['description']);
    }

    private static function detectLineType(mixed $product_number, float $unit_net_price, bool $billable): string
    {
        if (!$billable) {
            return 'text';
        }

        if ($product_number !== null && strtolower((string)$product_number) === 'totdiscount') {
            return 'discount';
        }

        if ($unit_net_price < 0) {
            return 'discount';
        }

        return $product_number !== null ? 'product' : 'text';
    }

    private static function normalizeText(string $text): string
    {
        $text = trim(strtolower($text));
        $text = preg_replace('/\s+/', ' ', $text);
        return $text ?? '';
    }

    private static function roundMap(array $map): array
    {
        $rounded = [];
        foreach ($map as $k => $v) {
            $rounded[(string)$k] = round((float)$v, 5);
        }
        return $rounded;
    }

    private static function toArray(object|array $value): array
    {
        if (is_array($value)) {
            return $value;
        }
        return json_decode(json_encode($value, JSON_UNESCAPED_UNICODE), true) ?: [];
    }
}
