<?php

namespace App\Services;

use App\Models\ClientService;
use App\Models\Domain;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Order;
use App\Models\User;
use App\Models\WhmcsSyncLog;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

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

    public function refreshUser(User $user, bool $force = false, array $modules = ['invoices', 'services', 'domains']): array
    {
        if (! (bool) config('zt.mirror.enabled', true)) {
            return ['ok' => true, 'skipped' => true, 'message' => 'WHMCS mirror refresh is disabled.'];
        }

        $clientId = (int) ($user->whmcs_client_id ?? 0);
        if ($clientId <= 0) {
            return ['ok' => true, 'skipped' => true, 'message' => 'User is not linked to a WHMCS client yet.'];
        }

        if (! $force && ! $this->shouldRefresh($user)) {
            return ['ok' => true, 'skipped' => true, 'message' => 'Mirror refresh is still inside TTL.'];
        }

        // WHMCS client currency is the source of truth for all raw invoice,
        // service and domain amounts returned by the API. Never attach the
        // current visitor/IP currency label to those unconverted amounts.
        $accountCurrency = $this->resolveWhmcsClientCurrency($user, $clientId, $force);
        $result = ['ok' => true, 'skipped' => false, 'currency' => $accountCurrency, 'modules' => []];

        if (in_array('invoices', $modules, true)) {
            $result['modules']['invoices'] = $this->refreshInvoices($user, $clientId, $force, $accountCurrency);
        }

        if (in_array('services', $modules, true)) {
            $result['modules']['services'] = $this->refreshServices($user, $clientId, $force, $accountCurrency);
        }

        if (in_array('domains', $modules, true)) {
            $result['modules']['domains'] = $this->refreshDomains($user, $clientId, $force, $accountCurrency);
        }

        $result['ok'] = collect($result['modules'])->every(fn ($module) => (bool) ($module['ok'] ?? false));

        $this->rememberRefreshed($user);

        return $result;
    }

    public function refreshInvoices(User $user, ?int $clientId = null, bool $fresh = false, ?string $accountCurrency = null): array
    {
        $clientId = $clientId ?: (int) ($user->whmcs_client_id ?? 0);
        if ($clientId <= 0) {
            return ['ok' => false, 'message' => 'Missing WHMCS client id.', 'count' => 0];
        }

        $accountCurrency = $this->normalizeCurrency($accountCurrency)
            ?: $this->resolveWhmcsClientCurrency($user, $clientId, $fresh);

        $payload = ['action' => 'GetInvoices', 'userid' => $clientId];
        $response = $this->api->clientInvoices($clientId, (int) config('zt.mirror.invoices_limit', 50), $fresh);
        $this->log($user, 'GetInvoices', $response['ok'] ?? false, $payload, $response['response'] ?? null, $response['message'] ?? null);

        if (! ($response['ok'] ?? false)) {
            return ['ok' => false, 'message' => $response['message'] ?? 'Could not refresh invoices.', 'count' => 0];
        }

        $count = 0;
        foreach ($response['invoices'] ?? [] as $row) {
            if (! is_array($row)) {
                continue;
            }

            $invoiceId = $this->intValue($row, ['id', 'invoiceid']);
            if ($invoiceId <= 0) {
                continue;
            }

            DB::transaction(function () use ($user, $clientId, $invoiceId, $row, $fresh, $accountCurrency, &$count) {
                $order = $this->placeholderOrder($user, 'invoice', $invoiceId, $row, $accountCurrency);
                $invoice = Invoice::updateOrCreate(
                    ['user_id' => $user->id, 'whmcs_invoice_id' => $invoiceId],
                    [
                        'order_id' => $order->id,
                        'invoice_number' => $this->invoiceNumber($invoiceId, $row),
                        'currency' => $this->currency($user, $row, $accountCurrency),
                        'subtotal' => $this->money($row, ['subtotal', 'subtotal_float']),
                        'discount' => $this->money($row, ['credit'], 0),
                        'tax' => $this->money($row, ['tax', 'tax1', 'tax2'], 0),
                        'total' => $this->money($row, ['total'], 0),
                        'status' => $this->localStatus($this->stringValue($row, ['status'], 'unpaid')),
                        'due_date' => $this->dateValue($row, ['duedate', 'due_date']),
                        'paid_at' => $this->dateTimeValue($row, ['datepaid', 'paid_at']),
                        'whmcs_client_id' => $clientId,
                        'whmcs_status' => $this->stringValue($row, ['status']),
                        'whmcs_sync_status' => 'synced',
                        'whmcs_sync_message' => null,
                        'whmcs_synced_at' => now(),
                        'whmcs_payload' => $this->limitedPayload($row),
                    ]
                );

                $this->refreshInvoiceDetail($user, $invoice, $invoiceId, $fresh, $accountCurrency);
                $count++;
            });
        }

        return ['ok' => true, 'message' => 'Invoices refreshed from WHMCS.', 'count' => $count];
    }

    public function refreshServices(User $user, ?int $clientId = null, bool $fresh = false, ?string $accountCurrency = null): array
    {
        $clientId = $clientId ?: (int) ($user->whmcs_client_id ?? 0);
        if ($clientId <= 0) {
            return ['ok' => false, 'message' => 'Missing WHMCS client id.', 'count' => 0];
        }

        $accountCurrency = $this->normalizeCurrency($accountCurrency)
            ?: $this->resolveWhmcsClientCurrency($user, $clientId, $fresh);

        $payload = ['action' => 'GetClientsProducts', 'clientid' => $clientId];
        $response = $this->api->clientProducts($clientId, (int) config('zt.mirror.services_limit', 50), $fresh);
        $this->log($user, 'GetClientsProducts', $response['ok'] ?? false, $payload, $response['response'] ?? null, $response['message'] ?? null);

        if (! ($response['ok'] ?? false)) {
            return ['ok' => false, 'message' => $response['message'] ?? 'Could not refresh services.', 'count' => 0];
        }

        $count = 0;
        foreach ($response['products'] ?? [] as $row) {
            if (! is_array($row)) {
                continue;
            }

            $serviceId = $this->intValue($row, ['id', 'serviceid']);
            if ($serviceId <= 0) {
                continue;
            }

            $order = $this->placeholderOrder($user, 'service', $serviceId, $row, $accountCurrency);
            ClientService::updateOrCreate(
                ['user_id' => $user->id, 'whmcs_service_id' => $serviceId],
                [
                    'order_id' => $order->id,
                    'api_pid' => $this->intValue($row, ['pid'], null),
                    'api_service_id' => (string) $serviceId,
                    'name' => Str::limit($this->stringValue($row, ['name', 'productname', 'groupname'], 'WHMCS Service #'.$serviceId), 250, ''),
                    'domain_name' => $this->stringValue($row, ['domain', 'domainname']),
                    'service_type' => $this->stringValue($row, ['type'], 'hosting') ?: 'hosting',
                    'status' => $this->localStatus($this->stringValue($row, ['status'], 'pending')),
                    'billing_cycle' => $this->stringValue($row, ['billingcycle', 'billing_cycle']),
                    'amount' => $this->money($row, ['recurringamount', 'amount'], 0),
                    'currency' => $this->currency($user, $row, $accountCurrency),
                    'next_due_date' => $this->dateValue($row, ['nextduedate', 'next_due_date']),
                    'whmcs_status' => $this->stringValue($row, ['status']),
                    'whmcs_sync_status' => 'synced',
                    'whmcs_synced_at' => now(),
                    'whmcs_payload' => $this->limitedPayload($row),
                ]
            );
            $count++;
        }

        return ['ok' => true, 'message' => 'Services refreshed from WHMCS.', 'count' => $count];
    }

    public function refreshDomains(User $user, ?int $clientId = null, bool $fresh = false, ?string $accountCurrency = null): array
    {
        $clientId = $clientId ?: (int) ($user->whmcs_client_id ?? 0);
        if ($clientId <= 0) {
            return ['ok' => false, 'message' => 'Missing WHMCS client id.', 'count' => 0];
        }

        $accountCurrency = $this->normalizeCurrency($accountCurrency)
            ?: $this->resolveWhmcsClientCurrency($user, $clientId, $fresh);

        $payload = ['action' => 'GetClientsDomains', 'clientid' => $clientId];
        $response = $this->api->clientDomains($clientId, (int) config('zt.mirror.domains_limit', 50), $fresh);
        $this->log($user, 'GetClientsDomains', $response['ok'] ?? false, $payload, $response['response'] ?? null, $response['message'] ?? null);

        if (! ($response['ok'] ?? false)) {
            return ['ok' => false, 'message' => $response['message'] ?? 'Could not refresh domains.', 'count' => 0];
        }

        $count = 0;
        foreach ($response['domains'] ?? [] as $row) {
            if (! is_array($row)) {
                continue;
            }

            $domainId = $this->intValue($row, ['id', 'domainid']);
            $domainName = $this->stringValue($row, ['domainname', 'domain']);
            if ($domainId <= 0 || $domainName === null) {
                continue;
            }

            Domain::updateOrCreate(
                ['user_id' => $user->id, 'whmcs_domain_id' => $domainId],
                [
                    'order_id' => null,
                    'domain_name' => strtolower($domainName),
                    'tld' => $this->extractTld($domainName),
                    'operation' => $this->stringValue($row, ['regtype', 'type', 'domaintype'], 'register') ?: 'register',
                    'status' => $this->localStatus($this->stringValue($row, ['status'], 'pending')),
                    'registration_period' => $this->intValue($row, ['registrationperiod', 'regperiod'], null),
                    'amount' => $this->money($row, ['recurringamount', 'amount'], 0),
                    'currency' => $this->currency($user, $row, $accountCurrency),
                    'expiry_date' => $this->dateValue($row, ['expirydate', 'nextduedate']),
                    'api_domain_id' => (string) $domainId,
                    'nameservers' => $this->nameservers($row),
                    'epp_code' => null,
                    'whmcs_status' => $this->stringValue($row, ['status']),
                    'whmcs_sync_status' => 'synced',
                    'whmcs_synced_at' => now(),
                    'whmcs_payload' => $this->limitedPayload($row),
                ]
            );
            $count++;
        }

        return ['ok' => true, 'message' => 'Domains refreshed from WHMCS.', 'count' => $count];
    }

    protected function refreshInvoiceDetail(User $user, Invoice $invoice, int $invoiceId, bool $fresh = false, ?string $accountCurrency = null): void
    {
        $response = $fresh ? $this->api->freshInvoice($invoiceId) : $this->api->invoice($invoiceId);
        $this->log($user, 'GetInvoice', $response['ok'] ?? false, ['action' => 'GetInvoice', 'invoiceid' => $invoiceId], $response['response'] ?? null, $response['message'] ?? null, $invoice);

        if (! ($response['ok'] ?? false) || ! is_array($response['invoice'] ?? null)) {
            return;
        }

        $detail = $response['invoice'];
        $invoice->update([
            'currency' => $this->currency($user, $detail, (string) ($invoice->currency ?: $accountCurrency)),
            'subtotal' => $this->money($detail, ['subtotal'], (float) $invoice->subtotal),
            'discount' => $this->money($detail, ['credit'], (float) $invoice->discount),
            'tax' => $this->money($detail, ['tax', 'tax1', 'tax2'], (float) $invoice->tax),
            'total' => $this->money($detail, ['total'], (float) $invoice->total),
            'status' => $this->localStatus($this->stringValue($detail, ['status'], $invoice->status)),
            'due_date' => $this->dateValue($detail, ['duedate', 'due_date']) ?: $invoice->due_date,
            'paid_at' => $this->dateTimeValue($detail, ['datepaid', 'paid_at']) ?: $invoice->paid_at,
            'whmcs_status' => $this->stringValue($detail, ['status'], $invoice->whmcs_status),
            'whmcs_payload' => $this->limitedPayload($detail),
        ]);

        $items = Arr::get($detail, 'items.item', []);
        if (isset($items['description'])) {
            $items = [$items];
        }

        if (! is_array($items)) {
            return;
        }

        $invoice->items()->delete();
        foreach ($items as $item) {
            if (! is_array($item)) {
                continue;
            }

            InvoiceItem::create([
                'invoice_id' => $invoice->id,
                'description' => Str::limit($this->stringValue($item, ['description'], 'WHMCS invoice item'), 250, ''),
                'quantity' => 1,
                'unit_price' => $this->money($item, ['amount'], 0),
                'total' => $this->money($item, ['amount'], 0),
            ]);
        }
    }

    protected function placeholderOrder(User $user, string $kind, int $whmcsId, array $payload, ?string $accountCurrency = null): Order
    {
        return Order::updateOrCreate(
            ['user_id' => $user->id, 'order_number' => 'WHMCS-'.strtoupper($kind).'-'.$whmcsId],
            [
                'currency' => $this->currency($user, $payload, $accountCurrency),
                'subtotal' => $this->money($payload, ['subtotal', 'recurringamount', 'amount', 'total'], 0),
                'discount' => 0,
                'tax' => $this->money($payload, ['tax', 'tax1', 'tax2'], 0),
                'total' => $this->money($payload, ['total', 'recurringamount', 'amount'], 0),
                'status' => $this->localStatus($this->stringValue($payload, ['status'], 'pending')),
                'payment_method' => $this->stringValue($payload, ['paymentmethod', 'payment_method']),
                'customer_note' => 'Created as local WHMCS mirror placeholder.',
                'price_locked_until' => null,
                'whmcs_client_id' => (int) ($user->whmcs_client_id ?? 0) ?: null,
                'whmcs_order_id' => $this->intValue($payload, ['orderid'], null),
                'whmcs_invoice_id' => $kind === 'invoice' ? $whmcsId : null,
                'whmcs_status' => $this->stringValue($payload, ['status']),
                'whmcs_sync_status' => 'synced',
                'whmcs_synced_at' => now(),
                'whmcs_payload' => $this->limitedPayload($payload),
            ]
        );
    }

    protected function shouldRefresh(User $user): bool
    {
        $ttl = (int) config('zt.mirror.refresh_ttl', 300);
        if ($ttl <= 0 || ! config('zt.cache.enabled', true)) {
            return true;
        }

        return ! Cache::has($this->refreshCacheKey($user));
    }

    protected function rememberRefreshed(User $user): void
    {
        $ttl = (int) config('zt.mirror.refresh_ttl', 300);
        if ($ttl <= 0 || ! config('zt.cache.enabled', true)) {
            return;
        }

        Cache::put($this->refreshCacheKey($user), now()->toDateTimeString(), $ttl);
    }

    protected function refreshCacheKey(User $user): string
    {
        return 'zt_whmcs_mirror_refreshed:user:'.$user->id.':'.((int) ($user->whmcs_client_id ?? 0));
    }

    protected function log(User $user, string $action, bool $success, array $requestPayload, ?array $responsePayload = null, ?string $message = null, ?Invoice $invoice = null): void
    {
        try {
            WhmcsSyncLog::create([
                'user_id' => $user->id,
                'order_id' => $invoice?->order_id,
                'invoice_id' => $invoice?->id,
                'action' => $action,
                'mode' => 'mirror_refresh',
                'status' => $success ? 'success' : 'failed',
                'whmcs_id' => $this->whmcsIdFromPayload($responsePayload),
                'request_payload' => $requestPayload,
                'response_payload' => $this->limitedPayload($responsePayload),
                'message' => $message,
                'attempted_at' => now(),
            ]);
        } catch (\Throwable $e) {
            report($e);
        }
    }

    protected function whmcsIdFromPayload(?array $payload): ?string
    {
        if (! is_array($payload)) {
            return null;
        }

        foreach (['invoiceid', 'id', 'serviceid', 'domainid'] as $key) {
            if (! empty($payload[$key])) {
                return (string) $payload[$key];
            }
        }

        return null;
    }

    protected function intValue(array $data, array $keys, ?int $default = 0): ?int
    {
        foreach ($keys as $key) {
            $value = Arr::get($data, $key);
            if ($value !== null && $value !== '' && is_numeric($value)) {
                return (int) $value;
            }
        }

        return $default;
    }

    protected function stringValue(array $data, array $keys, ?string $default = null): ?string
    {
        foreach ($keys as $key) {
            $value = Arr::get($data, $key);
            if ($value !== null && trim((string) $value) !== '') {
                return trim((string) $value);
            }
        }

        return $default;
    }

    protected function money(array $data, array $keys, float|int|string $default = 0): float
    {
        foreach ($keys as $key) {
            $value = Arr::get($data, $key);
            if ($value === null || $value === '') {
                continue;
            }

            $value = str_replace([',', '$', 'PKR', 'USD'], '', (string) $value);
            $value = trim($value);
            if (is_numeric($value)) {
                return round((float) $value, 2);
            }
        }

        return round((float) $default, 2);
    }

    protected function dateValue(array $data, array $keys): ?string
    {
        $date = $this->dateTimeValue($data, $keys);
        return $date ? $date->toDateString() : null;
    }

    protected function dateTimeValue(array $data, array $keys): ?Carbon
    {
        foreach ($keys as $key) {
            $value = $this->stringValue($data, [$key]);
            if (! $value || in_array($value, ['0000-00-00', '0000-00-00 00:00:00', '-'], true)) {
                continue;
            }

            try {
                return Carbon::parse($value);
            } catch (\Throwable) {
                continue;
            }
        }

        return null;
    }

    protected function localStatus(?string $status): string
    {
        $status = strtolower(trim((string) $status));
        $status = str_replace(' ', '_', $status);

        return $status !== '' ? $status : 'pending';
    }

    protected function currency(User $user, array $row, ?string $accountCurrency = null): string
    {
        // Some WHMCS responses expose numeric `currency` (the currency id),
        // while others expose `currencycode`. Only accept actual ISO codes.
        $currency = $this->normalizeCurrency($this->stringValue($row, ['currencycode']));
        if (! $currency) {
            $candidate = $this->stringValue($row, ['currency']);
            $currency = $this->normalizeCurrency($candidate);
        }

        return $currency
            ?: $this->normalizeCurrency($accountCurrency)
            ?: $this->normalizeCurrency((string) $user->currency)
            ?: 'USD';
    }

    protected function resolveWhmcsClientCurrency(User $user, int $clientId, bool $fresh = false): string
    {
        $response = $this->api->clientDetails($clientId, $fresh);
        $client = is_array($response['client'] ?? null) ? $response['client'] : [];
        $currency = $this->normalizeCurrency($this->stringValue($client, ['currency_code', 'currencycode']));

        if (! $currency) {
            // GetClientsDetails also returns a numeric currency id. Resolve it
            // through GetCurrencies instead of treating "1" or "3" as a code.
            $currencyId = $this->intValue($client, ['currency'], null);
            if ($currencyId) {
                foreach ($this->api->currencies($fresh) as $row) {
                    if ((int) ($row['id'] ?? 0) === $currencyId) {
                        $currency = $this->normalizeCurrency((string) ($row['code'] ?? ''));
                        break;
                    }
                }
            }
        }

        $currency = $currency ?: $this->normalizeCurrency((string) $user->currency) ?: 'USD';

        // This lock means later IP/VPN changes cannot relabel the account. WHMCS
        // remains authoritative because its raw billing amounts use this currency.
        if (strtoupper((string) $user->currency) !== $currency || ! $user->currency_locked_at) {
            $user->forceFill([
                'currency' => $currency,
                'currency_source' => 'whmcs_client',
                'currency_locked_at' => $user->currency_locked_at ?: now(),
                'whmcs_synced_at' => now(),
            ])->save();
        }

        return $currency;
    }

    protected function normalizeCurrency(?string $currency): ?string
    {
        $currency = strtoupper(trim((string) $currency));

        return in_array($currency, ['USD', 'PKR'], true) ? $currency : null;
    }

    protected function invoiceNumber(int $invoiceId, array $row): string
    {
        return $this->stringValue($row, ['invoicenum', 'invoice_number']) ?: 'WHMCS-'.$invoiceId;
    }

    protected function nameservers(array $row): array
    {
        $nameservers = [];
        foreach (['ns1', 'ns2', 'ns3', 'ns4', 'ns5'] as $key) {
            $value = $this->stringValue($row, [$key]);
            if ($value) {
                $nameservers[] = $value;
            }
        }

        return $nameservers;
    }

    protected function extractTld(string $domain): string
    {
        $domain = strtolower(trim($domain));
        foreach (['com.pk', 'net.pk', 'org.pk', 'edu.pk', 'gov.pk'] as $tld) {
            if (str_ends_with($domain, '.'.$tld)) {
                return $tld;
            }
        }

        return Str::afterLast($domain, '.');
    }

    protected function limitedPayload(?array $payload): ?array
    {
        if (! is_array($payload)) {
            return null;
        }

        $json = json_encode($payload);
        if ($json !== false && strlen($json) > 8000) {
            return ['_truncated' => true, '_preview' => substr($json, 0, 8000)];
        }

        return $payload;
    }
}
