<?php

namespace App\Services;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Str;

class ZtApiService
{
    public function request(array $params = [], int $ttlSeconds = 300): array
    {
        $action = strtolower((string) ($params['action'] ?? 'default'));

        // Never cache write/mutation actions. Caching AddClient/AddOrder/etc. is
        // dangerous because a retry could read an old response instead of making
        // an intentional WHMCS request.
        if ($ttlSeconds <= 0 || ! $this->cacheEnabled() || $this->isMutationAction($action)) {
            return $this->sendRequest($params);
        }

        $cacheKey = $this->apiCacheKey($params);

        if (Cache::has($cacheKey)) {
            $data = Cache::get($cacheKey);

            if ($this->debugEnabled()) {
                $this->logDebug('cache_hit', [
                    'action' => $params['action'] ?? null,
                    'cache_key' => $cacheKey,
                    'ttl_seconds' => $ttlSeconds,
                    'data_summary' => $this->summarizeApiData(is_array($data) ? $data : []),
                ]);
            }

            return is_array($data) ? $data : [];
        }

        if ($this->debugEnabled()) {
            $this->logDebug('cache_miss', [
                'action' => $params['action'] ?? null,
                'cache_key' => $cacheKey,
                'ttl_seconds' => $ttlSeconds,
            ]);
        }

        $data = $this->sendRequest($params);

        if ($this->isCacheableApiResponse($data)) {
            Cache::put($cacheKey, $data, $ttlSeconds);
        } elseif ($this->debugEnabled()) {
            $this->logDebug('cache_skip_uncacheable_response', [
                'action' => $params['action'] ?? null,
                'cache_key' => $cacheKey,
                'data_summary' => $this->summarizeApiData(is_array($data) ? $data : []),
            ]);
        }

        return $data;
    }

    public function mutation(array $params = []): array
    {
        return $this->sendRequest($params);
    }

    public function paymentMutation(array $params = []): array
    {
        return $this->sendRequest($params, $this->paymentCredentials());
    }

    protected function apiCacheKey(array $params): string
    {
        $fingerprint = [
            'api_url' => $this->apiUrl(),
            'auth_style' => config('zt.whmcs.auth_style', 'identifier'),
            'credentials' => hash('sha256', json_encode($this->credentials(), JSON_UNESCAPED_SLASHES)),
            'params' => $params,
        ];

        return 'zt_whmcs_api:v2:'.md5(json_encode($fingerprint, JSON_UNESCAPED_SLASHES));
    }

    protected function isCacheableApiResponse(array $data): bool
    {
        if (empty($data)) {
            return false;
        }

        return strtolower((string) Arr::get($data, 'result')) !== 'error';
    }

    public function credentials(): array
    {
        $identifier = config('zt.whmcs.identifier') ?: config('zt.whmcs.username');
        $secret = config('zt.whmcs.secret') ?: config('zt.whmcs.password');
        $authStyle = strtolower((string) config('zt.whmcs.auth_style', 'identifier'));

        $credentials = [
            'responsetype' => 'json',
        ];

        // WHMCS official docs use identifier/secret. username/password is kept
        // only as compatibility mode because WHMCS also accepts a valid
        // identifier/secret pair through those legacy field names.
        if ($authStyle === 'username_password' || $authStyle === 'legacy') {
            $credentials['username'] = $identifier;
            $credentials['password'] = $secret;
        } elseif ($authStyle === 'both') {
            $credentials['identifier'] = $identifier;
            $credentials['secret'] = $secret;
            $credentials['username'] = $identifier;
            $credentials['password'] = $secret;
        } else {
            $credentials['identifier'] = $identifier;
            $credentials['secret'] = $secret;
        }

        $credentials['accesskey'] = config('zt.whmcs.access_key');

        return array_filter($credentials, fn ($value) => $value !== null && $value !== '');
    }

    public function paymentCredentials(): array
    {
        $identifier = trim((string) config('zt.whmcs.payment_identifier', ''));
        $secret = trim((string) config('zt.whmcs.payment_secret', ''));

        if ($identifier === '' || $secret === '') {
            return $this->credentials();
        }

        return array_filter([
            'identifier' => $identifier,
            'secret' => $secret,
            'accesskey' => config('zt.whmcs.payment_access_key', config('zt.whmcs.access_key')),
            'responsetype' => 'json',
        ], fn ($value) => $value !== null && $value !== '');
    }

    public function apiUrl(): string
    {
        return (string) config('zt.api_base_url');
    }

    protected function sendRequest(array $params = [], ?array $credentials = null): array
    {
        $payload = array_merge($credentials ?? $this->credentials(), $params);
        $requestId = (string) Str::uuid();
        $startedAt = microtime(true);

        $this->logApiRequest($requestId, $payload);

        try {
            $response = Http::asForm()
                ->connectTimeout((int) config('zt.whmcs.connect_timeout', 3))
                ->timeout((int) config('zt.whmcs.timeout', 8))
                ->post($this->apiUrl(), $payload);

            $rawBody = $response->body();
            $normalized = $this->normalizeResponse($response->json() ?? $rawBody);

            $this->logApiResponse($requestId, $payload, $response->status(), $rawBody, $normalized, microtime(true) - $startedAt);

            if ($response->successful()) {
                return $normalized;
            }

            return [
                'result' => 'error',
                'message' => 'WHMCS HTTP request failed with status '.$response->status(),
                'http_status' => $response->status(),
                'body_preview' => $this->bodyPreview($rawBody),
            ];
        } catch (\Throwable $e) {
            report($e);
            $this->logApiException($requestId, $payload, $e, microtime(true) - $startedAt);

            return [
                'result' => 'error',
                'message' => $e->getMessage(),
            ];
        }
    }

    protected function isMutationAction(string $action): bool
    {
        return in_array(strtolower($action), [
            'addclient',
            'validatelogin',
            'resetpassword',
            'updateclient',
            'addorder',
            'acceptorder',
            'createinvoice',
            'addinvoicepayment',
            'addtransaction',
            'updateclientproduct',
            'upgradeproduct',
            'createsotoken',
            'createssotoken',
            'openticket',
            'addticketreply',
            'addticketnote',
            'updateticket',
            'domainregister',
            'domaintransfer',
            'domainrenew',
            'domainupdatenameservers',
            'updateclientdomain',
            'modulecreate',
            'modulechangepw',
        ], true);
    }

