<?php

namespace App\Modules\Registrar\Onlinenic;

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

class Onlinenic extends AbstractRegistrar
{
    /**
     * OnlineNIC API socket connection
     *
     * @var resource|null
     */
    protected $socket = null;

    /**
     * TLD to domain type mapping for OnlineNIC API
     */
    protected array $tldMapping = [
        'com' => '0',
        'net' => '0',
        'org' => '807',
        'cn' => '220',
        'biz' => '800',
        'info' => '805',
        'us' => '806',
        'in' => '808',
        'mobi' => '903',
        'eu' => '902',
        'asia' => '905',
        'me' => '906',
        'name' => '804',
        'tel' => '907',
        'tv' => '400',
        'tw' => '302',
        'uk' => '901',
        'co' => '908',
        'xxx' => '930',
        'pw' => '940',
        'bar' => '942',
        'host' => '943',
        'ink' => '944',
        'press' => '945',
        'rest' => '946',
        'website' => '947',
        'xyz' => '948',
        'wiki' => '949',
        'club' => '740',
        'top' => '770',
        'bike' => '2001',
        'clothing' => '2002',
        'guru' => '2003',
        'holdings' => '2004',
        'plumbing' => '2005',
        'singles' => '2006',
        'ventures' => '2007',
        'camera' => '2008',
        'equipment' => '2009',
        'estate' => '2010',
        'gallery' => '2011',
        'graphics' => '2012',
        'lighting' => '2013',
        'photography' => '2014',
        'construction' => '2015',
        'contractors' => '2016',
        'directory' => '2017',
        'kitchen' => '2018',
        'land' => '2019',
        'technology' => '2020',
        'today' => '2021',
        'diamonds' => '2022',
        'enterprises' => '2023',
        'tips' => '2024',
        'voyage' => '2025',
        'careers' => '2026',
        'photos' => '2027',
        'recipes' => '2028',
        'shoes' => '2029',
        'cab' => '2030',
        'company' => '2031',
        'domains' => '2032',
        'limo' => '2033',
        'academy' => '2034',
        'center' => '2035',
        'computer' => '2036',
        'management' => '2037',
        'systems' => '2038',
        'builders' => '2039',
        'email' => '2040',
        'solutions' => '2041',
        'support' => '2042',
        'training' => '2043',
        'camp' => '2044',
        'education' => '2045',
        'glass' => '2046',
        'institute' => '2047',
        'repair' => '2048',
        'coffee' => '2049',
        'florist' => '2050',
        'house' => '2051',
        'international' => '2052',
        'solar' => '2053',
        'marketing' => '2054',
        'viajes' => '2055',
        'farm' => '2056',
        'codes' => '2057',
        'cheap' => '2058',
        'zone' => '2059',
        'agency' => '2060',
        'bargains' => '2061',
        'boutique' => '2062',
        'cool' => '2063',
        'watch' => '2064',
        'works' => '2065',
        'expert' => '2066',
        'foundation' => '2067',
        'exposed' => '2068',
        'villas' => '2069',
        'flights' => '2070',
        'rentals' => '2071',
        'cruises' => '2072',
        'vacations' => '2073',
        'condos' => '2074',
        'properties' => '2075',
        'maison' => '2076',
        'tienda' => '2077',
        'dating' => '2078',
        'events' => '2079',
        'partners' => '2080',
        'productions' => '2081',
        'community' => '2082',
        'catering' => '2083',
        'cards' => '2084',
        'cleaning' => '2085',
        'tools' => '2086',
        'industries' => '2087',
        'parts' => '2088',
        'supplies' => '2089',
        'supply' => '2090',
        'report' => '2091',
        'vision' => '2092',
        'fish' => '2093',
        'services' => '2094',
        'capital' => '2095',
        'engineering' => '2096',
        'exchange' => '2097',
        'gripe' => '2098',
        'associates' => '2099',
        'lease' => '2100',
        'media' => '2101',
        'pictures' => '2102',
        'reisen' => '2103',
        'toys' => '2104',
        'university' => '2105',
        'town' => '2106',
        'wtf' => '2107',
        'fail' => '2108',
        'financial' => '2109',
        'limited' => '2110',
        'care' => '2111',
        'clinic' => '2112',
        'surgery' => '2113',
        'dental' => '2114',
        'tax' => '2115',
        'cash' => '2116',
        'fund' => '2117',
        'investments' => '2118',
        'furniture' => '2119',
        'discount' => '2120',
        'fitness' => '2121',
        'schule' => '2122',
        'sexy' => '2500',
        'tattoo' => '2501',
        'link' => '2502',
        'guitars' => '2503',
        'gift' => '2504',
        'pics' => '2505',
        'photo' => '2506',
        'christmas' => '2507',
        'blackfriday' => '2508',
        'hiphop' => '2509',
        'juegos' => '2510',
        'audio' => '2511',
        'click' => '2512',
        'hosting' => '2513',
        'property' => '2514',
    ];

    /**
     * OnlineNIC result code descriptions
     */
    protected array $resultCodes = [
        '1000' => 'Command completed successfully',
        '1300' => 'Command completed successfully; no messages',
        '1500' => 'Command completed successfully; ending session',
        '1700' => 'Command completed successfully; not in lib',
        '2001' => 'Command syntax error',
        '2002' => 'Command use error',
        '2003' => 'Required parameter missing',
        '2004' => 'Parameter value range error',
        '2005' => 'Parameter value syntax error',
        '2104' => 'Billing fail; not enough funds',
        '2201' => 'Authorization error',
        '2302' => 'Domain is currently with OnlineNIC',
        '2303' => 'Object does not exist',
        '2304' => 'Object status prohibits operation',
        '2305' => 'Object association prohibits operation',
        '2306' => 'Parameter value policy error',
        '2400' => 'Command fail',
        '2500' => 'Command failed; server ending session',
        '2501' => 'Timeout; server ending session',
        '5000' => 'Network error',
        '5500' => 'Not logged in',
        '6000' => 'Checksum error',
    ];

    // =========================================================================
    // Configuration
    // =========================================================================

