<?php

namespace App\Modules\Registrar\Realtimeregister;

use App\Core\Registrar\AbstractRegistrar;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

/**
 * Realtime Register (yoursrs) registrar module — **v2 REST API**.
 *
 * Re-targeted from the deprecated v1 "http API" (http.api.yoursrs.com/v1,
 * plaintext login_handle/login_pass body) to the current v2 REST API
 * (api.yoursrs.com/v2, `Authorization: ApiKey {key}` header, customer-scoped
 * contact resources). Verified against:
 *   - WiseCP  coremio/modules/Registrars/RealtimeRegister/api.php (base + ApiKey header)
 *   - HostBill includes/modules/Domain/realtime/api.php (v2 paths + contact schema)
 *   - dm.realtimeregister.com/docs
 *
 * Contacts (PRC-969): register/transfer source the registrant/admin/tech/billing
 * groups via $this->contact($params, $group) (populated canonical) and map to the
 * RTR v2 contact schema (name/organization/addressLine[]/city/state/country/
 * postalCode/email/voice). Contacts are customer-scoped resources that must be
 * upserted (PUT) before the domain create/transfer references them by handle.
 *
 * PRC-49: no DB queries — every field comes from $params; domain taken straight
 * from $params['domain']; credentials never logged.
 */
class Realtimeregister extends AbstractRegistrar
{
    /**
     * Production API base URL (v2 REST).
     */
    protected const API_URL = 'https://api.yoursrs.com/v2/';

    /**
     * Test (OTE) API base URL (v2 REST).
     */
    protected const API_URL_TEST = 'https://api.yoursrs-ote.com/v2/';

    /**
     * Country code to phone prefix mapping
     */
    protected const COUNTRY_PHONE_CODES = [
        'US' => '1', 'CA' => '1', 'EG' => '20', 'MA' => '212', 'DZ' => '213',
        'TN' => '216', 'LY' => '218', 'GM' => '220', 'SN' => '221', 'MR' => '222',
        'ML' => '223', 'GN' => '224', 'CI' => '225', 'BF' => '226', 'NE' => '227',
        'TG' => '228', 'BJ' => '229', 'MU' => '230', 'LR' => '231', 'SL' => '232',
        'GH' => '233', 'NG' => '234', 'TD' => '235', 'CF' => '236', 'CM' => '237',
        'CV' => '238', 'ST' => '239', 'GQ' => '240', 'GA' => '241', 'CG' => '242',
        'CD' => '243', 'AO' => '244', 'GW' => '245', 'IO' => '246', 'SC' => '248',
        'SD' => '249', 'RW' => '250', 'ET' => '251', 'SO' => '252', 'DJ' => '253',
        'KE' => '254', 'TZ' => '255', 'UG' => '256', 'BI' => '257', 'MZ' => '258',
        'ZM' => '260', 'MG' => '261', 'RE' => '262', 'ZW' => '263', 'NA' => '264',
        'MW' => '265', 'LS' => '266', 'BW' => '267', 'SZ' => '268', 'KM' => '269',
        'ZA' => '27', 'ER' => '291', 'AW' => '297', 'FO' => '298', 'GL' => '299',
        'GR' => '30', 'NL' => '31', 'BE' => '32', 'FR' => '33', 'ES' => '34',
        'GI' => '350', 'PT' => '351', 'LU' => '352', 'IE' => '353', 'IS' => '354',
        'AL' => '355', 'MT' => '356', 'CY' => '357', 'FI' => '358', 'BG' => '359',
        'HU' => '36', 'LT' => '370', 'LV' => '371', 'EE' => '372', 'MD' => '373',
        'AM' => '374', 'BY' => '375', 'AD' => '376', 'MC' => '377', 'SM' => '378',
        'UA' => '380', 'RS' => '381', 'ME' => '382', 'HR' => '385', 'SI' => '386',
        'BA' => '387', 'MK' => '389', 'IT' => '39', 'RO' => '40', 'CH' => '41',
        'CZ' => '420', 'SK' => '421', 'LI' => '423', 'AT' => '43', 'GB' => '44',
        'DK' => '45', 'SE' => '46', 'NO' => '47', 'PL' => '48', 'DE' => '49',
        'FK' => '500', 'BZ' => '501', 'GT' => '502', 'SV' => '503', 'HN' => '504',
        'NI' => '505', 'CR' => '506', 'PA' => '507', 'PM' => '508', 'HT' => '509',
        'PE' => '51', 'MX' => '52', 'CU' => '53', 'AR' => '54', 'BR' => '55',
        'CL' => '56', 'CO' => '57', 'VE' => '58', 'GP' => '590', 'BO' => '591',
        'GY' => '592', 'EC' => '593', 'GF' => '594', 'PY' => '595', 'MQ' => '596',
        'SR' => '597', 'UY' => '598', 'MY' => '60', 'AU' => '61', 'ID' => '62',
        'PH' => '63', 'NZ' => '64', 'SG' => '65', 'TH' => '66', 'TL' => '670',
        'BN' => '673', 'NR' => '674', 'PG' => '675', 'TO' => '676', 'SB' => '677',
        'VU' => '678', 'FJ' => '679', 'PW' => '680', 'WF' => '681', 'CK' => '682',
        'NU' => '683', 'WS' => '685', 'KI' => '686', 'NC' => '687', 'TV' => '688',
        'PF' => '689', 'TK' => '690', 'FM' => '691', 'MH' => '692', 'RU' => '7',
        'KZ' => '7', 'JP' => '81', 'KR' => '82', 'VN' => '84', 'KP' => '850',
        'HK' => '852', 'MO' => '853', 'KH' => '855', 'LA' => '856', 'CN' => '86',
        'TW' => '886', 'TR' => '90', 'IN' => '91', 'PK' => '92', 'AF' => '93',
        'LK' => '94', 'MM' => '95', 'MV' => '960', 'LB' => '961', 'JO' => '962',
        'SY' => '963', 'IQ' => '964', 'KW' => '965', 'SA' => '966', 'YE' => '967',
        'OM' => '968', 'PS' => '970', 'AE' => '971', 'IL' => '972', 'BH' => '973',
        'QA' => '974', 'BT' => '975', 'MN' => '976', 'NP' => '977', 'IR' => '98',
        'TJ' => '992', 'TM' => '993', 'AZ' => '994', 'GE' => '995', 'KG' => '996',
        'UZ' => '998',
    ];

    // -------------------------------------------------------------------------
    // Configuration
    // -------------------------------------------------------------------------

