<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * VMO Zoho Mail Integration
 * 
 * Provides email provisioning for users on managed hosts (Kinsta, WP Engine, etc.)
 * via Zoho Mail API. Free tier supports up to 5 email accounts.
 * 
 * Flow:
 * 1. User creates Zoho organization (guided)
 * 2. User adds domain and sets up DNS (guided)
 * 3. User generates Zoho API credentials
 * 4. VMO stores credentials and can create/manage email accounts
 * 
 * @since 8.2.0
 */

if (!defined('ABSPATH')) exit;

class VMO_Zoho_Mail_Integration {
    
    const OPTION_CREDENTIALS = 'vmo_zoho_mail_credentials';
    const OPTION_DOMAIN_STATUS = 'vmo_zoho_domain_status';
    const OAUTH_CALLBACK_ACTION = 'vmo_zoho_oauth_callback';
    const OAUTH_STATE_TTL = 600;
    const OAUTH_STATE_KEY_PREFIX = 'vmo_zoho_oauth_state_';
    const OAUTH_STATUS_KEY_PREFIX = 'vmo_zoho_oauth_status_';
    
    // Zoho API endpoints by data center (9 regions as of 2025)
    private static $api_endpoints = [
        'us' => 'https://mail.zoho.com/api',
        'eu' => 'https://mail.zoho.eu/api',
        'in' => 'https://mail.zoho.in/api',
        'au' => 'https://mail.zoho.com.au/api',
        'jp' => 'https://mail.zoho.jp/api',
        'ca' => 'https://mail.zohocloud.ca/api',
        'cn' => 'https://mail.zoho.com.cn/api',
        'ae' => 'https://mail.zoho.ae/api',
        'sa' => 'https://mail.zoho.sa/api'
    ];
    
    // Zoho OAuth endpoints (9 regions)
    private static $oauth_endpoints = [
        'us' => 'https://accounts.zoho.com/oauth/v2',
        'eu' => 'https://accounts.zoho.eu/oauth/v2',
        'in' => 'https://accounts.zoho.in/oauth/v2',
        'au' => 'https://accounts.zoho.com.au/oauth/v2',
        'jp' => 'https://accounts.zoho.jp/oauth/v2',
        'ca' => 'https://accounts.zohocloud.ca/oauth/v2',
        'cn' => 'https://accounts.zoho.com.cn/oauth/v2',
        'ae' => 'https://accounts.zoho.ae/oauth/v2',
        'sa' => 'https://accounts.zoho.sa/oauth/v2'
    ];
    
    public function __construct() {
        add_action('wp_ajax_vmo_zoho_get_setup_status', array($this, 'ajax_get_setup_status'));
        add_action('wp_ajax_vmo_zoho_save_credentials', array($this, 'ajax_save_credentials'));
        add_action('wp_ajax_vmo_zoho_verify_connection', array($this, 'ajax_verify_connection'));
        add_action('wp_ajax_vmo_zoho_get_domains', array($this, 'ajax_get_domains'));
        add_action('wp_ajax_vmo_zoho_create_email', array($this, 'ajax_create_email'));
        add_action('wp_ajax_vmo_zoho_list_emails', array($this, 'ajax_list_emails'));
        add_action('wp_ajax_vmo_zoho_get_dns_records', array($this, 'ajax_get_dns_records'));
        add_action('wp_ajax_vmo_zoho_check_domain_verification', array($this, 'ajax_check_domain_verification'));
        add_action('wp_ajax_vmo_zoho_exchange_code', array($this, 'ajax_exchange_code_for_refresh_token'));
        add_action('wp_ajax_vmo_zoho_get_auth_url', array($this, 'ajax_get_auth_url'));
        add_action('wp_ajax_vmo_zoho_get_redirect_uri', array($this, 'ajax_get_redirect_uri'));
        add_action('wp_ajax_vmo_zoho_check_token_status', array($this, 'ajax_check_token_status'));

        // Support both the new public callback endpoint and the legacy admin.php callback.
        add_action('init', array($this, 'maybe_handle_legacy_oauth_callback'));
        add_action('admin_post_' . self::OAUTH_CALLBACK_ACTION, array($this, 'handle_oauth_callback'));
        add_action('admin_post_nopriv_' . self::OAUTH_CALLBACK_ACTION, array($this, 'handle_oauth_callback'));
    }

    private function get_storage_user_id($user_id = null) {
        $resolved_user_id = $user_id !== null ? intval($user_id) : get_current_user_id();
        return $resolved_user_id > 0 ? $resolved_user_id : 0;
    }

    private function normalize_domain($domain) {
        $domain = strtolower(trim((string) $domain));
        $domain = preg_replace('#^https?://#i', '', $domain);
        $domain = preg_replace('#/.*$#', '', $domain);
        return trim($domain, ". \t\n\r\0\x0B");
    }

    private function get_scoped_credentials($user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);

        if ($storage_user_id > 0) {
            $stored = get_user_meta($storage_user_id, self::OPTION_CREDENTIALS, true);
            if (is_array($stored) && !empty($stored)) {
                return $stored;
            }
        }

