<?php

namespace App\Modules\Registrar\Enom;

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

class Enom extends AbstractRegistrar
{
    /**
     * eNom live API endpoint
     */
    protected const API_URL_LIVE = 'https://reseller.enom.com/interface.asp';

    /**
     * eNom test/sandbox API endpoint
     */
    protected const API_URL_TEST = 'https://resellertest.enom.com/interface.asp';

    /**
     * Contact types used by the eNom API
     */
    protected const CONTACT_TYPES = ['Registrant', 'Admin', 'Tech', 'AuxBilling'];

    // -------------------------------------------------------------------------
    // Helper / internal methods
    // -------------------------------------------------------------------------

    /**
     * Return the API base URL depending on test mode.
     */
    protected function getApiUrl(): string
    {
        $testMode = $this->config['test_mode'] ?? false;

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

    /**
     * Split a full domain name into SLD and TLD parts.
     *
     * @param  string  $domain  e.g. "example.co.uk"
     * @return array ['sld' => 'example', 'tld' => 'co.uk']
     */
    protected function splitDomain(string $domain): array
    {
        $parts = explode('.', $domain, 2);

        return [
            'sld' => $parts[0] ?? '',
            'tld' => $parts[1] ?? '',
        ];
    }

    /**
     * Execute an eNom API call via cURL.
     *
     * @param  string  $command  The eNom API command
     * @param  array   $params   Additional key-value pairs
     * @return array   Parsed XML response as associative array
     */
    protected function callEnomApi(string $command, array $params = []): array
    {
        $baseParams = [
            'uid'          => $this->config['username'] ?? '',
            'pw'           => $this->config['password'] ?? '',
            'command'      => $command,
            'ResponseType' => 'xml',
        ];

        $queryParams = array_merge($baseParams, $params);
        $url = $this->getApiUrl() . '?' . http_build_query($queryParams);

        Log::info('Enom: API request', [
            'command' => $command,
            'url'     => preg_replace('/pw=[^&]+/', 'pw=***', $url),
        ]);

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 120,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER     => ['Accept: text/xml'],
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            Log::error('Enom: cURL error', ['error' => $curlError, 'command' => $command]);

            return [
                'error'    => true,
                'message'  => $curlError,
                'ErrCount' => 1,
            ];
        }

        if ($httpCode !== 200) {
            Log::error('Enom: HTTP error', ['http_code' => $httpCode, 'command' => $command]);

            return [
                'error'    => true,
                'message'  => __('admin/modules.enom_http_error', ['code' => $httpCode]),
                'ErrCount' => 1,
            ];
        }

        return $this->parseXmlResponse($response);
    }

