<?php

namespace App\Modules\Registrar\INWX;

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

class INWX extends AbstractRegistrar
{
    /**
     * Live JSON-RPC API endpoint (ref Domrobot.php useJson() → /jsonrpc/).
     * ext-xmlrpc was removed in PHP8; INWX exposes an equivalent JSON-RPC
     * endpoint that speaks pure HTTP + JSON with a cookie-jar session.
     */
    protected string $liveUrl = 'https://api.domrobot.com/jsonrpc/';

    /**
     * OT&E (test) JSON-RPC API endpoint
     */
    protected string $testUrl = 'https://api.ote.domrobot.com/jsonrpc/';

    /**
     * Session cookie value returned by account.login (domrobot=...)
     */
    protected ?string $sessionCookie = null;

    // -------------------------------------------------------------------------
    //  Constructor
    // -------------------------------------------------------------------------

    public function __construct(array $config = [], ?Registrar $registrarModel = null)
    {
        parent::__construct($config, $registrarModel);
    }

    // -------------------------------------------------------------------------
    //  Helpers
    // -------------------------------------------------------------------------

    /**
     * Get the active API URL based on sandbox/test mode setting
     */
    protected function getApiUrl(): string
    {
        return ! empty($this->config['sandbox']) && $this->config['sandbox'] === 'true'
            ? $this->testUrl
            : $this->liveUrl;
    }

    /**
     * Authenticate with the INWX JSON-RPC API via account.login.
     * Stores the session cookie for subsequent requests.
     */
    protected function login(): void
    {
        if ($this->sessionCookie !== null) {
            return;
        }

        $username = $this->config['username'] ?? '';
        $password = $this->config['password'] ?? '';

        if (empty($username) || empty($password)) {
            throw new \Exception('INWX: Username and password are required for authentication.');
        }

        // Ref Domrobot.php:113-127 — account.login carries ONLY lang/user/pass;
        // the TOTP `tan` is NEVER a login param. 2FA is a separate account.unlock
        // call performed AFTER a successful login, gated on resData.tfa.
        $loginParams = [
            'lang' => 'en',
            'user' => $username,
            'pass' => $password,
        ];

        $response = $this->jsonRpcRequest('account.login', $loginParams, false);

        $code = (int) ($response['code'] ?? 0);

        if (empty($response['cookie'])) {
            $codeStr = $response['code'] ?? 'unknown';
            $msg = $response['msg'] ?? 'Login failed';
            throw new \Exception("INWX: Login failed [{$codeStr}] {$msg}");
        }

        // Session established — persist the cookie so the unlock call is authenticated.
        $this->sessionCookie = $response['cookie'];

        // 2FA step: only when a shared secret is configured, login completed (1000),
        // and the account actually requires TFA (resData.tfa present). Ref Domrobot.php:119-125.
        if (! empty($this->config['shared_secret'])
            && $code === 1000
            && ! empty($response['resData']['tfa'])) {
            $tan = $this->generateTan($this->config['shared_secret']);

            $unlock = $this->jsonRpcRequest('account.unlock', ['tan' => $tan]);
            $unlockCode = (int) ($unlock['code'] ?? 0);

            if ($unlockCode !== 1000) {
                // Fail closed — do not proceed with a half-authenticated session.
                $this->sessionCookie = null;
                $msg = $unlock['msg'] ?? '2FA unlock failed';
                throw new \Exception("INWX: 2FA unlock failed [{$unlockCode}] {$msg}");
            }
        }

        Log::debug('INWX: Login successful', ['user' => $username]);
    }

    /**
     * Generate a TOTP token from the shared secret for 2FA.
     */
    protected function generateTan(string $secret): string
    {
        // Base32 decode the secret
        $base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $secret = strtoupper(trim($secret));
        $buffer = '';
        foreach (str_split($secret) as $char) {
            $pos = strpos($base32Chars, $char);
            if ($pos !== false) {
                $buffer .= str_pad(decbin($pos), 5, '0', STR_PAD_LEFT);
            }
        }
        $binarySecret = '';
        foreach (str_split($buffer, 8) as $byte) {
            if (strlen($byte) === 8) {
                $binarySecret .= chr(bindec($byte));
            }
        }

        // TOTP calculation (RFC 6238)
        $timeStep = 30;
        $counter = intdiv(time(), $timeStep);
        $counterBytes = pack('N*', 0, $counter);

        $hash = hash_hmac('sha1', $counterBytes, $binarySecret, true);
        $offset = ord($hash[strlen($hash) - 1]) & 0x0F;

        $code = (
            ((ord($hash[$offset]) & 0x7F) << 24) |
            ((ord($hash[$offset + 1]) & 0xFF) << 16) |
            ((ord($hash[$offset + 2]) & 0xFF) << 8) |
            (ord($hash[$offset + 3]) & 0xFF)
        ) % 1000000;

        return str_pad((string) $code, 6, '0', STR_PAD_LEFT);
    }