    /**
     * Return the module configuration fields for the admin settings form.
     */
    public function getConfigFields(): array
    {
        return [
            'api_key' => [
                'type' => 'password',
                'label' => __('admin/modules.realtimeregister_field_api_key_label'),
                'required' => true,
                'description' => __('admin/modules.realtimeregister_field_api_key_description'),
                'placeholder' => 'ApiKey',
                'value' => $this->config['api_key'] ?? '',
            ],
            'customer' => [
                'type' => 'text',
                'label' => __('admin/modules.realtimeregister_field_customer_label'),
                'required' => true,
                'description' => __('admin/modules.realtimeregister_field_customer_description'),
                'placeholder' => 'your-customer-handle',
                'value' => $this->config['customer'] ?? '',
            ],
            'default_handle' => [
                'type' => 'text',
                'label' => __('admin/modules.realtimeregister_field_handle_label'),
                'required' => false,
                'description' => __('admin/modules.realtimeregister_field_handle_description'),
                'placeholder' => 'admin-handle',
                'value' => $this->config['default_handle'] ?? '',
            ],
            'trade_transfer' => [
                'type' => 'checkbox',
                'label' => __('admin/modules.realtimeregister_field_trade_label'),
                'required' => false,
                'description' => __('admin/modules.realtimeregister_field_trade_description'),
                'default' => false,
                'value' => $this->config['trade_transfer'] ?? false,
            ],
            'phone_format' => [
                'type' => 'text',
                'label' => __('admin/modules.realtimeregister_field_phone_format_label'),
                'required' => false,
                'description' => __('admin/modules.realtimeregister_field_phone_format_description'),
                'placeholder' => '+%c %a %s',
                'value' => $this->config['phone_format'] ?? '',
            ],
            'test_mode' => [
                'type' => 'checkbox',
                'label' => __('admin/modules.realtimeregister_field_test_mode_label'),
                'required' => false,
                'description' => __('admin/modules.realtimeregister_field_test_mode_description'),
                'default' => false,
                'value' => $this->config['test_mode'] ?? false,
            ],
        ];
    }

    // -------------------------------------------------------------------------
    // API helpers
    // -------------------------------------------------------------------------

    /**
     * Determine the correct API base URL (production or test).
     */
    protected function getApiBaseUrl(): string
    {
        $testMode = $this->config['test_mode'] ?? false;

        return $testMode ? self::API_URL_TEST : self::API_URL;
    }

    /**
     * The v2 customer handle scoping contact resources.
     */
    protected function getCustomerHandle(): string
    {
        return (string) ($this->config['customer'] ?? '');
    }

    /**
     * Build the v2 authentication headers.
     *
     * v2 authenticates with `Authorization: ApiKey {key}` (WiseCP api.php:62-65,
     * HostBill api.php:42). No login_handle/login_pass body credentials.
     *
     * @return array<string,string>
     */
    protected function buildAuthHeaders(): array
    {
        return [
            'Authorization' => 'ApiKey ' . ($this->config['api_key'] ?? ''),
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];
    }

