<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Commerce;

use WP_Error;

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

final class PromotionEngine
{
    public function __construct(private PromotionRepository $repository)
    {
    }

    /**
     * @param array<int, array<string, mixed>> $lineItems
     * @return array<string, mixed>|WP_Error
     */
    public function evaluate(
        string $code,
        float $subtotal,
        float $shipping,
        string $currency,
        array $lineItems
    ): array|WP_Error {
        $code = $this->repository->normalizeCode($code);
        if ($code === '') {
            return [
                'applied' => false,
                'promotion_id' => 0,
                'code' => '',
                'label' => '',
                'discount' => 0.0,
                'shipping_discount' => 0.0,
                'type' => '',
                'line_discounts' => [],
            ];
        }

        $promotion = $this->repository->getByCode($code);
        if (! is_array($promotion) || sanitize_key((string) ($promotion['status'] ?? '')) !== 'active') {
            return new WP_Error('h4bs_promotion_invalid', __('That promotion code is not valid.', 'help4-builder-suite'));
        }

        $now = time();
        $startsAt = strtotime((string) ($promotion['starts_at'] ?? ''));
        $endsAt = strtotime((string) ($promotion['ends_at'] ?? ''));
        if ($startsAt !== false && $startsAt > $now) {
            return new WP_Error('h4bs_promotion_not_started', __('That promotion has not started yet.', 'help4-builder-suite'));
        }
        if ($endsAt !== false && $endsAt < $now) {
            return new WP_Error('h4bs_promotion_expired', __('That promotion has expired.', 'help4-builder-suite'));
        }

        $promotionCurrency = strtoupper(sanitize_text_field((string) ($promotion['currency'] ?? '')));
        if ($promotionCurrency !== '' && $promotionCurrency !== strtoupper($currency)) {
            return new WP_Error('h4bs_promotion_currency', __('That promotion is not valid for this currency.', 'help4-builder-suite'));
        }
        if ($subtotal < (float) ($promotion['minimum_subtotal'] ?? 0)) {
            return new WP_Error('h4bs_promotion_minimum', __('The order subtotal does not meet this promotion minimum.', 'help4-builder-suite'));
        }
        $usageLimit = max(0, (int) ($promotion['usage_limit'] ?? 0));
        if ($usageLimit > 0 && (int) ($promotion['usage_count'] ?? 0) >= $usageLimit) {
            return new WP_Error('h4bs_promotion_limit_reached', __('That promotion has reached its usage limit.', 'help4-builder-suite'));
        }

        $payload = json_decode((string) ($promotion['payload'] ?? ''), true);
        $payload = is_array($payload) ? $payload : [];
        $included = $this->idList($payload['product_ids'] ?? []);
        $excluded = $this->idList($payload['excluded_product_ids'] ?? []);
        $eligibleTotal = 0.0;
        $eligibleLines = [];
        foreach ($lineItems as $lineIndex => $line) {
            if (! is_array($line)) {
                continue;
            }
            $productId = absint($line['product_id'] ?? 0);
            if (($included && ! in_array($productId, $included, true)) || in_array($productId, $excluded, true)) {
                continue;
            }
            $lineTotal = max(0.0, (float) ($line['line_total'] ?? 0));
            $lineUnits = max(0.0, (float) ($line['qty'] ?? 0));
            $eligibleTotal += $lineTotal;
            $eligibleLines[(int) $lineIndex] = [
                'total' => $lineTotal,
                'units' => $lineUnits,
            ];
        }
        if ($eligibleTotal <= 0 && sanitize_key((string) ($promotion['discount_type'] ?? '')) !== 'free_shipping') {
            return new WP_Error('h4bs_promotion_products', __('That promotion does not apply to the products in this order.', 'help4-builder-suite'));
        }

        $amount = max(0.0, (float) ($promotion['amount'] ?? 0));
        $type = sanitize_key((string) ($promotion['discount_type'] ?? 'percent'));
        $discount = 0.0;
        $shippingDiscount = 0.0;
        $rawLineDiscounts = [];
        if ($type === 'percent') {
            foreach ($eligibleLines as $lineIndex => $eligibleLine) {
                $rawLineDiscounts[$lineIndex] = (float) $eligibleLine['total'] * min(100.0, $amount) / 100;
            }
        } elseif ($type === 'fixed_product') {
            foreach ($eligibleLines as $lineIndex => $eligibleLine) {
                $rawLineDiscounts[$lineIndex] = min(
                    (float) $eligibleLine['total'],
                    $amount * (float) $eligibleLine['units']
                );
            }
        } elseif ($type === 'fixed_cart') {
            $cartDiscount = min($included || $excluded ? $eligibleTotal : $subtotal, $amount);
            foreach ($eligibleLines as $lineIndex => $eligibleLine) {
                $rawLineDiscounts[$lineIndex] = $eligibleTotal > 0
                    ? $cartDiscount * ((float) $eligibleLine['total'] / $eligibleTotal)
                    : 0.0;
            }
        } elseif ($type === 'free_shipping') {
            $shippingDiscount = max(0.0, $shipping);
        }

        $discount = array_sum($rawLineDiscounts);

        $maximumDiscount = max(0.0, (float) ($promotion['maximum_discount'] ?? 0));
        if ($maximumDiscount > 0) {
            $discount = min($discount, $maximumDiscount);
            $shippingDiscount = min($shippingDiscount, $maximumDiscount);
        }
        $discount = round(max(0.0, min($subtotal, $discount)), 2);
        $shippingDiscount = round(max(0.0, min($shipping, $shippingDiscount)), 2);
        $lineDiscounts = $this->roundLineDiscounts($rawLineDiscounts, $eligibleLines, $discount);
        $discount = round(array_sum($lineDiscounts), 2);

        return [
            'applied' => true,
            'promotion_id' => absint($promotion['id'] ?? 0),
            'code' => (string) ($promotion['code'] ?? $code),
            'label' => sanitize_text_field((string) ($promotion['name'] ?? $code)),
            'discount' => $discount,
            'shipping_discount' => $shippingDiscount,
            'type' => $type,
            'line_discounts' => $lineDiscounts,
        ];
    }