    /**
     * Parse the XML response from eNom into an associative array.
     */
    protected function parseXmlResponse(string $xml): array
    {
        $result = [];

        try {
            $xmlObj = @simplexml_load_string($xml);

            if ($xmlObj === false) {
                Log::error('Enom: Failed to parse XML response');

                return [
                    'error'    => true,
                    'message'  => __('admin/modules.enom_xml_parse_error'),
                    'ErrCount' => 1,
                ];
            }

            $result = $this->xmlToArray($xmlObj);

            // Preserve the raw XML so multi-record readers (getDNSRecords,
            // getNameservers, syncDomainTransfer, syncDomain/getDomainDetails)
            // can re-parse repeated/nested nodes with SimpleXML — the flat
            // xmlToArray() collapses repeated same-named elements to the last
            // occurrence, which is lossy for those responses.
            $result['__raw_xml'] = $xml;
        } catch (\Exception $e) {
            Log::error('Enom: XML parsing exception', ['exception' => $e->getMessage()]);

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

        return $result;
    }

    /**
     * Recursively convert a SimpleXMLElement to a flat associative array
     * that mirrors the key-value structure WHMCS uses (Values["key"]).
     */
    protected function xmlToArray(\SimpleXMLElement $xml): array
    {
        $result = [];

        foreach ($xml->children() as $key => $value) {
            $childCount = $value->count();

            if ($childCount > 0) {
                // Merge children into the flat array
                $childArray = $this->xmlToArray($value);
                foreach ($childArray as $ck => $cv) {
                    $result[$ck] = $cv;
                }
            } else {
                $result[$key] = (string) $value;
            }
        }

        return $result;
    }

    /**
     * Check the eNom response for errors.
     *
     * @return string|null  Error message or null if success
     */
    protected function getApiError(array $response): ?string
    {
        $errCount = (int) ($response['ErrCount'] ?? 0);

        if ($errCount > 0) {
            $errors = [];
            for ($i = 1; $i <= $errCount; $i++) {
                if (! empty($response['Err' . $i])) {
                    $errors[] = trim($response['Err' . $i]);
                }
            }

            return implode(', ', $errors) ?: __('admin/modules.enom_unknown_error');
        }

        return null;
    }

    // -------------------------------------------------------------------------
    // Pure multi-record XML / contact parsers (unit-testable, no network)
    // -------------------------------------------------------------------------

    /**
     * Load a raw eNom XML string as SimpleXML, or return null on failure.
     */
    protected function loadRawXml(?string $xml): ?\SimpleXMLElement
    {
        if (empty($xml)) {
            return null;
        }

        $obj = @simplexml_load_string($xml);

        return $obj === false ? null : $obj;
    }

    /**
     * Parse the repeated <host> elements of a `gethosts` XML response.
     *
     * eNom returns `HostRecordCount` + a repeated `<host>` element, each with
     * children hostid/name/type/address/ttl/mxpref. The flat xmlToArray()
     * collapses these to the last occurrence, so we parse the raw XML instead.
     *
     * @return array<int,array<string,mixed>>  DNS-record output shape
     */
    protected function parseHostRecords(?string $xml): array
    {
        $obj = $this->loadRawXml($xml);
        if ($obj === null || ! isset($obj->host)) {
            return [];
        }

        $records = [];
        foreach ($obj->host as $host) {
            $id = (string) ($host->hostid ?? '');

            $records[] = [
                'id'       => $id !== '' ? $id : (string) (count($records) + 1),
                'name'     => (string) ($host->name ?? ''),
                'type'     => (string) ($host->type ?? ''),
                'value'    => (string) ($host->address ?? ''),
                'ttl'      => (int) ((string) ($host->ttl ?? '') ?: 3600),
                'priority' => isset($host->mxpref) && (string) $host->mxpref !== ''
                    ? (string) $host->mxpref
                    : null,
            ];
        }

        return $records;
    }

    /**
     * Parse the repeated <dns> elements of a `getdns` XML response.
     *
     * @return array<int,string>  nameserver hostnames
     */
    protected function parseNameserversXml(?string $xml): array
    {
        $obj = $this->loadRawXml($xml);
        if ($obj === null || ! isset($obj->dns)) {
            return [];
        }

        $nameservers = [];
        foreach ($obj->dns as $ns) {
            $value = trim((string) $ns);
            if ($value !== '') {
                $nameservers[] = $value;
            }
        }

        return $nameservers;
    }

    /**
     * Parse nameservers from a `GetDomainInfo` XML response.
     *
     * This is the reference-proven, XML-safe path for reading a domain's
     * nameservers: BOTH WHMCS (enom_GetDomainInformation:1009-1020, XML mode)
     * AND WiseCP (eNom.php:605-622) read them from the repeated <dns> children
     * under the `dnsserver` service configuration:
     *
     *   GetDomainInfo->services->entry[@name="dnsserver"]->configuration->dns
     *
     * The standalone `getdns` command's XML shape is not reference-verified (in
     * WHMCS getdns is only ever used in name/value/text mode with NSCount +
     * DNS1..n), so getNameservers() drives off GetDomainInfo instead.
     *
     * @return array<int,string>  nameserver hostnames (in registry order)
     */
    protected function parseNameserversFromDomainInfo(?string $xml): array
    {
        $obj = $this->loadRawXml($xml);
        if ($obj === null || ! isset($obj->GetDomainInfo->services)) {
            return [];
        }

        $services = $obj->GetDomainInfo->services;

        // <services><entry name="dnsserver"><configuration><dns>...</dns></configuration></entry></services>
        $entries = isset($services->entry) ? $services->entry : $services->children();

        $nameservers = [];
        foreach ($entries as $entry) {
            $name = (string) ($entry['name'] ?? '');
            // Only the dnsserver service holds NS; skip others when named.
            if ($name !== '' && strtolower($name) !== 'dnsserver') {
                continue;
            }

            if (! isset($entry->configuration->dns)) {
                continue;
            }

            foreach ($entry->configuration->dns as $ns) {
                $value = trim((string) $ns);
                if ($value !== '') {
                    $nameservers[] = $value;
                }
            }
        }

        return $nameservers;
    }

    /**
     * Parse the nested <TransferOrder> status of a `TP_GetDetailsByDomain`
     * XML response.
     *
     * @return array{statusid:string,statusdesc:string}
     */
    protected function parseTransferStatus(?string $xml): array
    {
        $obj = $this->loadRawXml($xml);

        if ($obj === null || ! isset($obj->TransferOrder)) {
            return ['statusid' => '', 'statusdesc' => ''];
        }

        return [
            'statusid'   => (string) ($obj->TransferOrder->statusid ?? ''),
            'statusdesc' => (string) ($obj->TransferOrder->statusdesc ?? ''),
        ];
    }

    /**
     * Parse the transfer order id from a `TP_CreateOrder` XML response.
     *
     * eNom XML nests it under <transferorder><transferorderid> (lowercase) —
     * NOT a flat PascalCase `TransferOrderID` leaf (that only exists in WHMCS's
     * name/value ResponseType). WiseCP api.php:306 reads
     * $response->transferorder->transferorderid. We accept both casings.
     */
    protected function parseTransferOrderId(?string $xml): ?string
    {
        $obj = $this->loadRawXml($xml);
        if ($obj === null) {
            return null;
        }

        foreach (['transferorder', 'TransferOrder'] as $node) {
            if (isset($obj->{$node})) {
                foreach (['transferorderid', 'TransferOrderID', 'orderid', 'OrderID'] as $leaf) {
                    if (isset($obj->{$node}->{$leaf}) && (string) $obj->{$node}->{$leaf} !== '') {
                        return (string) $obj->{$node}->{$leaf};
                    }
                }
            }
        }

        return null;
    }

    /**
     * Parse a `GetNameSuggestions` XML response into suggestion rows.
     *
     * eNom returns a repeated <domain> element under <namespin><domains>, each
     * with children sld/tld/score/idn (and pricing when IncludePrice=true). The
     * flat xmlToArray() collapses these, so we parse the raw XML. Falls back to
     * numbered sld{i}/tld{i} leaves for name/value-style payloads.
     *
     * @return array<int,array{domain:string,sld:string,tld:string,score:?string,idn:bool}>
     */
    protected function parseNameSuggestions(?string $xml): array
    {
        $obj = $this->loadRawXml($xml);
        if ($obj === null) {
            return [];
        }

        $rows = [];

        // Preferred: repeated <domain> nodes (namespin/domains/domain).
        $domainNodes = null;
        if (isset($obj->namespin->domains->domain)) {
            $domainNodes = $obj->namespin->domains->domain;
        } elseif (isset($obj->domains->domain)) {
            $domainNodes = $obj->domains->domain;
        } elseif (isset($obj->domain)) {
            $domainNodes = $obj->domain;
        }

        if ($domainNodes !== null) {
            foreach ($domainNodes as $d) {
                $sld = (string) ($d->sld ?? '');
                $tld = (string) ($d->tld ?? '');
                if ($sld === '' && $tld === '') {
                    continue;
                }
                $rows[] = [
                    'domain' => strtolower($sld) . '.' . $tld,
                    'sld'    => strtolower($sld),
                    'tld'    => $tld,
                    'score'  => isset($d->score) ? (string) $d->score : null,
                    'idn'    => ((string) ($d->idn ?? '')) === 'true',
                ];
            }

            return $rows;
        }

        // Fallback: flat numbered leaves sld{i}/tld{i}/score{i}/idn{i}.
        $flat = $this->xmlToArray($obj);
        for ($i = 1; isset($flat['sld' . $i]); $i++) {
            $sld = (string) $flat['sld' . $i];
            $tld = (string) ($flat['tld' . $i] ?? '');
            $rows[] = [
                'domain' => strtolower($sld) . '.' . $tld,
                'sld'    => strtolower($sld),
                'tld'    => $tld,
                'score'  => isset($flat['score' . $i]) ? (string) $flat['score' . $i] : null,
                'idn'    => (($flat['idn' . $i] ?? '') === 'true'),
            ];
        }

        return $rows;
    }

    /**
     * Parse a `CheckNSStatus` XML response into a single glue-record row.
     *
     * eNom returns <CheckNsStatus><name>ns1.example.com</name><ipaddress>
     * 1.2.3.4</ipaddress></CheckNsStatus>. Some responses carry repeated
     * <ipaddress> children — all are collected.
     *
     * @return array{hostname:string,ips:array<int,string>}|null
     */
    protected function parseChildNameserverStatus(?string $xml): ?array
    {
        $obj = $this->loadRawXml($xml);
        if ($obj === null) {
            return null;
        }

        $node = null;
        foreach (['CheckNsStatus', 'CheckNSStatus', 'GetRegisteredNameServers'] as $candidate) {
            if (isset($obj->{$candidate})) {
                $node = $obj->{$candidate};
                break;
            }
        }
        if ($node === null) {
            // Some payloads put name/ipaddress at the top level.
            if (isset($obj->name) || isset($obj->ipaddress)) {
                $node = $obj;
            } else {
                return null;
            }
        }

        $hostname = (string) ($node->name ?? $node->NSName ?? '');
        if ($hostname === '') {
            return null;
        }

        $ips = [];
        if (isset($node->ipaddress)) {
            foreach ($node->ipaddress as $ip) {
                $val = trim((string) $ip);
                if ($val !== '') {
                    $ips[] = $val;
                }
            }
        }

        return ['hostname' => $hostname, 'ips' => $ips];
    }

    /**
     * Parse the nested expiration date from a `GetDomainInfo` XML response
     * (GetDomainInfo->status->expiration).
     */
    protected function parseDomainExpiration(?string $xml): string
    {
        $obj = $this->loadRawXml($xml);

        if ($obj === null || ! isset($obj->GetDomainInfo->status->expiration)) {
            return '';
        }

        return (string) $obj->GetDomainInfo->status->expiration;
    }

    /**
     * eNom tarih formatı "M/D/YYYY H:i:s" → kanonik "Y-m-d".
     * getDomainDetails + syncDomain aynı çıktıyı versin (tutarlılık).
     */
    protected function formatEnomDate(?string $raw): ?string
    {
        if (! $raw) {
            return null;
        }

        $dateOnly = explode(' ', trim($raw))[0] ?? '';
        $parts = explode('/', $dateOnly);
        if (count($parts) !== 3) {
            return null;
        }

        [$month, $day, $year] = $parts;
        if (! trim($year)) {
            return null;
        }

        return sprintf('%s-%s-%s', $year, str_pad($month, 2, '0', STR_PAD_LEFT), str_pad($day, 2, '0', STR_PAD_LEFT));
    }

    /**
     * Build the full Registrant/Admin/Tech/AuxBilling contact params for
     * register / transfer, sourced from the PRC-49 canonical contact groups.
     *
     * Registrant  ← registrant group
     * Admin/Tech  ← admin group (Tech falls back to admin)
     * AuxBilling  ← billing group (falls back to registrant)
     *
     * Each group is resolved through AbstractRegistrar::contact(), which
     * already applies per-field client_* fallback, so if a group array is
     * absent the flat client_* fields still populate the contact.
     *
     * @return array<string,string>  eNom {Prefix}FieldName => value
     */
    protected function buildContactParams(array $params): array
    {
        $groupMap = [
            'Registrant' => 'registrant',
            'Admin'      => 'admin',
            'Tech'       => 'tech',
            'AuxBilling' => 'billing',
        ];

        $out = [];

        foreach ($groupMap as $prefix => $group) {
            $c = $this->contact($params, $group);

            // AuxBilling / Tech / Admin fall back to registrant when the
            // dedicated group is empty (no email = effectively unpopulated).
            if ($group !== 'registrant' && ($c['email'] ?? '') === '') {
                $c = $this->contact($params, 'registrant');
            }

            $jobTitle = ! empty($c['company']) ? 'Director' : '';
            $country  = $c['country_code'] ?? '';

            $out[$prefix . 'FirstName']        = $c['first_name'] ?? '';
            $out[$prefix . 'LastName']         = $c['last_name'] ?? '';
            $out[$prefix . 'OrganizationName'] = $c['company'] ?? '';
            $out[$prefix . 'JobTitle']         = $jobTitle;
            $out[$prefix . 'Address1']         = $c['address1'] ?? '';
            $out[$prefix . 'Address2']         = $c['address2'] ?? '';
            $out[$prefix . 'City']             = $c['city'] ?? '';
            $out[$prefix . 'StateProvince']    = $c['state'] ?? '';
            $out[$prefix . 'PostalCode']       = $c['postcode'] ?? '';
            $out[$prefix . 'Country']          = $country;
            $out[$prefix . 'EmailAddress']     = $c['email'] ?? '';
            // Phone must be E.164-like '+{cc}.{number}' or some TLDs reject it
            // (WHMCS enom_SaveContactDetails:559-564, WiseCP whois_process).
            $out[$prefix . 'Phone']            = $this->formatPhone($c['phone'] ?? '', $country);

            // eNom uses a US state picklist ('S') vs free-text province ('P').
            $out[$prefix . 'StateProvinceChoice'] = ($country === 'US') ? 'S' : 'P';
        }

        return $out;
    }

    /**
     * Normalise a phone number into eNom's '+{callingcode}.{number}' shape.
     *
     * WHMCS (enom_SaveContactDetails:559-564) prefixes '+{callingcode}.' from a
     * country->callingcode lookup when the number does not already start with
     * '+'. PRC-49 forbids DB queries here, so we derive the calling code from
     * the canonical country_code via a self-contained ISO2→dial-code map, and
     * accept numbers already in '+cc.number' form untouched.
     */
    protected function formatPhone(string $phone, string $countryCode = ''): string
    {
        $phone = trim($phone);
        if ($phone === '') {
            return '';
        }

        // Already in the eNom '+cc.number' form → leave as-is.
        if (preg_match('/^\+\d{1,4}\.\d+$/', $phone)) {
            return $phone;
        }

        $cc = $this->callingCodeForCountry($countryCode);
        $digits = preg_replace('/\D+/', '', $phone);

        if ($digits === '') {
            return $phone;
        }

        // If the number already begins with the country calling code, split it
        // out; otherwise prefix the derived calling code.
        if ($cc !== '' && str_starts_with($phone, '+')) {
            if (str_starts_with($digits, $cc)) {
                $rest = substr($digits, strlen($cc));

                return '+' . $cc . '.' . $rest;
            }
        }

        if ($cc === '') {
            // No mapping — best-effort: keep raw digits (no calling code).
            return $digits;
        }

        // Strip a leading duplicate calling code if the raw number carried it.
        if (str_starts_with($digits, $cc)) {
            $digits = substr($digits, strlen($cc));
        }

        return '+' . $cc . '.' . $digits;
    }

    /**
     * Self-contained ISO-2 country → E.164 calling code map (PRC-49: no DB).
     * Covers the common registrar markets; unknown codes return '' so the raw
     * number is passed through unchanged.
     */
    protected function callingCodeForCountry(string $countryCode): string
    {
        $map = [
            'US' => '1', 'CA' => '1', 'GB' => '44', 'UK' => '44', 'TR' => '90',
            'DE' => '49', 'FR' => '33', 'NL' => '31', 'BE' => '32', 'ES' => '34',
            'IT' => '39', 'PT' => '351', 'IE' => '353', 'CH' => '41', 'AT' => '43',
            'SE' => '46', 'NO' => '47', 'DK' => '45', 'FI' => '358', 'PL' => '48',
            'CZ' => '420', 'SK' => '421', 'HU' => '36', 'RO' => '40', 'GR' => '30',
            'BG' => '359', 'RU' => '7', 'UA' => '380', 'AU' => '61', 'NZ' => '64',
            'JP' => '81', 'CN' => '86', 'IN' => '91', 'SG' => '65', 'HK' => '852',
            'KR' => '82', 'MY' => '60', 'ID' => '62', 'TH' => '66', 'PH' => '63',
            'VN' => '84', 'AE' => '971', 'SA' => '966', 'IL' => '972', 'ZA' => '27',
            'EG' => '20', 'NG' => '234', 'BR' => '55', 'AR' => '54', 'MX' => '52',
            'CL' => '56', 'CO' => '57', 'PE' => '51',
        ];

        return $map[strtoupper($countryCode)] ?? '';
    }

    /**
     * Map the builder's TLD-specific `additional_fields` (client_domain_fields,
     * WHMCS `additionalfields` human-readable labels) to eNom's lowercase extra
     * params for restricted TLDs (.us/.uk/.ca/.eu/.de/.it/.nl).
     *
     * Field-by-field port of WHMCS enom_populate_additional_fields
     * (enom.php:1108-1300+). Used by register + transfer so restricted TLDs are
     * not rejected by the registry.
     *
     * @param  string  $tld  full tld segment (e.g. 'co.uk', 'us')
     * @param  array<string,string>  $af  additional_fields map
     * @return array<string,string>  eNom extra params
     */
    protected function populateAdditionalFields(string $tld, array $af): array
    {
        $last = strtolower(trim($tld));
        // eNom keys on the last TLD segment (getLastTLDSegment): co.uk → uk.
        if (str_contains($last, '.')) {
            $parts = explode('.', $last);
            $last = end($parts);
        }

        $out = [];

        switch ($last) {
            case 'us':
                $nexus = $af['Nexus Category'] ?? '';
                $countryCode = $af['Nexus Country'] ?? '';
                $out['us_purpose'] = match ($af['Application Purpose'] ?? '') {
                    'Business use for profit' => 'P1',
                    'Non-profit business', 'Club', 'Association', 'Religious Organization' => 'P2',
                    'Personal Use', 'Educational purposes' => 'P4',
                    'Government purposes' => 'P5',
                    default => 'P3',
                };
                if (in_array($nexus, ['C11', 'C12', 'C21'], true)) {
                    $out['us_nexus'] = $nexus;
                } elseif (in_array($nexus, ['C31', 'C32'], true)) {
                    $out['us_nexus'] = $nexus;
                    $out['global_cc_us'] = $countryCode;
                }
                break;

            case 'uk':
                $ukLegalType = match ($af['Legal Type'] ?? '') {
                    'UK Limited Company' => 'LTD',
                    'UK Public Limited Company' => 'PLC',
                    'UK Partnership' => 'PTNR',
                    'UK Limited Liability Partnership' => 'LLP',
                    'Sole Trader' => 'STRA',
                    'UK Registered Charity' => 'RCHAR',
                    'UK Industrial/Provident Registered Company' => 'IP',
                    'UK School' => 'SCH',
                    'UK Government Body' => 'GOV',
                    'UK Corporation by Royal Charter' => 'CRC',
                    'UK Statutory Body' => 'STAT',
                    'Non-UK Individual' => 'FIND',
                    'Foreign Organization' => 'FCORP',
                    'Other foreign organizations' => 'FOTHER',
                    default => 'IND',
                };
                $out['uk_legal_type'] = $ukLegalType;
                $out['uk_reg_co_no'] = strtoupper($af['Company ID Number'] ?? '');
                $out['registered_for'] = $af['Registrant Name'] ?? '';
                $out['uk_reg_opt_out'] = (! empty($af['WHOIS Opt-out']) && $ukLegalType === 'IND') ? 'y' : 'n';
                break;

            case 'ca':
                $legalType = match ($af['Legal Type'] ?? '') {
                    'Corporation' => 'CCO',
                    'Canadian Citizen' => 'CCT',
                    'Permanent Resident of Canada' => 'RES',
                    'Government' => 'GOV',
                    'Canadian Educational Institution' => 'EDU',
                    'Canadian Unincorporated Association' => 'ASS',
                    'Canadian Hospital' => 'HOP',
                    'Partnership Registered in Canada' => 'PRT',
                    'Trade-mark registered in Canada' => 'TDM',
                    default => 'CCO',
                };
                $whoisOptOut = (! empty($af['WHOIS Opt-out']) && in_array($legalType, ['CCT', 'RES'], true))
                    ? 'PRIVATE' : 'FULL';
                $ciraAgreement = ! empty($af['CIRA Agreement']) ? 'Y' : 'N';
                $out['cira_legal_type'] = $legalType;
                $out['cira_whois_display'] = $whoisOptOut;
                $out['cira_language'] = 'en';
                $out['cira_agreement_version'] = '2.0';
                $out['cira_agreement_value'] = $ciraAgreement;
                break;

            case 'eu':
                $out['eu_whoispolicy'] = 'I AGREE';
                $out['eu_agreedelete'] = 'YES';
                $out['eu_adr_lang'] = 'EN';
                break;

            case 'de':
                $out['confirmaddress'] = 'DE';
                $out['de_agreedelete'] = 'YES';
                break;

            case 'nl':
                $out['nl_agreedelete'] = 'YES';
                break;

            case 'it':
                $out['it_consentforpublishing'] = ($af['Publish Personal Data'] ?? '') === 'on' ? '1' : '0';
                $out['it_agreedelete'] = 'YES';
                $out['it_entity_type'] = match ($af['Legal Type'] ?? '') {
                    'Italian and foreign natural persons' => '1',
                    'Companies/one man companies' => '2',
                    'Freelance workers/professionals' => '3',
                    'non-profit organizations' => '4',
                    'public organizations' => '5',
                    'other subjects' => '6',
                    'non natural foreigners' => '7',
                    default => ! empty($af['Company ID Number']) ? '2' : '1',
                };
                break;
        }

        return $out;
    }

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

    /**
     * Declare the TLD-specific extra-field schema for restricted TLDs so the
     * core order/basket flow collects them into client_domain_fields (which the
     * builder emits as `additional_fields`, consumed by populateAdditionalFields).
     * Labels match the WHMCS `additionalfields` human-readable keys.
     */
    public function getExtraFields(string $tld): array
    {
        $t = strtolower(ltrim($tld, '.'));
        // eNom keys on the last segment (co.uk → uk).
        $last = str_contains($t, '.') ? (explode('.', $t)[array_key_last(explode('.', $t))]) : $t;

        return match ($last) {
            'us' => [
                ['name' => 'Nexus Category', 'type' => 'text', 'required' => true],
                ['name' => 'Nexus Country', 'type' => 'text', 'required' => false],
                ['name' => 'Application Purpose', 'type' => 'dropdown', 'required' => true,
                    'options' => 'Business use for profit,Non-profit business,Club,Association,Religious Organization,Educational purposes,Government purposes,Personal Use'],
            ],
            'uk' => [
                ['name' => 'Legal Type', 'type' => 'text', 'required' => false],
                ['name' => 'Company ID Number', 'type' => 'text', 'required' => false],
                ['name' => 'Registrant Name', 'type' => 'text', 'required' => false],
                ['name' => 'WHOIS Opt-out', 'type' => 'tickbox', 'required' => false],
            ],
            'ca' => [
                ['name' => 'Legal Type', 'type' => 'text', 'required' => true],
                ['name' => 'CIRA Agreement', 'type' => 'tickbox', 'required' => true],
                ['name' => 'WHOIS Opt-out', 'type' => 'tickbox', 'required' => false],
            ],
            'it' => [
                ['name' => 'Legal Type', 'type' => 'text', 'required' => false],
                ['name' => 'Publish Personal Data', 'type' => 'tickbox', 'required' => false],
            ],
            default => [],
        };
    }

    /**
     * Register a new domain at eNom.
     */
    public function registerDomain(array $params)
    {
        $requiredParams = ['domain', 'period'];

        foreach ($requiredParams as $param) {
            if (empty($params[$param])) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_missing_param', ['param' => $param]),
                ];
            }
        }

