<?php

namespace App\Services;

use App\Models\AccountCreditDeposit;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use RuntimeException;

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

    /**
     * Return the authoritative WHMCS credit balance and account currency.
     * The browser country and session currency are intentionally ignored for
     * authenticated clients because WHMCS credit belongs to one fixed currency.
     */
    public function accountSnapshot(User $user, bool $fresh = true): array
    {
        $clientId = (int) ($user->whmcs_client_id ?? 0);
        if ($clientId <= 0) {
            throw new RuntimeException('Your portal account is not linked to a WHMCS client account.');
        }

        $response = $this->api->clientDetails($clientId, $fresh);
        if (! ($response['ok'] ?? false) || ! is_array($response['client'] ?? null)) {
            throw new RuntimeException($response['message'] ?? 'Could not load your WHMCS credit balance.');
        }

        $client = $response['client'];
        $currency = $this->resolveClientCurrency($client, $user);
        $balance = $this->moneyValue($client, ['credit', 'creditbalance', 'credit_balance']) ?? 0.0;
        if (strtoupper((string) $user->currency) !== $currency) {
            $user->forceFill([
                'currency' => $currency,
                'currency_locked_at' => $user->currency_locked_at ?: now(),
                'currency_source' => 'whmcs_client_credit',
            ])->save();
        }

        return [
            'client_id' => $clientId,
            'currency' => $currency,
            'balance' => round($balance, 2),
            'client' => $client,
        ];
    }

    public function validateDeposit(User $user, float $amount): array
    {
        $snapshot = $this->accountSnapshot($user, true);
        $amount = round($amount, 2);

        if ($amount <= 0) {
            throw new RuntimeException('The credit amount must be greater than zero.');
        }

        if ((bool) config('zt.credit.require_approved_order', true) && ! $this->hasApprovedOrder($snapshot['client_id'])) {
            throw new RuntimeException('You must have at least one Active WHMCS order before adding account credit.');
        }

        return $snapshot;
    }

    /**
     * Record a verified gateway payment as a WHMCS transaction and credit it to
     * the client account in one API operation. This is preferable to AddCredit
     * because WHMCS also receives the accounting transaction and gateway ID.
     */
    public function recordGatewayCredit(AccountCreditDeposit $deposit, string $transactionId): array
    {
        $clientId = (int) $deposit->whmcs_client_id;
        if ($clientId <= 0) {
            throw new RuntimeException('The credit deposit is missing its WHMCS client ID.');
        }

        // Basket-level locking prevents duplicate callback handling. This
        // client-level lock also serializes separate deposits for the same
        // WHMCS client while the gateway transaction is being recorded.
        $lock = Cache::lock('whmcs-credit-client:'.$clientId, 120);
        if (! $lock->get()) {
            throw new RuntimeException('Another account-credit update is currently being processed. This deposit will retry automatically.');
        }

        try {
            return $this->recordGatewayCreditUnlocked($deposit, $transactionId);
        } finally {
            optional($lock)->release();
        }
    }

    private function recordGatewayCreditUnlocked(AccountCreditDeposit $deposit, string $transactionId): array
    {
        $transactionId = trim($transactionId);
        if ($transactionId === '') {
            throw new RuntimeException('The gateway transaction ID is missing.');
        }

        $deposit->loadMissing('user');
        if (! $deposit->user) {
            throw new RuntimeException('The credit deposit is missing its user account.');
        }

        $snapshot = $this->accountSnapshot($deposit->user, true);
        if ((int) $snapshot['client_id'] !== (int) $deposit->whmcs_client_id) {
            throw new RuntimeException('The WHMCS client link changed after checkout. Manual review is required.');
        }

        if (! hash_equals($snapshot['currency'], strtoupper((string) $deposit->currency))) {
            throw new RuntimeException('The WHMCS client currency changed after checkout. Manual review is required.');
        }

        $existing = $this->api->transactions(
            clientId: null,
            transactionId: $transactionId,
            fresh: true,
        );

        if ($existing['ok'] ?? false) {
            $transaction = $this->findMatchingTransaction($existing['transactions'] ?? [], $transactionId);
            if ($transaction !== null) {
                $this->assertExistingTransactionMatches($transaction, $deposit);

                $fresh = $this->accountSnapshot($deposit->user, true);

                return [
                    'ok' => true,
                    'already_recorded' => true,
                    'balance' => $fresh['balance'],
                    'currency' => $fresh['currency'],
                    'transaction' => $transaction,
                ];
            }
        }

        $description = sprintf(
            'Account credit deposit via PayFast; charged %s; portal deposit #%d',
            $this->formatMoney((float) $deposit->payable_amount, (string) $deposit->payable_currency),
            $deposit->id,
        );

        $sourceRate = Arr::get((array) $deposit->currency_snapshot, 'source.rate');
        $sourceRate = is_numeric($sourceRate) && (float) $sourceRate > 0 ? (float) $sourceRate : null;
        $expectedBalance = round((float) $snapshot['balance'] + (float) $deposit->amount, 2);

        $response = $this->api->addCreditTransaction(
            clientId: (int) $deposit->whmcs_client_id,
            transactionId: $transactionId,
            gateway: (string) $deposit->gateway,
            amount: (float) $deposit->amount,
            description: $description,
            rate: $sourceRate,
        );

        if (! ($response['ok'] ?? false)) {
            $message = (string) ($response['message'] ?? 'WHMCS AddTransaction failed.');

            // A timeout can occur after WHMCS committed the transaction. Verify
            // by the unique gateway transaction ID before deciding it failed.
            $verify = $this->api->transactions(
                clientId: null,
                transactionId: $transactionId,
                fresh: true,
            );
            $transaction = ($verify['ok'] ?? false)
                ? $this->findMatchingTransaction($verify['transactions'] ?? [], $transactionId)
                : null;

            if ($transaction === null) {
                throw new RuntimeException('Payment was verified, but WHMCS could not add the account credit. '.$message);
            }

            $this->assertExistingTransactionMatches($transaction, $deposit);
        }

        $fresh = $this->accountSnapshot($deposit->user, true);
        if (((float) $fresh['balance'] + 0.001) < $expectedBalance) {
            throw new RuntimeException('WHMCS recorded the gateway transaction but did not increase the client credit balance as expected. Manual review is required.');
        }

        return [
            'ok' => true,
            'already_recorded' => false,
            'balance' => $fresh['balance'],
            'currency' => $fresh['currency'],
            'response' => $response['response'] ?? null,
        ];
    }

    private function hasApprovedOrder(int $clientId): bool
    {
        // Match WHMCS's native Add Funds fraud control: the client must have at
        // least one manually approved order whose order status is Active.
        $orders = $this->api->clientOrders(
            clientId: $clientId,
            limit: 1,
            fresh: true,
            status: 'Active',
        );

        if (! ($orders['ok'] ?? false)) {
            throw new RuntimeException($orders['message'] ?? 'WHMCS could not verify the active-order requirement.');
        }

        foreach ($orders['orders'] ?? [] as $order) {
            if (strtolower(trim((string) Arr::get($order, 'status'))) === 'active') {
                return true;
            }
        }

        return false;
    }

    private function resolveClientCurrency(array $client, User $user): string
    {
        foreach (['currency_code', 'currencycode'] as $key) {
            $candidate = strtoupper(trim((string) Arr::get($client, $key, '')));
            if (in_array($candidate, ['USD', 'PKR'], true)) {
                return $candidate;
            }
        }

        $rawCurrency = trim((string) Arr::get($client, 'currency', ''));
        $candidate = strtoupper($rawCurrency);
        if (in_array($candidate, ['USD', 'PKR'], true)) {
            return $candidate;
        }

        $currencyId = is_numeric($rawCurrency) ? (int) $rawCurrency : 0;
        if ($currencyId > 0) {
            foreach ($this->api->currencies(true) as $currency) {
                if ((int) Arr::get($currency, 'id', 0) !== $currencyId) {
                    continue;
                }

                $code = strtoupper(trim((string) Arr::get($currency, 'code', '')));
                if (in_array($code, ['USD', 'PKR'], true)) {
                    return $code;
                }
            }
        }

        $fallback = strtoupper(trim((string) $user->currency));
        if (in_array($fallback, ['USD', 'PKR'], true)) {
            return $fallback;
        }

        throw new RuntimeException('The WHMCS client currency could not be resolved as USD or PKR.');
    }

    private function findMatchingTransaction(array $transactions, string $transactionId): ?array
    {
        foreach ($transactions as $transaction) {
            if (! is_array($transaction)) {
                continue;
            }

            $candidate = trim((string) Arr::get($transaction, 'transid', ''));
            if ($candidate !== '' && hash_equals($transactionId, $candidate)) {
                return $transaction;
            }
        }

        return null;
    }

    private function assertExistingTransactionMatches(array $transaction, AccountCreditDeposit $deposit): void
    {
        $clientId = (int) (Arr::get($transaction, 'userid') ?: Arr::get($transaction, 'clientid'));
        if ($clientId > 0 && $clientId !== (int) $deposit->whmcs_client_id) {
            throw new RuntimeException('The gateway transaction ID already belongs to a different WHMCS client.');
        }

        $invoiceId = (int) Arr::get($transaction, 'invoiceid', 0);
        if ($invoiceId > 0) {
            throw new RuntimeException('The gateway transaction ID already belongs to a WHMCS invoice payment, not an account-credit deposit.');
        }

        $amount = $this->moneyValue($transaction, ['amountin']);
        if ($amount !== null && ! $this->sameMoney($amount, (float) $deposit->amount)) {
            throw new RuntimeException('The gateway transaction ID already exists in WHMCS with a different amount.');
        }

        $description = trim((string) Arr::get($transaction, 'description', ''));
        $expectedMarker = 'portal deposit #'.$deposit->id;
        if ($description !== '' && ! str_contains(strtolower($description), strtolower($expectedMarker))) {
            throw new RuntimeException('The gateway transaction ID already exists in WHMCS but is not linked to this portal credit deposit.');
        }
    }

    private function moneyValue(array $data, array $keys): ?float
    {
        foreach ($keys as $key) {
            $value = Arr::get($data, $key);
            if ($value !== null && $value !== '' && is_numeric($value)) {
                return round((float) $value, 2);
            }
        }

        return null;
    }

    private function sameMoney(float $left, float $right): bool
    {
        return (int) round($left * 100) === (int) round($right * 100);
    }

    private function formatMoney(float $amount, string $currency): string
    {
        $currency = strtoupper($currency);
        $formatted = number_format($amount, 2);

        return $currency === 'USD' ? '$'.$formatted.' USD' : 'PKR '.$formatted;
    }
}