    /**
     * @param array<int, float> $rawDiscounts
     * @param array<int, array{total: float, units: float}> $eligibleLines
     * @return array<int, float>
     */
    private function roundLineDiscounts(array $rawDiscounts, array $eligibleLines, float $target): array
    {
        $rawTotal = array_sum($rawDiscounts);
        if ($target <= 0 || $rawTotal <= 0 || ! $rawDiscounts) {
            return [];
        }

        $scale = min(1.0, $target / $rawTotal);
        $remaining = round($target, 2);
        $indexes = array_keys($rawDiscounts);
        $lastIndex = end($indexes);
        $rounded = [];
        foreach ($rawDiscounts as $lineIndex => $rawDiscount) {
            $lineTotal = max(0.0, (float) ($eligibleLines[$lineIndex]['total'] ?? 0));
            $lineDiscount = $lineIndex === $lastIndex
                ? $remaining
                : round(max(0.0, $rawDiscount) * $scale, 2);
            $lineDiscount = min($lineTotal, max(0.0, $lineDiscount));
            $rounded[$lineIndex] = round($lineDiscount, 2);
            $remaining = round(max(0.0, $remaining - $lineDiscount), 2);
        }

        return $rounded;
    }

    /**
     * @return array<int, int>
     */
    private function idList(mixed $value): array
    {
        if (is_string($value)) {
            $value = preg_split('/[\s,]+/', $value) ?: [];
        }
        if (! is_array($value)) {
            return [];
        }
        return array_values(array_unique(array_filter(array_map('absint', $value))));
    }
}