        $domainParts = $this->splitDomain($params['domain']);

        // Build registration parameters
        $apiParams = [
            'tld'         => $domainParts['tld'],
            'sld'         => $domainParts['sld'],
            'numyears'    => $params['period'],
            'IgnoreNSFail' => 'Yes',
            'EmailNotify' => '1',
        ];

        // Use default eNom nameservers or custom ones
        $useDefaultNs = $this->config['default_nameservers'] ?? false;

        if ($useDefaultNs) {
            $apiParams['UseDNS'] = 'default';
        } else {
            $nameservers = $params['nameservers'] ?? [];
            foreach ($nameservers as $index => $ns) {
                $apiParams['NS' . ($index + 1)] = $ns;
            }
        }

        // Contact information
        $this->addContactParams($apiParams, $params);

        // TLD-specific extra fields (.us/.uk/.ca/.eu/.de/.it/.nl). Without these
        // restricted TLD registrations are rejected by the registry.
        $apiParams = array_merge(
            $apiParams,
            $this->populateAdditionalFields($domainParts['tld'], $params['additional_fields'] ?? [])
        );

        $response = $this->callEnomApi('purchase', $apiParams);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Domain registration failed', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // Check RRPCode for additional errors
        if (isset($response['RRPCode']) && $response['RRPCode'] !== '200') {
            $rrpError = $response['RRPText'] ?? __('admin/modules.enom_registration_failed');

            Log::error('Enom: Domain registration RRP error', [
                'domain'   => $params['domain'],
                'rrp_code' => $response['RRPCode'],
                'rrp_text' => $rrpError,
            ]);

            return [
                'status'  => 'error',
                'message' => $rrpError,
            ];
        }