    /**
     * Perform a JSON-RPC request against the INWX API (ext-less, pure HTTP+JSON).
     * Ref Domrobot.php:137 — POST /jsonrpc/ application/json {method, params},
     * cookie-jar session (Set-Cookie: domrobot=...).
     *
     * @param  string  $method    Dotted method name (e.g. "domain.create")
     * @param  array   $params    Associative array of parameters
     * @param  bool    $withAuth  Whether to send the session cookie (false for login)
     * @return array   Parsed JSON response, with 'cookie' injected on login
     */
    protected function jsonRpcRequest(string $method, array $params = [], bool $withAuth = true): array
    {
        $url = $this->getApiUrl();

        $payload = json_encode([
            'method' => $method,
            'params' => (object) $params,
        ]);

        $headers = [
            'Content-Type: application/json',
            'Content-Length: ' . strlen($payload),
        ];

        if ($withAuth && $this->sessionCookie !== null) {
            $headers[] = 'Cookie: domrobot=' . $this->sessionCookie;
        }

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60);

        $rawResponse = curl_exec($ch);
        $curlError = curl_error($ch);
        $curlErrno = curl_errno($ch);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        curl_close($ch);

        if ($curlErrno || ! $rawResponse) {
            Log::error('INWX: cURL error', ['errno' => $curlErrno, 'error' => $curlError]);

            return [
                'code' => 0,
                'msg' => 'cURL error: ' . ($curlError ?: 'No response'),
            ];
        }

        $responseHeaders = substr($rawResponse, 0, $headerSize);
        $responseBody = substr($rawResponse, $headerSize);

        $decoded = json_decode($responseBody, true);

        if (! is_array($decoded)) {
            Log::error('INWX: Invalid JSON-RPC response', ['body' => substr($responseBody, 0, 500)]);

            return [
                'code' => 0,
                'msg' => 'Invalid JSON-RPC response from INWX API',
            ];
        }

        // Extract session cookie from headers for the login request
        if (! $withAuth && preg_match('/Set-Cookie:\s*domrobot=([^;\s]+)/i', $responseHeaders, $matches)) {
            $decoded['cookie'] = $matches[1];
        }