        $legacy = get_option(self::OPTION_CREDENTIALS, []);
        return is_array($legacy) ? $legacy : [];
    }

    private function save_scoped_credentials(array $credentials, $user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);
        if ($storage_user_id > 0) {
            update_user_meta($storage_user_id, self::OPTION_CREDENTIALS, $credentials);
            return;
        }
        update_option(self::OPTION_CREDENTIALS, $credentials, false);
    }

    private function get_scoped_domain_status($user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);

        if ($storage_user_id > 0) {
            $stored = get_user_meta($storage_user_id, self::OPTION_DOMAIN_STATUS, true);
            if (is_array($stored) && !empty($stored)) {
                return $stored;
            }
        }

        $legacy = get_option(self::OPTION_DOMAIN_STATUS, []);
        return is_array($legacy) ? $legacy : [];
    }

    private function save_scoped_domain_status(array $domain_status, $user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);
        if ($storage_user_id > 0) {
            update_user_meta($storage_user_id, self::OPTION_DOMAIN_STATUS, $domain_status);
            return;
        }
        update_option(self::OPTION_DOMAIN_STATUS, $domain_status, false);
    }

    private function mark_token_just_obtained($user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);
        if ($storage_user_id > 0) {
            update_user_meta($storage_user_id, 'vmo_zoho_token_just_obtained', 1);
            return;
        }
        update_option('vmo_zoho_token_just_obtained', true, false);
    }

    private function consume_token_just_obtained($user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);
        if ($storage_user_id > 0) {
            $flag = get_user_meta($storage_user_id, 'vmo_zoho_token_just_obtained', true);
            if ($flag) {
                delete_user_meta($storage_user_id, 'vmo_zoho_token_just_obtained');
                return true;
            }
            return false;
        }

        $flag = get_option('vmo_zoho_token_just_obtained', false);
        if ($flag) {
            delete_option('vmo_zoho_token_just_obtained');
            return true;
        }
        return false;
    }

    private function get_access_token_cache_key(array $credentials, $user_id = null) {
        $storage_user_id = $this->get_storage_user_id($user_id);
        $client_id = (string) ($credentials['client_id'] ?? '');
        $data_center = (string) ($credentials['data_center'] ?? 'us');
        return 'vmo_zoho_access_token_' . $storage_user_id . '_' . md5($client_id . '|' . $data_center);
    }

    private function extract_api_error_message($response, $fallback = 'Zoho request failed.') {
        if (!is_array($response)) {
            return $fallback;
        }

        $candidates = [
            $response['message'] ?? null,
            $response['error'] ?? null,
            $response['error_description'] ?? null,
            $response['status']['description'] ?? null,
            $response['status']['message'] ?? null,
            $response['data']['message'] ?? null,
            $response['data']['errorMessage'] ?? null,
            $response['data']['description'] ?? null,
            $response['data'][0]['message'] ?? null,
            $response['response']['message'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            if (is_string($candidate) && trim($candidate) !== '') {
                return trim($candidate);
            }
        }

        $raw = trim((string) ($response['_raw_body'] ?? ''));
        if ($raw !== '' && strlen($raw) < 600) {
            return $raw;
        }

        $status = intval($response['_http_status'] ?? 0);
        if ($status > 0) {
            return $fallback . ' (HTTP ' . $status . ')';
        }

        return $fallback;
    }

    private function ensure_organization_context(array &$credentials, $access_token, $user_id = null) {
        if (!empty($credentials['organization_id'])) {
            return true;
        }

        $org_info = $this->get_organization_info($access_token, $credentials['data_center'] ?? 'us');
        if (!is_array($org_info) || empty($org_info['zoid'])) {
            return false;
        }

        $credentials['organization_id'] = $org_info['zoid'];
        $credentials['organization_name'] = $org_info['companyName'] ?? ($credentials['organization_name'] ?? '');
        $this->save_scoped_credentials($credentials, $user_id);

        return true;
    }

    private function find_domain_record(array $domains, $domain) {
        $normalized_domain = $this->normalize_domain($domain);
        foreach ($domains as $record) {
            if (!is_array($record)) {
                continue;
            }
            $candidate = $this->normalize_domain($record['domainName'] ?? ($record['domain'] ?? ''));
            if ($candidate !== '' && $candidate === $normalized_domain) {
                return $record;
            }
        }
        return null;
    }

    private function get_matching_domain_record(array &$credentials, $access_token, $user_id, $domain, &$error_message = '') {
        $error_message = '';

        if (!$this->ensure_organization_context($credentials, $access_token, $user_id)) {
            $error_message = 'Connected, but VMO could not identify your Zoho organization. Please verify your credentials again.';
            return null;
        }

        $domains_response = $this->api_request(
            'GET',
            '/organization/' . $credentials['organization_id'] . '/domains',
            [],
            $access_token,
            $credentials['data_center']
        );

        if (empty($domains_response['_ok']) || empty($domains_response['data']) || !is_array($domains_response['data'])) {
            $error_message = $this->extract_api_error_message($domains_response, 'Unable to load domains from Zoho right now.');
            return null;
        }

        $record = $this->find_domain_record($domains_response['data'], $domain);
        if (!$record) {
            $error_message = 'This domain is not yet added inside your Zoho Mail organization. Add the domain in Zoho first, then return here.';
            return null;
        }

        return $record;
    }

    private function get_existing_account_by_email(array &$credentials, $access_token, $user_id, $full_email) {
        if (!$this->ensure_organization_context($credentials, $access_token, $user_id)) {
            return null;
        }

        $accounts_response = $this->api_request(
            'GET',
            '/organization/' . $credentials['organization_id'] . '/accounts',
            [],
            $access_token,
            $credentials['data_center']
        );

        if (empty($accounts_response['_ok']) || empty($accounts_response['data']) || !is_array($accounts_response['data'])) {
            return null;
        }

        $normalized_email = strtolower(trim((string) $full_email));
        foreach ($accounts_response['data'] as $account) {
            if (!is_array($account)) {
                continue;
            }
            $candidate = strtolower(trim((string) ($account['primaryEmailAddress'] ?? ($account['emailAddress'] ?? ''))));
            if ($candidate !== '' && $candidate === $normalized_email) {
                return $account;
            }
        }

        return null;
    }
    
    /**
     * Get current setup status
     */
    public function ajax_get_setup_status() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $requested_domain = $this->normalize_domain($_POST['domain'] ?? '');
        $credentials = $this->get_scoped_credentials($user_id);
        $domain_status = $this->get_scoped_domain_status($user_id);
        $stored_domain = $this->normalize_domain($domain_status['domain'] ?? '');
        
        $has_credentials = !empty($credentials['client_id']) && !empty($credentials['client_secret']);
        $has_refresh_token = !empty($credentials['refresh_token']);
        $domain_matches = $requested_domain === '' || ($stored_domain !== '' && $stored_domain === $requested_domain);
        $has_verified_domain = !empty($domain_status['verified']) && $domain_status['verified'] === true && $domain_matches;
        
        wp_send_json_success([
            'has_credentials' => $has_credentials,
            'has_refresh_token' => $has_refresh_token,
            'has_verified_domain' => $has_verified_domain,
            'domain' => $stored_domain ?: null,
            'domain_matches' => $domain_matches,
            'organization_id' => $credentials['organization_id'] ?? null,
            'data_center' => $credentials['data_center'] ?? 'us',
            'setup_complete' => $has_credentials && $has_refresh_token && $has_verified_domain
        ]);
    }
    
    /**
     * Save Zoho API credentials
     */
    public function ajax_save_credentials() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $client_id = sanitize_text_field($_POST['client_id'] ?? '');
        $client_secret = sanitize_text_field($_POST['client_secret'] ?? '');
        $refresh_token = sanitize_text_field($_POST['refresh_token'] ?? '');
        $organization_id = sanitize_text_field($_POST['organization_id'] ?? '');
        $data_center = sanitize_text_field($_POST['data_center'] ?? 'us');
        
        if (empty($client_id) || empty($client_secret)) {
            wp_send_json_error(['message' => 'Client ID and Client Secret are required']);
            return;
        }
        
        // Encrypt sensitive data
        // ✅ If refresh_token is not provided but one already exists (from OAuth flow), preserve it
        $existing_credentials = $this->get_scoped_credentials($user_id);
        $existing_refresh_token = $existing_credentials['refresh_token'] ?? '';
        
        $encrypted_refresh = '';
        if ($refresh_token) {
            // User provided a new refresh token — encrypt and save it
            $encrypted_refresh = $this->encrypt($refresh_token);
        } elseif ($existing_refresh_token && isset($_POST['refresh_token'])) {
            // User explicitly sent empty refresh_token — they may want to clear it, BUT
            // if it was just obtained via OAuth, preserve it
            $encrypted_refresh = $existing_refresh_token;
        } elseif ($existing_refresh_token) {
            // refresh_token field not in the request at all — preserve existing
            $encrypted_refresh = $existing_refresh_token;
        }
        
        $credentials = [
            'client_id' => $client_id,
            'client_secret' => $this->encrypt($client_secret),
            'refresh_token' => $encrypted_refresh,
            'organization_id' => $organization_id ?: ($existing_credentials['organization_id'] ?? ''),
            'data_center' => $data_center,
            'saved_at' => current_time('mysql'),
            'saved_by' => get_current_user_id(),
            'token_obtained_at' => $existing_credentials['token_obtained_at'] ?? ''
        ];
        
        $this->save_scoped_credentials($credentials, $user_id);
        
        wp_send_json_success([
            'message' => 'Credentials saved successfully',
            'needs_oauth' => empty($refresh_token)
        ]);
    }
    
    /**
     * Verify connection and get organization info
     */
    public function ajax_verify_connection() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $credentials = $this->get_scoped_credentials($user_id);
        
        if (empty($credentials['client_id'])) {
            wp_send_json_error(['message' => 'No credentials configured']);
            return;
        }
        
        // Get access token
        $access_token = $this->get_access_token($credentials, $user_id);
        
        if (!$access_token) {
            wp_send_json_error([
                'message' => 'Failed to authenticate with Zoho. Please check your credentials.',
                'needs_reauth' => true
            ]);
            return;
        }
        
        // Get organization info
        $org_info = $this->get_organization_info($access_token, $credentials['data_center']);
        
        if ($org_info) {
            // Update stored organization ID
            $credentials['organization_id'] = $org_info['zoid'];
            $credentials['organization_name'] = $org_info['companyName'] ?? '';
            $this->save_scoped_credentials($credentials, $user_id);
            
            wp_send_json_success([
                'connected' => true,
                'organization_id' => $org_info['zoid'],
                'organization_name' => $org_info['companyName'] ?? 'Your Organization',
                'email_accounts_used' => $org_info['usersCount'] ?? 0,
                'email_accounts_limit' => $org_info['usersLimit'] ?? 5
            ]);
        } else {
            wp_send_json_error(['message' => 'Connected but could not fetch organization info']);
        }
    }
    
    /**
     * Get domains in the organization
     */
    public function ajax_get_domains() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $credentials = $this->get_scoped_credentials($user_id);
        $access_token = $this->get_access_token($credentials, $user_id);
        
        if (!$access_token) {
            wp_send_json_error(['message' => 'Authentication failed']);
            return;
        }

        if (!$this->ensure_organization_context($credentials, $access_token, $user_id)) {
            wp_send_json_error(['message' => 'Connected, but VMO could not identify your Zoho organization. Please verify your credentials again.']);
            return;
        }
        
        $domains = $this->api_request(
            'GET',
            '/organization/' . $credentials['organization_id'] . '/domains',
            [],
            $access_token,
            $credentials['data_center']
        );
        
        if (!empty($domains['_ok']) && isset($domains['data']) && is_array($domains['data'])) {
            wp_send_json_success([
                'domains' => $domains['data'],
                'primary_domain' => $this->find_primary_domain($domains['data'])
            ]);
        } else {
            wp_send_json_error(['message' => $this->extract_api_error_message($domains, 'Failed to fetch domains.')]);
        }
    }
    
    /**
     * Get required DNS records for domain verification
     */
    public function ajax_get_dns_records() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $domain = $this->normalize_domain($_POST['domain'] ?? '');
        
        if (empty($domain)) {
            wp_send_json_error(['message' => 'Domain is required']);
            return;
        }
        
        $user_id = $this->get_storage_user_id();
        $credentials = $this->get_scoped_credentials($user_id);
        $access_token = $this->get_access_token($credentials, $user_id);
        
        if (!$access_token) {
            wp_send_json_error(['message' => 'Authentication failed']);
            return;
        }

        $domain_lookup_error = '';
        $matched_domain = $this->get_matching_domain_record($credentials, $access_token, $user_id, $domain, $domain_lookup_error);
        if (!$matched_domain) {
            wp_send_json_error(['message' => $domain_lookup_error ?: 'This domain is not ready in Zoho yet.']);
            return;
        }
        
        // Get domain verification records from Zoho
        $domain_info = $this->api_request(
            'GET',
            '/organization/' . $credentials['organization_id'] . '/domains/' . urlencode($domain),
            [],
            $access_token,
            $credentials['data_center']
        );
        if (empty($domain_info['_ok']) || !isset($domain_info['data']) || !is_array($domain_info['data'])) {
            $domain_info = ['data' => $matched_domain];
        }
        
        // Build DNS records list
        $dns_records = $this->build_dns_records($domain, $domain_info, $credentials['data_center']);
        
        wp_send_json_success([
            'domain' => $domain,
            'records' => $dns_records,
            'verification_status' => $domain_info['data']['isVerified'] ?? false,
            'registrar_guides' => $this->get_registrar_guides()
        ]);
    }
    
    /**
     * Build DNS records for domain setup
     */
    private function build_dns_records($domain, $domain_info, $data_center) {
        $records = [];
        
        // MX Records (for receiving email)
        $mx_hosts = $this->get_mx_hosts($data_center);
        foreach ($mx_hosts as $priority => $host) {
            $records[] = [
                'type' => 'MX',
                'host' => '@',
                'value' => $host,
                'priority' => $priority,
                'purpose' => 'Email receiving'
            ];
        }
        
        // SPF Record (for email authentication)
        $spf_tlds = [
            'us' => 'com', 'eu' => 'eu', 'in' => 'in', 'au' => 'com.au',
            'jp' => 'jp', 'ca' => 'cloud.ca', 'cn' => 'com.cn', 'ae' => 'ae', 'sa' => 'sa'
        ];
        $spf_tld = $spf_tlds[$data_center] ?? 'com';
        $records[] = [
            'type' => 'TXT',
            'host' => '@',
            'value' => 'v=spf1 include:zoho.' . $spf_tld . ' ~all',
            'purpose' => 'SPF - Prevents email spoofing'
        ];
        
        // DKIM Record (if available from domain_info)
        if (!empty($domain_info['data']['dkimTxtValue'])) {
            $records[] = [
                'type' => 'TXT',
                'host' => 'zmail._domainkey',
                'value' => $domain_info['data']['dkimTxtValue'],
                'purpose' => 'DKIM - Email signature verification'
            ];
        } else {
            $records[] = [
                'type' => 'TXT',
                'host' => 'zmail._domainkey',
                'value' => '(Will be provided after domain verification)',
                'purpose' => 'DKIM - Email signature verification'
            ];
        }
        
        // Verification TXT Record (if not yet verified)
        if (!empty($domain_info['data']['verificationCode'])) {
            $records[] = [
                'type' => 'TXT',
                'host' => '@',
                'value' => 'zoho-verification=' . $domain_info['data']['verificationCode'],
                'purpose' => 'Domain verification (can be removed after verified)'
            ];
        }
        
        // DMARC Record (recommended)
        $records[] = [
            'type' => 'TXT',
            'host' => '_dmarc',
            'value' => 'v=DMARC1; p=none; rua=mailto:dmarc@' . $domain,
            'purpose' => 'DMARC - Email policy (recommended)'
        ];
        
        return $records;
    }
    
    /**
     * Get MX hosts by data center
     */
    private function get_mx_hosts($data_center) {
        $mx_records = [
            'us' => [
                10 => 'mx.zoho.com',
                20 => 'mx2.zoho.com',
                50 => 'mx3.zoho.com'
            ],
            'eu' => [
                10 => 'mx.zoho.eu',
                20 => 'mx2.zoho.eu',
                50 => 'mx3.zoho.eu'
            ],
            'in' => [
                10 => 'mx.zoho.in',
                20 => 'mx2.zoho.in',
                50 => 'mx3.zoho.in'
            ],
            'au' => [
                10 => 'mx.zoho.com.au',
                20 => 'mx2.zoho.com.au',
                50 => 'mx3.zoho.com.au'
            ],
            'jp' => [
                10 => 'mx.zoho.jp',
                20 => 'mx2.zoho.jp',
                50 => 'mx3.zoho.jp'
            ],
            'ca' => [
                10 => 'mx.zohocloud.ca',
                20 => 'mx2.zohocloud.ca',
                50 => 'mx3.zohocloud.ca'
            ],
            'cn' => [
                10 => 'mx.zoho.com.cn',
                20 => 'mx2.zoho.com.cn',
                50 => 'mx3.zoho.com.cn'
            ],
            'ae' => [
                10 => 'mx.zoho.ae',
                20 => 'mx2.zoho.ae',
                50 => 'mx3.zoho.ae'
            ],
            'sa' => [
                10 => 'mx.zoho.sa',
                20 => 'mx2.zoho.sa',
                50 => 'mx3.zoho.sa'
            ]
        ];
        
        return $mx_records[$data_center] ?? $mx_records['us'];
    }
    
    /**
     * Get registrar-specific DNS setup guides
     */
    private function get_registrar_guides() {
        return [
            'namecheap' => [
                'name' => 'Namecheap',
                'url' => 'https://www.namecheap.com/support/knowledgebase/article.aspx/317/2237/how-do-i-add-txtspfdaboraaa-records-for-my-domain/',
                'steps' => [
                    'Log in to your Namecheap account',
                    'Go to Domain List → click Manage next to your domain',
                    'Click Advanced DNS tab',
                    'Add each record as shown below',
                    'Save changes (may take 24-48 hours to propagate)'
                ]
            ],
            'godaddy' => [
                'name' => 'GoDaddy',
                'url' => 'https://www.godaddy.com/help/add-an-mx-record-19234',
                'steps' => [
                    'Log in to GoDaddy',
                    'Go to My Products → DNS',
                    'Click Add Record for each entry',
                    'Save changes'
                ]
            ],
            'cloudflare' => [
                'name' => 'Cloudflare',
                'url' => 'https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/',
                'steps' => [
                    'Log in to Cloudflare dashboard',
                    'Select your domain',
                    'Go to DNS → Records',
                    'Click Add record for each entry',
                    'Note: For MX records, turn OFF proxy (gray cloud)'
                ]
            ],
            'squarespace' => [
                'name' => 'Squarespace Domains',
                'url' => 'https://support.squarespace.com/hc/en-us/articles/360002101888',
                'steps' => [
                    'Log in to your Squarespace account',
                    'Go to Settings → Domains → select your domain',
                    'Click DNS → DNS Settings',
                    'Add custom records for each entry'
                ]
            ]
        ];
    }
    
    /**
     * Check domain verification status
     */
    public function ajax_check_domain_verification() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $domain = $this->normalize_domain($_POST['domain'] ?? '');
        
        if (empty($domain)) {
            wp_send_json_error(['message' => 'Domain is required']);
            return;
        }
        
        $credentials = $this->get_scoped_credentials($user_id);
        $access_token = $this->get_access_token($credentials, $user_id);
        
        if (!$access_token) {
            wp_send_json_error(['message' => 'Authentication failed']);
            return;
        }

        $domain_lookup_error = '';
        $matched_domain = $this->get_matching_domain_record($credentials, $access_token, $user_id, $domain, $domain_lookup_error);
        if (!$matched_domain) {
            wp_send_json_error(['message' => $domain_lookup_error ?: 'This domain is not ready in Zoho yet.']);
            return;
        }
        
        // Trigger verification check
        $verify_result = $this->api_request(
            'POST',
            '/organization/' . $credentials['organization_id'] . '/domains/' . urlencode($domain) . '/verify',
            [],
            $access_token,
            $credentials['data_center']
        );
        
        if ($verify_result && isset($verify_result['data']['isVerified'])) {
            $is_verified = $verify_result['data']['isVerified'];
            
            // Update stored status
            if ($is_verified) {
                $this->save_scoped_domain_status([
                    'domain' => $domain,
                    'verified' => true,
                    'verified_at' => current_time('mysql')
                ], $user_id);
            }
            
            wp_send_json_success([
                'verified' => $is_verified,
                'domain' => $domain,
                'message' => $is_verified 
                    ? 'Domain verified successfully! You can now create email accounts.'
                    : 'Domain not yet verified. Please check your DNS records and allow up to 48 hours for propagation.'
            ]);
        } else {
            wp_send_json_error([
                'message' => $this->extract_api_error_message($verify_result, 'Could not check verification status.')
            ]);
        }
    }
    
    /**
     * Create email account
     */
    public function ajax_create_email() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $email_prefix = sanitize_text_field($_POST['email_prefix'] ?? '');
        $requested_domain = $this->normalize_domain($_POST['domain'] ?? '');
        $display_name = sanitize_text_field($_POST['display_name'] ?? '');
        $password = wp_unslash($_POST['password'] ?? '');
        $category = sanitize_text_field($_POST['category'] ?? 'general');

        if (strpos($email_prefix, '@') !== false) {
            list($raw_prefix, $raw_domain) = array_pad(explode('@', $email_prefix, 2), 2, '');
            $email_prefix = sanitize_text_field($raw_prefix);
            if ($requested_domain === '' && $raw_domain !== '') {
                $requested_domain = $this->normalize_domain($raw_domain);
            }
        }
        $email_prefix = preg_replace('/[^a-z0-9._-]+/i', '', $email_prefix);
        
        if (empty($email_prefix)) {
            wp_send_json_error(['message' => 'Email prefix is required']);
            return;
        }
        
        $credentials = $this->get_scoped_credentials($user_id);
        $domain_status = $this->get_scoped_domain_status($user_id);
        $verified_domain = $this->normalize_domain($domain_status['domain'] ?? '');
        
        if (empty($domain_status['verified']) || empty($verified_domain)) {
            wp_send_json_error(['message' => 'Domain not verified. Please complete domain setup first.']);
            return;
        }

        if ($requested_domain !== '' && $verified_domain !== $requested_domain) {
            wp_send_json_error(['message' => 'The verified email domain does not match this consultation. Please verify the current business domain first.']);
            return;
        }
        
        $domain = $verified_domain;
        $full_email = $email_prefix . '@' . $domain;
        
        // Generate password if not provided
        if (empty($password)) {
            $password = $this->generate_secure_password();
        }
        
        $access_token = $this->get_access_token($credentials, $user_id);
        
        if (!$access_token) {
            wp_send_json_error(['message' => 'Authentication failed']);
            return;
        }

        $domain_lookup_error = '';
        $matched_domain = $this->get_matching_domain_record($credentials, $access_token, $user_id, $domain, $domain_lookup_error);
        if (!$matched_domain) {
            wp_send_json_error(['message' => $domain_lookup_error ?: 'This domain is not ready in Zoho yet.']);
            return;
        }
        
        // Create the email account via Zoho API
        $result = $this->api_request(
            'POST',
            '/organization/' . $credentials['organization_id'] . '/accounts',
            [
                'primaryEmailAddress' => $full_email,
                'password' => $password,
                'displayName' => $display_name ?: $email_prefix,
                'role' => 'member'
            ],
            $access_token,
            $credentials['data_center']
        );
        
        if (!empty($result['_ok']) && isset($result['data']['accountId'])) {
            // Save email to VMO database
            $this->save_email_to_vmo($user_id, $full_email, $category, $result['data']['accountId'], $display_name ?: $email_prefix);
            
            // Get SMTP settings
            $smtp_settings = $this->get_smtp_settings($credentials['data_center'], $domain);
            
            wp_send_json_success([
                'message' => 'Email account created successfully!',
                'email' => $full_email,
                'password' => $password, // Show once
                'account_id' => $result['data']['accountId'],
                'category' => $category,
                'smtp_settings' => $smtp_settings,
                'webmail_url' => $this->get_webmail_url($credentials['data_center'])
            ]);
        } else {
            $error_msg = $this->extract_api_error_message($result, 'Failed to create email account.');
            
            // Check for common errors
            if (stripos($error_msg, 'already exists') !== false) {
                $existing_account = $this->get_existing_account_by_email($credentials, $access_token, $user_id, $full_email);
                if ($existing_account) {
                    $account_id = $existing_account['accountId'] ?? ($existing_account['zuid'] ?? '');
                    if ($account_id !== '') {
                        $this->save_email_to_vmo($user_id, $full_email, $category, $account_id, $display_name ?: $email_prefix);
                    }
                    wp_send_json_success([
                        'message' => 'This email address already exists and has been connected.',
                        'email' => $full_email,
                        'password' => '',
                        'account_id' => $account_id,
                        'category' => $category,
                        'already_exists' => true,
                        'smtp_settings' => $this->get_smtp_settings($credentials['data_center'], $domain),
                        'webmail_url' => $this->get_webmail_url($credentials['data_center'])
                    ]);
                }
                wp_send_json_error(['message' => 'This email address already exists']);
            } elseif (stripos($error_msg, 'limit') !== false) {
                wp_send_json_error(['message' => 'Email account limit reached. Free tier allows up to 5 accounts. Upgrade at zoho.com/mail/pricing']);
            } else {
                wp_send_json_error(['message' => $error_msg]);
            }
        }
    }
    
    /**
     * List existing email accounts
     */
    public function ajax_list_emails() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $credentials = $this->get_scoped_credentials($user_id);
        $access_token = $this->get_access_token($credentials, $user_id);
        
        if (!$access_token) {
            wp_send_json_error(['message' => 'Authentication failed']);
            return;
        }

        if (!$this->ensure_organization_context($credentials, $access_token, $user_id)) {
            wp_send_json_error(['message' => 'Connected, but VMO could not identify your Zoho organization. Please verify your credentials again.']);
            return;
        }
        
        $result = $this->api_request(
            'GET',
            '/organization/' . $credentials['organization_id'] . '/accounts',
            [],
            $access_token,
            $credentials['data_center']
        );
        
        if (!empty($result['_ok']) && isset($result['data']) && is_array($result['data'])) {
            $accounts = array_map(function($acc) {
                return [
                    'email' => $acc['primaryEmailAddress'] ?? $acc['emailAddress'] ?? '',
                    'display_name' => $acc['displayName'] ?? '',
                    'account_id' => $acc['accountId'] ?? $acc['zuid'] ?? '',
                    'status' => $acc['accountStatus'] ?? 'active',
                    'created_at' => $acc['createdTime'] ?? null
                ];
            }, $result['data']);
            
            wp_send_json_success(['accounts' => $accounts]);
        } else {
            wp_send_json_error(['message' => $this->extract_api_error_message($result, 'Failed to fetch email accounts.')]);
        }
    }
    
    /**
     * Get SMTP settings for domain
     */
    private function get_smtp_settings($data_center, $domain) {
        $hosts = [
            'us' => ['smtp' => 'smtp.zoho.com', 'imap' => 'imap.zoho.com'],
            'eu' => ['smtp' => 'smtp.zoho.eu', 'imap' => 'imap.zoho.eu'],
            'in' => ['smtp' => 'smtp.zoho.in', 'imap' => 'imap.zoho.in'],
            'au' => ['smtp' => 'smtp.zoho.com.au', 'imap' => 'imap.zoho.com.au'],
            'jp' => ['smtp' => 'smtp.zoho.jp', 'imap' => 'imap.zoho.jp'],
            'ca' => ['smtp' => 'smtp.zohocloud.ca', 'imap' => 'imap.zohocloud.ca'],
            'cn' => ['smtp' => 'smtp.zoho.com.cn', 'imap' => 'imap.zoho.com.cn'],
            'ae' => ['smtp' => 'smtp.zoho.ae', 'imap' => 'imap.zoho.ae'],
            'sa' => ['smtp' => 'smtp.zoho.sa', 'imap' => 'imap.zoho.sa']
        ];
        
        $host = $hosts[$data_center] ?? $hosts['us'];
        
        return [
            'type' => 'mailbox',
            'vmo_interface' => true,
            'interface_label' => 'VMO Email',
            'smtp_host' => $host['smtp'],
            'smtp_port' => 587,
            'smtp_encryption' => 'tls',
            'imap_host' => $host['imap'],
            'imap_port' => 993,
            'imap_encryption' => 'ssl'
        ];
    }
    
    /**
     * Get webmail URL
     */
    private function get_webmail_url($data_center) {
        $urls = [
            'us' => 'https://mail.zoho.com',
            'eu' => 'https://mail.zoho.eu',
            'in' => 'https://mail.zoho.in',
            'au' => 'https://mail.zoho.com.au',
            'jp' => 'https://mail.zoho.jp',
            'ca' => 'https://mail.zohocloud.ca',
            'cn' => 'https://mail.zoho.com.cn',
            'ae' => 'https://mail.zoho.ae',
            'sa' => 'https://mail.zoho.sa'
        ];
        return $urls[$data_center] ?? $urls['us'];
    }
    
    /**
     * ✅ Get the canonical redirect URI used by both auth URL and token exchange.
     * This ensures PHP and JS always use the EXACT same redirect URI.
     */
    public static function get_canonical_redirect_uri() {
        return add_query_arg(
            ['action' => self::OAUTH_CALLBACK_ACTION],
            admin_url('admin-post.php')
        );
    }

    /**
     * Keep the old admin.php?page=vmo-zoho-oauth-callback route working so
     * existing Zoho console setups do not hard-fail during rollout.
     */
    public function maybe_handle_legacy_oauth_callback() {
        if (!isset($_GET['page']) || $_GET['page'] !== 'vmo-zoho-oauth-callback') {
            return;
        }

        $this->process_oauth_callback();
    }

    private static function get_oauth_state_cache_key($state) {
        return self::OAUTH_STATE_KEY_PREFIX . md5((string) $state);
    }

    private static function get_oauth_status_cache_key($state) {
        return self::OAUTH_STATUS_KEY_PREFIX . md5((string) $state);
    }

    private function remember_oauth_state($state, $payload) {
        set_transient(self::get_oauth_state_cache_key($state), $payload, self::OAUTH_STATE_TTL);
    }

    private function get_oauth_state($state) {
        $payload = get_transient(self::get_oauth_state_cache_key($state));
        return is_array($payload) ? $payload : null;
    }

    private function clear_oauth_state($state) {
        delete_transient(self::get_oauth_state_cache_key($state));
    }

    private function remember_oauth_status($state, $status, $message = '') {
        if (empty($state)) {
            return;
        }

        set_transient(
            self::get_oauth_status_cache_key($state),
            [
                'status' => $status,
                'message' => $message,
                'updated_at' => time()
            ],
            self::OAUTH_STATE_TTL
        );
    }

    /**
     * ✅ AJAX: Return the canonical redirect URI so JS can display it for Zoho Console registration.
     * This prevents redirect_uri mismatch between JS-constructed and PHP admin_url() values.
     */
    public function ajax_get_redirect_uri() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        wp_send_json_success([
            'redirect_uri' => self::get_canonical_redirect_uri(),
            'homepage_url' => home_url('/')
        ]);
    }

    /**
     * ✅ Generate Zoho OAuth authorization URL
     * Returns the URL the user must visit to authorize and get an authorization code.
     */
    public function ajax_get_auth_url() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $credentials = $this->get_scoped_credentials($user_id);
        
        if (empty($credentials['client_id'])) {
            wp_send_json_error(['message' => 'Please save your Client ID first before generating a refresh token.']);
            return;
        }
        
        $data_center = $credentials['data_center'] ?? 'us';
        $oauth_base = self::$oauth_endpoints[$data_center] ?? self::$oauth_endpoints['us'];
        $client_id = $credentials['client_id'];
        
        // ✅ Use canonical redirect URI — same value used by handle_oauth_callback()
        $redirect_uri = self::get_canonical_redirect_uri();
        
        // Generate a per-attempt state token so parallel OAuth attempts do not collide.
        $state = wp_generate_password(48, false, false);
        $this->remember_oauth_state($state, [
            'user_id' => $user_id,
            'created_at' => time(),
            'redirect_uri' => $redirect_uri
        ]);
        $this->remember_oauth_status($state, 'pending', 'Waiting for Zoho authorization.');
        
        // Scopes needed for email provisioning
        $scopes = 'ZohoMail.organization.accounts.ALL,ZohoMail.organization.domains.ALL,ZohoMail.organization.ALL';
        
        $auth_url = $oauth_base . '/auth?' . http_build_query([
            'response_type' => 'code',
            'client_id' => $client_id,
            'scope' => $scopes,
            'redirect_uri' => $redirect_uri,
            'access_type' => 'offline',
            'state' => $state,
            'prompt' => 'consent'
        ]);
        
        wp_send_json_success([
            'auth_url' => $auth_url,
            'redirect_uri' => $redirect_uri,
            'oauth_state' => $state,
            'message' => 'Open the authorization URL to grant access. You will be redirected back here automatically.'
        ]);
    }
    
    /**
     * Handle OAuth callback for both the new admin-post endpoint and the legacy admin.php route.
     */
    public function handle_oauth_callback() {
        $this->process_oauth_callback();
    }

    private function process_oauth_callback() {
        
        $code = sanitize_text_field($_GET['code'] ?? '');
        $state = sanitize_text_field($_GET['state'] ?? '');
        $error = sanitize_text_field($_GET['error'] ?? '');
        
        // Handle errors from Zoho
        if ($error) {
            $this->finish_oauth_callback(false, 'Zoho returned an error: ' . $error, $state);
        }
        
        if (empty($code)) {
            $this->finish_oauth_callback(false, 'No authorization code received from Zoho.', $state);
        }
        
        // Verify CSRF state for this specific OAuth attempt.
        $state_context = $this->get_oauth_state($state);
        if (empty($state_context)) {
            $this->render_oauth_result_page(false, 'Security validation failed (state mismatch). Please try again from the VMO wizard.');
            exit;
        }
        
        // Make the state single-use even if Zoho retries the redirect.
        $this->clear_oauth_state($state);
        $owner_user_id = intval($state_context['user_id'] ?? 0);
        
        // Exchange the authorization code for tokens
        $credentials = $this->get_scoped_credentials($owner_user_id);
        
        if (empty($credentials['client_id']) || empty($credentials['client_secret'])) {
            $this->finish_oauth_callback(false, 'Client ID and Client Secret must be saved before generating a refresh token.', $state);
        }
        
        $data_center = $credentials['data_center'] ?? 'us';
        $oauth_url = self::$oauth_endpoints[$data_center] ?? self::$oauth_endpoints['us'];
        // ✅ Use the SAME canonical redirect URI as ajax_get_auth_url()
        $redirect_uri = self::get_canonical_redirect_uri();
        
        // ✅ Decrypt and validate client_secret before sending
        $client_secret = $this->decrypt($credentials['client_secret']);
        if (empty($client_secret)) {
            error_log('VMO Zoho OAuth callback: client_secret decryption failed.');
            $this->finish_oauth_callback(false, 'Failed to decrypt client secret. Please re-enter your Zoho credentials in the VMO wizard.', $state);
        }
        
        // Log the redirect_uri for debugging mismatches
        error_log('VMO Zoho OAuth: Token exchange redirect_uri = ' . $redirect_uri);
        
        $response = wp_remote_post($oauth_url . '/token', [
            'body' => [
                'grant_type' => 'authorization_code',
                'client_id' => $credentials['client_id'],
                'client_secret' => $client_secret,
                'redirect_uri' => $redirect_uri,
                'code' => $code
            ],
            'timeout' => 30
        ]);
        
        if (is_wp_error($response)) {
            $this->finish_oauth_callback(false, 'Failed to contact Zoho: ' . $response->get_error_message(), $state);
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if (empty($body['refresh_token'])) {
            $error_msg = $body['error'] ?? 'Unknown error';
            $this->finish_oauth_callback(false, 'Zoho did not return a refresh token. Error: ' . $error_msg . '. Please try again - make sure you click "Accept" on the Zoho authorization screen.', $state);
        }
        
        // Save the refresh token (encrypted)
        $credentials['refresh_token'] = $this->encrypt($body['refresh_token']);
        $credentials['token_obtained_at'] = current_time('mysql');
        $this->save_scoped_credentials($credentials, $owner_user_id);
        
        // Mark that the token was obtained (for the JS wizard to detect)
        $this->mark_token_just_obtained($owner_user_id);
        
        $this->finish_oauth_callback(true, 'Refresh token obtained and saved successfully! You can close this tab and return to the VMO wizard.', $state);
    }

    private function finish_oauth_callback($success, $message, $state = '') {
        $this->remember_oauth_status($state, $success ? 'success' : 'error', $message);
        $this->render_oauth_result_page($success, $message, $state);
        exit;
    }
    
    /**
     * ✅ Render a simple result page after OAuth callback
     */
    private function render_oauth_result_page($success, $message, $state = '') {
        $icon = $success ? '✅' : '❌';
        $color = $success ? '#22c55e' : '#ef4444';
        $bg = $success ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)';
        $payload = wp_json_encode([
            'type' => 'vmo_zoho_token_obtained',
            'success' => (bool) $success,
            'state' => (string) $state,
            'message' => (string) $message
        ]);
        
        echo '<!DOCTYPE html><html><head><title>VMO - Zoho Authorization</title>';
        echo '<style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#f1f5f9;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;}';
        echo '.card{background:#1e293b;border:1px solid ' . $color . ';border-radius:16px;padding:40px;max-width:500px;text-align:center;box-shadow:0 20px 40px rgba(0,0,0,0.3);}';
        echo '.icon{font-size:48px;margin-bottom:16px;}.msg{font-size:16px;line-height:1.6;color:#d1d5db;margin-bottom:24px;}';
        echo '.btn{display:inline-block;padding:12px 24px;border-radius:8px;background:' . $color . ';color:#fff;text-decoration:none;font-weight:600;font-size:14px;cursor:pointer;border:none;}';
        echo '</style></head><body>';
        echo '<div class="card"><div class="icon">' . $icon . '</div>';
        echo '<h2 style="color:' . $color . ';margin:0 0 12px 0;">' . ($success ? 'Authorization Successful!' : 'Authorization Failed') . '</h2>';
        echo '<p class="msg">' . esc_html($message) . '</p>';
        
        echo '<script>';
        echo 'if(window.opener){try{var targetOrigin=(window.location&&window.location.origin)?window.location.origin:"*";window.opener.postMessage(' . $payload . ',targetOrigin);}catch(e){}}';

        if ($success) {
            // Auto-close and notify opener window
            echo 'setTimeout(function(){window.close();},3000);';
            echo '<p style="color:#64748b;font-size:13px;">This window will close automatically in 3 seconds...</p>';
        } else {
            echo '<button class="btn" onclick="window.close()">Close</button>';
        }

        echo '</script>';
        
        echo '</div></body></html>';
    }
    
    /**
     * ✅ AJAX: Exchange authorization code for refresh token (manual code paste fallback)
     */
    public function ajax_exchange_code_for_refresh_token() {
        check_ajax_referer('vmo_builder_nonce', 'security');
        
        $user_id = $this->get_storage_user_id();
        $code = sanitize_text_field($_POST['code'] ?? '');
        
        if (empty($code)) {
            wp_send_json_error(['message' => 'Authorization code is required']);
            return;
        }
        
        $credentials = $this->get_scoped_credentials($user_id);
        
        if (empty($credentials['client_id']) || empty($credentials['client_secret'])) {
            wp_send_json_error(['message' => 'Save Client ID and Client Secret first']);
            return;
        }
        
        $data_center = $credentials['data_center'] ?? 'us';
        $oauth_url = self::$oauth_endpoints[$data_center] ?? self::$oauth_endpoints['us'];
        $redirect_uri = self::get_canonical_redirect_uri();
        
        $client_secret = $this->decrypt($credentials['client_secret']);
        if (empty($client_secret)) {
            wp_send_json_error(['message' => 'Failed to decrypt client secret. Please re-enter your credentials.']);
            return;
        }
        
        $response = wp_remote_post($oauth_url . '/token', [
            'body' => [
                'grant_type' => 'authorization_code',
                'client_id' => $credentials['client_id'],
                'client_secret' => $client_secret,
                'redirect_uri' => $redirect_uri,
                'code' => $code
            ],
            'timeout' => 30
        ]);
        
        if (is_wp_error($response)) {
            wp_send_json_error(['message' => 'Failed to contact Zoho: ' . $response->get_error_message()]);
            return;
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if (empty($body['refresh_token'])) {
            $error_msg = $body['error'] ?? 'unknown_error';
            wp_send_json_error(['message' => 'Zoho did not return a refresh token. Error: ' . $error_msg . '. The authorization code may have expired — they are single-use and expire quickly.']);
            return;
        }
        
        // Save the refresh token
        $credentials['refresh_token'] = $this->encrypt($body['refresh_token']);
        $credentials['token_obtained_at'] = current_time('mysql');
        $this->save_scoped_credentials($credentials, $user_id);
        
        wp_send_json_success([
            'message' => 'Refresh token obtained and saved!',
            'has_refresh_token' => true
        ]);
    }
    
    /**
     * ✅ AJAX: Check if refresh token was just obtained (polled by JS after OAuth popup)
     */
    public function ajax_check_token_status() {
        check_ajax_referer('vmo_builder_nonce', 'security');

        $state = sanitize_text_field($_POST['oauth_state'] ?? '');

        if (!empty($state)) {
            $status = get_transient(self::get_oauth_status_cache_key($state));

            if (is_array($status)) {
                $status_name = $status['status'] ?? 'pending';

                if ($status_name === 'success' || $status_name === 'error') {
                    delete_transient(self::get_oauth_status_cache_key($state));
                }

                wp_send_json_success([
                    'token_obtained' => $status_name === 'success',
                    'status' => $status_name,
                    'message' => $status['message'] ?? ''
                ]);
                return;
            }

            wp_send_json_success([
                'token_obtained' => false,
                'status' => 'pending',
                'message' => ''
            ]);
            return;
        }
        
        if ($this->consume_token_just_obtained($this->get_storage_user_id())) {
            wp_send_json_success(['token_obtained' => true]);
        } else {
            wp_send_json_success(['token_obtained' => false]);
        }
    }
    
    /**
     * Get access token using refresh token (with transient caching)
     */
    private function get_access_token($credentials, $user_id = null) {
        if (empty($credentials['refresh_token']) || empty($credentials['client_id'])) {
            return null;
        }
        
        // ✅ Check transient cache first — Zoho tokens are valid for 3600s
        $cache_key = $this->get_access_token_cache_key($credentials, $user_id);
        $cached = get_transient($cache_key);
        if ($cached) {
            return $cached;
        }
        
        $data_center = $credentials['data_center'] ?? 'us';
        $oauth_url = self::$oauth_endpoints[$data_center] ?? self::$oauth_endpoints['us'];
        
        // ✅ Validate decryption before sending to Zoho
        $refresh_token = $this->decrypt($credentials['refresh_token']);
        $client_secret = $this->decrypt($credentials['client_secret']);
        
        if (empty($refresh_token) || empty($client_secret)) {
            error_log('VMO Zoho: Decryption of refresh_token or client_secret failed.');
            return null;
        }
        
        $response = wp_remote_post($oauth_url . '/token', [
            'body' => [
                'refresh_token' => $refresh_token,
                'client_id' => $credentials['client_id'],
                'client_secret' => $client_secret,
                'grant_type' => 'refresh_token'
            ],
            'timeout' => 30
        ]);
        
        if (is_wp_error($response)) {
            error_log('VMO Zoho: Token refresh failed - ' . $response->get_error_message());
            return null;
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if (isset($body['access_token'])) {
            // ✅ Cache the access token — expire 100s before Zoho's 3600s TTL
            set_transient($cache_key, $body['access_token'], 3500);
            return $body['access_token'];
        }
        
        error_log('VMO Zoho: Token refresh failed - ' . json_encode($body));
        return null;
    }
    
    /**
     * Get organization info
     */
    private function get_organization_info($access_token, $data_center) {
        $result = $this->api_request('GET', '/organization', [], $access_token, $data_center);
        return !empty($result['_ok']) ? ($result['data'] ?? null) : null;
    }
    
    /**
     * Make API request to Zoho Mail
     */
    private function api_request($method, $endpoint, $data, $access_token, $data_center) {
        $base_url = self::$api_endpoints[$data_center] ?? self::$api_endpoints['us'];
        $url = $base_url . $endpoint;
        
        $args = [
            'method' => $method,
            'headers' => [
                'Authorization' => 'Zoho-oauthtoken ' . $access_token,
                'Content-Type' => 'application/json'
            ],
            'timeout' => 30
        ];
        
        if ($method === 'POST' || $method === 'PUT') {
            $args['body'] = json_encode($data);
        } elseif (!empty($data)) {
            $url .= '?' . http_build_query($data);
        }
        
        $response = wp_remote_request($url, $args);
        
        if (is_wp_error($response)) {
            error_log('VMO Zoho API Error: ' . $response->get_error_message());
            return [
                '_ok' => false,
                '_http_status' => 0,
                '_raw_body' => '',
                'message' => $response->get_error_message(),
            ];
        }

        $status = intval(wp_remote_retrieve_response_code($response));
        $raw_body = wp_remote_retrieve_body($response);
        $decoded = json_decode($raw_body, true);
        if (!is_array($decoded)) {
            $decoded = [];
        }

        $decoded['_ok'] = $status >= 200 && $status < 300;
        $decoded['_http_status'] = $status;
        $decoded['_raw_body'] = $raw_body;

        if (!$decoded['_ok'] && empty($decoded['message'])) {
            $decoded['message'] = $this->extract_api_error_message($decoded, 'Zoho request failed.');
        }

        if (!$decoded['_ok']) {
            error_log('VMO Zoho API HTTP ' . $status . ': ' . $decoded['message']);
        }

        return $decoded;
    }
    
    /**
     * Find primary domain
     */
    private function find_primary_domain($domains) {
        foreach ($domains as $domain) {
            if (!empty($domain['isPrimary'])) {
                return $domain['domainName'] ?? null;
            }
        }
        return $domains[0]['domainName'] ?? null;
    }
    
    /**
     * Save email to VMO database
     */
    private function save_email_to_vmo($user_id, $email, $category, $account_id, $display_name = '') {
        global $wpdb;
        $table = $wpdb->prefix . 'vmo_email_accounts';
        
        // Create table if needed
        $this->maybe_create_table();
        
        $domain = substr($email, strpos($email, '@') + 1);
        $storage_user_id = $this->get_storage_user_id($user_id);

        if (function_exists('vmo_upsert_email_account')) {
            return vmo_upsert_email_account($storage_user_id, array(
                'email' => $email,
                'email_address' => $email,
                'category' => $category,
                'role' => $category,
                'display_name' => $display_name,
                'domain' => $domain,
                'provider_preset' => 'zoho',
                'provider_type' => 'provisioned',
                'provisioned_at' => current_time('mysql'),
                'provisioned_via' => 'zoho',
                'external_id' => $account_id,
                'status' => 'active',
                'verified' => 1,
            ));
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $existing_id = $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$table} WHERE user_id = %d AND email_address = %s LIMIT 1",
            $storage_user_id,
            $email
        ));

        $payload = [
            'user_id' => $storage_user_id,
            'email_address' => $email,
            'category' => $category,
            'domain' => $domain,
            'provisioned_at' => current_time('mysql'),
            'provisioned_via' => 'zoho',
            'external_id' => $account_id,
            'status' => 'active'
        ];

        if ($existing_id) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->update($table, $payload, ['id' => intval($existing_id)]);
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->insert($table, $payload);
        }

        if (function_exists('vmo_sync_email_to_sender_identities')) {
            vmo_sync_email_to_sender_identities($storage_user_id, $email, $category, $display_name, 'verified');
        }

        return $result;
    }
    
    /**
     * Create table if needed
     */
    private function maybe_create_table() {
        global $wpdb;
        $table = $wpdb->prefix . 'vmo_email_accounts';
        
        if (function_exists('vmo_ensure_email_accounts_table')) {
            vmo_ensure_email_accounts_table();
            return;
        }

        if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) {
            return;
        }
        
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE $table (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL,
            email_address varchar(255) NOT NULL,
            category varchar(50) DEFAULT 'general',
            domain varchar(255) NOT NULL,
            provisioned_at datetime DEFAULT CURRENT_TIMESTAMP,
            provisioned_via varchar(50) DEFAULT 'unknown',
            external_id varchar(255) DEFAULT NULL,
            status varchar(20) DEFAULT 'active',
            PRIMARY KEY (id),
            KEY user_id (user_id),
            KEY email_address (email_address)
        ) $charset;";
        
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);
    }
    
    /**
     * Generate secure password
     */
    private function generate_secure_password($length = 16) {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
        $password = '';
        for ($i = 0; $i < $length; $i++) {
            $password .= $chars[random_int(0, strlen($chars) - 1)];
        }
        return $password;
    }
    
    /**
     * Encrypt sensitive data
     */
    private function encrypt($data) {
        $key = $this->get_encryption_key();
        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
        return base64_encode($iv . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- encryption/decryption
    }
    
    /**
     * Decrypt sensitive data
     */
    private function decrypt($encrypted) {
        $key = $this->get_encryption_key();
        $data = base64_decode($encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- encryption/decryption
        $iv = substr($data, 0, 16);
        $encrypted_data = substr($data, 16);
        return openssl_decrypt($encrypted_data, 'AES-256-CBC', $key, 0, $iv);
    }
    
    /**
     * Get encryption key
     */
    private function get_encryption_key() {
        $key = get_option('vmo_encryption_key');
        if (!$key) {
            $key = base64_encode(random_bytes(32)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- data encoding
            update_option('vmo_encryption_key', $key, false);
        }
        return base64_decode($key); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- data decoding
    }
}

// Initialize
new VMO_Zoho_Mail_Integration();