        // Enable ID protection if requested
        if (! empty($params['id_protection'])) {
            $this->enableIdProtection($domainParts, $params['period']);
        }

        Log::info('Enom: Domain registered successfully', [
            'domain' => $params['domain'],
            'period' => $params['period'],
        ]);

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_register_success'),
            'data'    => [
                'domain'     => $params['domain'],
                'period'     => $params['period'],
                'registrar'  => 'enom',
                'order_id'   => $response['OrderID'] ?? null,
            ],
        ];
    }

    /**
     * Transfer a domain to eNom.
     */
    public function transferDomain(array $params)
    {
        $requiredParams = ['domain', 'epp_code'];

        foreach ($requiredParams as $param) {
            if (empty($params[$param])) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_missing_param', ['param' => $param]),
                ];
            }
        }

        $domainParts = $this->splitDomain($params['domain']);
        $period = $params['period'] ?? 1;

        $apiParams = [
            'DomainCount'     => '1',
            'OrderType'       => 'Autoverification',
            'TLD1'            => $domainParts['tld'],
            'SLD1'            => $domainParts['sld'],
            'AuthInfo1'       => $params['epp_code'],
            'UseContacts'     => '0',
            'Lock'            => '1',
            'EmailNotify'     => '1',
        ];

        // Contact information
        $this->addContactParams($apiParams, $params);

        // TLD-specific extra fields — WHMCS enom_TransferDomain also populates
        // these (enom.php:420-424); restricted TLD transfers need them.
        $apiParams = array_merge(
            $apiParams,
            $this->populateAdditionalFields($domainParts['tld'], $params['additional_fields'] ?? [])
        );

        $response = $this->callEnomApi('TP_CreateOrder', $apiParams);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Domain transfer failed', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // eNom's XML TP_CreateOrder nests the id under
        // <transferorder><transferorderid> (lowercase) — the flat PascalCase
        // 'TransferOrderID' leaf only exists in WHMCS's name/value mode, so read
        // the raw XML instead (WiseCP api.php:306).
        $transferId = $this->parseTransferOrderId($response['__raw_xml'] ?? null);

        Log::info('Enom: Domain transfer initiated', [
            'domain'      => $params['domain'],
            'transfer_id' => $transferId,
        ]);

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_transfer_success'),
            'data'    => [
                'domain'      => $params['domain'],
                'transfer_id' => $transferId,
                'period'      => $period,
                'registrar'   => 'enom',
            ],
        ];
    }

    /**
     * Renew a domain at eNom.
     *
     * eNom uses different commands depending on domain status:
     *  - Registered -> "extend"
     *  - Expired    -> "UpdateExpiredDomains"
     *  - Otherwise  -> "Extend_RGP" (redemption grace period)
     */
    public function renewDomain(array $params)
    {
        $requiredParams = ['domain', 'period'];

        foreach ($requiredParams as $param) {
            if (empty($params[$param])) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_missing_param', ['param' => $param]),
                ];
            }
        }

        $domainParts = $this->splitDomain($params['domain']);

        // First, get the domain status to determine the correct renewal command
        $statusResponse = $this->callEnomApi('GetDomainInfo', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $statusError = $this->getApiError($statusResponse);

        if ($statusError) {
            Log::error('Enom: Failed to get domain status for renewal', [
                'domain' => $params['domain'],
                'error'  => $statusError,
            ]);

            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_renew_status_error'),
            ];
        }

        $registrationStatus = $statusResponse['registrationstatus'] ?? '';

        // Select the appropriate renewal command
        if (stripos($registrationStatus, 'Registered') !== false) {
            $renewCommand = 'extend';
            $renewParams = [
                'tld'      => $domainParts['tld'],
                'sld'      => $domainParts['sld'],
                'NumYears' => $params['period'],
            ];
        } elseif (stripos($registrationStatus, 'Expired') !== false) {
            $renewCommand = 'UpdateExpiredDomains';
            $renewParams = [
                'DomainName' => $domainParts['sld'] . '.' . $domainParts['tld'],
                'NumYears'   => $params['period'],
            ];
        } else {
            $renewCommand = 'Extend_RGP';
            $renewParams = [
                'tld' => $domainParts['tld'],
                'sld' => $domainParts['sld'],
            ];
        }

        $response = $this->callEnomApi($renewCommand, $renewParams);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Domain renewal failed', [
                'domain'  => $params['domain'],
                'command' => $renewCommand,
                'error'   => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // Renew ID protection if active
        if (! empty($params['id_protection'])) {
            $this->renewIdProtection($domainParts, $params['period']);
        }

        Log::info('Enom: Domain renewed successfully', [
            'domain'  => $params['domain'],
            'period'  => $params['period'],
            'command' => $renewCommand,
        ]);

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_renew_success'),
            'data'    => [
                'domain'    => $params['domain'],
                'period'    => $params['period'],
                'registrar' => 'enom',
            ],
        ];
    }

    /**
     * Update nameservers for a domain.
     */
    public function setNameservers(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

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

        $domainParts = $this->splitDomain($params['domain']);

        $apiParams = [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ];

        // eNom expects NS1..NS5
        foreach ($params['nameservers'] as $index => $ns) {
            $apiParams['NS' . ($index + 1)] = $ns;
        }

        $response = $this->callEnomApi('modifyns', $apiParams);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to update nameservers', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        Log::info('Enom: Nameservers updated', [
            'domain'      => $params['domain'],
            'nameservers' => $params['nameservers'],
        ]);

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

    /**
     * Get nameservers for a domain.
     */
    public function getNameservers(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        // Read nameservers from GetDomainInfo — the reference-proven XML path
        // (WHMCS enom_GetDomainInformation + WiseCP both read the repeated <dns>
        // nodes under services->entry[dnsserver]->configuration->dns). The
        // standalone `getdns` command's XML shape is NOT reference-verified, so
        // we do NOT rely on it (A3).
        $response = $this->callEnomApi('GetDomainInfo', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to get nameservers', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        $nameservers = $this->parseNameserversFromDomainInfo($response['__raw_xml'] ?? null);

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

    /**
     * Update contact details for a domain.
     *
     * Expects params['groups'] with keys Registrant, Admin, Tech.
     */
    public function setContactDetails(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

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

        $domainParts = $this->splitDomain($params['domain']);

        $apiParams = [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ];

        // Map procp contact field names to eNom parameter names
        $fieldMapping = [
            'firstname'    => 'FirstName',
            'lastname'     => 'LastName',
            'company'      => 'OrganizationName',
            'job_title'    => 'JobTitle',
            'email'        => 'EmailAddress',
            'address1'     => 'Address1',
            'address2'     => 'Address2',
            'city'         => 'City',
            'state'        => 'StateProvince',
            'postal_code'  => 'PostalCode',
            'country'      => 'Country',
            'phone'        => 'Phone',
            'fax'          => 'Fax',
        ];

        // Include AuxBilling so a supplied Billing group is written (B2).
        // Accept both the eNom PascalCase group names (Registrant/Admin/Tech/
        // AuxBilling) AND the procp canonical group names (registrant/admin/
        // tech/billing) so callers using either convention still write contacts.
        $groupAliases = [
            'Registrant' => ['Registrant', 'registrant'],
            'Admin'      => ['Admin', 'admin'],
            'Tech'       => ['Tech', 'tech'],
            'AuxBilling' => ['AuxBilling', 'billing', 'Billing'],
        ];

        foreach ($groupAliases as $contactType => $aliases) {
            $groupData = [];
            foreach ($aliases as $alias) {
                if (! empty($params['groups'][$alias]) && is_array($params['groups'][$alias])) {
                    $groupData = $params['groups'][$alias];
                    break;
                }
            }

            // Skip groups not supplied by the caller (avoids blanking them).
            if (empty($groupData)) {
                continue;
            }

            foreach ($fieldMapping as $procpField => $enomField) {
                if (isset($groupData[$procpField])) {
                    $value = $groupData[$procpField];

                    // Phone → '+{cc}.{number}' (WHMCS enom_SaveContactDetails:559-564).
                    if ($procpField === 'phone') {
                        $value = $this->formatPhone((string) $value, $groupData['country'] ?? '');
                    }

                    $apiParams[$contactType . $enomField] = $value;
                }
            }

            // Handle state/province choice based on country
            $country = $groupData['country'] ?? '';
            $apiParams[$contactType . 'StateProvinceChoice'] = ($country === 'US') ? 'S' : 'P';
        }

        $response = $this->callEnomApi('contacts', $apiParams);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to update contacts', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        Log::info('Enom: Contact details updated', ['domain' => $params['domain']]);

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

    /**
     * Get contact details for a domain.
     */
    public function getContactDetails(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('getcontacts', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to get contacts', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        $groups = [];
        $contactTypes = ['Registrant', 'Admin', 'Tech'];

        foreach ($contactTypes as $contactType) {
            // Check if registrant is updatable
            if ($contactType === 'Registrant') {
                $updatable = $response['RegistrantUpdatable'] ?? 'True';
                if ($updatable === 'False' || $updatable === false) {
                    continue;
                }
            }

            $groups[$contactType] = [
                'firstname'   => $response[$contactType . 'FirstName'] ?? '',
                'lastname'    => $response[$contactType . 'LastName'] ?? '',
                'company'     => $response[$contactType . 'OrganizationName'] ?? '',
                'job_title'   => $response[$contactType . 'JobTitle'] ?? '',
                'email'       => $response[$contactType . 'EmailAddress'] ?? '',
                'address1'    => $response[$contactType . 'Address1'] ?? '',
                'address2'    => $response[$contactType . 'Address2'] ?? '',
                'city'        => $response[$contactType . 'City'] ?? '',
                'state'       => $response[$contactType . 'StateProvince'] ?? '',
                'postal_code' => $response[$contactType . 'PostalCode'] ?? '',
                'country'     => $response[$contactType . 'Country'] ?? '',
                'phone'       => $response[$contactType . 'Phone'] ?? '',
                'fax'         => $response[$contactType . 'Fax'] ?? '',
            ];
        }

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

    // -------------------------------------------------------------------------
    // Optional methods – overriding AbstractRegistrar defaults
    // -------------------------------------------------------------------------

    /**
     * Get domain lock (registrar lock) status.
     */
    public function getDomainLock(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('getreglock', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        $isLockable = ($response['IsLockable'] ?? '') === 'True';
        $locked = false;

        if ($isLockable) {
            $locked = ($response['RegLock'] ?? '0') === '1';
        }

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_lock_status_retrieved'),
            'data'    => [
                'domain'     => $params['domain'],
                'locked'     => $locked,
                'is_lockable' => $isLockable,
            ],
        ];
    }

    /**
     * Enable or disable the registrar lock on a domain.
     */
    public function updateDomainLock(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        // eNom uses UnlockRegistrar: 0 = lock, 1 = unlock
        $lockStatus = ! empty($params['lock_status']) ? '0' : '1';

        $response = $this->callEnomApi('setreglock', [
            'tld'              => $domainParts['tld'],
            'sld'              => $domainParts['sld'],
            'UnlockRegistrar'  => $lockStatus,
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to update domain lock', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        Log::info('Enom: Domain lock updated', [
            'domain' => $params['domain'],
            'locked' => ! empty($params['lock_status']),
        ]);

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_lock_updated'),
            'data'    => [
                'domain'      => $params['domain'],
                'lock_status' => ! empty($params['lock_status']),
            ],
        ];
    }

    /**
     * Request the EPP / auth code for a domain.
     *
     * eNom sends the EPP code via email.
     */
    public function getEppCode(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('SynchAuthInfo', [
            'tld'              => $domainParts['tld'],
            'sld'              => $domainParts['sld'],
            'EmailEPP'         => 'True',
            'RunSynchAutoInfo' => 'True',
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to get EPP code', [
                'domain' => $params['domain'],
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        Log::info('Enom: EPP code requested', ['domain' => $params['domain']]);

        // SynchAuthInfo (EmailEPP=True) EMAILS the auth code to the registrant;
        // it does NOT return the code in the response (WHMCS enom_GetEPPCode
        // returns only an error). Reporting an epp_code here would always be
        // null and misleading, so we expose delivery state instead.
        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_epp_sent'),
            'data'    => [
                'domain'        => $params['domain'],
                'epp_code'      => null,
                'sent_by_email' => true,
            ],
        ];
    }

    /**
     * Retrieve detailed information about a domain.
     */
    public function getDomainDetails(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('GetDomainInfo', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        $status = $response['registrationstatus'] ?? 'unknown';

        // Expiration is nested under GetDomainInfo->status->expiration (A4);
        // the flat ExpirationDate/CreatedDate leaves do not exist in XML.
        $expiresAt = $this->formatEnomDate($this->parseDomainExpiration($response['__raw_xml'] ?? null));

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_domain_details_retrieved'),
            'data'    => [
                'domain'              => $params['domain'],
                'status'              => strtolower($status),
                'registration_status' => $status,
                'expires_at'          => $expiresAt,
                'created_at'          => null,
                'registrar'           => 'eNom',
            ],
        ];
    }

    /**
     * Check if a domain is available for registration.
     */
    public function checkDomainAvailability(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('check', [
            'tld'          => $domainParts['tld'],
            'sld'          => $domainParts['sld'],
            'IncludePrice' => 'true',
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // RRPCode 210 = available, 211 = not available
        $rrpCode = $response['RRPCode'] ?? '';
        $available = ($rrpCode == '210');

        $data = [
            'domain'    => $params['domain'],
            'available' => $available,
            'rrp_code'  => $rrpCode,
        ];

        // PRC-295 Phase A — premium domain pricing (single registration fee).
        // Renewal/transfer pricing are deliberately ignored — premium is a
        // one-time fee, follow-on years use the standard TLD price.
        if (($response['IsPremiumName'] ?? '') === 'true') {
            $register = isset($response['PremiumPrice']) && $response['PremiumPrice'] !== ''
                ? (float) $response['PremiumPrice']
                : null;

            $data['premium'] = true;
            $data['premium_register_price'] = $register;
            $data['premium_currency'] = 'USD';
        }

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

    /**
     * Suggest available domain names for a search term.
     *
     * eNom supports this via the GetNameSuggestions command (WHMCS
     * enom_GetDomainSuggestions:79-156). SpinType=0, MaxResults, and an
     * OnlyTldList built from the caller's TLD list.
     */
    public function checkDomainSuggestions(array $params)
    {
        $searchTerm = $params['keyword'] ?? $params['search_term'] ?? $params['sld'] ?? '';
        if ($searchTerm === '' && ! empty($params['domain'])) {
            $searchTerm = $this->splitDomain($params['domain'])['sld'];
        }

        if ($searchTerm === '') {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'keyword']),
            ];
        }

        $tlds = $params['tlds'] ?? [];
        if (! is_array($tlds)) {
            $tlds = array_filter(array_map('trim', explode(',', (string) $tlds)));
        }
        // Normalise to the leading-dot-free form eNom expects (com, net, ...).
        $tlds = array_map(fn ($t) => ltrim(strtolower((string) $t), '.'), $tlds);

        $maxResults = (int) ($params['max_results'] ?? 20);
        if ($maxResults < 1) {
            $maxResults = 20;
        }

        $apiParams = [
            'SearchTerm'   => $searchTerm,
            'OnlyTldList'  => implode(',', $tlds),
            'ExclueTldList' => '',
            'MaxResults'   => $maxResults,
            'SpinType'     => '0',
            'Adult'        => 'False',
            'AllGA'        => 'False',
            'IncludePrice' => 'true',
        ];

        $response = $this->callEnomApi('GetNameSuggestions', $apiParams);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        $suggestions = $this->parseNameSuggestions($response['__raw_xml'] ?? null);

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_suggestions_retrieved'),
            'data'    => [
                'search_term' => $searchTerm,
                'suggestions' => $suggestions,
            ],
        ];
    }

    /**
     * Synchronise local domain data with eNom.
     */
    public function syncDomain(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        // GetDomainInfo returns the expiration nested under
        // GetDomainInfo->status->expiration in XML (A4). GetDomainExp's flat
        // ExpirationDate leaf never survives the flattener.
        $response = $this->callEnomApi('GetDomainInfo', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            // "Domain name not found" means it was transferred away
            if (stripos($error, 'Domain name not found') !== false) {
                return [
                    'status'  => 'success',
                    'message' => __('admin/modules.enom_sync_transferred_away'),
                    'data'    => [
                        'domain'           => $params['domain'],
                        'status'           => 'transferred_away',
                        'transferred_away' => true,
                    ],
                ];
            }

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // Expiration is nested under GetDomainInfo->status->expiration (A4).
        $expiryDate = $this->parseDomainExpiration($response['__raw_xml'] ?? null);
        $formattedExpiry = $this->formatEnomDate($expiryDate);
        $domainStatus = $formattedExpiry !== null ? 'active' : 'unknown';

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_sync_success'),
            'data'    => [
                'domain'     => $params['domain'],
                'status'     => $domainStatus,
                'expires_at' => $formattedExpiry,
            ],
        ];
    }

    /**
     * Synchronise an in-progress domain transfer.
     */
    public function syncDomainTransfer(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('TP_GetDetailsByDomain', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // Cancelled / failed status IDs
        $cancelledIds = [
            '2', '4', '6', '7', '8', '15', '16', '17', '18', '19', '20',
            '21', '22', '23', '24', '25', '26', '27', '30', '31', '32',
            '33', '34', '36', '37', '45',
        ];

        // Pending status IDs
        $pendingIds = [
            '0', '1', '3', '9', '10', '11', '12', '13', '14', '28', '29', '35',
        ];

        // eNom's XML `TP_GetDetailsByDomain` nests status under
        // <TransferOrder><statusid>/<statusdesc> — NOT top-level
        // ordercount/statusid{n} leaves (A2).
        $transferStatus = $this->parseTransferStatus($response['__raw_xml'] ?? null);
        $statusId = $transferStatus['statusid'];
        $statusDesc = $transferStatus['statusdesc'];

        if ($statusId === '5') {
            // Transfer completed
            return [
                'status'  => 'success',
                'message' => __('admin/modules.enom_transfer_completed'),
                'data'    => [
                    'domain'    => $params['domain'],
                    'completed' => true,
                ],
            ];
        }

        if (in_array($statusId, $cancelledIds, true)) {
            return [
                'status'  => 'success',
                'message' => __('admin/modules.enom_transfer_failed'),
                'data'    => [
                    'domain' => $params['domain'],
                    'failed' => true,
                    'reason' => $statusDesc,
                ],
            ];
        }

        if (in_array($statusId, $pendingIds, true)) {
            return [
                'status'  => 'success',
                'message' => __('admin/modules.enom_transfer_pending'),
                'data'    => [
                    'domain'           => $params['domain'],
                    'pending_transfer' => true,
                    'reason'           => $statusDesc,
                ],
            ];
        }

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_transfer_unknown_status'),
            'data'    => [
                'domain'    => $params['domain'],
                'status_id' => $statusId,
                'reason'    => $statusDesc,
            ],
        ];
    }

    // -------------------------------------------------------------------------
    // DNS Management
    // -------------------------------------------------------------------------

    /**
     * Get DNS host records for a domain.
     */
    public function getDNSRecords(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('gethosts', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        // eNom's XML `gethosts` response returns repeated <host> elements
        // (HostRecordCount), NOT numbered HostName1..n leaves — parse the raw
        // XML so all records survive (A1).
        $records = $this->parseHostRecords($response['__raw_xml'] ?? null);

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

    /**
     * Add a DNS record (eNom requires sending ALL records via sethosts).
     */
    public function addDNSRecord(array $params)
    {
        // eNom does not support adding individual records; you must send
        // the full set via sethosts. Retrieve existing, append, and save.
        $existing = $this->getDNSRecords($params);

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

        $records = $existing['data']['records'] ?? [];
        $records[] = [
            'name'     => $params['name'] ?? '@',
            'type'     => $params['type'] ?? 'A',
            'value'    => $params['value'] ?? '',
            'priority' => $params['priority'] ?? null,
        ];

        return $this->saveAllDnsRecords($params['domain'], $records);
    }

    /**
     * Update a DNS record by index.
     */
    public function updateDNSRecord(array $params)
    {
        $existing = $this->getDNSRecords($params);

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

        $records = $existing['data']['records'] ?? [];
        $recordId = $params['record_id'] ?? null;

        foreach ($records as &$record) {
            if ($record['id'] === $recordId) {
                $record['name']     = $params['name'] ?? $record['name'];
                $record['type']     = $params['type'] ?? $record['type'];
                $record['value']    = $params['value'] ?? $record['value'];
                $record['priority'] = $params['priority'] ?? $record['priority'];
                break;
            }
        }

        return $this->saveAllDnsRecords($params['domain'], $records);
    }

    /**
     * Delete a DNS record by index.
     */
    public function deleteDNSRecord(array $params)
    {
        $existing = $this->getDNSRecords($params);

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

        $records = $existing['data']['records'] ?? [];
        $recordId = $params['record_id'] ?? null;

        $records = array_filter($records, fn($r) => $r['id'] !== $recordId);

        return $this->saveAllDnsRecords($params['domain'], array_values($records));
    }

    /**
     * Bulk-save all DNS records via eNom's sethosts command.
     */
    protected function saveAllDnsRecords(string $domain, array $records): array
    {
        $domainParts = $this->splitDomain($domain);

        $apiParams = [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ];

        $index = 1;
        foreach ($records as $record) {
            if (empty($record['value'])) {
                continue;
            }

            $apiParams['HostName' . $index] = $record['name'] ?? '@';
            $apiParams['RecordType' . $index] = $record['type'] ?? 'A';
            $apiParams['Address' . $index] = $record['value'];

            if (strtoupper($record['type'] ?? '') === 'MX') {
                $apiParams['MXPref' . $index] = $record['priority'] ?? 10;
            }

            $index++;
        }

        $response = $this->callEnomApi('sethosts', $apiParams);

        $error = $this->getApiError($response);

        if ($error) {
            Log::error('Enom: Failed to save DNS records', [
                'domain' => $domain,
                'error'  => $error,
            ]);

            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

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

    // -------------------------------------------------------------------------
    // Child Nameservers (Glue Records)
    // -------------------------------------------------------------------------

    /**
     * List the registered child nameservers (glue records) for a domain.
     *
     * eNom has no bulk "list glue records for a domain" call, so we probe each
     * candidate hostname via CheckNSStatus. Candidates come from the caller
     * ($params['hostnames']) or default to ns1/ns2.<domain>. Each existing host
     * yields its registered IP(s).
     */
    public function getChildNameservers(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];

        // Caller-supplied candidate hostnames, else sensible defaults.
        $candidates = $params['hostnames'] ?? $params['child_nameservers'] ?? [];
        if (! is_array($candidates) || empty($candidates)) {
            $candidates = ['ns1.' . $domain, 'ns2.' . $domain];
        }

        $records = [];
        foreach ($candidates as $hostname) {
            $hostname = trim((string) $hostname);
            if ($hostname === '') {
                continue;
            }

            $response = $this->callEnomApi('CheckNSStatus', [
                'CheckNSName' => $hostname,
            ]);

            // A "not registered" host returns an error/ResponseString — skip it,
            // it just means no glue record exists for that hostname.
            if ($this->getApiError($response) || ! empty($response['ResponseString1'])) {
                continue;
            }

            $parsed = $this->parseChildNameserverStatus($response['__raw_xml'] ?? null);
            if ($parsed === null) {
                continue;
            }

            $records[] = [
                'hostname' => $parsed['hostname'],
                'ips'      => $parsed['ips'],
                'ip'       => $parsed['ips'][0] ?? null,
            ];
        }

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

    /**
     * Register a child nameserver (glue record).
     */
    public function addChildNameserver(array $params)
    {
        $requiredParams = ['hostname', 'ip'];

        foreach ($requiredParams as $param) {
            if (empty($params[$param])) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_missing_param', ['param' => $param]),
                ];
            }
        }

        $response = $this->callEnomApi('RegisterNameServer', [
            'Add'    => 'true',
            'NSName' => $params['hostname'],
            'IP'     => $params['ip'],
        ]);

        $error = $this->getApiError($response);
        $responseString = $response['ResponseString1'] ?? null;

        if ($error || $responseString) {
            $errorMsg = $error ?: $responseString;

            Log::error('Enom: Failed to register child nameserver', [
                'hostname' => $params['hostname'],
                'error'    => $errorMsg,
            ]);

            return [
                'status'  => 'error',
                'message' => $errorMsg,
            ];
        }

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_child_ns_created'),
            'data'    => [
                'hostname' => $params['hostname'],
                'ip'       => $params['ip'],
            ],
        ];
    }

    /**
     * Update a child nameserver IP address.
     */
    public function updateChildNameserver(array $params)
    {
        $requiredParams = ['hostname', 'old_ip', 'new_ip'];

        foreach ($requiredParams as $param) {
            if (empty($params[$param])) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_missing_param', ['param' => $param]),
                ];
            }
        }

        $response = $this->callEnomApi('UpdateNameServer', [
            'Add'   => 'true',
            'NS'    => $params['hostname'],
            'OldIP' => $params['old_ip'],
            'NewIP' => $params['new_ip'],
        ]);

        $error = $this->getApiError($response);
        $responseString = $response['ResponseString1'] ?? null;

        if ($error || $responseString) {
            $errorMsg = $error ?: $responseString;

            return [
                'status'  => 'error',
                'message' => $errorMsg,
            ];
        }

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_child_ns_updated'),
            'data'    => [
                'hostname' => $params['hostname'],
                'ip'       => $params['new_ip'],
            ],
        ];
    }

    /**
     * Delete a child nameserver.
     */
    public function deleteChildNameserver(array $params)
    {
        $requiredParams = ['hostname'];

        foreach ($requiredParams as $param) {
            if (empty($params[$param])) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_missing_param', ['param' => $param]),
                ];
            }
        }

        $response = $this->callEnomApi('DeleteNameServer', [
            'Add' => 'true',
            'NS'  => $params['hostname'],
        ]);

        $error = $this->getApiError($response);
        $responseString = $response['ResponseString1'] ?? null;

        if ($error || $responseString) {
            $errorMsg = $error ?: $responseString;

            return [
                'status'  => 'error',
                'message' => $errorMsg,
            ];
        }

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

    // -------------------------------------------------------------------------
    // Privacy (ID Protection)
    // -------------------------------------------------------------------------

    /**
     * Get the ID protection / WHOIS privacy status.
     */
    public function getPrivacySettings(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);

        $response = $this->callEnomApi('GetWPPSInfo', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $error = $this->getApiError($response);

        if ($error) {
            return [
                'status'  => 'error',
                'message' => $error,
            ];
        }

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_privacy_retrieved'),
            'data'    => [
                'domain'           => $params['domain'],
                'privacy_enabled'  => (($response['WPPSEnabled'] ?? '') == '1'),
                'available'        => (($response['WPPSAllowed'] ?? '') == '1'),
                'subscription'     => (($response['WPPSExists'] ?? '') == '1'),
            ],
        ];
    }

    /**
     * Toggle ID protection on/off.
     */
    public function updatePrivacySettings(array $params)
    {
        if (empty($params['domain'])) {
            return [
                'status'  => 'error',
                'message' => __('admin/modules.enom_missing_param', ['param' => 'domain']),
            ];
        }

        $domainParts = $this->splitDomain($params['domain']);
        $enablePrivacy = ! empty($params['privacy_enabled']);

        // First check current status
        $infoResponse = $this->callEnomApi('GetWPPSInfo', [
            'tld' => $domainParts['tld'],
            'sld' => $domainParts['sld'],
        ]);

        $allowed = ($infoResponse['WPPSAllowed'] ?? '') == '1';
        $subscriptionExists = ($infoResponse['WPPSExists'] ?? '') == '1';

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

        if ($subscriptionExists) {
            // Enable or disable existing subscription
            $command = $enablePrivacy ? 'ENABLESERVICES' : 'DISABLESERVICES';

            $response = $this->callEnomApi($command, [
                'tld'     => $domainParts['tld'],
                'sld'     => $domainParts['sld'],
                'Service' => 'WPPS',
            ]);

            $error = $this->getApiError($response);

            if ($error) {
                return [
                    'status'  => 'error',
                    'message' => $error,
                ];
            }
        } else {
            if (! $enablePrivacy) {
                return [
                    'status'  => 'error',
                    'message' => __('admin/modules.enom_privacy_not_enabled'),
                ];
            }

            // Purchase new ID protection subscription
            $period = $params['period'] ?? 1;

            $response = $this->callEnomApi('PURCHASESERVICES', [
                'tld'      => $domainParts['tld'],
                'sld'      => $domainParts['sld'],
                'Service'  => 'WPPS',
                'NumYears' => $period,
            ]);

            $error = $this->getApiError($response);

            if ($error) {
                return [
                    'status'  => 'error',
                    'message' => $error,
                ];
            }
        }

        Log::info('Enom: Privacy settings updated', [
            'domain'  => $params['domain'],
            'enabled' => $enablePrivacy,
        ]);

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_privacy_updated'),
            'data'    => [
                'domain'          => $params['domain'],
                'privacy_enabled' => $enablePrivacy,
            ],
        ];
    }

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

    /**
     * Return the configuration fields for the admin panel.
     */
    public function getConfigFields(): array
    {
        return [
            'username' => [
                'type'        => 'text',
                'label'       => __('admin/modules.enom_field_username_label'),
                'required'    => true,
                'description' => __('admin/modules.enom_field_username_description'),
                'placeholder' => 'reseller_username',
                'value'       => $this->config['username'] ?? '',
            ],
            'password' => [
                'type'        => 'password',
                'label'       => __('admin/modules.enom_field_password_label'),
                'required'    => true,
                'description' => __('admin/modules.enom_field_password_description'),
                'placeholder' => '********',
                'value'       => $this->config['password'] ?? '',
            ],
            'test_mode' => [
                'type'        => 'checkbox',
                'label'       => __('admin/modules.enom_field_test_mode_label'),
                'required'    => false,
                'description' => __('admin/modules.enom_field_test_mode_description'),
                'default'     => true,
                'value'       => $this->config['test_mode'] ?? true,
            ],
            'default_nameservers' => [
                'type'        => 'checkbox',
                'label'       => __('admin/modules.enom_field_default_ns_label'),
                'required'    => false,
                'description' => __('admin/modules.enom_field_default_ns_description'),
                'default'     => false,
                'value'       => $this->config['default_nameservers'] ?? false,
            ],
        ];
    }

    /**
     * Validate the registrar configuration.
     */
    public function validateConfig()
    {
        $errors = [];

        if (empty($this->config['username'])) {
            $errors[] = __('admin/modules.enom_validation_username_required');
        }

        if (empty($this->config['password'])) {
            $errors[] = __('admin/modules.enom_validation_password_required');
        }

        if (! empty($errors)) {
            return [
                'status'  => 'error',
                'message' => implode(', ', $errors),
            ];
        }

        // Try a simple API call to verify credentials
        $response = $this->callEnomApi('GetBalance', []);

        $error = $this->getApiError($response);

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

        return [
            'status'  => 'success',
            'message' => __('admin/modules.enom_validation_success'),
            'data'    => [
                'balance' => $response['Balance'] ?? null,
            ],
        ];
    }

    /**
     * Return module info / metadata.
     */
    public function getModuleInfo(): array
    {
        return array_merge(parent::getModuleInfo(), [
            'name'        => 'enom',
            'label'       => 'eNom',
            'description' => __('admin/modules.enom_module_description'),
            'version'     => '1.0.0',
            'author'      => 'ProCP',
        ]);
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    /**
     * Add Registrant/Admin/Tech/AuxBilling contact parameters for
     * registration / transfer, sourced from the PRC-49 canonical contact
     * groups via buildContactParams().
     */
    protected function addContactParams(array &$apiParams, array $params): void
    {
        $apiParams = array_merge($apiParams, $this->buildContactParams($params));
    }

    /**
     * Enable ID protection after domain registration via eNom's cart system.
     */
    protected function enableIdProtection(array $domainParts, int $period): void
    {
        // Add ID Protect to cart
        $this->callEnomApi('AddToCart', [
            'ProductType' => 'IDProtect',
            'TLD'         => $domainParts['tld'],
            'SLD'         => $domainParts['sld'],
            'Quantity'    => $period,
            'ClearItems'  => 'yes',
        ]);

        // Place the order
        $this->callEnomApi('InsertNewOrder', []);
    }

    /**
     * Renew ID protection via eNom's cart system.
     */
    protected function renewIdProtection(array $domainParts, int $period): void
    {
        // Add ID Protect Renewal to cart
        $this->callEnomApi('AddToCart', [
            'ProductType' => 'IDProtectRenewal',
            'TLD'         => $domainParts['tld'],
            'SLD'         => $domainParts['sld'],
            'Quantity'    => $period,
            'ClearItems'  => 'yes',
        ]);

        // Place the order
        $this->callEnomApi('InsertNewOrder', []);
    }
}
