<?php

namespace App\Services;

use App\Models\Cart;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use RuntimeException;

class PromotionService
{
    public function __construct(private readonly ZtApiService $api) {}

    public function apply(Cart $cart, string $code, ?User $user = null): array
    {
        $code = strtoupper(trim($code));
        if ($code === '') {
            throw new RuntimeException('Enter a promo code.');
        }

        $promotion = $this->findPromotion($code);
        $result = $this->calculate($cart, $promotion, $user);

        $cart->update([
            'promo_code' => $code,
            'promo_id' => (int) ($promotion['id'] ?? 0) ?: null,
            'promo_type' => (string) ($promotion['type'] ?? ''),
            'promo_value' => (float) ($promotion['value'] ?? 0),
            'discount' => $result['discount'],
            'promo_metadata' => [
                'code' => $code,
                'type' => $promotion['type'] ?? null,
                'value' => (float) ($promotion['value'] ?? 0),
                'description' => $promotion['description'] ?? null,
                'eligible_subtotal' => $result['eligible_subtotal'],
                'applies_to_item_ids' => $result['applies_to_item_ids'],
                'promotion_value_in_cart_currency' => $result['promotion_value_in_cart_currency'],
                'cart_currency' => strtoupper((string) $cart->currency),
                'recurring' => $this->truthy($promotion['recurring'] ?? false),
            ],
        ]);

        return $result + ['code' => $code, 'promotion' => $promotion];
    }

    public function refresh(Cart $cart, ?User $user = null): void
    {
        if (! $cart->promo_code) {
            return;
        }

        try {
            $this->apply($cart->fresh('items'), (string) $cart->promo_code, $user);
        } catch (\Throwable) {
            $this->clear($cart);
        }
    }

    public function clear(Cart $cart): void
    {
        $cart->update([
            'promo_code' => null,
            'promo_id' => null,
            'promo_type' => null,
            'promo_value' => null,
            'discount' => 0,
            'promo_metadata' => null,
        ]);
    }

    private function findPromotion(string $code): array
    {
        $response = $this->api->request([
            'action' => 'GetPromotions',
            'code' => $code,
        ], 60);

        if (strtolower((string) Arr::get($response, 'result')) !== 'success') {
            throw new RuntimeException('This promo code could not be verified.');
        }

        $promotions = Arr::get($response, 'promotions.promotion', Arr::get($response, 'promotions', []));
        if (isset($promotions['id']) || isset($promotions['code'])) {
            $promotions = [$promotions];
        }

        foreach (is_array($promotions) ? $promotions : [] as $promotion) {
            if (is_array($promotion) && strtoupper(trim((string) ($promotion['code'] ?? ''))) === $code) {
                return $promotion;
            }
        }

        throw new RuntimeException('This promo code is invalid or no longer available.');
    }