    /**
     * Return configuration fields for this registrar module
     */
    public function getConfigFields(): array
    {
        return [
            'username' => [
                'type' => 'text',
                'label' => __('admin/modules.onlinenic_field_username_label'),
                'required' => true,
                'description' => __('admin/modules.onlinenic_field_username_description'),
                'placeholder' => '135610',
                'value' => $this->config['username'] ?? '',
            ],
            'password' => [
                'type' => 'password',
                'label' => __('admin/modules.onlinenic_field_password_label'),
                'required' => true,
                'description' => __('admin/modules.onlinenic_field_password_description'),
                'placeholder' => '********',
                'value' => $this->config['password'] ?? '',
            ],
            'test_mode' => [
                'type' => 'checkbox',
                'label' => __('admin/modules.onlinenic_field_test_mode_label'),
                'required' => false,
                'description' => __('admin/modules.onlinenic_field_test_mode_description'),
                'default' => false,
                'value' => $this->config['test_mode'] ?? false,
            ],
        ];
    }

    // =========================================================================
    // Abstract method implementations (required)
    // =========================================================================

    /**
     * Register a new domain
     */
    public function registerDomain(array $params)
    {
        $requiredParams = ['domain', 'period'];
        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.onlinenic_missing_param', ['param' => $param]),
                ];
            }
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);
        $period = (int) $params['period'];

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        // PRC-969: contact 4-grubunu kanonik helper'dan al (register'da BOŞ gitmesin).
        $registrant = $this->contact($params, 'registrant');
        $admin = $this->contact($params, 'admin');

        // Register registrant contact (WHMCS onlinenic_RegisterContact — crtcontact)
        $registrantResult = $this->registerContact($username, $password, $domainType, $registrant);
        if ($registrantResult['status'] === 'error') {
            $this->logout($username, $password);
            return $registrantResult;
        }
        $registrantId = $registrantResult['data']['contact_id'];

        // Register admin contact (tech/billing OnlineNIC'te admin ile aynı — WHMCS:258-260)
        $adminResult = $this->registerContact($username, $password, $domainType, $admin);
        if ($adminResult['status'] === 'error') {
            $this->logout($username, $password);
            return $adminResult;
        }
        $adminId = $adminResult['data']['contact_id'];
        $techId = $adminId;
        $billingId = $adminId;

        $nameservers = $params['nameservers'] ?? [];
        $ns1 = $nameservers[0] ?? $params['ns1'] ?? '';
        $ns2 = $nameservers[1] ?? $params['ns2'] ?? '';
        $ns3 = $nameservers[2] ?? $params['ns3'] ?? '';
        $ns4 = $nameservers[3] ?? $params['ns4'] ?? '';
        $ns5 = $nameservers[4] ?? $params['ns5'] ?? '';

        $authPassword = $this->generatePassword();
        $clTrid = $this->generateTransactionId($domain);

        $checksumParts = $username . $password . $clTrid . 'crtdomain' . $domainType . $domain . $period . $ns1 . $ns2 . $registrantId . $adminId . $techId . $billingId . $authPassword;
        if ($tld === 'eu' || $tld === 'cc') {
            $checksumParts = $username . $password . $clTrid . 'crtdomain' . $domainType . $domain . $period . $ns1 . $ns2 . $registrantId . $authPassword;
        } elseif ($tld === 'tv') {
            $checksumParts = $username . $password . $clTrid . 'crtdomain' . $domainType . $domain . $period . $ns1 . $ns2 . $registrantId . $techId . $authPassword;
        }
        $checksum = md5($checksumParts);

        // PRC-49: TLD-özel ek alanlar (.us nexuscategory/apppurpose, .uk/.eu vb.)
        // client_domain_fields'ten forward edilir; checksum'a girmez (WHMCS paritesi).
        $extraFieldsXml = '';
        foreach (($params['additional_fields'] ?? []) as $key => $value) {
            if ($value === '' || $value === null) {
                continue;
            }
            $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $key);
            if ($safeKey === '') {
                continue;
            }
            $extraFieldsXml .= '<domain:' . $safeKey . '>' . htmlspecialchars((string) $value, ENT_XML1) . '</domain:' . $safeKey . '>';
        }

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<create>'
            . '<domain:create>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<domain:name>' . $domain . '</domain:name>'
            . '<domain:period>' . $period . '</domain:period>'
            . '<domain:ns1>' . $ns1 . '</domain:ns1>'
            . '<domain:ns2>' . $ns2 . '</domain:ns2>'
            . '<domain:ns3>' . $ns3 . '</domain:ns3>'
            . '<domain:ns4>' . $ns4 . '</domain:ns4>'
            . '<domain:ns5>' . $ns5 . '</domain:ns5>'
            . '<domain:registrant>' . $registrantId . '</domain:registrant>'
            . '<domain:contact type="admin">' . $adminId . '</domain:contact>'
            . '<domain:contact type="tech">' . $techId . '</domain:contact>'
            . '<domain:contact type="billing">' . $billingId . '</domain:contact>'
            . $extraFieldsXml
            . '<domain:authInfo type="pw">' . $authPassword . '</domain:authInfo>'
            . '</domain:create>'
            . '</create>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Domain registration failed', [
                'domain' => $domain,
                'error' => $errorMessage,
            ]);

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

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

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_register_success'),
            'data' => [
                'domain' => $domain,
                'period' => $period,
                'registrar' => 'onlinenic',
                'registered_at' => now()->toISOString(),
                'expires_at' => now()->addYears($period)->toISOString(),
                'nameservers' => array_filter([$ns1, $ns2, $ns3, $ns4, $ns5]),
            ],
        ];
    }

    /**
     * Transfer a domain to OnlineNIC
     */
    public function transferDomain(array $params)
    {
        $requiredParams = ['domain'];
        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.onlinenic_missing_param', ['param' => $param]),
                ];
            }
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        // HostBill class.onlinenic.php:142 — transfer authInfo losing registrar'ın
        // EPP/auth kodudur; rastgele pw kabul EDİLMEZ. Builder epp_code emit eder.
        $authPassword = (string) ($params['epp_code'] ?? '');
        if ($authPassword === '') {
            $this->logout($username, $password);

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

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'transferdomain' . $domainType . $domain);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<transfer>'
            . '<domain:transfer>'
            . '<domain:name>' . $domain . '</domain:name>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<domain:pw>' . $authPassword . '</domain:pw>'
            . '</domain:transfer>'
            . '</transfer>'
            . '<unspec/>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000' && $resultCode !== '1001') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Domain transfer failed', [
                'domain' => $domain,
                'error' => $errorMessage,
            ]);

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

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

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_transfer_success'),
            'data' => [
                'domain' => $domain,
                'registrar' => 'onlinenic',
                'transfer_started_at' => now()->toISOString(),
                'estimated_completion' => now()->addDays(5)->toISOString(),
            ],
        ];
    }

    /**
     * Renew a domain registration
     */
    public function renewDomain(array $params)
    {
        $requiredParams = ['domain', 'period'];
        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.onlinenic_missing_param', ['param' => $param]),
                ];
            }
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);
        $period = (int) $params['period'];

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'renewdomain' . $domainType . $domain . $period);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<renew>'
            . '<domain:renew>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<domain:name>' . $domain . '</domain:name>'
            . '<domain:period>' . $period . '</domain:period>'
            . '</domain:renew>'
            . '</renew>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Domain renewal failed', [
                'domain' => $domain,
                'error' => $errorMessage,
            ]);

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

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

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

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

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'getdomaininfo');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<getdomaininfo>'
            . '<clID>' . $username . '</clID>'
            . '<domain>' . $domain . '</domain>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</getdomaininfo>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        if (! $result) {
            $this->logout($username, $password);

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

        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        $nameservers = array_filter([
            trim($this->extractValue($result, '<dns1>', '</dns1>')),
            trim($this->extractValue($result, '<dns2>', '</dns2>')),
            trim($this->extractValue($result, '<dns3>', '</dns3>')),
            trim($this->extractValue($result, '<dns4>', '</dns4>')),
            trim($this->extractValue($result, '<dns5>', '</dns5>')),
        ]);

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

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

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $nameservers = $params['nameservers'] ?? [];
        $ns1 = $nameservers[0] ?? $params['ns1'] ?? '';
        $ns2 = $nameservers[1] ?? $params['ns2'] ?? '';
        $ns3 = $nameservers[2] ?? $params['ns3'] ?? '';
        $ns4 = $nameservers[3] ?? $params['ns4'] ?? '';
        $ns5 = $nameservers[4] ?? $params['ns5'] ?? '';

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'upddomain' . $domainType . $domain . $ns1 . $ns2);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<update>'
            . '<domain:update>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<domain:name>' . $domain . '</domain:name>'
            . '<domain:rep>'
            . '<domain:ns1>' . $ns1 . '</domain:ns1>'
            . '<domain:ns2>' . $ns2 . '</domain:ns2>'
            . '<domain:ns3>' . $ns3 . '</domain:ns3>'
            . '<domain:ns4>' . $ns4 . '</domain:ns4>'
            . '<domain:ns5>' . $ns5 . '</domain:ns5>'
            . '</domain:rep>'
            . '</domain:update>'
            . '</update>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Set nameservers failed', [
                'domain' => $domain,
                'error' => $errorMessage,
            ]);

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

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

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_set_nameservers_success'),
            'data' => [
                'domain' => $domain,
                'nameservers' => array_values(array_filter([$ns1, $ns2, $ns3, $ns4, $ns5])),
                'updated_at' => now()->toISOString(),
            ],
        ];
    }

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

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'getdomaininfo');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<getdomaininfo>'
            . '<clID>' . $username . '</clID>'
            . '<domain>' . $domain . '</domain>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</getdomaininfo>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_get_contacts_success'),
            'groups' => [
                'Registrant' => [
                    'fullname' => $this->extractValue($result, '<r_name>', '</r_name>'),
                    'company' => $this->extractValue($result, '<r_org>', '</r_org>'),
                    'address' => $this->extractValue($result, '<r_addr>', '</r_addr>'),
                    'city' => $this->extractValue($result, '<r_city>', '</r_city>'),
                    'state' => $this->extractValue($result, '<r_sp>', '</r_sp>'),
                    'postal_code' => $this->extractValue($result, '<r_pc>', '</r_pc>'),
                    'country' => $this->extractValue($result, '<r_cc>', '</r_cc>'),
                    'phone' => $this->extractValue($result, '<r_phone>', '</r_phone>'),
                    'fax' => $this->extractValue($result, '<r_fax>', '</r_fax>'),
                    'email' => $this->extractValue($result, '<r_email>', '</r_email>'),
                ],
                'Admin' => [
                    'fullname' => $this->extractValue($result, '<a_name>', '</a_name>'),
                    'company' => $this->extractValue($result, '<a_org>', '</a_org>'),
                    'address' => $this->extractValue($result, '<a_addr>', '</a_addr>'),
                    'city' => $this->extractValue($result, '<a_city>', '</a_city>'),
                    'state' => $this->extractValue($result, '<a_sp>', '</a_sp>'),
                    'postal_code' => $this->extractValue($result, '<a_pc>', '</a_pc>'),
                    'country' => $this->extractValue($result, '<a_cc>', '</a_cc>'),
                    'phone' => $this->extractValue($result, '<a_phone>', '</a_phone>'),
                    'fax' => $this->extractValue($result, '<a_fax>', '</a_fax>'),
                    'email' => $this->extractValue($result, '<a_email>', '</a_email>'),
                ],
                'Tech' => [
                    'fullname' => $this->extractValue($result, '<t_name>', '</t_name>'),
                    'company' => $this->extractValue($result, '<t_org>', '</t_org>'),
                    'address' => $this->extractValue($result, '<t_addr>', '</t_addr>'),
                    'city' => $this->extractValue($result, '<t_city>', '</t_city>'),
                    'state' => $this->extractValue($result, '<t_sp>', '</t_sp>'),
                    'postal_code' => $this->extractValue($result, '<t_pc>', '</t_pc>'),
                    'country' => $this->extractValue($result, '<t_cc>', '</t_cc>'),
                    'phone' => $this->extractValue($result, '<t_phone>', '</t_phone>'),
                    'fax' => $this->extractValue($result, '<t_fax>', '</t_fax>'),
                    'email' => $this->extractValue($result, '<t_email>', '</t_email>'),
                ],
            ],
        ];
    }

    /**
     * Set contact details for a domain
     */
    public function setContactDetails(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

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

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        // WHMCS onlinenic_SaveContactDetails:513-525 — updcontact öncesi getdomaininfo
        // ile mevcut contact pwd'si (<pwd>) alınıp updcontact:pw'de yeniden kullanılır.
        $serverPwd = $this->fetchContactPassword($username, $password, $domain, $domainType);

        // Contact type mapping: 4=Registrant, 1=Admin, 2=Tech
        $contactTypeMap = [
            'Registrant' => '4',
            'Admin' => '1',
            'Tech' => '2',
        ];

        foreach ($contactTypeMap as $groupName => $contactType) {
            if (! isset($params['groups'][$groupName])) {
                continue;
            }

            $contact = $this->normalizeContactGroup($params['groups'][$groupName]);
            // WHMCS:527 — Registrant (type 4) adı OnlineNIC'te değişmezdir; boş gönderilir.
            $name = $contactType === '4' ? '' : $contact['fullname'];
            $company = $contact['company'];
            $address = $contact['address'];
            $city = $contact['city'];
            $state = $contact['state'];
            $postcode = $contact['postal_code'];
            $country = $contact['country'];
            $tel = $this->formatPhone($contact['phone'], $country);
            $fax = $contact['fax'] !== '' ? $this->formatPhone($contact['fax'], $country) : $tel;
            $email = $contact['email'];

            // Registrant için server pwd yeniden kullanılır (WHMCS:524,540); diğerleri random.
            $authPassword = $contactType === '4' && $serverPwd !== '' ? $serverPwd : $this->generatePassword();
            $clTrid = $this->generateTransactionId($domain);
            $checksum = md5($username . $password . $clTrid . 'updcontact' . $domainType . $domain . $contactType . $name . $company . $email);

            $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
                . '<epp>'
                . '<command>'
                . '<update>'
                . '<contact:update>'
                . '<contact:domaintype>' . $domainType . '</contact:domaintype>'
                . '<contact:domain>' . $domain . '</contact:domain>'
                . '<contact:contacttype>' . $contactType . '</contact:contacttype>'
                . '<contact:ascii>'
                . '<contact:name>' . $name . '</contact:name>'
                . '<contact:org>' . $company . '</contact:org>'
                . '<contact:addr>'
                . '<contact:street1>' . $address . '</contact:street1>'
                . '<contact:city>' . $city . '</contact:city>'
                . '<contact:sp>' . $state . '</contact:sp>'
                . '<contact:pc>' . $postcode . '</contact:pc>'
                . '<contact:cc>' . $country . '</contact:cc>'
                . '</contact:addr>'
                . '</contact:ascii>'
                . '<contact:voice>' . $tel . '</contact:voice>'
                . '<contact:fax>' . $fax . '</contact:fax>'
                . '<contact:email>' . $email . '</contact:email>'
                . '<contact:pw>' . $authPassword . '</contact:pw>'
                . '</contact:update>'
                . '</update>'
                . '<clTRID>' . $clTrid . '</clTRID>'
                . '<chksum>' . $checksum . '</chksum>'
                . '</command>'
                . '</epp>';

            $result = $this->sendCommand($xml);
            $resultCode = $this->getResultCode($result);

            if ($resultCode !== '1000') {
                $errorMessage = $this->parseErrorMessage($result, $resultCode);
                $this->logout($username, $password);
                Log::error('OnlineNIC: Set contact details failed', [
                    'domain' => $domain,
                    'contact_type' => $groupName,
                    'error' => $errorMessage,
                ]);

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

        $this->logout($username, $password);

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

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

    // =========================================================================
    // Optional method overrides
    // =========================================================================

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

        $domain = $params['domain'];

        // OnlineNIC stores lock status locally; retrieve from cache/config
        $locked = $this->config['lock_status_' . md5($domain)] ?? false;

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

    /**
     * Update domain lock status
     */
    public function updateDomainLock(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);
        $locked = $params['lock_status'] ?? true;

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'upddomain' . $domainType . $domain);

        $xml = '<?xml version="1.0" encoding="UTF-8"?>'
            . '<epp>'
            . '<command>'
            . '<update>'
            . '<domain:update>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<domain:name>' . $domain . '</domain:name>';

        if ($locked) {
            $xml .= '<domain:add>'
                . '<domain:status s="clientTransferProhibited"/>'
                . '</domain:add>';
        } else {
            $xml .= '<domain:rem>'
                . '<domain:status s="clientTransferProhibited"/>'
                . '</domain:rem>';
        }

        $xml .= '</domain:update>'
            . '</update>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Update domain lock failed', [
                'domain' => $domain,
                'error' => $errorMessage,
            ]);

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

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

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

    /**
     * Get EPP / auth code for a domain
     */
    public function getEppCode(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'getdomaininfo');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<getdomaininfo>'
            . '<clID>' . $username . '</clID>'
            . '<domain>' . $domain . '</domain>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</getdomaininfo>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        // WHMCS legacy protokolde getdomaininfo'nun <pwd> değeri domain authInfo'sudur
        // (transfer için gereken EPP kodu — WHMCS SaveContactDetails:524 de bunu okur).
        $eppCode = trim($this->extractValue($result, '<pwd>', '</pwd>'));

        if ($eppCode === '') {
            Log::warning('OnlineNIC: EPP code empty in getdomaininfo response', ['domain' => $domain]);

            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_get_epp_failed', ['error' => 'EPP/authInfo not returned by server']),
            ];
        }

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

    /**
     * Register a child nameserver (glue record)
     */
    public function addChildNameserver(array $params)
    {
        $requiredParams = ['domain', 'hostname', 'ip'];
        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.onlinenic_missing_param', ['param' => $param]),
                ];
            }
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);
        $nameserver = $params['hostname'];
        $ipAddress = $params['ip'];

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'crthost' . $domainType . $nameserver . $ipAddress);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp xmlns="urn:iana:xml:ns:epp-1.0"'
            . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
            . ' xsi:schemaLocation="urn:iana:xml:ns:epp-1.0 epp-1.0.xsd">'
            . '<command>'
            . '<create>'
            . '<host:create xmlns:host="urn:iana:xml:ns:host-1.0"'
            . ' xsi:schemaLocation="urn:iana:xml:ns:host-1.0 host-1.0.xsd">'
            . '<host:domaintype>' . $domainType . '</host:domaintype>'
            . '<host:name>' . $nameserver . '</host:name>'
            . '<host:addr ip="v4">' . $ipAddress . '</host:addr>'
            . '</host:create>'
            . '</create>'
            . '<unspec/>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Register child nameserver failed', [
                'domain' => $domain,
                'hostname' => $nameserver,
                'error' => $errorMessage,
            ]);

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

        Log::info('OnlineNIC: Child nameserver registered', [
            'action' => 'add_child_ns',
            'domain' => $domain,
            'hostname' => $nameserver,
            'ip' => $ipAddress,
        ]);

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

    /**
     * Update a child nameserver (glue record) IP address
     */
    public function updateChildNameserver(array $params)
    {
        $requiredParams = ['domain', 'hostname'];
        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.onlinenic_missing_param', ['param' => $param]),
                ];
            }
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);
        $nameserver = $params['hostname'];
        $currentIp = $params['current_ip'] ?? '';
        $newIp = $params['ip'] ?? '';

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'updhost' . $domainType . $nameserver . $newIp . $currentIp);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp xmlns="urn:iana:xml:ns:epp-1.0"'
            . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
            . ' xsi:schemaLocation="urn:iana:xml:ns:epp-1.0 epp-1.0.xsd">'
            . '<command>'
            . '<update>'
            . '<host:update xmlns:host="urn:iana:xml:ns:host-1.0"'
            . ' xsi:schemaLocation="urn:iana:xml:ns:host-1.0 host-1.0.xsd">'
            . '<host:domaintype>' . $domainType . '</host:domaintype>'
            . '<host:name>' . $nameserver . '</host:name>'
            . '<host:add>'
            . '<host:addr ip="v4">' . $newIp . '</host:addr>'
            . '</host:add>'
            . '<host:rem>'
            . '<host:addr ip="v4">' . $currentIp . '</host:addr>'
            . '</host:rem>'
            . '</host:update>'
            . '</update>'
            . '<unspec/>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Update child nameserver failed', [
                'domain' => $domain,
                'hostname' => $nameserver,
                'error' => $errorMessage,
            ]);

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

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

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

    /**
     * Delete a child nameserver (glue record)
     */
    public function deleteChildNameserver(array $params)
    {
        $requiredParams = ['domain', 'hostname'];
        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.onlinenic_missing_param', ['param' => $param]),
                ];
            }
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);
        $nameserver = $params['hostname'];

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'delhost' . $domainType . $nameserver);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp xmlns="urn:iana:xml:ns:epp-1.0"'
            . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
            . ' xsi:schemaLocation="urn:iana:xml:ns:epp-1.0 epp-1.0.xsd">'
            . '<command>'
            . '<delete>'
            . '<host:delete xmlns:host="urn:iana:xml:ns:host-1.0"'
            . ' xsi:schemaLocation="urn:iana:xml:ns:host-1.0 host-1.0.xsd">'
            . '<host:domaintype>' . $domainType . '</host:domaintype>'
            . '<host:name>' . $nameserver . '</host:name>'
            . '</host:delete>'
            . '</delete>'
            . '<unspec/>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Delete child nameserver failed', [
                'domain' => $domain,
                'hostname' => $nameserver,
                'error' => $errorMessage,
            ]);

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

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

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_delete_child_ns_success'),
            'data' => [
                'hostname' => $nameserver,
                'deleted_at' => now()->toISOString(),
            ],
        ];
    }

    /**
     * Sync domain status and expiry date from OnlineNIC
     */
    public function syncDomain(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'getdomaininfo');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<getdomaininfo>'
            . '<clID>' . $username . '</clID>'
            . '<domain>' . $domain . '</domain>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</getdomaininfo>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        $expiryDateRaw = $this->extractValue($result, '<expdate>', '</expdate>');
        $expiryDate = null;
        if ($expiryDateRaw) {
            $timestamp = strtotime($expiryDateRaw);
            if ($timestamp !== false) {
                $expiryDate = date('Y-m-d', $timestamp);
            }
        }

        // HostBill synchInfo:282-288 — gerçek status okunur; 'active' uydurulmaz.
        $status = $this->normalizeDomainStatus($result, $expiryDate);

        $nameservers = array_values(array_filter([
            trim($this->extractValue($result, '<dns1>', '</dns1>')),
            trim($this->extractValue($result, '<dns2>', '</dns2>')),
            trim($this->extractValue($result, '<dns3>', '</dns3>')),
            trim($this->extractValue($result, '<dns4>', '</dns4>')),
            trim($this->extractValue($result, '<dns5>', '</dns5>')),
        ]));

        Log::info('OnlineNIC: Domain synced', [
            'action' => 'sync',
            'domain' => $domain,
            'expires_at' => $expiryDate,
            'domain_status' => $status,
        ]);

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

    /**
     * getdomaininfo yanıtındaki <status> etiketlerini procp domain durumuna map'ler.
     * OnlineNIC EPP status'leri: ok/serverHold/clientHold/pendingDelete/redemptionPeriod...
     */
    protected function normalizeDomainStatus(string $result, ?string $expiryDate): string
    {
        $statusBlob = strtolower($result);

        if (str_contains($statusBlob, 'redemption')) {
            return 'redemption';
        }
        if (str_contains($statusBlob, 'pendingdelete')) {
            return 'pending_delete';
        }
        if (str_contains($statusBlob, 'pendingtransfer')) {
            return 'pending_transfer';
        }
        if (str_contains($statusBlob, 'hold')) {
            return 'suspended';
        }

        // expdate geçmişte ise expired.
        if ($expiryDate !== null && strtotime($expiryDate) !== false && strtotime($expiryDate) < time()) {
            return 'expired';
        }

        return 'active';
    }

    /**
     * Sync a pending domain transfer. Legacy socket protokolde ayrı bir
     * transfer-poll komutu yoktur; getdomaininfo status'ü poll edilir.
     * Domain OnlineNIC'e geçmişse (2303 → yok, 1000 + status ok → tamam).
     */
    public function syncDomainTransfer(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        [$result, $resultCode] = $this->getDomainInfo($username, $password, $domain, $domainType);
        $this->logout($username, $password);

        // 2303 = object does not exist → transfer henüz OnlineNIC'e düşmedi.
        if ($resultCode === '2303') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.onlinenic_sync_success'),
                'data' => [
                    'domain' => $domain,
                    'transfer_completed' => false,
                    'transfer_status' => 'pending',
                ],
            ];
        }

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        $expiryDateRaw = $this->extractValue($result, '<expdate>', '</expdate>');
        $expiryDate = null;
        if ($expiryDateRaw && ($ts = strtotime($expiryDateRaw)) !== false) {
            $expiryDate = date('Y-m-d', $ts);
        }

        $domainStatus = $this->normalizeDomainStatus($result, $expiryDate);
        $completed = $domainStatus === 'active';

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_sync_success'),
            'data' => [
                'domain' => $domain,
                'transfer_completed' => $completed,
                'transfer_status' => $completed ? 'completed' : $domainStatus,
                'expires_at' => $expiryDate,
            ],
        ];
    }

    /**
     * Check domain availability. Legacy socket protokolünde ayrı bir checkDomain
     * komutu yoktur (WHMCS core lookup'a bırakır); getdomaininfo ile 2303 (yok) =
     * available, 1000 (var) = taken olarak türetilir.
     */
    public function checkDomainAvailability(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        [$result, $resultCode] = $this->getDomainInfo($username, $password, $domain, $domainType);
        $this->logout($username, $password);

        // 2303 = object does not exist → müsait. 1000 = kayıtlı → dolu.
        if ($resultCode === '2303') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.onlinenic_check_success'),
                'data' => [
                    'domain' => $domain,
                    'available' => true,
                ],
            ];
        }

        if ($resultCode === '1000') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.onlinenic_check_success'),
                'data' => [
                    'domain' => $domain,
                    'available' => false,
                ],
            ];
        }

        $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

    /**
     * Consolidated domain details (tek getdomaininfo çağrısı; HostBill getDomainInfo
     * paritesi). Ayrı getNameservers/getContactDetails/getEppCode round-trip'lerini
     * gereksiz kılar.
     */
    public function getDomainDetails(array $params)
    {
        if (! isset($params['domain']) || empty($params['domain'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_missing_param', ['param' => 'domain']),
            ];
        }

        $domain = $params['domain'];
        [$sld, $tld] = $this->splitDomain($domain);
        $domainType = $this->getDomainType($tld, $sld);

        $credentials = $this->getCredentials();
        $username = $credentials['username'];
        $password = $credentials['password'];

        $loginResult = $this->login($username, $password);
        if ($loginResult['status'] === 'error') {
            return $loginResult;
        }

        [$result, $resultCode] = $this->getDomainInfo($username, $password, $domain, $domainType);
        $this->logout($username, $password);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        $expiryDateRaw = $this->extractValue($result, '<expdate>', '</expdate>');
        $expiryDate = null;
        if ($expiryDateRaw && ($ts = strtotime($expiryDateRaw)) !== false) {
            $expiryDate = date('Y-m-d', $ts);
        }

        $nameservers = array_values(array_filter([
            trim($this->extractValue($result, '<dns1>', '</dns1>')),
            trim($this->extractValue($result, '<dns2>', '</dns2>')),
            trim($this->extractValue($result, '<dns3>', '</dns3>')),
            trim($this->extractValue($result, '<dns4>', '</dns4>')),
            trim($this->extractValue($result, '<dns5>', '</dns5>')),
        ]));

        return [
            'status' => 'success',
            'message' => __('admin/modules.onlinenic_sync_success'),
            'data' => [
                'domain' => $domain,
                'status' => $this->normalizeDomainStatus($result, $expiryDate),
                'expires_at' => $expiryDate,
                'nameservers' => $nameservers,
                'epp_code' => trim($this->extractValue($result, '<pwd>', '</pwd>')),
            ],
        ];
    }

    /**
     * Child nameserver okuma legacy OnlineNIC socket protokolünde yoktur
     * (WHMCS'te GetRegisteredNameservers yok, HostBill API4'te de host-list yok).
     * add/update/delete desteklenir ama enumerate edilemez.
     */
    public function getChildNameservers(array $params)
    {
        return [
            'status' => 'error',
            'message' => __('admin/modules.onlinenic_child_ns_list_unsupported'),
        ];
    }

    // WHOIS privacy is intentionally NOT overridden: the legacy OnlineNIC socket
    // protocol has no privacy command in any reference. Declaring an error-stub
    // would make getSupportedFeatures() falsely advertise 'privacy' (an always-
    // erroring UI toggle); inheriting AbstractRegistrar's throwing default keeps
    // the capability un-advertised.

    /**
     * Domain suggestions OnlineNIC modülünde desteklenmez (hiçbir referansta yok).
     */
    public function checkDomainSuggestions(array $params)
    {
        return [
            'status' => 'error',
            'message' => __('admin/modules.onlinenic_suggestions_unsupported'),
        ];
    }

    // =========================================================================
    // API communication helpers (private/protected)
    // =========================================================================

    /**
     * getdomaininfo komutunu gönderir; ham sonuç + result code döner.
     * Socket açık olmalı (caller login/logout'u yönetir).
     *
     * @return array{0:string,1:string}  [rawResult, resultCode]
     */
    protected function getDomainInfo(string $username, string $password, string $domain, string $domainType): array
    {
        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'getdomaininfo');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<getdomaininfo>'
            . '<clID>' . $username . '</clID>'
            . '<domain>' . $domain . '</domain>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</getdomaininfo>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);

        return [$result, $this->getResultCode($result)];
    }

    /**
     * Get API credentials, applying test mode overrides when enabled
     */
    protected function getCredentials(): array
    {
        $testMode = $this->config['test_mode'] ?? false;

        if ($testMode) {
            return [
                'username' => '135610',
                'password' => md5('654123'),
            ];
        }

        return [
            'username' => $this->config['username'] ?? '',
            'password' => md5($this->config['password'] ?? ''),
        ];
    }

    /**
     * Open a socket connection and authenticate with OnlineNIC API
     *
     * @return array ['status' => 'success'] or ['status' => 'error', 'message' => '...']
     */
    protected function login(string $username, string $password): array
    {
        $testMode = $this->config['test_mode'] ?? false;
        $server = $testMode ? '218.5.81.149' : 'www.onlinenic.com';
        $port = 20001;

        $this->socket = @fsockopen($server, $port, $errno, $errstr, 90);
        if (! $this->socket) {
            Log::error('OnlineNIC: Connection failed', [
                'server' => $server,
                'errno' => $errno,
                'errstr' => $errstr,
            ]);

            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_connection_failed', ['error' => $errno . ' - ' . $errstr]),
            ];
        }

        // Read greeting
        $greeting = $this->readResponse();

        if (! preg_match('|</greeting></epp>$|i', $greeting)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.onlinenic_connection_error'),
            ];
        }

        $clTrid = $this->generateTransactionId('login');
        $checksum = md5($username . $password . $clTrid . 'login');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<creds>'
            . '<clID>' . $username . '</clID>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</creds>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<login>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</login>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);
            Log::error('OnlineNIC: Login failed', ['error' => $errorMessage]);

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

        return ['status' => 'success'];
    }

    /**
     * Send logout command and close socket
     */
    protected function logout(string $username, string $password): void
    {
        if (! $this->socket) {
            return;
        }

        $clTrid = $this->generateTransactionId('logout');
        $checksum = md5($username . $password . $clTrid . 'logout');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<logout/>'
            . '<unspec/>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $this->sendCommand($xml);
        $this->closeSocket();
    }

    /**
     * Register a contact via OnlineNIC API
     *
     * @return array ['status' => 'success', 'data' => ['contact_id' => '...']] or error
     */
    protected function registerContact(
        string $username,
        string $password,
        string $domainType,
        array $contact
    ): array {
        $firstname = $contact['first_name'] ?? '';
        $lastname = $contact['last_name'] ?? '';
        $companyname = $contact['company'] ?? '';
        $address1 = $contact['address1'] ?? '';
        $address2 = $contact['address2'] ?? '';
        $city = $contact['city'] ?? '';
        $province = $contact['state'] ?? '';
        $country = $contact['country_code'] ?? '';
        $postalcode = $contact['postcode'] ?? '';
        $email = $contact['email'] ?? '';

        $fullname = trim($firstname . ' ' . $lastname);

        if (trim($companyname) === '') {
            $companyname = 'None';
        }

        // WHMCS onlinenic_FormatPhone — voice+fax +CC.NNNN formatı zorunlu.
        $phone = $this->formatPhone($contact['phone'] ?? '', $country);
        $fax = $phone;
        $telephone = $phone;

        $authPassword = $this->generatePassword();
        $clTrid = $this->generateTransactionId($fullname);
        $checksum = md5($username . $password . $clTrid . 'crtcontact' . $fullname . $companyname . $email);

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<create>'
            . '<contact:create>'
            . '<contact:domaintype>' . $domainType . '</contact:domaintype>'
            . '<contact:ascii>'
            . '<contact:name>' . $fullname . '</contact:name>'
            . '<contact:org>' . $companyname . '</contact:org>'
            . '<contact:addr>'
            . '<contact:street1>' . $address1 . '</contact:street1>';

        if ($address2 !== '') {
            $xml .= '<contact:street2>' . $address2 . '</contact:street2>';
        }

        $xml .= '<contact:city>' . $city . '</contact:city>'
            . '<contact:sp>' . $province . '</contact:sp>'
            . '<contact:pc>' . $postalcode . '</contact:pc>'
            . '<contact:cc>' . $country . '</contact:cc>'
            . '</contact:addr>'
            . '</contact:ascii>'
            . '<contact:voice>' . $telephone . '</contact:voice>'
            . '<contact:fax>' . $fax . '</contact:fax>'
            . '<contact:email>' . $email . '</contact:email>'
            . '<contact:pw>' . $authPassword . '</contact:pw>'
            . '</contact:create>'
            . '</create>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);
        $resultCode = $this->getResultCode($result);

        if ($resultCode !== '1000') {
            $errorMessage = $this->parseErrorMessage($result, $resultCode);

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

        $contactId = $this->extractValue($result, '<contact:id>', '</contact:id>');

        return [
            'status' => 'success',
            'data' => [
                'contact_id' => $contactId,
            ],
        ];
    }

    /**
     * getdomaininfo ile mevcut contact auth pwd'sini (<pwd>) getir — updcontact'ta
     * Registrant için yeniden kullanılır (WHMCS onlinenic_SaveContactDetails).
     * Socket açık olmalı; kendi login/logout'unu yönetmez.
     */
    protected function fetchContactPassword(string $username, string $password, string $domain, string $domainType): string
    {
        $clTrid = $this->generateTransactionId($domain);
        $checksum = md5($username . $password . $clTrid . 'getdomaininfo');

        $xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
            . '<epp>'
            . '<command>'
            . '<getdomaininfo>'
            . '<clID>' . $username . '</clID>'
            . '<domain>' . $domain . '</domain>'
            . '<domain:type>' . $domainType . '</domain:type>'
            . '<options>'
            . '<version>1.0</version>'
            . '<lang>en</lang>'
            . '</options>'
            . '</getdomaininfo>'
            . '<clTRID>' . $clTrid . '</clTRID>'
            . '<chksum>' . $checksum . '</chksum>'
            . '</command>'
            . '</epp>';

        $result = $this->sendCommand($xml);

        if ($this->getResultCode($result) !== '1000') {
            return '';
        }

        return $this->extractValue($result, '<pwd>', '</pwd>');
    }

    /**
     * setContactDetails grup verisini kanonik tek şekle indirger. Hem
     * getContactDetails çıktısı (fullname/address/postal_code) hem de kanonik
     * contact anahtarları (first_name/last_name/postcode/country_code) kabul edilir.
     *
     * @return array{fullname:string,company:string,address:string,city:string,state:string,postal_code:string,country:string,phone:string,fax:string,email:string}
     */
    protected function normalizeContactGroup(array $c): array
    {
        $fullname = $c['fullname'] ?? '';
        if ($fullname === '') {
            $fullname = trim(($c['first_name'] ?? '') . ' ' . ($c['last_name'] ?? ''));
        }

        return [
            'fullname' => $fullname,
            'company' => $c['company'] ?? '',
            'address' => $c['address'] ?? ($c['address1'] ?? ''),
            'city' => $c['city'] ?? '',
            'state' => $c['state'] ?? '',
            'postal_code' => $c['postal_code'] ?? ($c['postcode'] ?? ''),
            'country' => $c['country'] ?? ($c['country_code'] ?? ''),
            'phone' => $c['phone'] ?? '',
            'fax' => $c['fax'] ?? '',
            'email' => $c['email'] ?? '',
        ];
    }

    /**
     * WHMCS onlinenic_FormatPhone paritesi — telefonu +CC.NNNN biçimine getirir.
     * Boş telefon +CC.0000000 döner (OnlineNIC voice/fax alanı zorunlu).
     */
    protected function formatPhone(string $phone, string $countryCode = ''): string
    {
        $phone = trim($phone);
        $cc = $this->dialCodeForCountry($countryCode);

        // Zaten +cc.number biçimindeyse normalize et.
        if (strpos($phone, '.') !== false && strpos($phone, '+') === 0) {
            return '+' . ltrim(str_replace(' ', '', $phone), '+');
        }

        $digits = preg_replace('/[^0-9]/', '', $phone);
        if ($digits === '') {
            return '+' . ($cc !== '' ? $cc : '1') . '.0000000';
        }

        if ($cc !== '') {
            if (strncmp($digits, $cc, strlen($cc)) === 0 && strlen($digits) > strlen($cc)) {
                $rest = substr($digits, strlen($cc));
            } else {
                $rest = ltrim($digits, '0');
            }
            $ccPart = $cc;
        } else {
            $ccLen = strlen($digits) > 10 ? strlen($digits) - 10 : 1;
            $ccLen = max(1, min(3, $ccLen));
            $ccPart = substr($digits, 0, $ccLen);
            $rest = substr($digits, $ccLen);
        }

        if ($rest === '' || $rest === false) {
            return '+' . $digits;
        }

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

    /**
     * ISO-2 ülke → sayısal arama kodu (telefon splitter için, best-effort).
     */
    protected function dialCodeForCountry(string $countryCode): string
    {
        $map = [
            'TR' => '90', 'US' => '1', 'CA' => '1', 'GB' => '44', 'DE' => '49',
            'FR' => '33', 'NL' => '31', 'ES' => '34', 'IT' => '39', 'AU' => '61',
            'IN' => '91', 'RU' => '7', 'BR' => '55', 'CN' => '86', 'JP' => '81',
            'AT' => '43', 'BE' => '32', 'BG' => '359', 'CH' => '41', 'CY' => '357',
            'CZ' => '420', 'DK' => '45', 'EE' => '372', 'FI' => '358', 'GR' => '30',
            'HR' => '385', 'HU' => '36', 'IE' => '353', 'IS' => '354', 'LT' => '370',
            'LU' => '352', 'LV' => '371', 'MT' => '356', 'NO' => '47', 'PL' => '48',
            'PT' => '351', 'RO' => '40', 'RS' => '381', 'SE' => '46', 'SI' => '386',
            'SK' => '421', 'UA' => '380',
            'AE' => '971', 'SA' => '966', 'QA' => '974', 'KW' => '965', 'IL' => '972',
            'EG' => '20', 'ZA' => '27', 'NG' => '234', 'KE' => '254', 'MA' => '212',
            'AZ' => '994', 'GE' => '995', 'KZ' => '7',
            'HK' => '852', 'SG' => '65', 'MY' => '60', 'ID' => '62', 'TH' => '66',
            'VN' => '84', 'PH' => '63', 'KR' => '82', 'TW' => '886', 'PK' => '92',
            'BD' => '880', 'LK' => '94', 'NZ' => '64',
            'MX' => '52', 'AR' => '54', 'CL' => '56', 'CO' => '57', 'PE' => '51',
            'VE' => '58', 'UY' => '598',
        ];

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

    /**
     * Send an XML command over the open socket
     */
    protected function sendCommand(string $command): string
    {
        if (! $this->socket) {
            return '';
        }

        fputs($this->socket, $command);

        return $this->readResponse();
    }

    /**
     * Read XML response from the socket until </epp> is found
     */
    protected function readResponse(): string
    {
        if (! $this->socket) {
            return '';
        }

        $result = '';
        $iterations = 0;

        while (! feof($this->socket)) {
            $iterations++;
            $line = fgets($this->socket, 2);
            $result .= $line;

            if (preg_match('|</epp>$|i', $result)) {
                break;
            }

            if ($iterations > 5000) {
                break;
            }
        }

        return $result;
    }

    /**
     * Close the socket connection
     */
    protected function closeSocket(): void
    {
        if ($this->socket) {
            @fclose($this->socket);
            $this->socket = null;
        }
    }

    // =========================================================================
    // Utility / parsing helpers
    // =========================================================================

    /**
     * Split a full domain name into [sld, tld]
     *
     * @return array [sld, tld]
     */
    protected function splitDomain(string $domain): array
    {
        $parts = explode('.', $domain, 2);

        return [
            $parts[0] ?? '',
            $parts[1] ?? '',
        ];
    }

    /**
     * Get the OnlineNIC domain type code for the given TLD
     */
    protected function getDomainType(string $tld, string $sld = ''): string
    {
        $tld = strtolower(trim($tld, ' .'));

        // Special handling for .cc IDN domains
        if ($tld === 'cc') {
            $encoded = idn_to_ascii($sld, INTL_IDNA_VARIANT_UTS46);
            if ($encoded !== false && $encoded === $sld) {
                return '600';
            }

            return '610';
        }

        return $this->tldMapping[$tld] ?? '0';
    }

    /**
     * Extract the result code from OnlineNIC XML response
     */
    protected function getResultCode(string $result): string
    {
        $startPos = strpos($result, '<result code="');
        if ($startPos === false) {
            return '';
        }

        return substr($result, $startPos + 14, 4);
    }

    /**
     * Extract a value between two XML tags from a response string
     */
    protected function extractValue(string $message, string $startTag, string $endTag): string
    {
        $startPos = strpos($message, $startTag);
        $endPos = strpos($message, $endTag);

        if ($startPos === false || $endPos === false) {
            return '';
        }

        $startPos += strlen($startTag);

        return substr($message, $startPos, $endPos - $startPos);
    }

    /**
     * Parse an error message from an API response
     */
    protected function parseErrorMessage(string $result, string $resultCode): string
    {
        $msg = $this->extractValue($result, '<msg>', '</msg>');
        $value = $this->extractValue($result, '<value>', '</value>');
        $detail = $msg;
        if ($value !== '') {
            $detail .= ' - ' . $value;
        }

        $resultText = $this->resultCodes[$resultCode] ?? 'No response from OnlineNIC';

        return $resultCode . ' - ' . $resultText . ': ' . $detail;
    }

    /**
     * Generate a unique transaction ID for OnlineNIC API commands
     */
    protected function generateTransactionId(string $seed): string
    {
        return substr(md5($seed), 0, 10) . mt_rand(1000000000, 9999999999);
    }

    /**
     * Generate a random password for OnlineNIC API auth fields
     * Contains digits, lowercase, uppercase, and special characters
     */
    protected function generatePassword(): string
    {
        $pw = '';
        $length = 3;

        // Digits
        $seeds = '0123456789';
        $seedsCount = strlen($seeds) - 1;
        for ($i = 0; $i < $length; $i++) {
            $pw .= $seeds[rand(0, $seedsCount)];
        }

        // Lowercase
        $seeds = 'abcdefghijklmnopqrstuvwxyz';
        $seedsCount = strlen($seeds) - 1;
        for ($i = 0; $i < $length; $i++) {
            $pw .= $seeds[rand(0, $seedsCount)];
        }

        // Uppercase
        $seeds = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $seedsCount = strlen($seeds) - 1;
        for ($i = 0; $i < $length; $i++) {
            $pw .= $seeds[rand(0, $seedsCount)];
        }

        // Special characters
        $seeds = '!#$%()*+,-./=?@[]^';
        $seedsCount = strlen($seeds) - 1;
        for ($i = 0; $i < $length; $i++) {
            $pw .= $seeds[rand(0, $seedsCount)];
        }

        return $pw;
    }

    /**
     * Destructor: ensure socket is closed
     */
    public function __destruct()
    {
        $this->closeSocket();
    }
}
