<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Commerce;

if (! defined('ABSPATH')) {
    exit;
}

final class TaxCalculator
{
    /**
     * @param array<int, array<string, mixed>> $lineItems
     * @param array<string, mixed> $address
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    public function calculate(
        array $lineItems,
        float $subtotal,
        float $discount,
        float $shipping,
        array $address,
        array $settings
    ): array {
        $rates = $this->normalizeRates($settings['commerce_tax_rates'] ?? []);
        if (! $rates) {
            return ['total' => 0.0, 'lines' => [], 'taxable_subtotal' => 0.0, 'shipping_taxable' => 0.0];
        }

        $taxableByClass = [];
        $hasLineDiscounts = false;
        foreach ($lineItems as $line) {
            if (! is_array($line)) {
                continue;
            }
            $taxStatus = sanitize_key((string) ($line['tax_status'] ?? 'taxable'));
            if ($taxStatus !== 'taxable') {
                continue;
            }
            $taxClass = sanitize_key((string) ($line['tax_class'] ?? 'standard')) ?: 'standard';
            $lineTotal = max(0.0, (float) ($line['line_total'] ?? 0));
            if (array_key_exists('discount_total', $line)) {
                $hasLineDiscounts = true;
                $lineTotal = max(0.0, $lineTotal - max(0.0, (float) $line['discount_total']));
            }
            $taxableByClass[$taxClass] = ($taxableByClass[$taxClass] ?? 0.0) + $lineTotal;
        }

        $taxableSubtotal = array_sum($taxableByClass);
        if (! $hasLineDiscounts && $taxableSubtotal > 0 && $discount > 0 && $subtotal > 0) {
            $discountRatio = min(1.0, $discount / $subtotal);
            foreach ($taxableByClass as $taxClass => $taxableAmount) {
                $taxableByClass[$taxClass] = max(0.0, $taxableAmount * (1 - $discountRatio));
            }
            $taxableSubtotal = array_sum($taxableByClass);
        }

        $country = strtoupper(substr(sanitize_text_field((string) ($address['country'] ?? '')), 0, 2));
        $state = strtoupper(sanitize_text_field((string) ($address['state'] ?? '')));
        $postcode = strtoupper(sanitize_text_field((string) ($address['postcode'] ?? ($address['postal_code'] ?? ($address['zip'] ?? '')))));
        $taxLines = [];
        $taxTotal = 0.0;
        foreach ($rates as $rate) {
            if (! $this->matches($rate, $country, $state, $postcode)) {
                continue;
            }
            $taxClass = (string) $rate['tax_class'];
            $base = $taxClass === 'all'
                ? array_sum($taxableByClass)
                : (float) ($taxableByClass[$taxClass] ?? 0);
            if (! empty($rate['shipping'])) {
                $base += max(0.0, $shipping);
            }
            if (! empty($rate['compound'])) {
                $base += $taxTotal;
            }
            if ($base <= 0) {
                continue;
            }
            $amount = round($base * (float) $rate['rate'] / 100, 2);
            if ($amount <= 0) {
                continue;
            }
            $taxTotal += $amount;
            $taxLines[] = [
                'id' => (string) $rate['id'],
                'label' => (string) $rate['label'],
                'rate' => (float) $rate['rate'],
                'tax_class' => $taxClass,
                'base' => round($base, 2),
                'amount' => $amount,
                'compound' => (bool) $rate['compound'],
                'shipping' => (bool) $rate['shipping'],
            ];
        }

        return [
            'total' => round($taxTotal, 2),
            'lines' => $taxLines,
            'taxable_subtotal' => round($taxableSubtotal, 2),
            'shipping_taxable' => round($shipping, 2),
        ];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function normalizeRates(mixed $rawRates): array
    {
        if (is_string($rawRates)) {
            $decoded = json_decode($rawRates, true);
            $rawRates = is_array($decoded) ? $decoded : [];
        }
        if (! is_array($rawRates)) {
            return [];
        }

        $rates = [];
        foreach ($rawRates as $index => $rawRate) {
            if (! is_array($rawRate) || empty($rawRate['enabled'])) {
                continue;
            }
            $rate = max(0.0, min(100.0, (float) ($rawRate['rate'] ?? 0)));
            if ($rate <= 0) {
                continue;
            }
            $rates[] = [
                'id' => sanitize_key((string) ($rawRate['id'] ?? 'rate_' . ($index + 1))),
                'label' => sanitize_text_field((string) ($rawRate['label'] ?? __('Tax', 'help4-builder-suite'))),
                'country' => strtoupper(substr(sanitize_text_field((string) ($rawRate['country'] ?? '')), 0, 2)),
                'state' => strtoupper(sanitize_text_field((string) ($rawRate['state'] ?? ''))),
                'postcode_prefix' => strtoupper(sanitize_text_field((string) ($rawRate['postcode_prefix'] ?? ''))),
                'tax_class' => sanitize_key((string) ($rawRate['tax_class'] ?? 'standard')) ?: 'standard',
                'rate' => $rate,
                'priority' => max(1, min(9999, (int) ($rawRate['priority'] ?? 100))),
                'compound' => ! empty($rawRate['compound']),
                'shipping' => ! empty($rawRate['shipping']),
                'enabled' => true,
            ];
        }
        usort($rates, static fn(array $a, array $b): int => ((int) $a['priority']) <=> ((int) $b['priority']));
        return $rates;
    }

    /**
     * @param array<string, mixed> $rate
     */
    private function matches(array $rate, string $country, string $state, string $postcode): bool
    {
        if ((string) $rate['country'] !== '' && (string) $rate['country'] !== $country) {
            return false;
        }
        if ((string) $rate['state'] !== '' && (string) $rate['state'] !== $state) {
            return false;
        }
        $prefix = (string) $rate['postcode_prefix'];
        return $prefix === '' || str_starts_with($postcode, $prefix);
    }
}
