<?php

namespace classes;

class economic_v2_compare_engine
{
    private const TOLERANCE = 0.01;

    public static function compare(array $internal, ?array $draft, ?array $booked): array
    {
        $draft_result = self::compareTarget($internal, $draft, 'draft');
        $booked_result = self::compareTarget($internal, $booked, 'booked');

        return [
            'totals' => [
                'internal_net_total' => (float)($internal['totals']['net_total'] ?? 0.0),
            ],
            'targets' => [
                'draft' => $draft_result,
                'booked' => $booked_result,
            ],
            'warnings' => array_values(array_unique(array_merge(
                (array)($internal['warnings'] ?? []),
                (array)($draft_result['warnings'] ?? []),
                (array)($booked_result['warnings'] ?? [])
            ))),
        ];
    }

    public static function compareTarget(array $internal, ?array $target, string $target_name): array
    {
        if ($target === null) {
            return [
                'target' => $target_name,
                'status' => 'missing_target',
                'overall_match' => false,
                'totals' => [
                    'internal_net_total' => (float)($internal['totals']['net_total'] ?? 0.0),
                    'target_net_total' => null,
                    'difference' => null,
                    'abs_difference' => null,
                    'matches' => false,
                ],
                'lines' => [
                    'summary' => [
                        'internal_billable_count' => (int)($internal['totals']['billable_line_count'] ?? 0),
                        'target_billable_count' => 0,
                        'mismatch_count' => (int)($internal['totals']['billable_line_count'] ?? 0),
                    ],
                    'diff' => [],
                ],
                'departments' => [
                    'matches' => false,
                    'diff' => [],
                ],
                'mismatch_reasons' => ['missing_target'],
                'warnings' => ['Missing ' . $target_name . ' invoice target'],
            ];
        }

        $totals = self::compareTotals(
            (float)($internal['totals']['net_total'] ?? 0.0),
            (float)($target['totals']['net_total'] ?? 0.0)
        );

        $lines = self::compareLines($internal['lines'] ?? [], $target['lines'] ?? []);
        $departments = self::compareDepartments($internal['departments'] ?? [], $target['departments'] ?? []);

        $mismatch_reasons = array_values(array_unique(array_merge(
            $lines['mismatch_reasons'],
            $departments['mismatch_reasons'],
            $totals['matches'] ? [] : ['total_mismatch']
        )));

        $overall_match = $totals['matches'] && $lines['summary']['mismatch_count'] === 0 && $departments['matches'];
        $status = $overall_match
            ? 'exact_match'
            : ($totals['matches'] ? 'partial_mismatch' : 'total_mismatch');

        return [
            'target' => $target_name,
            'status' => $status,
            'overall_match' => $overall_match,
            'totals' => $totals,
            'lines' => [
                'summary' => $lines['summary'],
                'diff' => $lines['diff'],
            ],
            'departments' => [
                'matches' => $departments['matches'],
                'diff' => $departments['diff'],
            ],
            'mismatch_reasons' => $mismatch_reasons,
            'warnings' => array_values(array_unique(array_merge(
                (array)($target['warnings'] ?? []),
                (array)$lines['warnings'],
                (array)$departments['warnings']
            ))),
        ];
    }

    private static function compareTotals(float $internal_total, float $target_total): array
    {
        $difference = $target_total - $internal_total;
        $abs = abs($difference);
        return [
            'internal_net_total' => round($internal_total, 5),
            'target_net_total' => round($target_total, 5),
            'difference' => round($difference, 5),
            'abs_difference' => round($abs, 5),
            'matches' => $abs <= self::TOLERANCE,
        ];
    }