    public function products(): array
    {
        $params = ['action' => 'GetProducts'];
        $allowedProductIds = $this->configuredProductIds('allowed_product_ids');

        // WHMCS GetProducts supports fetching a comma-separated pid list. Use
        // this only when a whitelist is configured. Hidden/excluded pids are
        // filtered locally because WHMCS does not provide an exclude-pid option.
        if (count($allowedProductIds) > 0) {
            $params['pid'] = implode(',', $allowedProductIds);
        }

        $data = $this->request($params, (int) config('zt.cache.products_ttl', 3600));
        $products = Arr::get($data, 'products.product');

        if (isset($products['pid'])) {
            $products = [$products];
        }

        $productCount = is_array($products) ? count($products) : 0;
        $filtered = is_array($products) ? $this->filterProducts(array_values($products)) : [];

        if ($this->debugEnabled()) {
            $debug = [
                'action' => 'GetProducts',
                'api_url' => $this->apiUrl(),
                'result' => Arr::get($data, 'result'),
                'message' => Arr::get($data, 'message'),
                'http_status' => Arr::get($data, 'http_status'),
                'raw_product_count' => $productCount,
                'filtered_product_count' => count($filtered),
                'hidden_product_ids' => $this->configuredProductIds('hidden_product_ids'),
                'allowed_product_ids' => $allowedProductIds,
                'response_summary' => $this->summarizeApiData($data),
                'created_at' => now()->toDateTimeString(),
            ];

            Cache::put('zt_whmcs_last_catalog_debug', $debug, 3600);
            $this->logDebug('catalog_get_products_parsed', $debug);
        }

        if ($productCount === 0) {
            if ($this->debugEnabled()) {
                Cache::put('zt_whmcs_last_catalog_error', [
                    'action' => 'GetProducts',
                    'api_url' => $this->apiUrl(),
                    'result' => Arr::get($data, 'result'),
                    'message' => Arr::get($data, 'message'),
                    'http_status' => Arr::get($data, 'http_status'),
                    'response' => $this->limitedPayload($data),
                    'created_at' => now()->toDateTimeString(),
                ], 3600);
            }

            if ((bool) config('zt.products.use_fallback_catalog', false)) {
                $fallback = $this->filterProducts($this->fallbackProducts());
                $this->logDebug('catalog_using_fallback_products', ['fallback_count' => count($fallback)]);

                return $fallback;
            }

            return [];
        }

        if ($this->debugEnabled()) {
            Cache::forget('zt_whmcs_last_catalog_error');
        }

        return $filtered;
    }

    public function currencies(bool $fresh = false): array
    {
        $ttl = $fresh ? 0 : (int) config('zt.cache.currencies_ttl', 3600);
        $data = $this->request(['action' => 'GetCurrencies'], $ttl);
        $currencies = Arr::get($data, 'currencies.currency', []);

        if (isset($currencies['id'])) {
            $currencies = [$currencies];
        }

        return is_array($currencies) ? array_values($currencies) : [];
    }

    public function currencyId(string $code): ?int
    {
        $code = strtoupper(trim($code));
        foreach ($this->currencies() as $currency) {
            if (strtoupper((string) Arr::get($currency, 'code')) === $code) {
                return (int) Arr::get($currency, 'id');
            }
        }
        return null;
    }

    public function tldPricing(string $currency = 'USD'): array
    {
        $params = ['action' => 'GetTLDPricing'];
        $currencyId = $this->currencyId($currency);
        if ($currencyId) {
            $params['currencyid'] = $currencyId;
        }
        $data = $this->request($params, (int) config('zt.cache.tld_pricing_ttl', 21600));
        $pricing = $this->normalizeTldPricing(Arr::get($data, 'pricing'));
        $tldCount = count($pricing);

        if ($this->debugEnabled()) {
            $debug = [
                'action' => 'GetTLDPricing',
                'api_url' => $this->apiUrl(),
                'result' => Arr::get($data, 'result'),
                'message' => Arr::get($data, 'message'),
                'http_status' => Arr::get($data, 'http_status'),
                'tld_count' => $tldCount,
                'response_summary' => $this->summarizeApiData($data),
                'created_at' => now()->toDateTimeString(),
            ];

            Cache::put('zt_whmcs_last_tld_debug', $debug, 3600);
            $this->logDebug('catalog_get_tld_pricing_parsed', $debug);
        }

        if ($tldCount === 0) {
            if ($this->debugEnabled()) {
                Cache::put('zt_whmcs_last_tld_error', [
                    'action' => 'GetTLDPricing',
                    'api_url' => $this->apiUrl(),
                    'result' => Arr::get($data, 'result'),
                    'message' => Arr::get($data, 'message'),
                    'http_status' => Arr::get($data, 'http_status'),
                    'response' => $this->limitedPayload($data),
                    'created_at' => now()->toDateTimeString(),
                ], 3600);
            }

            if ((bool) config('zt.products.use_fallback_catalog', false)) {
                return $this->fallbackTldPricing();
            }

            return [];
        }

        if ($this->debugEnabled()) {
            Cache::forget('zt_whmcs_last_tld_error');
        }

        return $pricing;
    }

    public function paymentMethods(): array
    {
        $data = $this->request(['action' => 'GetPaymentMethods'], (int) config('zt.cache.payment_methods_ttl', 3600));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            if ($this->debugEnabled()) {
                Cache::put('zt_whmcs_last_payment_methods_error', [
                    'action' => 'GetPaymentMethods',
                    'api_url' => $this->apiUrl(),
                    'result' => Arr::get($data, 'result'),
                    'message' => Arr::get($data, 'message'),
                    'http_status' => Arr::get($data, 'http_status'),
                    'response' => $this->limitedPayload($data),
                    'created_at' => now()->toDateTimeString(),
                ], 3600);
            }

            return [];
        }

        $methods = Arr::get($data, 'paymentmethods.paymentmethod', Arr::get($data, 'paymentmethods', []));

        if (isset($methods['paymentmethod'])) {
            $methods = $methods['paymentmethod'];
        }

        if (isset($methods['module'])) {
            $methods = [$methods];
        }

        if (! is_array($methods)) {
            return [];
        }

        $normalized = collect($methods)
            ->map(function ($method, $index) {
                if (! is_array($method)) {
                    return null;
                }

                $module = trim((string) Arr::get($method, 'module', ''));
                $display = trim((string) Arr::get($method, 'displayname', $module));

                if ($module === '') {
                    return null;
                }

                return [
                    'name' => $display !== '' ? $display : strtoupper($module),
                    'slug' => $module,
                    'instructions' => 'Pay using '.($display !== '' ? $display : strtoupper($module)).'.',
                    'is_active' => true,
                    'sort_order' => $index + 1,
                    'source' => 'whmcs',
                ];
            })
            ->filter()
            ->values()
            ->all();

        if ($this->debugEnabled()) {
            Cache::forget('zt_whmcs_last_payment_methods_error');
            Cache::put('zt_whmcs_last_payment_methods_debug', [
                'action' => 'GetPaymentMethods',
                'api_url' => $this->apiUrl(),
                'count' => count($normalized),
                'methods' => collect($normalized)->map(fn ($method) => Arr::only($method, ['name', 'slug']))->all(),
                'created_at' => now()->toDateTimeString(),
            ], 3600);
        }