        return $decoded;
    }

    /**
     * Make an authenticated API call to INWX.
     * Automatically logs in if no session exists.
     *
     * @param  string  $method  INWX API method (e.g. "domain.create")
     * @param  array   $params  Associative array of parameters
     * @return array   Standardized response with 'status', 'message', 'data'
     */
    protected function callApi($method, $params = [], $data = [])
    {
        try {
            $this->login();
        } catch (\Exception $e) {
            Log::error('INWX: Authentication failed', ['error' => $e->getMessage()]);

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

        Log::debug('INWX: API request', ['method' => $method, 'params' => array_diff_key($params, ['pass' => 1])]);

        $response = $this->jsonRpcRequest($method, $params);

        $code = (int) ($response['code'] ?? 0);
        $msg = $response['msg'] ?? 'Unknown error';
        $resData = $response['resData'] ?? [];

        // INWX success codes: 1000 = completed successfully, 1001 = completed; action pending
        if ($code === 1000 || $code === 1001) {
            return [
                'status' => 'success',
                'message' => $msg,
                'data' => $resData,
                'code' => $code,
            ];
        }

        Log::warning('INWX: API error', ['method' => $method, 'code' => $code, 'msg' => $msg]);

        return [
            'status' => 'error',
            'message' => "[{$code}] {$msg}",
            'code' => $code,
        ];
    }

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

    /**
     * Get configuration fields for the module settings page
     */
    public function getConfigFields(): array
    {
        return [
            'username' => [
                'type' => 'text',
                'label' => __('admin/modules.inwx_field_username_label'),
                'required' => true,
                'description' => __('admin/modules.inwx_field_username_description'),
                'placeholder' => 'username',
                'value' => $this->config['username'] ?? '',
            ],
            'password' => [
                'type' => 'password',
                'label' => __('admin/modules.inwx_field_password_label'),
                'required' => true,
                'description' => __('admin/modules.inwx_field_password_description'),
                'placeholder' => '********',
                'value' => $this->config['password'] ?? '',
            ],
            'shared_secret' => [
                'type' => 'password',
                'label' => __('admin/modules.inwx_field_shared_secret_label'),
                'required' => false,
                'description' => __('admin/modules.inwx_field_shared_secret_description'),
                'placeholder' => '',
                'value' => $this->config['shared_secret'] ?? '',
            ],
            'sandbox' => [
                'type' => 'checkbox',
                'label' => __('admin/modules.inwx_field_sandbox_label'),
                'required' => false,
                'description' => __('admin/modules.inwx_field_sandbox_description'),
                'value' => isset($this->config['sandbox']) && $this->config['sandbox'] === 'true' ? 'true' : 'false',
            ],
        ];
    }

    /**
     * Validate module configuration
     */
    public function validateConfig()
    {
        if (empty($this->config['username'])) {
            throw new \Exception(__('admin/modules.inwx_config_username_missing'));
        }

        if (empty($this->config['password'])) {
            throw new \Exception(__('admin/modules.inwx_config_password_missing'));
        }

        return true;
    }

    // -------------------------------------------------------------------------
    //  Contact & extData helpers
    // -------------------------------------------------------------------------

    /**
     * Split a raw phone number into INWX's `+ccode.number` voice format.
     * Ref class.inwx.php:380 voice = "+".ccode.".".number.
     * Best-effort without libphonenumber: strip separators, split off the
     * country dialing prefix when present.
     */
    protected function formatPhone(string $phone, string $countryCode = ''): string
    {
        $phone = trim($phone);
        if ($phone === '') {
            return '';
        }

        // Already in INWX format
        if (preg_match('/^\+\d+\.\d+$/', $phone)) {
            return $phone;
        }

        $hasPlus = str_starts_with($phone, '+');
        $digits = preg_replace('/\D+/', '', $phone);
        if ($digits === '') {
            return '';
        }

        // If E.164 (+CC...), take first 1-3 digits as country code heuristically.
        if ($hasPlus) {
            $ccLen = strlen($digits) > 10 ? 2 : 1;
            $ccode = substr($digits, 0, $ccLen);
            $number = substr($digits, $ccLen);

            return '+' . $ccode . '.' . $number;
        }

        // No leading +: use ISO→dialing map for common cases, else emit raw number.
        $dial = $this->dialCode($countryCode);
        if ($dial !== '') {
            return '+' . $dial . '.' . ltrim($digits, '0');
        }

        return $digits;
    }

    /**
     * Minimal ISO alpha-2 → international dialing code map for phone splitting.
     */
    protected function dialCode(string $iso): string
    {
        $map = [
            'DE' => '49', 'AT' => '43', 'CH' => '41', 'US' => '1', 'GB' => '44',
            'TR' => '90', 'FR' => '33', 'NL' => '31', 'BE' => '32', 'IT' => '39',
            'ES' => '34', 'PL' => '48', 'CZ' => '420', 'DK' => '45', 'SE' => '46',
        ];

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

    /**
     * Build INWX contact.create params from the canonical contact group.
     * Ref class.inwx.php:380 — single `name` (fname+lname), org/street/city/
     * pc/cc/sp/voice(+cc.num)/fax/email; type ORG when company present.
     */
    protected function buildContactParams(array $c): array
    {
        $name = trim(($c['first_name'] ?? '') . ' ' . ($c['last_name'] ?? ''));
        $company = $c['company'] ?? '';
        if ($name === '' && $company !== '') {
            $name = $company;
        }

        $params = [
            'type' => $company !== '' ? 'ORG' : 'PERSON',
            'name' => $name,
            'street' => $c['address1'] ?? '',
            'city' => $c['city'] ?? '',
            'pc' => $c['postcode'] ?? '',
            'cc' => $c['country_code'] ?? '',
            'voice' => $this->formatPhone($c['phone'] ?? '', $c['country_code'] ?? ''),
            'email' => $c['email'] ?? '',
        ];

        if ($company !== '') {
            $params['org'] = $company;
        }
        if (! empty($c['state'])) {
            $params['sp'] = $c['state'];
        }

        return $params;
    }

    /**
     * Create an INWX contact handle for a given group from the module $params.
     * Uses the PRC-969 canonical contact() helper — never reads $params['contact'].
     * Register/transfer also forward TLD-specific extData (additional_fields).
     *
     * @return int|null  Contact handle id, or null on failure/empty.
     */
    protected function createContactHandle(array $params, string $group): ?int
    {
        $c = $this->contact($params, $group);

        if (($c['first_name'] ?? '') === '' && ($c['last_name'] ?? '') === '' && ($c['email'] ?? '') === '') {
            return null;
        }

        $apiParams = $this->buildContactParams($c);
        $this->applyExtData($apiParams, $params);

        $result = $this->callApi('contact.create', $apiParams);

        if ($result['status'] === 'success') {
            $id = $result['data']['id'] ?? null;

            return $id !== null ? (int) $id : null;
        }

        Log::warning('INWX: Failed to create contact', [
            'group' => $group,
            'error' => $result['message'] ?? 'Unknown error',
        ]);

        return null;
    }

    /**
     * Forward TLD-specific extra fields into INWX extData.
     * Ref class.inwx.php:52 — $params['extData'][$key] = $value.
     * Builder emits these under $params['additional_fields'].
     */
    protected function applyExtData(array &$apiParams, array $params): void
    {
        $extra = $params['additional_fields'] ?? [];
        if (! is_array($extra)) {
            return;
        }
        foreach ($extra as $key => $value) {
            if ($value !== '' && $value !== null) {
                $apiParams['extData'][$key] = $value;
            }
        }
    }

    /**
     * INWX period string ("1Y", "2Y", …). Ref class.inwx.php:47/92.
     */
    protected function periodString(array $params): string
    {
        $period = (int) ($params['period'] ?? 1);
        if ($period < 1) {
            $period = 1;
        }

        return $period . 'Y';
    }

    /**
     * Normalize an INWX date value (string "Y-m-d H:i:s" | array{timestamp}) to Y-m-d.
     * Ref class.inwx.php:252-263 — exDate/crDate may arrive as either shape.
     */
    protected function normalizeDate($value): string
    {
        if (is_array($value)) {
            if (isset($value['timestamp'])) {
                return date('Y-m-d', (int) $value['timestamp']);
            }

            return '';
        }
        if (is_string($value) && $value !== '') {
            $ts = strtotime($value);

            return $ts ? date('Y-m-d', $ts) : $value;
        }

        return '';
    }

    // -------------------------------------------------------------------------
    //  Abstract Method Implementations
    // -------------------------------------------------------------------------

    /**
     * Register a new domain. Ref class.inwx.php:41-67 (domain.create).
     */
    public function registerDomain(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $registrantId = $this->createContactHandle($params, 'registrant');
        $adminId = $this->createContactHandle($params, 'admin');
        $techId = $this->createContactHandle($params, 'tech');
        $billingId = $this->createContactHandle($params, 'billing');

        if (! $registrantId) {
            return $this->errorResponse(__('admin/modules.inwx_contact_create_failed'));
        }

        $apiParams = [
            'domain' => $params['domain'],
            'period' => $this->periodString($params),
            'registrant' => $registrantId,
            'admin' => $adminId ?: $registrantId,
            'tech' => $techId ?: $registrantId,
            'billing' => $billingId ?: $registrantId,
            'renewalMode' => 'AUTOEXPIRE',
        ];

        $nameservers = array_values(array_filter((array) ($params['nameservers'] ?? [])));
        if (! empty($nameservers)) {
            $apiParams['ns'] = $nameservers;
        }

        $this->applyExtData($apiParams, $params);

        $result = $this->callApi('domain.create', $apiParams);

        if ($result['status'] === 'success') {
            Log::info('INWX: Domain registered', ['domain' => $params['domain']]);

            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_register_success'),
                'data' => [
                    'domain' => $params['domain'],
                    'period' => (int) ($params['period'] ?? 1),
                    'registered_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }

    /**
     * Transfer a domain to INWX. Ref class.inwx.php:68-89 (domain.transfer).
     */
    public function transferDomain(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $registrantId = $this->createContactHandle($params, 'registrant');
        $adminId = $this->createContactHandle($params, 'admin');
        $techId = $this->createContactHandle($params, 'tech');
        $billingId = $this->createContactHandle($params, 'billing');

        if (! $registrantId) {
            return $this->errorResponse(__('admin/modules.inwx_contact_create_failed'));
        }

        $apiParams = [
            'domain' => $params['domain'],
            'registrant' => $registrantId,
            'admin' => $adminId ?: $registrantId,
            'tech' => $techId ?: $registrantId,
            'billing' => $billingId ?: $registrantId,
            'nsTakeover' => false,
            'contactTakeover' => false,
            'renewalMode' => 'AUTOEXPIRE',
        ];

        $nameservers = array_values(array_filter((array) ($params['nameservers'] ?? [])));
        if (! empty($nameservers)) {
            $apiParams['ns'] = $nameservers;
        }

        // authCode only when present and not the sentinel "not-required" (ref:71)
        $authCode = $params['epp_code'] ?? $params['authCode'] ?? '';
        if (! empty($authCode) && $authCode !== 'not-required') {
            $apiParams['authCode'] = $authCode;
        }

        $this->applyExtData($apiParams, $params);

        $result = $this->callApi('domain.transfer', $apiParams);

        if ($result['status'] === 'success') {
            Log::info('INWX: Domain transfer initiated', ['domain' => $params['domain']]);

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

        return $result;
    }

    /**
     * Renew a domain. Ref class.inwx.php:90-100 — period "1Y", expiration Y-m-d.
     */
    public function renewDomain(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $apiParams = [
            'domain' => $params['domain'],
            'period' => $this->periodString($params),
        ];

        // INWX domain.renew expects `expiration` (current expiry, Y-m-d), not currentExpDate.
        $expiration = $params['expiration']
            ?? $params['expire_date']
            ?? $params['current_exp_date']
            ?? $params['expiry_date']
            ?? null;
        if (! empty($expiration)) {
            $apiParams['expiration'] = $this->normalizeDate($expiration) ?: $expiration;
        }

        $result = $this->callApi('domain.renew', $apiParams);

        if ($result['status'] === 'success') {
            Log::info('INWX: Domain renewed', ['domain' => $params['domain']]);

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

        return $result;
    }

    /**
     * Set nameservers for a domain. Ref class.inwx.php:127-136 (domain.update ns).
     */
    public function setNameservers(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $nameservers = array_values(array_filter((array) ($params['nameservers'] ?? [])));

        $result = $this->callApi('domain.update', [
            'domain' => $params['domain'],
            'ns' => $nameservers,
        ]);

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

        return $result;
    }

    /**
     * Get nameservers for a domain. Ref class.inwx.php:119-126 (domain.info ns).
     */
    public function getNameservers(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.info', [
            'domain' => $params['domain'],
        ]);

        if ($result['status'] === 'success') {
            $nameservers = $result['data']['ns'] ?? [];

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

        return $result;
    }

    /**
     * Set/update contact details for a domain. Ref class.inwx.php:157-226
     * updateContactInfo — updates the EXISTING handle in place (contact.update)
     * then domain.update with only the changed types.
     */
    public function setContactDetails(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        // Read current handles from domain.info (ref:159 get_domain).
        $info = $this->callApi('domain.info', ['domain' => $params['domain']]);
        if ($info['status'] !== 'success') {
            return $info;
        }
        $domainData = $info['data'];

        $updateParams = ['domain' => $params['domain']];

        foreach (['registrant', 'admin', 'tech', 'billing'] as $type) {
            $c = $this->contact($params, $type);
            if (($c['first_name'] ?? '') === '' && ($c['last_name'] ?? '') === '' && ($c['email'] ?? '') === '') {
                continue; // nothing supplied for this group → leave as-is
            }

            $currentHandle = $domainData[$type] ?? null;

            if ($currentHandle) {
                // Update the existing handle in place (no orphan handles).
                $contactParams = $this->buildContactParams($c);
                $contactParams['id'] = (int) $currentHandle;
                $this->applyExtData($contactParams, $params);

                $upd = $this->callApi('contact.update', $contactParams);
                // "Nothing to update" is not a failure.
                if ($upd['status'] !== 'success' && ! str_contains(strtolower($upd['message'] ?? ''), 'nothing')) {
                    return $upd;
                }
                // Same handle stays attached — no domain.update needed for this type.
            } else {
                // No existing handle → create a fresh one and attach it.
                $newId = $this->createContactHandle($params, $type);
                if ($newId) {
                    $updateParams[$type] = $newId;
                }
            }
        }

        // domain.update only when a handle actually changed.
        if (count($updateParams) > 1) {
            $result = $this->callApi('domain.update', $updateParams);
            if ($result['status'] !== 'success') {
                return $result;
            }
        }

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

    /**
     * Get contact details for a domain. Ref class.inwx.php:137-156 — parse
     * resData.contact.name (single field → explode), org/street/city/sp/cc/pc/
     * voice/fax/email.
     */
    public function getContactDetails(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.info', [
            'domain' => $params['domain'],
        ]);

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

        $data = $result['data'];
        $groups = [];

        foreach (['registrant', 'admin', 'tech', 'billing'] as $type) {
            $contactId = $data[$type] ?? null;
            if (! $contactId) {
                continue;
            }

            $contactResult = $this->callApi('contact.info', ['id' => $contactId]);
            if ($contactResult['status'] !== 'success') {
                continue;
            }

            // INWX returns resData.contact (callApi already unwraps resData → data).
            $c = $contactResult['data']['contact'] ?? $contactResult['data'];
            $name = explode(' ', (string) ($c['name'] ?? ''), 2);

            $groups[ucfirst($type)] = [
                'id' => $contactId,
                'firstname' => $name[0] ?? '',
                'lastname' => $name[1] ?? '',
                'company' => $c['org'] ?? '',
                'address1' => $c['street'] ?? '',
                'city' => $c['city'] ?? '',
                'state' => $c['sp'] ?? '',
                'postal_code' => $c['pc'] ?? '',
                'country' => $c['cc'] ?? '',
                'phone' => $c['voice'] ?? '',
                'fax' => $c['fax'] ?? '',
                'email' => $c['email'] ?? '',
            ];
        }

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

    // -------------------------------------------------------------------------
    //  Domain info / lock / EPP / availability / suggestions
    // -------------------------------------------------------------------------

    /**
     * Get domain details / info. Ref class.inwx.php:411-421 + date-shape handling.
     */
    public function getDomainDetails(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.info', [
            'domain' => $params['domain'],
        ]);

        if ($result['status'] === 'success') {
            $data = $result['data'];

            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_domain_info_retrieved'),
                'data' => [
                    'domain' => $params['domain'],
                    'status' => $data['status'] ?? '',
                    'created_at' => $this->normalizeDate($data['crDate'] ?? ''),
                    'updated_at' => $this->normalizeDate($data['upDate'] ?? ''),
                    'expires_at' => $this->normalizeDate($data['exDate'] ?? ''),
                    'locked' => ! empty($data['transferLock']),
                    'nameservers' => array_values((array) ($data['ns'] ?? [])),
                    'registrant' => $data['registrant'] ?? '',
                    'admin' => $data['admin'] ?? '',
                    'tech' => $data['tech'] ?? '',
                    'billing' => $data['billing'] ?? '',
                ],
            ];
        }

        return $result;
    }

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

        $result = $this->callApi('domain.info', [
            'domain' => $params['domain'],
        ]);

        if ($result['status'] === 'success') {
            $locked = ! empty($result['data']['transferLock']);

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

        return $result;
    }

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

        $lock = ! empty($params['lock_status']);

        $result = $this->callApi('domain.update', [
            'domain' => $params['domain'],
            'transferLock' => $lock ? 1 : 0,
        ]);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_lock_updated'),
                'data' => [
                    'domain' => $params['domain'],
                    'locked' => $lock,
                    'updated_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }

    /**
     * Get EPP / auth code. Ref class.inwx.php:111-118 (domain.info authCode).
     */
    public function getEppCode(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.info', [
            'domain' => $params['domain'],
        ]);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_epp_retrieved'),
                'data' => [
                    'domain' => $params['domain'],
                    'epp_code' => $result['data']['authCode'] ?? '',
                ],
            ];
        }

        return $result;
    }

    /**
     * Check domain availability. Ref class.inwx.php:270-286 — resData.domain[0].avail
     * is a boolean/truthy flag (NOT the string "avail").
     */
    public function checkDomainAvailability(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.check', [
            'domain' => $params['domain'],
        ]);

        if ($result['status'] === 'success') {
            $checks = $result['data']['domain'] ?? [];

            // domain.check can return a single assoc row or a list.
            if (isset($checks['avail']) || isset($checks['domain'])) {
                $checks = [$checks];
            }

            $available = false;
            foreach ($checks as $check) {
                if (! empty($check['avail'])) {
                    $available = true;
                    break;
                }
            }

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

        return $result;
    }

    /**
     * Suggest related domains. INWX exposes wide/related availability via
     * domain.check checkRelated; when unavailable, degrade gracefully instead
     * of throwing (ref: no HostBill parity — graceful error).
     */
    public function checkDomainSuggestions(array $params)
    {
        $domain = $params['domain'] ?? '';
        if ($domain === '') {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.check', [
            'domain' => $domain,
            'checkRelated' => 1,
        ]);

        if ($result['status'] !== 'success') {
            // Not fatal — return an empty, well-formed suggestions envelope.
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_suggestions_unsupported'),
                'data' => ['domain' => $domain, 'suggestions' => []],
            ];
        }

        $rows = $result['data']['domain'] ?? [];
        if (isset($rows['avail']) || isset($rows['domain'])) {
            $rows = [$rows];
        }

        $suggestions = [];
        foreach ($rows as $row) {
            $name = $row['domain'] ?? '';
            if ($name === '' || strcasecmp($name, $domain) === 0) {
                continue;
            }
            $suggestions[] = [
                'domain' => $name,
                'available' => ! empty($row['avail']),
            ];
        }

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

    /**
     * WHOIS privacy / ID protection. INWX handles WHOIS privacy as a TLD-specific
     * trustee/extData service, not a universal domain.update flag; expose a clear
     * unsupported response so callers don't 500.
     */
    public function updatePrivacySettings(array $params)
    {
        return [
            'status' => 'error',
            'message' => __('admin/modules.inwx_privacy_unsupported'),
            'data' => [
                'domain' => $params['domain'] ?? '',
                'supported' => false,
            ],
        ];
    }

    // -------------------------------------------------------------------------
    //  Sync (cron)
    // -------------------------------------------------------------------------

    /**
     * Sync domain state from the registrar. Ref class.inwx.php:227-269 synchInfo:
     * domain.info status OK→ACTIVE, exDate (string|array) → expires, exDate<now →
     * EXPIRED; on missing domain, inspect domain.log for the latest FAILED entry.
     */
    public function syncDomain(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.info', ['domain' => $params['domain']]);

        if ($result['status'] !== 'success') {
            // Domain not readable → surface latest FAILED log line if any.
            $failure = $this->latestFailedLog($params['domain']);

            return [
                'status' => 'error',
                'message' => $failure ?: ($result['message'] ?? __('admin/modules.inwx_sync_failed')),
                'data' => ['domain' => $params['domain']],
            ];
        }

        $data = $result['data'];
        $rawStatus = $data['status'] ?? '';
        $status = strtoupper((string) $rawStatus) === 'OK' ? 'active' : strtolower((string) $rawStatus);

        $expiry = $this->normalizeDate($data['exDate'] ?? '');
        if ($expiry !== '' && strtotime($expiry) !== false && strtotime($expiry) < time()) {
            $status = 'expired';
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.inwx_sync_success'),
            'data' => [
                'domain' => $params['domain'],
                'status' => $status,
                'expiry_date' => $expiry,
                'nameservers' => array_values((array) ($data['ns'] ?? [])),
                'registrant' => $data['registrant'] ?? '',
                'locked' => ! empty($data['transferLock']),
            ],
        ];
    }

    /**
     * Sync a pending transfer. INWX reports transfer completion via domain.info
     * status (OK/active = completed); a missing domain with a FAILED domain.log
     * entry = failed; otherwise still pending. Ref synchInfo domain.log path.
     */
    public function syncDomainTransfer(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('domain.info', ['domain' => $params['domain']]);

        if ($result['status'] === 'success') {
            $rawStatus = strtoupper((string) ($result['data']['status'] ?? ''));
            $completed = in_array($rawStatus, ['OK', 'ACTIVE'], true);

            return [
                'status' => 'success',
                'message' => $completed
                    ? __('admin/modules.inwx_transfer_completed')
                    : __('admin/modules.inwx_transfer_pending'),
                'data' => [
                    'domain' => $params['domain'],
                    'status' => $completed ? 'completed' : 'pending',
                    'expiry_date' => $this->normalizeDate($result['data']['exDate'] ?? ''),
                ],
            ];
        }

        // Domain not present yet → check for a FAILED transfer log entry.
        $failure = $this->latestFailedLog($params['domain']);
        if ($failure) {
            return [
                'status' => 'success',
                'message' => $failure,
                'data' => ['domain' => $params['domain'], 'status' => 'failed'],
            ];
        }

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

    /**
     * Return the latest FAILED domain.log userText for a domain, or '' if none.
     * Ref class.inwx.php:231-244.
     */
    protected function latestFailedLog(string $domain): string
    {
        $logs = $this->callApi('domain.log', ['domain' => $domain]);
        if ($logs['status'] !== 'success') {
            return '';
        }

        $entries = $logs['data']['domain'] ?? [];
        if (isset($entries['status'])) {
            $entries = [$entries];
        }

        $latest = null;
        $latestTime = 0;
        foreach ($entries as $entry) {
            $ts = (int) ($entry['date']['timestamp'] ?? 0);
            if ($ts >= $latestTime) {
                $latest = $entry;
                $latestTime = $ts;
            }
        }

        if ($latest && isset($latest['status']) && str_contains((string) $latest['status'], 'FAILED')) {
            return (string) ($latest['userText'] ?? __('admin/modules.inwx_sync_failed'));
        }

        return '';
    }

    // -------------------------------------------------------------------------
    //  Child Nameservers (Glue / Host records) — INWX host.* API
    // -------------------------------------------------------------------------

    /**
     * List child nameservers (glue records) for a domain via host.info.
     */
    public function getChildNameservers(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $result = $this->callApi('host.info', ['domain' => $params['domain']]);

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

        $rawHosts = $result['data']['host'] ?? $result['data']['ns'] ?? [];
        if (isset($rawHosts['hostname']) || isset($rawHosts['name'])) {
            $rawHosts = [$rawHosts];
        }

        $hosts = [];
        foreach ((array) $rawHosts as $host) {
            $hostname = $host['hostname'] ?? $host['name'] ?? '';
            if ($hostname === '') {
                continue;
            }
            $hosts[] = [
                'hostname' => $hostname,
                'ip' => array_values((array) ($host['ip'] ?? $host['ips'] ?? [])),
            ];
        }

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

    /**
     * Add a child nameserver (glue record) via host.create.
     */
    public function addChildNameserver(array $params)
    {
        $hostname = $this->resolveChildHostname($params);
        if ($hostname === '') {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_child_ns_hostname_required')];
        }

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

        $result = $this->callApi('host.create', [
            'hostname' => $hostname,
            'ip' => $ips,
        ]);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_child_ns_added'),
                'data' => [
                    'hostname' => $hostname,
                    'ip' => $ips,
                    'created_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }

    /**
     * Update a child nameserver's IPs via host.update.
     */
    public function updateChildNameserver(array $params)
    {
        $hostname = $this->resolveChildHostname($params);
        if ($hostname === '') {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_child_ns_hostname_required')];
        }

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

        $result = $this->callApi('host.update', [
            'hostname' => $hostname,
            'ip' => $ips,
        ]);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_child_ns_updated'),
                'data' => [
                    'hostname' => $hostname,
                    'ip' => $ips,
                    'updated_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }

    /**
     * Delete a child nameserver via host.delete.
     */
    public function deleteChildNameserver(array $params)
    {
        $hostname = $this->resolveChildHostname($params);
        if ($hostname === '') {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_child_ns_hostname_required')];
        }

        $result = $this->callApi('host.delete', [
            'hostname' => $hostname,
        ]);

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

        return $result;
    }

    /**
     * Resolve a fully-qualified child nameserver hostname from the params.
     * Accepts an already-FQDN `hostname`/`nameserver`, or a `prefix`+`domain`.
     */
    protected function resolveChildHostname(array $params): string
    {
        $host = trim((string) ($params['hostname'] ?? $params['nameserver'] ?? ''));
        $domain = trim((string) ($params['domain'] ?? ''));

        if ($host === '') {
            $prefix = trim((string) ($params['prefix'] ?? $params['name'] ?? ''));
            if ($prefix !== '' && $domain !== '') {
                return $prefix . '.' . $domain;
            }

            return '';
        }

        // If a bare prefix was given alongside a domain, FQDN it.
        if ($domain !== '' && ! str_contains($host, '.')) {
            return $host . '.' . $domain;
        }

        return $host;
    }

    /**
     * Resolve the IP list for a child nameserver from various caller shapes.
     *
     * @return array<int,string>
     */
    protected function resolveChildIps(array $params): array
    {
        $ips = $params['ip'] ?? $params['ips'] ?? $params['ipaddress'] ?? $params['ip_address'] ?? [];
        if (is_string($ips)) {
            $ips = preg_split('/[\s,]+/', $ips) ?: [];
        }

        return array_values(array_filter(array_map('trim', (array) $ips)));
    }

    // -------------------------------------------------------------------------
    //  DNS Management (nameserver.* API)
    // -------------------------------------------------------------------------

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

        $result = $this->callApi('nameserver.info', [
            'domain' => $params['domain'],
        ]);

        if ($result['status'] === 'success') {
            $rawRecords = $result['data']['record'] ?? [];
            if (! is_array($rawRecords)) {
                $rawRecords = [];
            }

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

            $records = [];
            foreach ($rawRecords as $record) {
                $records[] = [
                    'id' => $record['id'] ?? null,
                    'name' => $record['name'] ?? '',
                    'type' => $record['type'] ?? '',
                    'value' => $record['content'] ?? '',
                    'ttl' => (int) ($record['ttl'] ?? 3600),
                    'priority' => (int) ($record['prio'] ?? 0),
                ];
            }

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

        return $result;
    }

    /**
     * Add a DNS record. Ref class.inwx.php:491-506 (nameserver.createRecord).
     */
    public function addDNSRecord(array $params)
    {
        if (empty($params['domain'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_domain_required')];
        }

        $apiParams = [
            'domain' => $params['domain'],
            'name' => $params['name'] ?? '',
            'type' => $params['type'] ?? 'A',
            'content' => $params['value'] ?? $params['content'] ?? '',
            'ttl' => (int) ($params['ttl'] ?? 3600),
        ];

        if (! empty($params['priority'])) {
            $apiParams['prio'] = (int) $params['priority'];
        }

        $result = $this->callApi('nameserver.createRecord', $apiParams);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_dns_record_created'),
                'data' => [
                    'domain' => $params['domain'],
                    'record_id' => $result['data']['id'] ?? null,
                    'created_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }

    /**
     * Update an existing DNS record. Ref class.inwx.php:507-525 (nameserver.updateRecord).
     */
    public function updateDNSRecord(array $params)
    {
        if (empty($params['record_id']) && empty($params['id'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_record_id_required')];
        }

        $recordId = $params['record_id'] ?? $params['id'];

        $apiParams = [
            'id' => (int) $recordId,
        ];

        if (isset($params['name'])) {
            $apiParams['name'] = $params['name'];
        }
        if (isset($params['type'])) {
            $apiParams['type'] = $params['type'];
        }
        if (isset($params['value']) || isset($params['content'])) {
            $apiParams['content'] = $params['value'] ?? $params['content'];
        }
        if (isset($params['ttl'])) {
            $apiParams['ttl'] = (int) $params['ttl'];
        }
        if (isset($params['priority'])) {
            $apiParams['prio'] = (int) $params['priority'];
        }

        $result = $this->callApi('nameserver.updateRecord', $apiParams);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_dns_record_updated'),
                'data' => [
                    'record_id' => $recordId,
                    'updated_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }

    /**
     * Delete a DNS record. Ref class.inwx.php:526-537 (nameserver.deleteRecord).
     */
    public function deleteDNSRecord(array $params)
    {
        if (empty($params['record_id']) && empty($params['id'])) {
            return ['status' => 'error', 'message' => __('admin/modules.inwx_record_id_required')];
        }

        $recordId = $params['record_id'] ?? $params['id'];

        $result = $this->callApi('nameserver.deleteRecord', [
            'id' => (int) $recordId,
        ]);

        if ($result['status'] === 'success') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.inwx_dns_record_deleted'),
                'data' => [
                    'record_id' => $recordId,
                    'deleted_at' => now()->toISOString(),
                ],
            ];
        }

        return $result;
    }
}