    private static function compareLines(array $internal_lines, array $target_lines): array
    {
        $internal_billable = array_values(array_filter($internal_lines, static fn($l) => (bool)($l['billable'] ?? false)));
        $target_billable = array_values(array_filter($target_lines, static fn($l) => (bool)($l['billable'] ?? false)));

        $internal_grouped = self::groupByKey($internal_billable, 'match_key');
        $target_grouped = self::groupByKey($target_billable, 'match_key');

        $keys = array_values(array_unique(array_merge(array_keys($internal_grouped), array_keys($target_grouped))));
        sort($keys);

        $diff = [];
        $mismatch_reasons = [];
        $warnings = [];
        $unmatched_internal = [];
        $unmatched_target = [];

        foreach ($keys as $key) {
            $left = $internal_grouped[$key] ?? [];
            $right = $target_grouped[$key] ?? [];
            $max = max(count($left), count($right));

            for ($i = 0; $i < $max; $i++) {
                $internal_line = $left[$i] ?? null;
                $target_line = $right[$i] ?? null;

                if ($internal_line === null) {
                    $unmatched_target[] = $target_line;
                    continue;
                }
                if ($target_line === null) {
                    $unmatched_internal[] = $internal_line;
                    continue;
                }

                $reasons = self::lineMismatchReasons($internal_line, $target_line);
                if (!empty($reasons)) {
                    $diff[] = [
                        'match_key' => $key,
                        'reasons' => $reasons,
                        'internal_line' => $internal_line,
                        'target_line' => $target_line,
                    ];
                    $mismatch_reasons = array_merge($mismatch_reasons, $reasons);
                }
            }
        }

        // Secondary pairing by reference/description to convert missing/extra into explicit product mismatch when possible.
        [$paired_diff, $still_unmatched_internal, $still_unmatched_target] = self::secondaryPairAndCompare($unmatched_internal, $unmatched_target);
        $diff = array_merge($diff, $paired_diff);
        foreach ($paired_diff as $entry) {
            $mismatch_reasons = array_merge($mismatch_reasons, $entry['reasons']);
        }

        foreach ($still_unmatched_internal as $line) {
            $diff[] = [
                'match_key' => (string)($line['match_key'] ?? ''),
                'reasons' => ['missing_in_target'],
                'internal_line' => $line,
                'target_line' => null,
            ];
            $mismatch_reasons[] = 'missing_in_target';
        }
        foreach ($still_unmatched_target as $line) {
            $diff[] = [
                'match_key' => (string)($line['match_key'] ?? ''),
                'reasons' => ['extra_in_target'],
                'internal_line' => null,
                'target_line' => $line,
            ];
            $mismatch_reasons[] = 'extra_in_target';
        }

        $internal_non_billable = count($internal_lines) - count($internal_billable);
        $target_non_billable = count($target_lines) - count($target_billable);
        if ($internal_non_billable !== $target_non_billable) {
            $warnings[] = 'Non-billable line count differs: internal=' . $internal_non_billable . ', target=' . $target_non_billable;
        }

        return [
            'summary' => [
                'internal_billable_count' => count($internal_billable),
                'target_billable_count' => count($target_billable),
                'mismatch_count' => count($diff),
            ],
            'diff' => $diff,
            'mismatch_reasons' => array_values(array_unique($mismatch_reasons)),
            'warnings' => $warnings,
        ];
    }

    private static function secondaryPairAndCompare(array $unmatched_internal, array $unmatched_target): array
    {
        $left_by_secondary = self::groupByKey(array_values($unmatched_internal), 'secondary_key');
        $right_by_secondary = self::groupByKey(array_values($unmatched_target), 'secondary_key');

        $secondary_keys = array_values(array_unique(array_merge(array_keys($left_by_secondary), array_keys($right_by_secondary))));
        sort($secondary_keys);

        $paired_diff = [];
        $left_remainder = [];
        $right_remainder = [];

        foreach ($secondary_keys as $secondary_key) {
            $left = $left_by_secondary[$secondary_key] ?? [];
            $right = $right_by_secondary[$secondary_key] ?? [];
            $max = max(count($left), count($right));
            for ($i = 0; $i < $max; $i++) {
                $internal_line = $left[$i] ?? null;
                $target_line = $right[$i] ?? null;

                if ($internal_line === null) {
                    if ($target_line !== null) {
                        $right_remainder[] = $target_line;
                    }
                    continue;
                }
                if ($target_line === null) {
                    $left_remainder[] = $internal_line;
                    continue;
                }

                $reasons = self::lineMismatchReasons($internal_line, $target_line);
                if ((string)($internal_line['product_number'] ?? '') !== (string)($target_line['product_number'] ?? '')) {
                    $reasons[] = 'product_mismatch';
                }
                $reasons = array_values(array_unique($reasons));
                $paired_diff[] = [
                    'match_key' => (string)($internal_line['match_key'] ?? ''),
                    'reasons' => $reasons,
                    'internal_line' => $internal_line,
                    'target_line' => $target_line,
                ];
            }
        }

        return [$paired_diff, $left_remainder, $right_remainder];
    }