        return $normalized;
    }

    public function supportDepartments(): array
    {
        $data = $this->request([
            'action' => 'GetSupportDepartments',
            'ignore_dept_assignments' => (bool) config('zt.whmcs.support_ignore_department_assignments', false),
        ], (int) config('zt.cache.support_departments_ttl', 900));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load support departments.'), 'departments' => []];
        }

        return ['ok' => true, 'departments' => $this->normalizeList(Arr::get($data, 'departments.department', []), 'id')];
    }

    public function supportStatuses(): array
    {
        $data = $this->request(['action' => 'GetSupportStatuses'], (int) config('zt.cache.support_statuses_ttl', 900));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load support statuses.'), 'statuses' => []];
        }

        return ['ok' => true, 'statuses' => $this->normalizeList(Arr::get($data, 'statuses.status', Arr::get($data, 'statuses', [])), 'title')];
    }

    public function tickets(?int $clientId = null, ?string $email = null, int $limit = 25): array
    {
        $payload = [
            'action' => 'GetTickets',
            'limitnum' => $limit,
            'ignore_dept_assignments' => (bool) config('zt.whmcs.support_ignore_department_assignments', false),
        ];

        if ($clientId) {
            $payload['clientid'] = $clientId;
        } elseif ($email) {
            $payload['email'] = $email;
        }

        $data = $this->request($payload, (int) config('zt.cache.support_tickets_ttl', 60));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load tickets.'), 'tickets' => []];
        }

        return ['ok' => true, 'tickets' => $this->normalizeList(Arr::get($data, 'tickets.ticket', []), 'id')];
    }

    public function ticket(int|string $ticketId): array
    {
        $data = $this->request([
            'action' => 'GetTicket',
            'ticketid' => $ticketId,
            'repliessort' => 'ASC',
        ], (int) config('zt.cache.support_ticket_ttl', 30));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load this ticket.'), 'ticket' => null];
        }

        return ['ok' => true, 'ticket' => $data];
    }

    public function openTicket(array $payload): array
    {
        $response = $this->mutation(array_filter(array_merge([
            'action' => 'OpenTicket',
            'priority' => 'Medium',
            'markdown' => true,
        ], $payload), fn ($value) => $value !== null && $value !== ''));

        return strtolower((string) Arr::get($response, 'result')) === 'success'
            ? ['ok' => true, 'response' => $response]
            : ['ok' => false, 'message' => Arr::get($response, 'message', 'WHMCS OpenTicket failed.'), 'response' => $response];
    }

    public function addTicketReply(array $payload): array
    {
        $response = $this->mutation(array_filter(array_merge([
            'action' => 'AddTicketReply',
            'markdown' => true,
        ], $payload), fn ($value) => $value !== null && $value !== ''));

        return strtolower((string) Arr::get($response, 'result')) === 'success'
            ? ['ok' => true, 'response' => $response]
            : ['ok' => false, 'message' => Arr::get($response, 'message', 'WHMCS AddTicketReply failed.'), 'response' => $response];
    }

    public function clientDetails(int $clientId, bool $fresh = false): array
    {
        if ($clientId <= 0) {
            return ['ok' => false, 'message' => 'Invalid WHMCS client id.', 'client' => null, 'response' => null];
        }

        $data = $this->request([
            'action' => 'GetClientsDetails',
            'clientid' => $clientId,
            'stats' => false,
        ], $fresh ? 0 : (int) config('zt.mirror.refresh_ttl', 300));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS client details.'), 'client' => null, 'response' => $data];
        }

        $client = Arr::get($data, 'client');
        if (! is_array($client)) {
            $client = $data;
        }

        return ['ok' => true, 'client' => $client, 'response' => $data];
    }

    public function clientOrders(int $clientId, int $limit = 100, bool $fresh = false, ?string $status = null): array
    {
        $params = [
            'action' => 'GetOrders',
            'userid' => $clientId,
            'limitnum' => $limit,
        ];

        if ($status !== null && trim($status) !== '') {
            $params['status'] = trim($status);
        }

        $data = $this->request($params, $fresh ? 0 : (int) config('zt.mirror.refresh_ttl', 300));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS orders.'), 'orders' => [], 'response' => $data];
        }

        return ['ok' => true, 'orders' => $this->normalizeList(Arr::get($data, 'orders.order', []), 'id'), 'response' => $data];
    }

    public function createDomainRenewalOrder(int $clientId, string $domain, int $years, string $paymentMethod): array
    {
        $domain = strtolower(rtrim(trim($domain), '.'));
        $paymentMethod = trim($paymentMethod);

        if ($clientId <= 0 || $domain === '' || $years <= 0 || $paymentMethod === '') {
            return [
                'ok' => false,
                'message' => 'Invalid domain renewal order details.',
                'response' => null,
            ];
        }

        // AddOrder creates the renewal invoice but does not contact the
        // registrar. The existing paid-invoice provisioning flow accepts the
        // order with sendregistrar only after WHMCS marks the invoice Paid.
        $data = $this->mutation([
            'action' => 'AddOrder',
            'clientid' => $clientId,
            'paymentmethod' => $paymentMethod,
            'domainrenewals' => [$domain => $years],
            'noinvoice' => false,
            'noinvoiceemail' => false,
            'noemail' => false,
        ]);

        return strtolower((string) Arr::get($data, 'result')) === 'success'
            ? ['ok' => true, 'response' => $data]
            : ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS AddOrder domain renewal failed.'), 'response' => $data];
    }

    public function acceptOrder(int $orderId, bool $autoSetup = true, bool $sendRegistrar = true, bool $sendEmail = true): array
    {
        $data = $this->mutation([
            'action' => 'AcceptOrder',
            'orderid' => $orderId,
            'autosetup' => $autoSetup,
            'sendregistrar' => $sendRegistrar,
            'sendemail' => $sendEmail,
        ]);

        return strtolower((string) Arr::get($data, 'result')) === 'success'
            ? ['ok' => true, 'response' => $data]
            : ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS AcceptOrder failed.'), 'response' => $data];
    }

    public function moduleCreate(int $serviceId): array
    {
        $data = $this->mutation(['action' => 'ModuleCreate', 'serviceid' => $serviceId]);

        return strtolower((string) Arr::get($data, 'result')) === 'success'
            ? ['ok' => true, 'response' => $data]
            : ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS ModuleCreate failed.'), 'response' => $data];
    }

    public function domainRegister(int $domainId): array
    {
        $data = $this->mutation(['action' => 'DomainRegister', 'domainid' => $domainId]);

        return strtolower((string) Arr::get($data, 'result')) === 'success'
            ? ['ok' => true, 'response' => $data]
            : ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS DomainRegister failed.'), 'response' => $data];
    }

    public function domainTransfer(int $domainId): array
    {
        $data = $this->mutation(['action' => 'DomainTransfer', 'domainid' => $domainId]);

        return strtolower((string) Arr::get($data, 'result')) === 'success'
            ? ['ok' => true, 'response' => $data]
            : ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS DomainTransfer failed.'), 'response' => $data];
    }

    public function moduleQueue(int $limit = 100, bool $fresh = true): array
    {
        $data = $this->request([
            'action' => 'GetModuleQueue',
            'limitnum' => $limit,
        ], $fresh ? 0 : 60);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS module queue.'), 'items' => [], 'response' => $data];
        }

        $items = Arr::get($data, 'modulequeue.item', Arr::get($data, 'queue.item', []));

        return ['ok' => true, 'items' => $this->normalizeList($items, 'id'), 'response' => $data];
    }

    public function clientInvoices(int $clientId, int $limit = 50, bool $fresh = false): array
    {
        $data = $this->request([
            'action' => 'GetInvoices',
            'userid' => $clientId,
            'limitnum' => $limit,
        ], $fresh ? 0 : (int) config('zt.mirror.refresh_ttl', 300));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS invoices.'), 'invoices' => [], 'response' => $data];
        }

        return ['ok' => true, 'invoices' => $this->normalizeList(Arr::get($data, 'invoices.invoice', []), 'id'), 'response' => $data];
    }

    public function invoice(int|string $invoiceId): array
    {
        $data = $this->request([
            'action' => 'GetInvoice',
            'invoiceid' => $invoiceId,
        ], (int) config('zt.mirror.refresh_ttl', 300));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS invoice.'), 'invoice' => null, 'response' => $data];
        }

        return ['ok' => true, 'invoice' => $data, 'response' => $data];
    }

    public function freshInvoice(int|string $invoiceId): array
    {
        $data = $this->request([
            'action' => 'GetInvoice',
            'invoiceid' => $invoiceId,
        ], 0);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS invoice.'), 'invoice' => null, 'response' => $data];
        }

        return ['ok' => true, 'invoice' => $data, 'response' => $data];
    }

    public function createSsoTokenForInvoice(int $clientId, int $invoiceId, ?int $userId = null): array
    {
        return $this->createSsoTokenCustomRedirect(
            $clientId,
            'viewinvoice.php?id='.$invoiceId,
            $userId
        );
    }

    public function createSsoTokenCustomRedirect(int $clientId, string $redirectPath, ?int $userId = null): array
    {
        $redirectPath = ltrim(trim($redirectPath), '/');

        if ($redirectPath === '' || preg_match('#^https?://#i', $redirectPath)) {
            return [
                'ok' => false,
                'message' => 'Invalid WHMCS redirect path.',
                'response' => null,
            ];
        }

        $payload = array_filter([
            'action' => 'CreateSsoToken',
            'client_id' => $clientId,
            'user_id' => $userId,
            'destination' => 'sso:custom_redirect',
            'sso_redirect_path' => $redirectPath,
        ], fn ($value) => $value !== null && $value !== '');

        $data = $this->mutation($payload);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not create WHMCS SSO token.'), 'response' => $data];
        }

        return [
            'ok' => true,
            'redirect_url' => $this->trustedSsoRedirectUrl($data),
            'whmcs_redirect_url' => Arr::get($data, 'redirect_url'),
            'access_token' => Arr::get($data, 'access_token'),
            'response' => $data,
        ];
    }

    protected function trustedSsoRedirectUrl(array $data): ?string
    {
        $accessToken = trim((string) Arr::get($data, 'access_token', ''));
        $clientAreaUrl = $this->whmcsClientAreaUrl();

        // WHMCS builds redirect_url from the WHMCS System URL. On installations
        // that previously used a WordPress/WHMCS bridge, that returned URL can
        // still point to the old cart. Build the one-time SSO endpoint from the
        // real WHMCS API/client-area host instead.
        if ($accessToken !== '' && $clientAreaUrl !== '') {
            return rtrim($clientAreaUrl, '/').'/oauth/singlesignon.php?access_token='.rawurlencode($accessToken);
        }

        $redirectUrl = Arr::get($data, 'redirect_url');

        return is_string($redirectUrl) && trim($redirectUrl) !== '' ? trim($redirectUrl) : null;
    }

    protected function whmcsClientAreaUrl(): string
    {
        $configured = trim((string) config('zt.whmcs.client_area_url', ''));
        if ($configured !== '') {
            return rtrim($configured, '/');
        }

        $apiUrl = trim($this->apiUrl());
        if ($apiUrl === '') {
            return '';
        }

        $parts = parse_url($apiUrl);
        if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
            return '';
        }

        $base = $parts['scheme'].'://'.$parts['host'];
        if (! empty($parts['port'])) {
            $base .= ':'.$parts['port'];
        }

        $path = (string) ($parts['path'] ?? '');
        $path = preg_replace('#/includes/api\.php$#', '', $path) ?? '';
        $path = rtrim($path, '/');

        return rtrim($base.$path, '/');
    }

    public function changeServicePassword(int $serviceId, string $password): array
    {
        $moduleData = $this->mutation([
            'action' => 'ModuleChangePw',
            'serviceid' => $serviceId,
            'servicepassword' => $password,
        ]);

        if (strtolower((string) Arr::get($moduleData, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($moduleData, 'message', 'WHMCS ModuleChangePw failed.'), 'response' => $moduleData];
        }

        $productData = $this->mutation([
            'action' => 'UpdateClientProduct',
            'serviceid' => $serviceId,
            'servicepassword' => $password,
        ]);

        if (strtolower((string) Arr::get($productData, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($productData, 'message', 'cPanel password changed, but WHMCS stored service password could not be updated.'), 'response' => $productData];
        }

        return ['ok' => true, 'response' => ['module' => $moduleData, 'product' => $productData]];
    }

    public function upgradeProduct(int $serviceId, int $newProductId, string $billingCycle, string $paymentMethod = 'banktransfer'): array
    {
        $data = $this->mutation([
            'action' => 'UpgradeProduct',
            'serviceid' => $serviceId,
            'paymentmethod' => $paymentMethod,
            'type' => 'product',
            'newproductid' => $newProductId,
            'newproductbillingcycle' => $billingCycle,
        ]);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS UpgradeProduct failed.'), 'response' => $data];
        }

        return ['ok' => true, 'response' => $data];
    }

    public function addInvoicePayment(int $invoiceId, string $transactionId, string $gateway, ?float $amount = null, ?float $fees = null, bool $noEmail = false): array
    {
        $payload = array_filter([
            'action' => 'AddInvoicePayment',
            'invoiceid' => $invoiceId,
            'transid' => $transactionId,
            'gateway' => $gateway,
            'date' => now()->format('Y-m-d H:i:s'),
            'amount' => $amount,
            'fees' => $fees,
            'noemail' => $noEmail,
        ], fn ($value) => $value !== null && $value !== '');

        $data = $this->paymentMutation($payload);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'WHMCS AddInvoicePayment failed.'), 'response' => $data];
        }

        return ['ok' => true, 'response' => $data];
    }

    public function addCreditTransaction(
        int $clientId,
        string $transactionId,
        string $gateway,
        float $amount,
        string $description,
        ?float $rate = null
    ): array {
        $payload = array_filter([
            'action' => 'AddTransaction',
            'paymentmethod' => $gateway,
            'userid' => $clientId,
            'transid' => $transactionId,
            'date' => now()->format('Y-m-d H:i:s'),
            'description' => $description,
            'amountin' => number_format($amount, 2, '.', ''),
            // Preserve the WHMCS client-currency rate captured at checkout so
            // base-currency accounting does not change before the callback.
            'rate' => $rate !== null && $rate > 0 ? $rate : null,
            'credit' => true,
            'allowduplicatetransid' => false,
        ], fn ($value) => $value !== null && $value !== '');

        $data = $this->paymentMutation($payload);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return [
                'ok' => false,
                'message' => Arr::get($data, 'message', 'WHMCS AddTransaction failed.'),
                'response' => $data,
            ];
        }

        return ['ok' => true, 'response' => $data];
    }

    public function transactions(?int $clientId = null, ?string $transactionId = null, bool $fresh = false): array
    {
        $params = [
            'action' => 'GetTransactions',
        ];

        if ($clientId !== null && $clientId > 0) {
            $params['clientid'] = $clientId;
        }

        if ($transactionId !== null && trim($transactionId) !== '') {
            $params['transid'] = trim($transactionId);
        }

        $data = $this->request($params, $fresh ? 0 : 60);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return [
                'ok' => false,
                'message' => Arr::get($data, 'message', 'Could not load WHMCS transactions.'),
                'transactions' => [],
                'response' => $data,
            ];
        }

        return [
            'ok' => true,
            'transactions' => $this->normalizeList(Arr::get($data, 'transactions.transaction', []), 'id'),
            'response' => $data,
        ];
    }

    public function clientProducts(int $clientId, int $limit = 50, bool $fresh = false): array
    {
        $data = $this->request([
            'action' => 'GetClientsProducts',
            'clientid' => $clientId,
            'limitnum' => $limit,
        ], $fresh ? 0 : (int) config('zt.mirror.refresh_ttl', 300));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS services.'), 'products' => [], 'response' => $data];
        }

        return ['ok' => true, 'products' => $this->normalizeList(Arr::get($data, 'products.product', []), 'id'), 'response' => $data];
    }

    public function clientDomains(int $clientId, int $limit = 50, bool $fresh = false): array
    {
        $data = $this->request([
            'action' => 'GetClientsDomains',
            'clientid' => $clientId,
            'limitnum' => $limit,
        ], $fresh ? 0 : (int) config('zt.mirror.refresh_ttl', 300));

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return ['ok' => false, 'message' => Arr::get($data, 'message', 'Could not load WHMCS domains.'), 'domains' => [], 'response' => $data];
        }

        return ['ok' => true, 'domains' => $this->normalizeList(Arr::get($data, 'domains.domain', []), 'id'), 'response' => $data];
    }

    public function domainNameservers(int $domainId, bool $fresh = true): array
    {
        $data = $this->request([
            'action' => 'DomainGetNameservers',
            'domainid' => $domainId,
        ], $fresh ? 0 : 120);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return [
                'ok' => false,
                'message' => Arr::get($data, 'message', 'Could not load the domain nameservers.'),
                'nameservers' => [],
                'response' => $data,
            ];
        }

        $nameservers = collect(['ns1', 'ns2', 'ns3', 'ns4', 'ns5'])
            ->map(fn (string $key) => strtolower(rtrim(trim((string) Arr::get($data, $key, '')), '.')))
            ->filter()
            ->values()
            ->all();

        return ['ok' => true, 'nameservers' => $nameservers, 'response' => $data];
    }

    public function updateDomainNameservers(int $domainId, array $nameservers): array
    {
        $nameservers = collect($nameservers)
            ->map(fn ($value) => strtolower(rtrim(trim((string) $value), '.')))
            ->filter()
            ->take(5)
            ->values();

        if ($domainId <= 0 || $nameservers->count() < 2) {
            return ['ok' => false, 'message' => 'A valid domain and at least two nameservers are required.'];
        }

        $payload = [
            'action' => 'DomainUpdateNameservers',
            'domainid' => $domainId,
        ];

        foreach ($nameservers as $index => $nameserver) {
            $payload['ns'.($index + 1)] = $nameserver;
        }

        $data = $this->mutation($payload);

        if (strtolower((string) Arr::get($data, 'result')) !== 'success') {
            return [
                'ok' => false,
                'message' => Arr::get($data, 'message', 'The registrar did not accept the nameserver update.'),
                'response' => $data,
            ];
        }

        return ['ok' => true, 'nameservers' => $nameservers->all(), 'response' => $data];
    }

    protected function normalizeList(mixed $items, string $singleKey): array
    {
        if (! is_array($items)) {
            return [];
        }

        if (isset($items[$singleKey])) {
            $items = [$items];
        }

        return collect($items)
            ->filter(fn ($item) => is_array($item))
            ->values()
            ->all();
    }

    protected function normalizeTldPricing(mixed $pricing): array
    {
        if (! is_array($pricing)) {
            return [];
        }

        $normalized = [];

        foreach ($pricing as $tld => $row) {
            if (! is_array($row)) {
                continue;
            }

            $key = strtolower(ltrim((string) $tld, '.'));

            if ($key === '') {
                continue;
            }

            $normalized[$key] = $row;
        }

        return $normalized;
    }

    protected function filterProducts(array $products): array
    {
        $hiddenProductIds = $this->configuredProductIds('hidden_product_ids');
        $allowedProductIds = $this->configuredProductIds('allowed_product_ids');

        return collect($products)
            ->filter(function ($product) use ($hiddenProductIds, $allowedProductIds) {
                $pid = (int) ($product['pid'] ?? 0);

                if ($pid <= 0) {
                    return false;
                }

                if (count($allowedProductIds) > 0 && ! in_array($pid, $allowedProductIds, true)) {
                    return false;
                }

                if (in_array($pid, $hiddenProductIds, true)) {
                    return false;
                }

                return true;
            })
            ->values()
            ->all();
    }

    protected function configuredProductIds(string $key): array
    {
        $ids = config('zt.products.'.$key, []);

        if (is_string($ids)) {
            $ids = explode(',', $ids);
        }

        if (! is_array($ids)) {
            return [];
        }

        return collect($ids)
            ->map(fn ($id) => (int) trim((string) $id))
            ->filter(fn ($id) => $id > 0)
            ->unique()
            ->values()
            ->all();
    }

    public function checkDomainAvailability(string $domain): array
    {
        $domain = $this->normalizeDomain($domain);

        return $this->checkMultipleDomainAvailability([$domain])[$domain]
            ?? ['status' => 'unknown', 'source' => 'DomainWhois', 'raw' => null];
    }

    public function checkMultipleDomainAvailability(array $domains): array
    {
        $domains = collect($domains)
            ->map(fn ($domain) => $this->normalizeDomain((string) $domain))
            ->filter()
            ->unique()
            ->values()
            ->all();

        $ttl = $this->cacheEnabled() ? max(0, (int) config('zt.cache.domain_availability_ttl', 0)) : 0;
        $results = [];
        $pending = [];

        $this->logDebug('domain_lookup_started', [
            'domains' => $domains,
            'count' => count($domains),
            'api_url' => $this->apiUrl(),
        ]);

        foreach ($domains as $domain) {
            $cacheKey = $this->domainAvailabilityCacheKey($domain);

            if ($ttl > 0 && Cache::has($cacheKey)) {
                $results[$domain] = Cache::get($cacheKey);
                $this->logDebug('domain_lookup_cache_hit', [
                    'domain' => $domain,
                    'result' => $results[$domain],
                ]);
                continue;
            }

            if ($this->domainHasDns($domain)) {
                $results[$domain] = ['status' => 'unavailable', 'source' => 'dns_precheck', 'raw' => null];
                $this->rememberDomainAvailability($domain, $results[$domain], $ttl);
                $this->logDebug('domain_lookup_dns_precheck_unavailable', [
                    'domain' => $domain,
                    'result' => $results[$domain],
                ]);
                continue;
            }

            $pending[] = $domain;
        }

        if (count($pending) === 0) {
            return $results;
        }

        $credentials = $this->credentials();
        $requestIds = [];

        if ($this->debugEnabled()) {
            foreach ($pending as $domain) {
                $payload = array_merge($credentials, [
                    'action' => 'DomainWhois',
                    'domain' => $domain,
                ]);
                $requestIds[$domain] = (string) Str::uuid();
                $this->logApiRequest($requestIds[$domain], $payload, ['batch' => true]);
            }
        }

        try {
            $startedAt = microtime(true);
            $responses = Http::pool(function (Pool $pool) use ($pending, $credentials) {
                $requests = [];

                foreach ($pending as $domain) {
                    $requests[] = $pool->as($domain)
                        ->asForm()
                        ->connectTimeout((int) config('zt.whmcs.connect_timeout', 3))
                        ->timeout((int) config('zt.whmcs.timeout', 8))
                        ->post($this->apiUrl(), array_merge($credentials, [
                            'action' => 'DomainWhois',
                            'domain' => $domain,
                        ]));
                }

                return $requests;
            });

            foreach ($pending as $domain) {
                $response = $responses[$domain] ?? null;
                $data = [];
                $rawBody = '';
                $status = null;

                if ($response && method_exists($response, 'status')) {
                    $status = $response->status();
                    $rawBody = $response->body();
                }

                if ($response && method_exists($response, 'successful') && $response->successful()) {
                    $data = $this->normalizeResponse($response->json() ?? $rawBody);
                } else {
                    $data = [
                        'result' => 'error',
                        'message' => $status ? 'WHMCS HTTP request failed with status '.$status : 'WHMCS DomainWhois response missing.',
                        'http_status' => $status,
                        'body_preview' => $this->bodyPreview($rawBody),
                    ];
                }

                $payload = array_merge($credentials, ['action' => 'DomainWhois', 'domain' => $domain]);
                $this->logApiResponse($requestIds[$domain] ?? (string) Str::uuid(), $payload, $status, $rawBody, $data, microtime(true) - $startedAt);

                $results[$domain] = $this->availabilityFromWhoisData($data, $domain);
                $this->rememberDomainAvailability($domain, $results[$domain], $ttl);
                if ($this->debugEnabled()) {
                    Cache::put('zt_whmcs_last_domain_debug:'.$domain, [
                        'domain' => $domain,
                        'api_url' => $this->apiUrl(),
                        'request' => $this->sanitizePayload($payload),
                        'http_status' => $status,
                        'response_summary' => $this->summarizeApiData($data),
                        'availability_result' => $results[$domain],
                        'created_at' => now()->toDateTimeString(),
                    ], 3600);
                }
            }
        } catch (\Throwable $e) {
            report($e);
            $this->logDebug('domain_lookup_pool_exception', [
                'message' => $e->getMessage(),
                'domains' => $pending,
            ], 'error');

            foreach ($pending as $domain) {
                $data = $this->request(['action' => 'DomainWhois', 'domain' => $domain], $ttl);
                $results[$domain] = $this->availabilityFromWhoisData($data, $domain);
                $this->rememberDomainAvailability($domain, $results[$domain], $ttl);
            }
        }

        return $results;
    }

    protected function domainAvailabilityCacheKey(string $domain): string
    {
        $fingerprint = md5(json_encode([
            'api_url' => $this->apiUrl(),
            'auth_style' => config('zt.whmcs.auth_style', 'identifier'),
            'credentials' => hash('sha256', json_encode($this->credentials(), JSON_UNESCAPED_SLASHES)),
        ], JSON_UNESCAPED_SLASHES));

        return 'zt_domain_availability:v2:'.$fingerprint.':'.$this->normalizeDomain($domain);
    }

    protected function rememberDomainAvailability(string $domain, array $result, int $ttl): void
    {
        if ($ttl <= 0) {
            return;
        }

        $status = (string) ($result['status'] ?? 'unknown');

        if (! in_array($status, ['available', 'unavailable'], true)) {
            return;
        }

        Cache::put($this->domainAvailabilityCacheKey($domain), $result, $ttl);
    }

    protected function availabilityFromWhoisData(array $data, ?string $domain = null): array
    {
        $status = strtolower(trim((string) Arr::get($data, 'status', '')));
        $result = strtolower(trim((string) Arr::get($data, 'result', '')));
        $whois = (string) Arr::get($data, 'whois', '');

        $availableStatuses = ['available', 'not_registered', 'not registered', 'notregistered', 'free', 'not found'];
        $unavailableStatuses = ['unavailable', 'registered', 'active', 'taken'];

        if (in_array($status, $availableStatuses, true)) {
            return ['status' => 'available', 'source' => 'DomainWhois', 'raw' => $data];
        }

        if (in_array($status, $unavailableStatuses, true)) {
            return ['status' => 'unavailable', 'source' => 'DomainWhois', 'raw' => $data];
        }

        foreach (['available', 'is_available', 'isAvailable'] as $key) {
            $value = Arr::get($data, $key);
            if ($value === true || $value === 1 || $value === '1' || strtolower((string) $value) === 'true') {
                return ['status' => 'available', 'source' => 'DomainWhois', 'raw' => $data];
            }
        }

        if ($whois !== '') {
            if ($this->looksAvailableFromWhois($whois)) {
                return ['status' => 'available', 'source' => 'DomainWhois', 'raw' => $data];
            }

            if ($this->looksUnavailableFromWhois($whois)) {
                return ['status' => 'unavailable', 'source' => 'DomainWhois', 'raw' => $data];
            }
        }

        // Your current api.zthosting.com wrapper sometimes returns the products/TLD page or
        // a print_r response that has no DomainWhois status. For user-facing lookup we can
        // safely fall back to DNS-negative only when WHMCS did not give an error and the
        // domain has no DNS records. This keeps youtube.com etc. unavailable because they
        // are caught before this point by the DNS pre-check.
        if (config('zt.domain_lookup.trust_no_dns_fallback', false) && $domain && ! $this->domainHasDns($domain) && $result !== 'error') {
            return ['status' => 'available', 'source' => 'dns_no_records_fallback', 'raw' => $data];
        }

        return ['status' => 'unknown', 'source' => 'DomainWhois', 'raw' => $data];
    }

    protected function cacheEnabled(): bool
    {
        return (bool) config('zt.cache.enabled', true);
    }

    protected function debugEnabled(): bool
    {
        return (bool) config('zt.whmcs.debug_log_enabled', false);
    }

    protected function logApiRequest(string $requestId, array $payload, array $extra = []): void
    {
        if (! $this->debugEnabled()) {
            return;
        }

        $action = (string) ($payload['action'] ?? 'unknown');
        $context = array_merge([
            'request_id' => $requestId,
            'action' => $action,
            'api_url' => $this->apiUrl(),
            'method' => 'POST',
            'auth_style' => config('zt.whmcs.auth_style', 'identifier'),
            'payload' => $this->sanitizePayload($payload),
            'created_at' => now()->toDateTimeString(),
        ], $extra);

        $this->logDebug('whmcs_api_request', $context);
        Cache::put('zt_whmcs_last_api_request:'.$action, $context, 3600);
    }

    protected function logApiResponse(string $requestId, array $payload, ?int $status, string $rawBody, array $normalized, float $durationSeconds): void
    {
        if (! $this->debugEnabled()) {
            return;
        }

        $action = (string) ($payload['action'] ?? 'unknown');
        $context = [
            'request_id' => $requestId,
            'action' => $action,
            'api_url' => $this->apiUrl(),
            'http_status' => $status,
            'duration_ms' => round($durationSeconds * 1000, 2),
            'request' => $this->sanitizePayload($payload),
            'response_summary' => $this->summarizeApiData($normalized),
            'response_body_preview' => $this->bodyPreview($rawBody),
            'created_at' => now()->toDateTimeString(),
        ];

        $level = ($status && $status >= 400) || strtolower((string) Arr::get($normalized, 'result')) === 'error' ? 'warning' : 'info';
        $this->logDebug('whmcs_api_response', $context, $level);
        Cache::put('zt_whmcs_last_api_debug:'.$action, $context, 3600);
    }

    protected function logApiException(string $requestId, array $payload, \Throwable $e, float $durationSeconds): void
    {
        if (! $this->debugEnabled()) {
            return;
        }

        $action = (string) ($payload['action'] ?? 'unknown');
        $context = [
            'request_id' => $requestId,
            'action' => $action,
            'api_url' => $this->apiUrl(),
            'duration_ms' => round($durationSeconds * 1000, 2),
            'request' => $this->sanitizePayload($payload),
            'exception' => get_class($e),
            'message' => $e->getMessage(),
            'created_at' => now()->toDateTimeString(),
        ];

        $this->logDebug('whmcs_api_exception', $context, 'error');
        Cache::put('zt_whmcs_last_api_debug:'.$action, $context, 3600);
    }

    protected function logDebug(string $event, array $context, string $level = 'debug'): void
    {
        if (! $this->debugEnabled()) {
            return;
        }

        $context = array_merge(['event' => $event], $context);
        $message = 'WHMCS '.$event;

        match ($level) {
            'error' => Log::error($message, $context),
            'warning' => Log::warning($message, $context),
            'info' => Log::info($message, $context),
            default => Log::debug($message, $context),
        };
    }

    protected function sanitizePayload(array $payload): array
    {
        $sensitive = ['identifier', 'secret', 'username', 'password', 'password2', 'passwordhash', 'accesskey', 'access_key', 'servicepassword'];

        foreach ($sensitive as $key) {
            if (array_key_exists($key, $payload)) {
                $payload[$key] = $this->maskSecret($payload[$key]);
            }
        }

        return $payload;
    }

    protected function maskSecret(mixed $value): string
    {
        $value = (string) $value;

        if ($value === '') {
            return '';
        }

        if (strlen($value) <= 8) {
            return str_repeat('*', strlen($value));
        }

        return substr($value, 0, 4).str_repeat('*', max(strlen($value) - 8, 4)).substr($value, -4);
    }

    protected function bodyPreview(string $body): ?string
    {
        if ($body === '') {
            return null;
        }

        if (! (bool) config('zt.whmcs.debug_log_response_body', true)) {
            return '[body logging disabled]';
        }

        $body = trim($body);
        $limit = (int) config('zt.whmcs.debug_body_limit', 4000);

        if ($limit > 0 && strlen($body) > $limit) {
            return substr($body, 0, $limit).'... [truncated]';
        }

        return $body;
    }

    protected function summarizeApiData(array $data): array
    {
        return [
            'result' => Arr::get($data, 'result'),
            'message' => Arr::get($data, 'message'),
            'status' => Arr::get($data, 'status'),
            'http_status' => Arr::get($data, 'http_status'),
            'products_count' => is_array(Arr::get($data, 'products.product')) ? count(Arr::get($data, 'products.product')) : null,
            'tld_count' => is_array(Arr::get($data, 'pricing')) ? count(Arr::get($data, 'pricing')) : null,
            'keys' => array_slice(array_keys($data), 0, 20),
        ];
    }

    protected function limitedPayload(array $payload): array
    {
        $json = json_encode($payload);
        $limit = (int) config('zt.whmcs.debug_body_limit', 4000);

        if ($json === false || $limit <= 0 || strlen($json) <= $limit) {
            return $payload;
        }

        return [
            '_truncated' => true,
            '_preview' => substr($json, 0, $limit),
        ];
    }

    protected function normalizeResponse(mixed $response): array
    {
        if (is_array($response)) {
            return $response;
        }

        if (! is_string($response)) {
            return [];
        }

        $text = html_entity_decode(strip_tags($response), ENT_QUOTES | ENT_HTML5);
        $text = trim($text);

        $json = json_decode($text, true);
        if (is_array($json)) {
            return $json;
        }

        if (str_contains($text, '[products] => Array') || str_contains($text, '[pricing] => Array')) {
            return $this->parseWhmcsDebugPage($text);
        }

        return $this->parseFlatWhmcsResponse($text);
    }

    protected function parseFlatWhmcsResponse(string $text): array
    {
        $data = [];

        preg_match_all('/\[([A-Za-z0-9_]+)\]\s*=>\s*(.*?)(?=\n\s*\[[A-Za-z0-9_]+\]\s*=>|\n\s*\)\s*$|$)/s', $text, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            $key = trim($match[1]);
            $value = trim($match[2]);

            if ($key === '' || str_contains($value, 'Array')) {
                continue;
            }

            $data[$key] = $value;
            $data[strtolower($key)] = $value;
        }

        return $data;
    }

    protected function parseWhmcsDebugPage(string $text): array
    {
        return array_filter([
            'result' => str_contains($text, '[result] => success') ? 'success' : null,
            'products' => ['product' => $this->parseProducts($text)],
            'pricing' => $this->parseTldPricing($text),
        ]);
    }

    protected function parseProducts(string $text): array
    {
        $section = $text;
        if (preg_match('/## Products(.*?)(## TLD Pricing|$)/s', $text, $m)) {
            $section = $m[1];
        }

        preg_match_all('/\n\s*\[\d+\]\s*=>\s*Array\s*\(\s*\[pid\]\s*=>.*?(?=\n\s*\[\d+\]\s*=>\s*Array\s*\(\s*\[pid\]\s*=>|## TLD Pricing|$)/s', $section, $matches);

        $products = [];
        foreach ($matches[0] ?? [] as $block) {
            $product = [
                'pid' => $this->fieldInt($block, 'pid'),
                'gid' => $this->fieldInt($block, 'gid'),
                'type' => $this->field($block, 'type'),
                'name' => $this->field($block, 'name'),
                'groupname' => $this->field($block, 'groupname'),
                'slug' => $this->field($block, 'slug'),
                'product_url' => $this->field($block, 'product_url'),
                'module' => $this->field($block, 'module'),
                'paytype' => $this->field($block, 'paytype'),
                'description' => $this->descriptionField($block),
                'pricing' => $this->parseProductPricing($block),
            ];

            if (! empty($product['pid']) && ! empty($product['name']) && ! empty($product['pricing'])) {
                $product['slug'] = $product['slug'] ?: Str::slug($product['name']);
                $products[] = $product;
            }
        }

        return $products;
    }

    protected function parseProductPricing(string $block): array
    {
        $pricing = [];
        foreach (['PKR', 'USD'] as $currency) {
            if (preg_match('/\['.$currency.'\]\s*=>\s*Array\s*\(\s*(.*?)\s*\)/s', $block, $m)) {
                preg_match_all('/\[(monthly|quarterly|semiannually|annually|biennially|triennially|msetupfee|qsetupfee|ssetupfee|asetupfee|bsetupfee|tsetupfee)\]\s*=>\s*([^\n\r]*)/', $m[1], $prices, PREG_SET_ORDER);
                foreach ($prices as $row) {
                    $pricing[$currency][$row[1]] = is_numeric(trim($row[2])) ? (float) trim($row[2]) : trim($row[2]);
                }
            }
        }

        return $pricing;
    }

    protected function parseTldPricing(string $text): array
    {
        if (! preg_match('/## TLD Pricing(.*)$/s', $text, $m)) {
            return [];
        }

        $section = $m[1];
        preg_match_all('/\n\s*\[([a-z0-9.-]+)\]\s*=>\s*Array\s*\(\s*(.*?)(?=\n\s*\[[a-z0-9.-]+\]\s*=>\s*Array\s*\(|\n\s*\)\s*\n\s*$|$)/is', $section, $matches, PREG_SET_ORDER);

        $pricing = [];
        foreach ($matches as $match) {
            $tld = strtolower(ltrim($match[1], '.'));
            $block = $match[2];
            foreach (['register', 'transfer', 'renew'] as $operation) {
                if (preg_match('/\['.$operation.'\]\s*=>\s*Array\s*\(\s*(.*?)\s*\)/s', $block, $opMatch)) {
                    preg_match_all('/\[(\d+)\]\s*=>\s*([\d.]+)/', $opMatch[1], $rows, PREG_SET_ORDER);
                    foreach ($rows as $row) {
                        $pricing[$tld][$operation][(int) $row[1]] = (float) $row[2];
                    }
                }
            }
        }

        return $pricing;
    }

    protected function normalizeDomain(string $domain): string
    {
        $domain = strtolower(trim($domain));
        $domain = preg_replace('#^https?://#', '', $domain);
        $domain = explode('/', $domain)[0];
        $domain = preg_replace('/\s+/', '', $domain);

        if (! str_contains($domain, '.')) {
            $domain .= '.com';
        }

        return $domain;
    }

    protected function domainHasDns(string $domain): bool
    {
        return checkdnsrr($domain, 'NS') || checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA') || checkdnsrr($domain, 'MX');
    }

    protected function looksAvailableFromWhois(string $whois): bool
    {
        $whois = strtolower($whois);

        $markers = [
            'no match for',
            'not found',
            'no entries found',
            'no data found',
            'no object found',
            'object does not exist',
            'domain not found',
            'status: free',
            'is free',
            'available for registration',
        ];

        foreach ($markers as $marker) {
            if (str_contains($whois, $marker)) {
                return true;
            }
        }

        return false;
    }

    protected function looksUnavailableFromWhois(string $whois): bool
    {
        $whois = strtolower($whois);

        return str_contains($whois, 'domain name:')
            || str_contains($whois, 'registrar:')
            || str_contains($whois, 'creation date:')
            || str_contains($whois, 'registry expiry date:')
            || str_contains($whois, 'updated date:')
            || str_contains($whois, 'name server:');
    }

    protected function field(string $block, string $field): ?string
    {
        if (! preg_match('/\['.preg_quote($field, '/').'\]\s*=>\s*([^\n\r]*)/', $block, $m)) {
            return null;
        }

        $value = trim(html_entity_decode($m[1], ENT_QUOTES));
        return $value !== '' ? $value : null;
    }

    protected function fieldInt(string $block, string $field): ?int
    {
        $value = $this->field($block, $field);
        return is_numeric($value) ? (int) $value : null;
    }

    protected function descriptionField(string $block): string
    {
        if (preg_match('/\[description\]\s*=>\s*(.*?)(?=\n\s*\[(module|paytype|pricing|customfields|configoptions)\]\s*=>)/s', $block, $m)) {
            return trim(html_entity_decode($m[1], ENT_QUOTES));
        }

        return '';
    }

    protected function fallbackProducts(): array
    {
        return [
            [
                'pid' => 1,
                'gid' => 1,
                'type' => 'hostingaccount',
                'name' => 'cPanel Shared Starter',
                'groupname' => 'cPanel Web Hosting',
                'slug' => 'cpanel-shared-starter',
                'product_url' => 'https://my.zthosting.com/index.php?rp=/store/web-hosting/cpanel-shared-starter',
                'description' => '10 Websites, 5GB NVMe Storage, Unlimited Bandwidth, cPanel, Free SSL, Free Migration.',
                'pricing' => [
                    'USD' => ['monthly' => 3.99, 'annually' => 24.00, 'biennially' => 48.00, 'triennially' => 72.00],
                    'PKR' => ['monthly' => 1157.10, 'annually' => 6960.00, 'biennially' => 13920.00, 'triennially' => 20880.00],
                ],
            ],
            [
                'pid' => 2,
                'gid' => 17,
                'type' => 'hostingaccount',
                'name' => 'Business Hosting Pro',
                'groupname' => 'Pro Business Hosting',
                'slug' => 'business-hosting-pro',
                'product_url' => 'https://my.zthosting.com/index.php?rp=/store/corporate-business-hosting/business-hosting-pro',
                'description' => 'Business hosting with high resources, SSL, email, backups and support.',
                'pricing' => [
                    'USD' => ['monthly' => 8.99, 'annually' => 89.00, 'biennially' => 178.00, 'triennially' => 267.00],
                    'PKR' => ['monthly' => 2607.10, 'annually' => 25810.00, 'biennially' => 51620.00, 'triennially' => 77430.00],
                ],
            ],
            [
                'pid' => 3,
                'gid' => 16,
                'type' => 'hostingaccount',
                'name' => 'Managed WordPress',
                'groupname' => 'Pro WordPress Hosting',
                'slug' => 'managed-wordpress',
                'product_url' => 'https://my.zthosting.com/index.php?rp=/store/managed-wordpress-hosting/managed-wordpress',
                'description' => 'Optimized WordPress hosting with SSL, cache, backups and support.',
                'pricing' => [
                    'USD' => ['monthly' => 6.99, 'annually' => 69.00, 'biennially' => 138.00, 'triennially' => 207.00],
                    'PKR' => ['monthly' => 2027.10, 'annually' => 20010.00, 'biennially' => 40020.00, 'triennially' => 60030.00],
                ],
            ],
        ];
    }

    protected function fallbackTldPricing(): array
    {
        return [
            'com' => ['register' => [1 => 15.00, 2 => 31.00, 3 => 46.00], 'transfer' => [1 => 15.00], 'renew' => [1 => 15.00]],
            'net' => ['register' => [1 => 16.00], 'transfer' => [1 => 16.00], 'renew' => [1 => 16.00]],
            'org' => ['register' => [1 => 15.50], 'transfer' => [1 => 15.50], 'renew' => [1 => 15.50]],
            'info' => ['register' => [1 => 18.00], 'transfer' => [1 => 18.00], 'renew' => [1 => 18.00]],
            'biz' => ['register' => [1 => 18.00], 'transfer' => [1 => 18.00], 'renew' => [1 => 18.00]],
            'pk' => ['register' => [1 => 18.00], 'transfer' => [1 => 18.00], 'renew' => [1 => 18.00]],
            'com.pk' => ['register' => [1 => 18.00], 'transfer' => [1 => 18.00], 'renew' => [1 => 18.00]],
        ];
    }
}