    private function calculate(Cart $cart, array $promotion, ?User $user): array
    {
        $cart->loadMissing('items');
        $today = now()->startOfDay();
        $startDate = $this->dateValue($promotion['startdate'] ?? null);
        $expiryDate = $this->dateValue($promotion['expirationdate'] ?? null);

        if ($startDate && $today->lt($startDate)) {
            throw new RuntimeException('This promo code is not active yet.');
        }
        if ($expiryDate && $today->gt($expiryDate)) {
            throw new RuntimeException('This promo code has expired.');
        }

        $maxUses = (int) ($promotion['maxuses'] ?? 0);
        $uses = (int) ($promotion['uses'] ?? 0);
        if ($maxUses > 0 && $uses >= $maxUses) {
            throw new RuntimeException('This promo code has reached its usage limit.');
        }

        if ($this->truthy($promotion['existingclient'] ?? false) && ! ($user?->whmcs_client_id)) {
            throw new RuntimeException('This promo code is available to existing customers only.');
        }
        if ($this->truthy($promotion['newsignups'] ?? false) && $user?->whmcs_client_id) {
            throw new RuntimeException('This promo code is available to new customers only.');
        }

        $requiredIds = $this->idList($promotion['requires'] ?? '');
        if ($requiredIds !== []) {
            $cartProductIds = $cart->items->where('item_type', 'hosting')->pluck('api_pid')->map(fn ($id) => (int) $id)->all();
            if (count(array_intersect($requiredIds, $cartProductIds)) === 0) {
                throw new RuntimeException('Your cart does not contain the product required by this promo code.');
            }
        }

        $appliesTo = $this->idList($promotion['appliesto'] ?? '');
        $cycles = $this->stringList($promotion['cycles'] ?? '');
        $eligible = $cart->items->filter(function ($item) use ($appliesTo, $cycles) {
            if ($item->item_type !== 'hosting') {
                return false;
            }
            if ($appliesTo !== [] && ! in_array((int) $item->api_pid, $appliesTo, true)) {
                return false;
            }
            if ($cycles !== [] && ! in_array(strtolower((string) $item->billing_cycle), $cycles, true)) {
                return false;
            }

            return true;
        })->values();

        if ($this->truthy($promotion['applyonce'] ?? false)) {
            $eligible = $eligible->take(1);
        }

        if ($eligible->isEmpty()) {
            throw new RuntimeException('This promo code does not apply to the selected plan or billing period.');
        }

        $eligibleSubtotal = (float) $eligible->sum('total');
        $value = max(0, (float) ($promotion['value'] ?? 0));
        $type = strtolower(trim((string) ($promotion['type'] ?? '')));
        $cartCurrencyValue = in_array($type, ['fixed amount', 'fixedamount', 'price override', 'priceoverride'], true)
            ? $this->convertBaseCurrencyValue($value, (string) $cart->currency)
            : $value;

        $discount = match ($type) {
            'percentage' => $eligibleSubtotal * ($value / 100),
            'fixed amount', 'fixedamount' => $cartCurrencyValue,
            'price override', 'priceoverride' => max(0, $eligibleSubtotal - $cartCurrencyValue),
            'free setup', 'freesetup' => 0,
            default => throw new RuntimeException('This promo type is not supported by the checkout display.'),
        };

        $subtotal = (float) $cart->items->sum('total');
        $discount = round(min(max($discount, 0), $subtotal), 2);

        return [
            'discount' => $discount,
            'eligible_subtotal' => round($eligibleSubtotal, 2),
            'applies_to_item_ids' => $eligible->pluck('id')->all(),
            'promotion_value_in_cart_currency' => round($cartCurrencyValue, 2),
        ];
    }

    private function convertBaseCurrencyValue(float $amount, string $cartCurrency): float
    {
        if ($amount <= 0) {
            return 0;
        }

        $cartCurrency = strtoupper(trim($cartCurrency));
        $currencies = $this->api->currencies();

        foreach ($currencies as $currency) {
            if (! is_array($currency) || strtoupper(trim((string) Arr::get($currency, 'code'))) !== $cartCurrency) {
                continue;
            }

            $rate = (float) Arr::get($currency, 'rate', 0);
            if ($rate <= 0) {
                break;
            }

            // WHMCS stores fixed promotion values in the base currency. Currency
            // rates are relative to that base, so convert the value before
            // presenting a discount in a USD/PKR cart.
            return round($amount * $rate, 2);
        }

        throw new RuntimeException('This promo code could not be priced in the selected currency.');
    }

    private function idList(mixed $value): array
    {
        return collect($this->stringList($value))
            ->map(fn ($id) => (int) $id)
            ->filter(fn ($id) => $id > 0)
            ->unique()
            ->values()
            ->all();
    }

    private function stringList(mixed $value): array
    {
        if (is_array($value)) {
            $value = implode(',', $value);
        }

        return collect(preg_split('/[,;|]/', strtolower((string) $value)) ?: [])
            ->map(fn ($item) => trim($item))
            ->filter()
            ->values()
            ->all();
    }

    private function truthy(mixed $value): bool
    {
        return in_array(strtolower(trim((string) $value)), ['1', 'true', 'yes', 'on'], true);
    }

    private function dateValue(mixed $value): ?Carbon
    {
        $value = trim((string) $value);
        if ($value === '' || $value === '0000-00-00') {
            return null;
        }

        try {
            return Carbon::parse($value)->startOfDay();
        } catch (\Throwable) {
            return null;
        }
    }
}