    private static function lineMismatchReasons(array $internal_line, array $target_line): array
    {
        $reasons = [];

        if ((string)($internal_line['product_number'] ?? '') !== (string)($target_line['product_number'] ?? '')) {
            $reasons[] = 'product_mismatch';
        }

        if (self::normalizeText((string)($internal_line['description'] ?? '')) !== self::normalizeText((string)($target_line['description'] ?? ''))) {
            $reasons[] = 'description_mismatch';
        }

        if (!self::matchesNumber((float)($internal_line['quantity'] ?? 0), (float)($target_line['quantity'] ?? 0))) {
            $reasons[] = 'quantity_mismatch';
        }

        if (!self::matchesNumber((float)($internal_line['unit_net_price'] ?? 0), (float)($target_line['unit_net_price'] ?? 0))) {
            $reasons[] = 'unit_price_mismatch';
        }

        if (!self::matchesNumber((float)($internal_line['line_net_amount'] ?? 0), (float)($target_line['line_net_amount'] ?? 0))) {
            $reasons[] = 'line_total_mismatch';
        }

        if (!self::departmentDistributionMatches(
            (array)($internal_line['department_distribution'] ?? []),
            (array)($target_line['department_distribution'] ?? [])
        )) {
            $reasons[] = 'departmental_distribution_mismatch';
        }

        return $reasons;
    }

    private static function compareDepartments(array $internal_departments, array $target_departments): array
    {
        $keys = array_values(array_unique(array_merge(array_keys($internal_departments), array_keys($target_departments))));
        sort($keys);

        $diff = [];
        $mismatch_reasons = [];

        foreach ($keys as $key) {
            $internal_amount = (float)($internal_departments[$key] ?? 0.0);
            $target_amount = (float)($target_departments[$key] ?? 0.0);
            $difference = $target_amount - $internal_amount;
            $matches = abs($difference) <= self::TOLERANCE;

            $diff[] = [
                'department_key' => (string)$key,
                'internal_amount' => round($internal_amount, 5),
                'target_amount' => round($target_amount, 5),
                'difference' => round($difference, 5),
                'matches' => $matches,
            ];
            if (!$matches) {
                $mismatch_reasons[] = 'department_total_mismatch';
            }
        }

        return [
            'matches' => empty($mismatch_reasons),
            'diff' => $diff,
            'mismatch_reasons' => array_values(array_unique($mismatch_reasons)),
            'warnings' => [],
        ];
    }

    private static function groupByKey(array $lines, string $preferred_key): array
    {
        $grouped = [];
        foreach ($lines as $line) {
            $secondary_key = self::normalizeText((string)($line['description'] ?? '')) .
                '|ref:' . self::normalizeText((string)($line['reference'] ?? ''));
            $line['secondary_key'] = $secondary_key;
            $key = (string)($line[$preferred_key] ?? $secondary_key);
            if (!isset($grouped[$key])) {
                $grouped[$key] = [];
            }
            $grouped[$key][] = $line;
        }

        foreach ($grouped as &$bucket) {
            usort($bucket, static function ($a, $b) {
                return ((int)($a['source_line_id'] ?? 0)) <=> ((int)($b['source_line_id'] ?? 0));
            });
        }

        return $grouped;
    }

    private static function departmentDistributionMatches(array $left, array $right): bool
    {
        $keys = array_values(array_unique(array_merge(array_keys($left), array_keys($right))));
        foreach ($keys as $key) {
            if (!self::matchesNumber((float)($left[$key] ?? 0.0), (float)($right[$key] ?? 0.0))) {
                return false;
            }
        }
        return true;
    }

    private static function matchesNumber(float $left, float $right): bool
    {
        return abs($left - $right) <= self::TOLERANCE;
    }

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