    /**
     * Send a request to the Realtime Register v2 REST API.
     *
     * @param  string  $endpoint  Relative endpoint path (e.g. "domains/example.com")
     * @param  string  $method    HTTP verb (GET|POST|PUT|DELETE)
     * @param  array   $data      JSON body payload (empty for GET)
     * @return array Normalized response (['status' => ..., 'response' => ...])
     */
    protected function sendApiRequest(string $endpoint, string $method = 'GET', array $data = []): array
    {
        $url = $this->getApiBaseUrl() . ltrim($endpoint, '/');

        try {
            $request = Http::timeout(30)
                ->withHeaders($this->buildAuthHeaders())
                ->withOptions([
                    'verify' => ! ($this->config['test_mode'] ?? false),
                ]);

            $method = strtoupper($method);

            $response = match ($method) {
                'GET' => $request->get($url),
                'DELETE' => $request->delete($url),
                'PUT' => $request->put($url, $data),
                default => $request->post($url, $data),
            };

            $json = $response->json();

            // v2 error envelope: { type, message } or { errors: [...] } /
            // { violations: [{ field, message }] } (WiseCP api.php:85-110).
            if ($response->failed() || $this->responseHasError($json)) {
                $errorMessage = $this->extractErrorMessage($json, $response->body());

                Log::error('Realtimeregister API error', [
                    'endpoint' => $endpoint,
                    'method' => $method,
                    'status' => $response->status(),
                    'error' => $errorMessage,
                ]);

                return [
                    'status' => 'error',
                    'message' => __('admin/modules.realtimeregister_api_error', ['error' => $errorMessage]),
                    'data' => ['status_code' => $response->status()],
                ];
            }

            return [
                'status' => 'success',
                'response' => is_array($json) ? $json : [],
            ];
        } catch (\Exception $e) {
            Log::error('Realtimeregister API exception', [
                'endpoint' => $endpoint,
                'exception' => $e->getMessage(),
            ]);

            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_connection_error', ['error' => $e->getMessage()]),
            ];
        }
    }

    /**
     * Detect a v2 application-level error envelope.
     */
    protected function responseHasError(mixed $json): bool
    {
        if (! is_array($json)) {
            return false;
        }

        if (! empty($json['errors']) || ! empty($json['violations'])) {
            return true;
        }

        // { type, message } error shape (no domain payload).
        return isset($json['type'], $json['message']) && ! empty($json['message']);
    }

    /**
     * Extract a human-readable message from a v2 error envelope.
     */
    protected function extractErrorMessage(mixed $json, string $fallbackBody): string
    {
        if (is_array($json)) {
            if (! empty($json['violations']) && is_array($json['violations'])) {
                $parts = [];
                foreach ($json['violations'] as $v) {
                    $parts[] = trim(($v['field'] ?? '') . ' ' . ($v['message'] ?? ''));
                }

                return implode(', ', array_filter($parts));
            }

            if (! empty($json['errors']) && is_array($json['errors'])) {
                return is_string($json['errors'][0] ?? null)
                    ? implode(', ', $json['errors'])
                    : json_encode($json['errors']);
            }

            if (! empty($json['message'])) {
                return (string) $json['message'];
            }
        }

        return $fallbackBody !== '' ? $fallbackBody : __('admin/modules.realtimeregister_unknown_error');
    }

    /**
     * Convert a phone number to E.164 format (+cc.number) using the configured pattern.
     *
     * @param  string  $phone  Raw phone number
     * @param  string  $countryCode  Two-letter ISO country code
     * @return string Formatted phone number
     */
    protected function convertPhoneNumber(string $phone, string $countryCode): string
    {
        $format = $this->config['phone_format'] ?? '';

        if (empty($phone)) {
            return $phone;
        }

        // RTR `voice` MUST be E.164 `+cc.number` (WiseCP whois_process:124,
        // HostBill create_contact:696-698 always get_phone_info). When no format
        // pattern is configured, derive it from the country prefix + digits so a
        // raw local number never leaks to the registry.
        if (empty($format)) {
            return $this->buildE164($phone, $countryCode);
        }

        $regex = $format;

        // Escape regex special characters
        $escapeChars = '\\()[]{|.?^$+*';
        for ($pos = 0; $pos < strlen($escapeChars); $pos++) {
            $regex = str_replace($escapeChars[$pos], '\\' . $escapeChars[$pos], $regex);
        }

        $phonePrefix = self::COUNTRY_PHONE_CODES[strtoupper($countryCode)] ?? null;

        if ($phonePrefix) {
            $regex = str_replace('%c', '(?<country_code>' . $phonePrefix . ')', $regex);
        } else {
            $regex = str_replace('%c', '(?<country_code>[1-9][0-9]{0,2})', $regex);
        }

        $regex = str_replace('%a', '(?<area_code>[1-9][0-9]{0,6})', $regex);
        $regex = str_replace('%s', '(?<subscriber_number>[1-9][0-9 ]{2,15})', $regex);
        $regex = '/' . $regex . '/';

        preg_match($regex, $phone, $matches);

        if (! isset($matches['country_code']) && $phonePrefix) {
            $matches['country_code'] = $phonePrefix;
        }

        if (! isset($matches['area_code'])) {
            $matches['area_code'] = '';
        }

        $subscriberNumber = str_replace(' ', '', $matches['subscriber_number'] ?? '');

        if (empty($matches['country_code']) || empty($subscriberNumber)) {
            return $phone;
        }

        return '+' . $matches['country_code'] . '.' . $matches['area_code'] . $subscriberNumber;
    }

    /**
     * Derive an E.164 `+cc.number` voice value from a raw phone + country code.
     *
     * Used when no phone_format pattern is configured. If the number already
     * carries a leading `+cc`, the prefix is split off; otherwise the country's
     * dialing code is prepended (WiseCP `+cc.number`, HostBill get_phone_info).
     */
    protected function buildE164(string $phone, string $countryCode): string
    {
        $prefix = self::COUNTRY_PHONE_CODES[strtoupper($countryCode)] ?? '';

        // Already `+cc.subscriber` — pass through untouched.
        if (preg_match('/^\+\d+\.\d+/', $phone)) {
            return $phone;
        }

        if (str_starts_with($phone, '+')) {
            // `+<digits>` (possibly spaced) — split known country prefix off the front.
            $digits = preg_replace('/[^0-9]/', '', substr($phone, 1)) ?? '';
            if ($prefix !== '' && str_starts_with($digits, $prefix)) {
                $subscriber = substr($digits, strlen($prefix));

                return $subscriber !== '' ? '+' . $prefix . '.' . $subscriber : $phone;
            }

            return $digits !== '' ? '+' . $digits : $phone;
        }

        $subscriber = preg_replace('/[^0-9]/', '', $phone) ?? '';
        if ($subscriber === '') {
            return $phone;
        }

        if ($prefix !== '' && str_starts_with($subscriber, $prefix) && strlen($subscriber) > strlen($prefix)) {
            $subscriber = substr($subscriber, strlen($prefix));
        }

        return $prefix !== '' ? '+' . $prefix . '.' . $subscriber : $phone;
    }

    /**
     * Normalize country codes (e.g. UK -> GB).
     */
    protected function normalizeCountryCode(string $country): string
    {
        return strtoupper($country) === 'UK' ? 'GB' : strtoupper($country);
    }

    /**
     * Build a Realtime Register v2 contact payload from a populated canonical group.
     *
     * Sources the group via $this->contact() (PRC-969 populated canonical) and maps
     * the 11 canonical fields → the RTR v2 contact schema. Field names verified vs
     * HostBill api.php:230-243 (name/organization/addressLine/city/state/country/
     * postalCode/email/voice).
     *
     * @param  string  $group  registrant|admin|tech|billing
     * @return array<string,mixed>
     */
    protected function buildContactData(array $params, string $group = 'registrant'): array
    {
        $c = $this->contact($params, $group);

        $countryCode = $this->normalizeCountryCode($c['country_code'] ?? '');
        $phone = $this->convertPhoneNumber($c['phone'] ?? '', $countryCode);

        $addressLine = array_values(array_filter([
            $c['address1'] ?? '',
            $c['address2'] ?? '',
        ], fn ($v) => $v !== ''));

        $name = trim(($c['first_name'] ?? '') . ' ' . ($c['last_name'] ?? ''));

        return [
            'name' => $name,
            'organization' => $c['company'] ?? '',
            'addressLine' => $addressLine,
            'city' => $c['city'] ?? '',
            'state' => $c['state'] ?? '',
            'country' => $countryCode,
            'postalCode' => $c['postcode'] ?? '',
            'email' => $c['email'] ?? '',
            'voice' => $phone,
        ];
    }

    /**
     * Derive a stable, RTR-safe contact handle for a group.
     *
     * v2 contacts are customer-scoped named resources upserted via PUT. The handle
     * is derived from the contact email + role so it is deterministic and idempotent
     * (HostBill get_contact_handle(): email + "_" + type, sanitized, <=40 chars).
     */
    protected function buildContactHandle(array $params, string $group): string
    {
        $c = $this->contact($params, $group);
        $email = $c['email'] ?? '';

        $base = strlen($email) > 29 ? substr($email, 0, 29) : $email;
        $handle = $base . '_' . $group;
        $handle = preg_replace('/[^a-zA-Z0-9\-_]/', '', $handle) ?? '';

        return substr($handle, 0, 40);
    }

    /**
     * Upsert (PUT) a customer-scoped contact resource for a group, returning its handle.
     *
     * v2: PUT /v2/customers/{customer}/contacts/{handle}. Returns
     * ['status' => 'success', 'handle' => ...] or the error envelope from the API.
     */
    protected function upsertContact(array $params, string $group): array
    {
        $customer = $this->getCustomerHandle();
        $handle = $this->buildContactHandle($params, $group);
        $endpoint = 'customers/' . rawurlencode($customer) . '/contacts/' . rawurlencode($handle);

        // The handle is deterministic (email + role), so a plain POST to an already
        // existing contact 409/errors and fails the whole register/transfer (e.g.
        // admin/tech/billing sharing one email → same handle POSTed twice; or a
        // repeat order for the same client). Mirror HostBill create_contact: GET
        // first, and only create when the contact does not yet exist.
        // (WiseCP sidesteps this with a random per-order handle; we key on email.)
        $existing = $this->sendApiRequest($endpoint, 'GET');
        if ($existing['status'] === 'success' && ! empty($existing['response']['handle'])) {
            return ['status' => 'success', 'handle' => $handle];
        }

        // RTR v2 creates contacts with POST (both WiseCP + HostBill oracles use POST
        // to customers/{customer}/contacts/{handle}); PUT 404/405s.
        $payload = $this->buildContactData($params, $group);
        $result = $this->sendApiRequest($endpoint, 'POST', $payload);

        if ($result['status'] === 'error') {
            return $result;
        }

        return ['status' => 'success', 'handle' => $handle];
    }

    /**
     * Resolve the four contact handles for a domain create/transfer request.
     *
     * When a default_handle is configured it is reused for admin/tech/billing
     * (registrant is always sourced from the client). Otherwise a distinct contact
     * is upserted per group.
     *
     * @return array{status:string, message?:string, data?:array, handles?:array<string,string>}
     */
    protected function resolveContactHandles(array $params): array
    {
        $registrant = $this->upsertContact($params, 'registrant');
        if ($registrant['status'] === 'error') {
            return $registrant;
        }

        $handles = ['registrant' => $registrant['handle']];

        $defaultHandle = $this->config['default_handle'] ?? '';

        foreach (['admin', 'tech', 'billing'] as $group) {
            if (! empty($defaultHandle)) {
                $handles[$group] = $defaultHandle;
                continue;
            }

            $contact = $this->upsertContact($params, $group);
            if ($contact['status'] === 'error') {
                return $contact;
            }
            $handles[$group] = $contact['handle'];
        }

        return ['status' => 'success', 'handles' => $handles];
    }

    /**
     * Build the v2 `contacts[]` block (ADMIN/TECH/BILLING role/handle pairs).
     *
     * @return array<int,array{role:string,handle:string}>
     */
    protected function buildContactsBlock(array $handles): array
    {
        return [
            ['role' => 'ADMIN', 'handle' => $handles['admin']],
            ['role' => 'TECH', 'handle' => $handles['tech']],
            ['role' => 'BILLING', 'handle' => $handles['billing']],
        ];
    }

    /**
     * Forward TLD-specific extra fields (.nl/.de/.fr…) onto the registrant contact.
     *
     * RTR ties extra attributes to a contact via
     * POST /v2/customers/{customer}/contacts/{handle}/{provider} with
     * { properties: {...}, intendedUsage: REGISTRANT } (WiseCP:181-192,
     * HostBill add_contact_properties api.php:917-935). The provider + the set of
     * mandatory property names come from GET /v2/tlds/{tld}/info. Values are taken
     * from $params['additional_fields'] (builder emits from client_domain_fields).
     * No-op (success) when the TLD has no contactProperties or no extras supplied.
     *
     * PRC-49: TLD is derived from the domain string, never from $tld->tld.
     */
    protected function applyContactProperties(array $params, string $registrantHandle): array
    {
        $additional = $params['additional_fields'] ?? [];
        if (empty($additional) || ! is_array($additional) || $registrantHandle === '') {
            return ['status' => 'success'];
        }

        $domain = $params['domain'] ?? '';
        $tld = strtolower(explode('.', $domain, 2)[1] ?? '');
        if ($tld === '') {
            return ['status' => 'success'];
        }

        // tlds/{tld}/info → provider + metadata.contactProperties[] (WiseCP:157).
        $tldKey = str_replace('.', '', $tld);
        $info = $this->sendApiRequest('tlds/' . rawurlencode($tldKey) . '/info');
        if ($info['status'] === 'error') {
            return $info;
        }

        $meta = $info['response']['metadata'] ?? [];
        $contactProperties = $meta['contactProperties'] ?? [];
        if (empty($contactProperties)) {
            return ['status' => 'success'];
        }

        $provider = $info['response']['provider'] ?? '';
        if ($provider === '') {
            return ['status' => 'success'];
        }

        // Only forward property names the registry declares (HostBill:927-929).
        $properties = [];
        foreach ($contactProperties as $property) {
            $name = $property['name'] ?? '';
            if ($name !== '' && array_key_exists($name, $additional) && $additional[$name] !== '') {
                $properties[$name] = $additional[$name];
            }
        }

        if (empty($properties)) {
            return ['status' => 'success'];
        }

        $customer = $this->getCustomerHandle();
        $result = $this->sendApiRequest(
            'customers/' . rawurlencode($customer) . '/contacts/' . rawurlencode($registrantHandle) . '/' . rawurlencode($provider),
            'POST',
            [
                'properties' => $properties,
                'intendedUsage' => 'REGISTRANT',
            ]
        );

        return $result['status'] === 'error' ? $result : ['status' => 'success'];
    }

    // -------------------------------------------------------------------------
    // Required (abstract) methods
    // -------------------------------------------------------------------------

    /**
     * Register a new domain.
     */
    public function registerDomain(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $handlesResult = $this->resolveContactHandles($params);
        if ($handlesResult['status'] === 'error') {
            return $handlesResult;
        }
        $handles = $handlesResult['handles'];

        // TLD-özel extra alanlar (.nl/.de/.fr…) registrant contact'a properties
        // olarak yazılır (WiseCP RealtimeRegister.php:181-192, HostBill
        // register_domain api.php:813-824). Domain create'ten ÖNCE olmalı.
        $extraResult = $this->applyContactProperties($params, $handles['registrant']);
        if ($extraResult['status'] === 'error') {
            return $extraResult;
        }

        $apiData = [
            'customer' => $this->getCustomerHandle(),
            'registrant' => $handles['registrant'],
            'contacts' => $this->buildContactsBlock($handles),
            'autoRenew' => ($params['auto_renew'] ?? false) ? true : false,
        ];

        // RTR v2 `period` AY biriminde beklenir; procp `period` YIL (WiseCP
        // year*12 RealtimeRegister.php:199, HostBill numyears*12 api.php:836).
        if (! empty($params['period'])) {
            $apiData['period'] = ((int) $params['period']) * 12;
        }

        // Nameservers (v2: flat array of hostnames).
        $nameservers = array_values(array_filter($params['nameservers'] ?? []));
        if (! empty($nameservers)) {
            $apiData['ns'] = array_map('strtolower', $nameservers);
        }

        if (! empty($params['id_protection'])) {
            $apiData['privacyProtect'] = true;
        }

        // v2 domain create: POST /v2/domains/{domain}
        $result = $this->sendApiRequest('domains/' . rawurlencode($domain), 'POST', $apiData);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Domain registered', [
            'action' => 'registered',
            'domain' => $domain,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_register_success'),
            'data' => [
                'domain' => $domain,
                'registrar' => 'realtimeregister',
                'registered_at' => now()->toISOString(),
            ],
        ];
    }

    /**
     * Initiate a domain transfer.
     */
    public function transferDomain(array $params)
    {
        $domain = $params['domain'] ?? '';
        $eppCode = $params['epp_code'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        if (empty($eppCode)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_epp_code'),
            ];
        }

        $handlesResult = $this->resolveContactHandles($params);
        if ($handlesResult['status'] === 'error') {
            return $handlesResult;
        }
        $handles = $handlesResult['handles'];

        // TLD-özel extra alanları registrant contact'a yaz (register ile aynı).
        $extraResult = $this->applyContactProperties($params, $handles['registrant']);
        if ($extraResult['status'] === 'error') {
            return $extraResult;
        }

        $apiData = [
            'customer' => $this->getCustomerHandle(),
            'authcode' => $eppCode,
            'registrant' => $handles['registrant'],
            'contacts' => $this->buildContactsBlock($handles),
            'autoRenew' => ($params['auto_renew'] ?? false) ? true : false,
        ];

        // EPP-code'lu transfer'de period çoğu registry'de kullanılmaz — WiseCP
        // `if($epp_code) unset($params['period'])` (RealtimeRegister.php:220).
        // Ham (yanlış birimli) period göndermek registry-side hata/uzatma riski.
        // period gönderilecekse RTR v2 AY bekler (YIL*12).

        $nameservers = array_values(array_filter($params['nameservers'] ?? []));
        if (! empty($nameservers)) {
            $apiData['ns'] = array_map('strtolower', $nameservers);
        }

        // v2 transfer: POST /v2/domains/{domain}/transfer
        $result = $this->sendApiRequest('domains/' . rawurlencode($domain) . '/transfer', 'POST', $apiData);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Domain transfer initiated', [
            'action' => 'transfer',
            'domain' => $domain,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_transfer_success'),
            'data' => [
                'domain' => $domain,
                'registrar' => 'realtimeregister',
                'transfer_started_at' => now()->toISOString(),
            ],
        ];
    }

    /**
     * Renew a domain for the specified period.
     */
    public function renewDomain(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        // RTR v2 renew `period` AY biriminde beklenir (WiseCP year*12
        // RealtimeRegister.php:307, HostBill numyears*12 class.realtime.php:396).
        $renewData = [
            'period' => ((int) ($params['period'] ?? 1)) * 12,
        ];

        // REDEMPTION_PERIOD'daki domain /renew ile yenilenemez → /restore
        // gerekir (HostBill Renew class.realtime.php:397-402).
        $info = $this->sendApiRequest('domains/' . rawurlencode($domain));
        $statuses = [];
        if ($info['status'] === 'success') {
            $raw = $info['response']['status'] ?? [];
            $statuses = is_array($raw) ? $raw : [$raw];
        }
        $inRedemption = in_array('REDEMPTION_PERIOD', array_map('strtoupper', $statuses), true);

        // v2: POST /v2/domains/{domain}/renew (normal) veya /restore (redemption).
        $endpoint = 'domains/' . rawurlencode($domain) . ($inRedemption ? '/restore' : '/renew');
        $body = $inRedemption ? ['reason' => 'Registrant Error'] : $renewData;

        $result = $this->sendApiRequest($endpoint, 'POST', $body);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Domain renewed', [
            'action' => 'renewed',
            'domain' => $domain,
        ]);

        // Fetch the fresh expiry (renew returns 200/empty on success).
        $newExpiry = null;
        $info = $this->sendApiRequest('domains/' . rawurlencode($domain));
        if ($info['status'] === 'success') {
            $newExpiry = $this->parseDate($info['response']['expiryDate'] ?? null);
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_renew_success'),
            'data' => [
                'domain' => $domain,
                'registrar' => 'realtimeregister',
                'renewed_at' => now()->toISOString(),
                'expires_at' => $newExpiry,
            ],
        ];
    }

    /**
     * Get the nameservers for a domain.
     */
    public function getNameservers(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_nameservers_success'),
            'data' => [
                'domain' => $domain,
                'nameservers' => $this->extractNameservers($result['response'] ?? []),
            ],
        ];
    }

    /**
     * Update the nameservers for a domain.
     */
    public function setNameservers(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $nameservers = $params['nameservers'] ?? [];
        if (empty($nameservers) || ! is_array($nameservers)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_nameservers'),
            ];
        }

        $apiData = [
            'ns' => array_map('strtolower', array_values(array_filter($nameservers))),
        ];

        // v2 update: POST /v2/domains/{domain}/update
        $result = $this->sendApiRequest('domains/' . rawurlencode($domain) . '/update', 'POST', $apiData);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Nameservers updated', [
            'action' => 'set_nameservers',
            'domain' => $domain,
            'nameservers' => $nameservers,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_update_nameservers_success'),
            'data' => [
                'domain' => $domain,
                'nameservers' => $nameservers,
                'updated_at' => now()->toISOString(),
            ],
        ];
    }

    /**
     * Get contact details for a domain.
     *
     * Returns groups: Registrant, Admin, Tech, Billing
     */
    public function getContactDetails(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        // v2 domain info exposes registrant handle + contacts[] role/handle pairs.
        $domainInfo = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($domainInfo['status'] === 'error') {
            return $domainInfo;
        }

        $response = $domainInfo['response'] ?? [];
        $customer = $this->getCustomerHandle();

        // Map role => handle. Registrant is a top-level field; the rest live in
        // contacts[] (ADMIN/TECH/BILLING — HostBill info_contact maps all roles).
        $roleHandles = ['Registrant' => $response['registrant'] ?? null];
        foreach ($response['contacts'] ?? [] as $contact) {
            $role = strtolower($contact['role'] ?? '');
            if ($role === 'admin') {
                $roleHandles['Admin'] = $contact['handle'] ?? null;
            } elseif ($role === 'tech') {
                $roleHandles['Tech'] = $contact['handle'] ?? null;
            } elseif ($role === 'billing') {
                $roleHandles['Billing'] = $contact['handle'] ?? null;
            }
        }

        $groups = [];

        foreach ($roleHandles as $label => $handle) {
            if (empty($handle)) {
                continue;
            }

            $contactResult = $this->sendApiRequest(
                'customers/' . rawurlencode($customer) . '/contacts/' . rawurlencode($handle)
            );

            if ($contactResult['status'] === 'error') {
                continue;
            }

            $data = $contactResult['response'] ?? [];
            $lines = $data['addressLine'] ?? [];

            // v2 stores a single `name`; split on first space for first/last.
            $nameParts = explode(' ', trim($data['name'] ?? ''), 2);

            $groups[$label] = [
                'firstname' => $nameParts[0] ?? '',
                'lastname' => $nameParts[1] ?? '',
                'company' => $data['organization'] ?? '',
                'email' => $data['email'] ?? '',
                'phone' => $data['voice'] ?? '',
                'fax' => $data['fax'] ?? '',
                'address1' => $lines[0] ?? '',
                'address2' => $lines[1] ?? '',
                'address3' => $lines[2] ?? '',
                'city' => $data['city'] ?? '',
                'state' => $data['state'] ?? '',
                'postal_code' => $data['postalCode'] ?? '',
                'country' => $data['country'] ?? '',
            ];
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_contact_success'),
            'groups' => $groups,
        ];
    }

    /**
     * Update contact details for a domain.
     *
     * Expects $params['groups'] with keys: Registrant, Admin, Tech, Billing
     */
    public function setContactDetails(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $groups = $params['groups'] ?? [];
        if (empty($groups) || ! is_array($groups)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_contact_groups'),
            ];
        }

        // v2 domain info to resolve current contact handles.
        $domainInfo = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($domainInfo['status'] === 'error') {
            return $domainInfo;
        }

        $response = $domainInfo['response'] ?? [];
        $customer = $this->getCustomerHandle();

        $roleHandles = ['Registrant' => $response['registrant'] ?? null];
        foreach ($response['contacts'] ?? [] as $contact) {
            $role = strtolower($contact['role'] ?? '');
            if ($role === 'admin') {
                $roleHandles['Admin'] = $contact['handle'] ?? null;
            } elseif ($role === 'tech') {
                $roleHandles['Tech'] = $contact['handle'] ?? null;
            } elseif ($role === 'billing') {
                $roleHandles['Billing'] = $contact['handle'] ?? null;
            }
        }

        foreach ($roleHandles as $label => $handle) {
            if (! isset($groups[$label]) || empty($handle)) {
                continue;
            }

            $contactData = $groups[$label];

            $country = $this->normalizeCountryCode($contactData['country'] ?? '');

            $updateData = [
                'name' => trim(($contactData['firstname'] ?? '') . ' ' . ($contactData['lastname'] ?? '')),
                'organization' => $contactData['company'] ?? '',
                'addressLine' => array_values(array_filter([
                    $contactData['address1'] ?? '',
                    $contactData['address2'] ?? '',
                    $contactData['address3'] ?? '',
                ], fn ($v) => $v !== '')),
                'city' => $contactData['city'] ?? '',
                'state' => $contactData['state'] ?? '',
                'country' => $country,
                'postalCode' => $contactData['postal_code'] ?? '',
                'email' => $contactData['email'] ?? '',
                // RTR `voice` E.164 `+cc.number` bekler (create/update tutarlı).
                'voice' => $this->convertPhoneNumber($contactData['phone'] ?? '', $country),
            ];

            // v2 contact update: POST /v2/customers/{customer}/contacts/{handle}/update
            $contactResult = $this->sendApiRequest(
                'customers/' . rawurlencode($customer) . '/contacts/' . rawurlencode($handle) . '/update',
                'POST',
                $updateData
            );

            if ($contactResult['status'] === 'error') {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.realtimeregister_update_contact_failed', ['type' => $label]),
                    'data' => $contactResult['data'] ?? [],
                ];
            }
        }

        Log::info('Realtimeregister: Contact details updated', [
            'action' => 'set_contacts',
            'domain' => $domain,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_update_contact_success'),
            'data' => [
                'domain' => $domain,
                'updated_at' => now()->toISOString(),
            ],
        ];
    }

    // -------------------------------------------------------------------------
    // Optional methods (overrides of AbstractRegistrar defaults)
    // -------------------------------------------------------------------------

    /**
     * Get full domain details from the registrar.
     */
    public function getDomainDetails(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        $response = $result['response'] ?? [];

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_domain_info_success'),
            'data' => [
                'domain' => $domain,
                'status' => $this->mapDomainStatus($response),
                'locked' => $this->isLocked($response),
                'created_at' => $this->parseDate($response['createdDate'] ?? null),
                'updated_at' => $this->parseDate($response['updatedDate'] ?? null),
                'expires_at' => $this->parseDate($response['expiryDate'] ?? null),
                'nameservers' => $this->extractNameservers($response),
                'registrar' => 'Realtime Register',
                'epp_code' => $response['authcode'] ?? null,
            ],
        ];
    }

    /**
     * Get the registrar lock status.
     */
    public function getDomainLock(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_lock_status_success'),
            'data' => [
                'domain' => $domain,
                'locked' => $this->isLocked($result['response'] ?? []),
            ],
        ];
    }

    /**
     * Set / unset the registrar lock.
     */
    public function updateDomainLock(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $lockEnabled = $params['lock_status'] ?? true;

        // v2 lock is a flag inside the domain's status[] array. Read-modify-write
        // so other statuses aren't wiped (mirrors WiseCP ModifyTransferLock) —
        // a blind overwrite would drop unrelated flags / clear all on unlock.
        $current = $this->sendApiRequest('domains/' . rawurlencode($domain), 'GET');
        $statuses = [];
        // sendApiRequest puts a GET success payload under ['response']; ['data'] is
        // only populated on the error path (see sendApiRequest). Reading ['data']
        // here always yielded null → every other status flag was silently wiped
        // on lock/unlock. Read from ['response'] so the read-modify-write survives.
        if ($current['status'] === 'success' && is_array($current['response']['status'] ?? null)) {
            $statuses = array_values(array_filter(
                $current['response']['status'],
                fn ($s) => strtoupper((string) $s) !== 'CLIENT_TRANSFER_PROHIBITED'
            ));
        }
        if ($lockEnabled) {
            $statuses[] = 'CLIENT_TRANSFER_PROHIBITED';
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain) . '/update', 'POST', [
            'status' => array_values(array_unique($statuses)),
        ]);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Domain lock updated', [
            'action' => 'update_lock',
            'domain' => $domain,
            'locked' => $lockEnabled,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_set_lock_success'),
            'data' => [
                'domain' => $domain,
                'lock_status' => (bool) $lockEnabled,
                'updated_at' => now()->toISOString(),
            ],
        ];
    }

    /**
     * Get the EPP / auth code for a domain.
     */
    public function getEppCode(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        $eppCode = $result['response']['authcode'] ?? null;

        if (empty($eppCode)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_epp_not_available'),
            ];
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_epp_success'),
            'data' => [
                'domain' => $domain,
                'epp_code' => $eppCode,
            ],
        ];
    }

    /**
     * Sync domain status and expiry from registrar.
     */
    public function syncDomain(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        $response = $result['response'] ?? [];

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_sync_success'),
            'data' => [
                'domain' => $domain,
                // EPP status'larını lifecycle sözlüğüne map'le — ham
                // 'CLIENT_TRANSFER_PROHIBITED' gibi kodlar döndürme (WiseCP sync
                // PENDING_TRANSFER/EXPIRED/UNKNOWN→expired RealtimeRegister.php:706).
                'status' => $this->mapDomainStatus($response),
                'expires_at' => $this->parseDate($response['expiryDate'] ?? null),
                'created_at' => $this->parseDate($response['createdDate'] ?? null),
                'locked' => $this->isLocked($response),
                'synced_at' => now()->toISOString(),
            ],
        ];
    }

    /**
     * Sync domain transfer status.
     */
    public function syncDomainTransfer(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        // v2 exposes the in-flight transfer at /domains/{domain}/transfer.
        $transferInfo = $this->sendApiRequest('domains/' . rawurlencode($domain) . '/transfer');

        if ($transferInfo['status'] === 'success') {
            $trStatus = strtolower($transferInfo['response']['status'] ?? '');

            // RTR transfer status vocabulary (HostBill synchInfo
            // class.realtime.php:453-458):
            //   in progress: pendingwhois/pendingfoa/pendingvalidation/pending/approved
            //   failed:      cancelled/rejected/failed
            //   done:        completed
            $inProgress = ['pendingwhois', 'pendingfoa', 'pendingvalidation', 'pending', 'approved', 'requested'];
            $failed = ['cancelled', 'rejected', 'failed'];

            if (in_array($trStatus, $inProgress, true)) {
                return [
                    'status' => 'success',
                    'message' => __('admin/modules.realtimeregister_transfer_in_progress'),
                    'data' => [
                        'domain' => $domain,
                        'transfer_complete' => false,
                        'transfer_status' => $trStatus,
                        'synced_at' => now()->toISOString(),
                    ],
                ];
            }

            if (in_array($trStatus, $failed, true)) {
                return [
                    'status' => 'success',
                    'message' => __('admin/modules.realtimeregister_transfer_failed'),
                    'data' => [
                        'domain' => $domain,
                        'transfer_complete' => false,
                        'transfer_failed' => true,
                        'transfer_status' => $trStatus,
                        'synced_at' => now()->toISOString(),
                    ],
                ];
            }

            // Non-'completed', unrecognised status → treat as still pending
            // (never fabricate completion for an unknown state).
            if ($trStatus !== '' && $trStatus !== 'completed') {
                return [
                    'status' => 'success',
                    'message' => __('admin/modules.realtimeregister_transfer_in_progress'),
                    'data' => [
                        'domain' => $domain,
                        'transfer_complete' => false,
                        'transfer_status' => $trStatus,
                        'synced_at' => now()->toISOString(),
                    ],
                ];
            }
        }

        // status=completed OR transfer resource gone → the domain now resolves via info.
        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.realtimeregister_transfer_in_progress'),
                'data' => [
                    'domain' => $domain,
                    'transfer_complete' => false,
                    'synced_at' => now()->toISOString(),
                ],
            ];
        }

        $response = $result['response'] ?? [];

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_transfer_complete'),
            'data' => [
                'domain' => $domain,
                'transfer_complete' => true,
                'expires_at' => $this->parseDate($response['expiryDate'] ?? null),
                'synced_at' => now()->toISOString(),
            ],
        ];
    }

    // -------------------------------------------------------------------------
    // Privacy (WHOIS) protection
    // -------------------------------------------------------------------------

    /**
     * Read the current WHOIS privacy state for a domain.
     *
     * v2: privacy is the `privacyProtect` boolean on the domain object
     * (WiseCP getWhoisPrivacy RealtimeRegister.php:537-548).
     */
    public function getPrivacySettings(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_privacy_success'),
            'data' => [
                'domain' => $domain,
                'privacy' => (bool) ($result['response']['privacyProtect'] ?? false),
            ],
        ];
    }

    /**
     * Enable / disable WHOIS privacy protection.
     *
     * v2: POST /v2/domains/{domain}/update { privacyProtect: bool } (WiseCP
     * modifyPrivacyProtection RealtimeRegister.php:550-565, HostBill
     * updateIDProtection class.realtime.php:228-235 via update_domain).
     */
    public function updatePrivacySettings(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $enable = ! empty($params['privacy'])
            || ! empty($params['id_protection'])
            || ! empty($params['enable'])
            || (isset($params['status']) && $params['status'] === 'enable');

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain) . '/update', 'POST', [
            'privacyProtect' => $enable,
        ]);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Privacy updated', [
            'action' => 'update_privacy',
            'domain' => $domain,
            'privacy' => $enable,
        ]);

        return [
            'status' => 'success',
            'message' => $enable
                ? __('admin/modules.realtimeregister_privacy_enabled')
                : __('admin/modules.realtimeregister_privacy_disabled'),
            'data' => [
                'domain' => $domain,
                'privacy' => $enable,
                'updated_at' => now()->toISOString(),
            ],
        ];
    }

    // -------------------------------------------------------------------------
    // Child nameservers (glue records) — global /v2/hosts resource
    // -------------------------------------------------------------------------

    /**
     * List child (glue) nameservers registered under a domain.
     *
     * v2: the domain object carries `childHosts[]` (hostnames); each host's
     * addresses come from GET /v2/hosts/{hostname} (`addresses[].address`).
     * Verified vs HostBill getRegisterNameServers (class.realtime.php:503-516)
     * + get_hosts (api.php:289-307) and WiseCP CNSList (RealtimeRegister.php:567-605).
     */
    public function getChildNameservers(array $params)
    {
        $domain = $params['domain'] ?? '';

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $result = $this->sendApiRequest('domains/' . rawurlencode($domain));

        if ($result['status'] === 'error') {
            return $result;
        }

        $childHosts = $result['response']['childHosts'] ?? [];
        $records = [];

        foreach ($childHosts as $host) {
            $hostname = is_array($host) ? ($host['hostname'] ?? $host['host'] ?? '') : (string) $host;
            if ($hostname === '') {
                continue;
            }

            $ips = [];
            $hostResult = $this->sendApiRequest('hosts/' . rawurlencode($hostname));
            if ($hostResult['status'] === 'success') {
                foreach ($hostResult['response']['addresses'] ?? [] as $addr) {
                    $ip = is_array($addr) ? ($addr['address'] ?? '') : (string) $addr;
                    if ($ip !== '') {
                        $ips[] = $ip;
                    }
                }
            }

            $records[] = [
                'nameserver' => $hostname,
                'ips' => $ips,
            ];
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_get_child_ns_success'),
            'data' => [
                'domain' => $domain,
                'nameservers' => $records,
            ],
        ];
    }

    /**
     * Register a child (glue) nameserver.
     *
     * v2: POST /v2/hosts/{hostname} { addresses:[{ipVersion:V4|V6, address}] }.
     * Host is a GLOBAL resource (not domain-scoped). Verified vs WiseCP addCNS
     * (RealtimeRegister.php:607-630) + HostBill create_host (api.php:308-333).
     */
    public function addChildNameserver(array $params)
    {
        $domain = $params['domain'] ?? '';
        $hostname = $this->childHostname($params);

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        if ($hostname === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_child_ns'),
            ];
        }

        $addresses = $this->buildHostAddresses($params);
        if (empty($addresses)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_child_ns_ip'),
            ];
        }

        $result = $this->sendApiRequest('hosts/' . rawurlencode($hostname), 'POST', [
            'addresses' => $addresses,
        ]);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Child nameserver added', [
            'action' => 'add_child_ns',
            'domain' => $domain,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_add_child_ns_success'),
            'data' => [
                'domain' => $domain,
                'nameserver' => $hostname,
            ],
        ];
    }

    /**
     * Update a child (glue) nameserver's addresses.
     *
     * v2: POST /v2/hosts/{hostname}/update { addresses:[...] }. We read the current
     * addresses, drop the old IP, add the new one (HostBill update_host
     * api.php:334-367; WiseCP ModifyCNS is delete+add RealtimeRegister.php:632-643).
     */
    public function updateChildNameserver(array $params)
    {
        $domain = $params['domain'] ?? '';
        $hostname = $this->childHostname($params);

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        if ($hostname === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_child_ns'),
            ];
        }

        $newIps = $this->extractIps($params['ips'] ?? $params['ip'] ?? $params['new_ip'] ?? []);
        $oldIps = $this->extractIps($params['old_ip'] ?? $params['old_ips'] ?? []);

        // Read current addresses to compute the new set (drop old, add new).
        $current = [];
        $hostResult = $this->sendApiRequest('hosts/' . rawurlencode($hostname));
        if ($hostResult['status'] === 'success') {
            foreach ($hostResult['response']['addresses'] ?? [] as $addr) {
                $ip = is_array($addr) ? ($addr['address'] ?? '') : (string) $addr;
                if ($ip !== '' && ! in_array($ip, $oldIps, true) && ! in_array($ip, $newIps, true)) {
                    $current[] = $ip;
                }
            }
        }

        $addresses = $this->buildAddressList(array_merge($current, $newIps));
        if (empty($addresses)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_child_ns_ip'),
            ];
        }

        $result = $this->sendApiRequest('hosts/' . rawurlencode($hostname) . '/update', 'POST', [
            'addresses' => $addresses,
        ]);

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Child nameserver updated', [
            'action' => 'update_child_ns',
            'domain' => $domain,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_update_child_ns_success'),
            'data' => [
                'domain' => $domain,
                'nameserver' => $hostname,
            ],
        ];
    }

    /**
     * Delete a child (glue) nameserver.
     *
     * v2: DELETE /v2/hosts/{hostname} (WiseCP DeleteCNS RealtimeRegister.php:645-658,
     * HostBill delete_host api.php:368-377).
     */
    public function deleteChildNameserver(array $params)
    {
        $domain = $params['domain'] ?? '';
        $hostname = $this->childHostname($params);

        if (empty($domain)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        if ($hostname === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_child_ns'),
            ];
        }

        $result = $this->sendApiRequest('hosts/' . rawurlencode($hostname), 'DELETE');

        if ($result['status'] === 'error') {
            return $result;
        }

        Log::info('Realtimeregister: Child nameserver deleted', [
            'action' => 'delete_child_ns',
            'domain' => $domain,
        ]);

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_delete_child_ns_success'),
            'data' => [
                'domain' => $domain,
                'nameserver' => $hostname,
            ],
        ];
    }

    /**
     * Resolve the fully-qualified child nameserver hostname from params.
     */
    protected function childHostname(array $params): string
    {
        $host = $params['nameserver'] ?? $params['hostname'] ?? $params['child_nameserver'] ?? '';

        return strtolower(trim((string) $host));
    }

    /**
     * Normalize an IP list (array or comma-separated string) to a flat array.
     *
     * @return array<int,string>
     */
    protected function extractIps(mixed $ips): array
    {
        if (is_string($ips)) {
            $ips = explode(',', $ips);
        }

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

        return array_values(array_filter(array_map(
            static fn ($ip) => trim((string) $ip),
            $ips
        ), static fn ($ip) => $ip !== ''));
    }

    /**
     * Build the v2 host `addresses[]` block for the supplied IP params.
     *
     * @return array<int,array{ipVersion:string,address:string}>
     */
    protected function buildHostAddresses(array $params): array
    {
        return $this->buildAddressList(
            $this->extractIps($params['ips'] ?? $params['ip'] ?? [])
        );
    }

    /**
     * Map a flat IP list → v2 addresses[] (IPv4/IPv6 detected via filter_var).
     *
     * @return array<int,array{ipVersion:string,address:string}>
     */
    protected function buildAddressList(array $ips): array
    {
        $addresses = [];
        foreach ($ips as $ip) {
            $ip = trim((string) $ip);
            if ($ip === '') {
                continue;
            }
            $isV6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
            $addresses[] = [
                'ipVersion' => $isV6 ? 'V6' : 'V4',
                'address' => $ip,
            ];
        }

        return $addresses;
    }

    // -------------------------------------------------------------------------
    // Availability & suggestions
    // -------------------------------------------------------------------------

    /**
     * Check domain availability.
     *
     * v2: GET /v2/domains/{domain}/check → { available, premium, price, currency }
     * (WiseCP questioning RealtimeRegister.php:92-117, HostBill checkDomain
     * api.php:97-103 + lookupDomain class.realtime.php:851-863).
     */
    public function checkDomainAvailability(array $params)
    {
        $domains = $params['domains'] ?? $params['domain_names'] ?? [];
        if (! is_array($domains)) {
            $domains = array_filter([$domains]);
        }
        if (empty($domains) && ! empty($params['domain'])) {
            $domains = [$params['domain']];
        }

        if (empty($domains)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.realtimeregister_missing_domain'),
            ];
        }

        $results = [];
        foreach ($domains as $domainName) {
            $domainName = strtolower(trim((string) $domainName));
            if ($domainName === '') {
                continue;
            }

            $check = $this->sendApiRequest('domains/' . rawurlencode($domainName) . '/check');

            if ($check['status'] === 'error') {
                $results[] = [
                    'domain' => $domainName,
                    'available' => false,
                    'error' => $check['message'] ?? __('admin/modules.realtimeregister_unknown_error'),
                ];
                continue;
            }

            $resp = $check['response'] ?? [];
            $premium = (bool) ($resp['premium'] ?? false);

            $results[] = [
                'domain' => $domainName,
                'available' => (bool) ($resp['available'] ?? false),
                'premium' => $premium,
                'price' => $premium ? ($resp['price'] ?? null) : null,
                'currency' => $premium ? ($resp['currency'] ?? null) : null,
            ];
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.realtimeregister_check_availability_success'),
            'data' => [
                'results' => $results,
            ],
        ];
    }

    /**
     * Domain name suggestions.
     *
     * RTR exposes suggestions only through a separate ADAC WebSocket API
     * (wss://adac.api.yoursrs.com/ws) with its own credentials — outside the
     * v2 REST surface (HostBill suggestDomains class.realtime.php:865-890; WiseCP
     * has no suggestion method). Return a clear unsupported message rather than
     * fabricating a success.
     */
    public function checkDomainSuggestions(array $params)
    {
        return [
            'status' => 'error',
            'message' => __('admin/modules.realtimeregister_suggestions_unsupported'),
        ];
    }

    // -------------------------------------------------------------------------
    // v2 response parsing helpers
    // -------------------------------------------------------------------------

    /**
     * Parse a v2 date (ISO-8601 / date-time) to ISO-8601, or null.
     */
    protected function parseDate(mixed $value): ?string
    {
        if (empty($value)) {
            return null;
        }

        $ts = strtotime((string) $value);

        return $ts !== false ? date('c', $ts) : (string) $value;
    }

    /**
     * v2 `status` is an array of EPP-ish status strings; return the primary one.
     */
    protected function primaryStatus(array $response): string
    {
        $statuses = $response['status'] ?? [];
        if (is_string($statuses)) {
            return $statuses;
        }

        return is_array($statuses) ? ((string) ($statuses[0] ?? '')) : '';
    }

    /**
     * Map the v2 EPP status[] array to a lifecycle status token.
     *
     * RTR exposes EPP-ish statuses (OK/INACTIVE/EXPIRED/REDEMPTION_PERIOD/
     * PENDING_TRANSFER/...); downstream lifecycle expects active/expired/pending.
     * Verified vs WiseCP sync (PENDING_TRANSFER/EXPIRED/UNKNOWN→expired,
     * RealtimeRegister.php:706-709) + HostBill info_domain status buckets
     * (api.php:149-176: EXPIRED/REDEMPTION_PERIOD→Expired, OK/holds→Active).
     */
    protected function mapDomainStatus(array $response): string
    {
        $raw = $response['status'] ?? [];
        $statuses = array_map('strtoupper', is_array($raw) ? $raw : [$raw]);

        if (array_intersect(['EXPIRED', 'REDEMPTION_PERIOD', 'UNKNOWN'], $statuses)) {
            return 'expired';
        }

        if (array_intersect(['PENDING_TRANSFER'], $statuses)) {
            return 'pending';
        }

        return 'active';
    }

    /**
     * A domain is locked when it carries a CLIENT_TRANSFER_PROHIBITED status.
     */
    protected function isLocked(array $response): bool
    {
        $statuses = $response['status'] ?? [];
        $statuses = is_array($statuses) ? $statuses : [$statuses];

        return in_array('CLIENT_TRANSFER_PROHIBITED', $statuses, true);
    }

    /**
     * v2 `ns` is a flat array of hostnames.
     *
     * @return array<int,string>
     */
    protected function extractNameservers(array $response): array
    {
        $out = [];
        foreach ($response['ns'] ?? [] as $entry) {
            if (is_string($entry)) {
                $out[] = $entry;
            } elseif (is_array($entry)) {
                $out[] = $entry['host'] ?? $entry['hostname'] ?? '';
            }
        }

        return array_values(array_filter($out));
    }
}
