<?php

/**
 * Spellmatic_BYOK
 * Secure Bring-Your-Own-Key handler for LLM providers.
 * The user's API key is stored encrypted in wp_options.
 * It is NEVER exposed in REST responses, source code, or logs.
 *
 * Supported providers: openai | claude | gemini | grok | openrouter
 * Phase 5: Full LLM proxy — contextual error detection, REST endpoints.
 */
defined( 'ABSPATH' ) || exit;
class Spellmatic_BYOK {
    const OPTION_KEY = 'spellmatic_byok';

    const OPTION_OR_MODEL = 'spellmatic_or_model';

    /** Default model per provider (cheapest/fastest for spell-check use case) */
    const MODELS = [
        'openai'     => 'gpt-4o-mini',
        'claude'     => 'claude-haiku-4-5-20251001',
        'gemini'     => 'gemini-2.5-flash',
        'openrouter' => 'arcee-ai/trinity-mini:free',
        'grok'       => 'grok-3-mini',
    ];

    // =========================================================================
    // Key storage
    // =========================================================================
    public static function save_key( string $provider, string $raw_key ) : bool {
        $provider = sanitize_key( $provider );
        $raw_key = sanitize_text_field( trim( $raw_key ) );
        if ( empty( $raw_key ) ) {
            return self::delete_key( $provider );
        }
        $stored = get_option( self::OPTION_KEY, [] );
        $stored[$provider] = self::encrypt( $raw_key );
        return update_option( self::OPTION_KEY, $stored, false );
    }

    public static function delete_key( string $provider ) : bool {
        $stored = get_option( self::OPTION_KEY, [] );
        unset($stored[sanitize_key( $provider )]);
        return update_option( self::OPTION_KEY, $stored, false );
    }

    public static function get_key( string $provider ) : string {
        $stored = get_option( self::OPTION_KEY, [] );
        $encrypted = $stored[sanitize_key( $provider )] ?? '';
        if ( empty( $encrypted ) ) {
            return '';
        }
        return self::decrypt( $encrypted );
    }

    public static function has_key( string $provider ) : bool {
        $stored = get_option( self::OPTION_KEY, [] );
        return !empty( $stored[sanitize_key( $provider )] );
    }

    /** Status for settings UI — never exposes the full key. */
    public static function get_status() : array {
        $status = [];
        $providers = self::FREE_PROVIDERS;
        foreach ( $providers as $p ) {
            $key = self::get_key( $p );
            $status[$p] = [
                'connected' => !empty( $key ),
                'masked'    => ( !empty( $key ) ? self::mask( $key ) : '' ),
            ];
        }
        return $status;
    }

    /** Return the currently configured active provider, or auto-select first connected one. */
    public static function get_active_provider() : string {
        $settings = get_option( 'spellmatic_settings', [] );
        $explicit = sanitize_key( $settings['byok_provider'] ?? '' );
        $providers = self::FREE_PROVIDERS;
        if ( $explicit && in_array( $explicit, $providers, true ) && self::has_key( $explicit ) ) {
            return $explicit;
        }
        foreach ( $providers as $p ) {
            if ( self::has_key( $p ) ) {
                return $p;
            }
        }
        return '';
    }

    public static function get_openrouter_model() : string {
        return sanitize_text_field( get_option( self::OPTION_OR_MODEL, self::MODELS['openrouter'] ) );
    }

    public static function set_openrouter_model( string $model ) : bool {
        return update_option( self::OPTION_OR_MODEL, sanitize_text_field( $model ), false );
    }

    // =========================================================================
    // AI scan settings (enabled toggle + max items)
    // =========================================================================
    /** Approximate cost per item for each provider (in USD, shown as a label). */
    const CREDIT_ESTIMATES = [
        'openai'     => [
            'label'    => 'GPT-4o mini',
            'per_item' => '< $0.001',
            'note'     => '$0.15 / 1M input tokens',
        ],
        'claude'     => [
            'label'    => 'Claude Haiku',
            'per_item' => '~$0.001',
            'note'     => '$0.80 / 1M input tokens',
        ],
        'gemini'     => [
            'label'    => 'Gemini 2.5 Flash',
            'per_item' => '< $0.001',
            'note'     => '$0.075 / 1M input tokens',
        ],
        'grok'       => [
            'label'    => 'Grok 3 Mini',
            'per_item' => '~$0.001',
            'note'     => '$0.30 / 1M input tokens',
        ],
        'openrouter' => [
            'label'    => 'OpenRouter (free tier)',
            'per_item' => 'Free',
            'note'     => 'Free models available',
        ],
    ];

    public static function get_ai_settings() : array {
        $settings = get_option( 'spellmatic_settings', [] );
        return [
            'ai_enabled'   => ( isset( $settings['ai_enabled'] ) ? (bool) $settings['ai_enabled'] : true ),
            'ai_max_items' => max( 1, min( 50, (int) ($settings['ai_max_items'] ?? 5) ) ),
        ];
    }

    // =========================================================================
    // REST: GET /byok/status
    // =========================================================================
    public static function status_via_rest( WP_REST_Request $request ) : WP_REST_Response {
        return new WP_REST_Response([
            'providers'       => self::get_status(),
            'activeProvider'  => self::get_active_provider(),
            'openrouterModel' => self::get_openrouter_model(),
            'aiSettings'      => self::get_ai_settings(),
            'creditEstimates' => self::CREDIT_ESTIMATES,
        ], 200);
    }

    // =========================================================================
    // REST: POST /byok/ai-settings
    // =========================================================================
    public static function save_ai_settings_via_rest( WP_REST_Request $request ) : WP_REST_Response {
        $settings = get_option( 'spellmatic_settings', [] );
        $enabled = $request->get_param( 'ai_enabled' );
        if ( $enabled !== null ) {
            $settings['ai_enabled'] = (bool) $enabled;
        }
        $max = $request->get_param( 'ai_max_items' );
        if ( $max !== null ) {
            $settings['ai_max_items'] = max( 1, min( 50, (int) $max ) );
        }
        update_option( 'spellmatic_settings', $settings, false );
        return new WP_REST_Response([
            'saved'      => true,
            'aiSettings' => self::get_ai_settings(),
        ], 200);
    }

    // =========================================================================
    // REST: POST /byok/save
    // =========================================================================
    public static function save_via_rest( WP_REST_Request $request ) : WP_REST_Response {
        $provider = sanitize_key( $request->get_param( 'provider' ) ?? '' );
        $key = sanitize_text_field( $request->get_param( 'key' ) ?? '' );
        $allowed = self::FREE_PROVIDERS;
        if ( !in_array( $provider, $allowed, true ) ) {
            return new WP_REST_Response([
                'error' => 'Invalid provider.',
            ], 400);
        }
        $saved = ( empty( $key ) ? self::delete_key( $provider ) : self::save_key( $provider, $key ) );
        return new WP_REST_Response([
            'saved'    => $saved,
            'provider' => $provider,
            'status'   => self::get_status()[$provider],
        ], 200);
    }

    // =========================================================================
    // REST: POST /byok/test
    // =========================================================================
    public static function test_via_rest( WP_REST_Request $request ) : WP_REST_Response {
        $provider = sanitize_key( $request->get_param( 'provider' ) ?? '' );
        $allowed = self::FREE_PROVIDERS;
        if ( !in_array( $provider, $allowed, true ) ) {
            return new WP_REST_Response([
                'ok'      => false,
                'message' => 'This provider is not available.',
            ], 200);
        }
        $api_key = self::get_key( $provider );
        if ( empty( $api_key ) ) {
            return new WP_REST_Response([
                'ok'      => false,
                'message' => 'No API key stored for this provider.',
            ], 200);
        }
        // Deliberately contains "their/they're" and "to/too" errors so
        // a successful connection always returns at least one finding.
        $test_items = [[
            'post_id'   => 0,
            'post_type' => 'test',
            'field'     => 'test',
            'text'      => 'Their going to the store to buy to many things. Its a great day and there very happy.',
        ]];
        try {
            switch ( $provider ) {
                case 'openai':
                    self::call_openai( $api_key, $test_items );
                    break;
                case 'claude':
                    self::call_claude( $api_key, $test_items );
                    break;
                case 'gemini':
                    self::call_gemini( $api_key, $test_items );
                    break;
                case 'grok':
                    self::call_grok( $api_key, $test_items );
                    break;
                case 'openrouter':
                    self::call_openrouter( $api_key, $test_items );
                    break;
                default:
                    return new WP_REST_Response([
                        'ok'      => false,
                        'message' => 'Unknown provider.',
                    ], 200);
            }
            return new WP_REST_Response([
                'ok'      => true,
                'message' => 'Connection successful.',
            ], 200);
        } catch ( \Throwable $e ) {
            return new WP_REST_Response([
                'ok'      => false,
                'message' => esc_html( $e->getMessage() ),
            ], 200);
        }
    }

    // =========================================================================
    // REST: POST /scan/ai
    // =========================================================================
    /** Providers available in the free version. */
    const FREE_PROVIDERS = ['openai'];

    /** All supported AI providers. */
    const ALL_PROVIDERS = [
        'openai',
        'claude',
        'gemini',
        'grok',
        'openrouter'
    ];

    public static function scan_ai( WP_REST_Request $request ) : WP_REST_Response {
        $scan_id = sanitize_text_field( $request->get_param( 'scan_id' ) ?? '' );
        $provider = sanitize_key( $request->get_param( 'provider' ) ?? '' );
        $items = $request->get_param( 'items' );
        if ( !is_array( $items ) || empty( $items ) ) {
            return new WP_REST_Response([
                'error' => 'No items provided.',
            ], 400);
        }
        // Sanitise each item.
        $clean = [];
        foreach ( $items as $item ) {
            if ( empty( $item['text'] ) ) {
                continue;
            }
            $clean[] = [
                'post_id'   => absint( $item['post_id'] ?? 0 ),
                'post_type' => sanitize_key( $item['post_type'] ?? 'post' ),
                'field'     => sanitize_key( $item['field'] ?? 'post_content' ),
                'text'      => sanitize_textarea_field( $item['text'] ),
            ];
        }
        // Cap at 5 items — one REST call = one LLM API request.
        $clean = array_slice( $clean, 0, 5 );
        if ( empty( $clean ) ) {
            return new WP_REST_Response([
                'stored'      => 0,
                'errors'      => [],
                'error_count' => 0,
            ], 200);
        }
        // Resolve provider.
        if ( empty( $provider ) || !self::has_key( $provider ) ) {
            $provider = self::get_active_provider();
        }
        if ( empty( $provider ) ) {
            return new WP_REST_Response([
                'stored'      => 0,
                'errors'      => [],
                'error_count' => 0,
                'warning'     => 'No AI provider configured.',
            ], 200);
        }
        $api_key = self::get_key( $provider );
        $ai_warning = '';
        $ai_errors = [];
        $used_provider = $provider;
        // AI HTTP calls can take up to 45 s; extend PHP execution limit for this request only.
        // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Intentional: AI API calls need extra time.
        set_time_limit( 90 );
        try {
            $ai_errors = match ($provider) {
                'openai' => self::call_openai( $api_key, $clean ),
                'claude' => self::call_claude( $api_key, $clean ),
                'gemini' => self::call_gemini( $api_key, $clean ),
                'grok' => self::call_grok( $api_key, $clean ),
                'openrouter' => self::call_openrouter( $api_key, $clean ),
                default => [],
            };
        } catch ( \Throwable $e ) {
            $ai_warning = $e->getMessage();
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Guarded by WP_DEBUG; intentional diagnostic logging.
                error_log( 'Spellmatic BYOK [' . $provider . ']: ' . $ai_warning );
            }
        }
        // Persist errors to the results table — skip duplicates of Harper findings.
        $stored = 0;
        $skipped = 0;
        $stored_errors = [];
        if ( !empty( $ai_errors ) && !empty( $scan_id ) ) {
            global $wpdb;
            foreach ( $ai_errors as $err ) {
                if ( empty( $err['original'] ) || empty( $err['suggestion'] ) ) {
                    continue;
                }
                $post_id = absint( $err['post_id'] ?? 0 );
                $original = sanitize_text_field( $err['original'] );
                // Check for existing open result with same post + word.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dedup check inside a write loop; caching not applicable.
                $existing = $wpdb->get_var( $wpdb->prepare( 
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name built from $wpdb->prefix, safe.
                    "SELECT id FROM {$wpdb->prefix}spellmatic_results\n                     WHERE post_id = %d AND original = %s AND status = 'open'\n                     LIMIT 1",
                    $post_id,
                    $original
                 ) );
                if ( $existing ) {
                    $skipped++;
                    continue;
                }
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Intentional insert into custom table.
                $wpdb->insert( $wpdb->prefix . 'spellmatic_results', [
                    'post_id'      => $post_id,
                    'post_type'    => sanitize_key( $err['post_type'] ?? 'post' ),
                    'field'        => sanitize_key( $err['field'] ?? 'post_content' ),
                    'error_type'   => sanitize_key( $err['error_type'] ?? 'grammar' ),
                    'original'     => $original,
                    'suggestion'   => sanitize_text_field( $err['suggestion'] ),
                    'context'      => sanitize_text_field( $err['context'] ?? '' ),
                    'offset_start' => 0,
                    'offset_end'   => 0,
                    'status'       => 'open',
                    'scan_id'      => $scan_id,
                    'created_at'   => current_time( 'mysql' ),
                    'updated_at'   => current_time( 'mysql' ),
                ] );
                $err['id'] = $wpdb->insert_id;
                $err['post_title'] = ( get_the_title( $post_id ) ?: 'Post #' . $post_id );
                $stored_errors[] = $err;
                $stored++;
            }
        }
        $resp = [
            'stored'      => $stored,
            'errors'      => $stored_errors,
            'error_count' => count( $stored_errors ),
            'provider'    => $used_provider,
        ];
        if ( $ai_warning ) {
            $resp['warning'] = $ai_warning;
        }
        return new WP_REST_Response($resp, 200);
    }

    // =========================================================================
    // Provider: OpenAI
    // =========================================================================
    private static function call_openai( string $api_key, array $items ) : array {
        $body = wp_json_encode( [
            'model'           => self::MODELS['openai'],
            'messages'        => [[
                'role'    => 'system',
                'content' => self::system_prompt(),
            ], [
                'role'    => 'user',
                'content' => self::build_prompt( $items ),
            ]],
            'response_format' => [
                'type' => 'json_object',
            ],
            'max_tokens'      => 4096,
            'temperature'     => 0,
        ] );
        $r = self::http_post(
            'https://api.openai.com/v1/chat/completions',
            ['Authorization: Bearer ' . $api_key, 'Content-Type: application/json'],
            $body,
            'OpenAI'
        );
        $data = json_decode( $r['body'], true );
        if ( $r['code'] !== 200 ) {
            $msg = $data['error']['message'] ?? "HTTP {$r['code']}";
            throw new \RuntimeException('OpenAI error: ' . esc_html( $msg ));
        }
        $llm_content = $data['choices'][0]['message']['content'] ?? '{}';
        return self::parse_llm_response( $llm_content, $items );
    }

    // =========================================================================
    // Provider: Claude (Anthropic)
    // =========================================================================
    private static function call_claude( string $api_key, array $items ) : array {
        $body = wp_json_encode( [
            'model'      => self::MODELS['claude'],
            'max_tokens' => 4096,
            'system'     => self::system_prompt(),
            'messages'   => [[
                'role'    => 'user',
                'content' => self::build_prompt( $items ),
            ]],
        ] );
        $r = self::http_post(
            'https://api.anthropic.com/v1/messages',
            ['x-api-key: ' . $api_key, 'anthropic-version: 2023-06-01', 'content-type: application/json'],
            $body,
            'Claude'
        );
        $data = json_decode( $r['body'], true );
        if ( $r['code'] !== 200 ) {
            $msg = $data['error']['message'] ?? "HTTP {$r['code']}";
            throw new \RuntimeException('Claude error: ' . esc_html( $msg ));
        }
        return self::parse_llm_response( $data['content'][0]['text'] ?? '[]', $items );
    }

    // =========================================================================
    // Provider: Google Gemini
    // =========================================================================
    private static function call_gemini( string $api_key, array $items ) : array {
        $model = self::MODELS['gemini'];
        $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";
        $prompt = self::system_prompt() . "\n\n" . self::build_prompt( $items );
        $body = wp_json_encode( [
            'contents'         => [[
                'parts' => [[
                    'text' => $prompt,
                ]],
            ]],
            'generationConfig' => [
                'responseMimeType' => 'application/json',
                'maxOutputTokens'  => 4096,
                'temperature'      => 0,
            ],
        ] );
        $r = self::http_post(
            $url,
            ['Content-Type: application/json'],
            $body,
            'Gemini'
        );
        $data = json_decode( $r['body'], true );
        if ( $r['code'] !== 200 ) {
            $msg = $data['error']['message'] ?? "HTTP {$r['code']}";
            throw new \RuntimeException('Gemini error: ' . esc_html( $msg ));
        }
        return self::parse_llm_response( $data['candidates'][0]['content']['parts'][0]['text'] ?? '[]', $items );
    }

    // =========================================================================
    // Provider: Grok (xAI)
    // =========================================================================
    private static function call_grok( string $api_key, array $items ) : array {
        $body = wp_json_encode( [
            'model'       => self::MODELS['grok'],
            'messages'    => [[
                'role'    => 'system',
                'content' => self::system_prompt(),
            ], [
                'role'    => 'user',
                'content' => self::build_prompt( $items ),
            ]],
            'max_tokens'  => 4096,
            'temperature' => 0,
        ] );
        $r = self::http_post(
            'https://api.x.ai/v1/chat/completions',
            ['Authorization: Bearer ' . $api_key, 'Content-Type: application/json'],
            $body,
            'Grok'
        );
        $data = json_decode( $r['body'], true );
        if ( $r['code'] !== 200 ) {
            $msg = $data['error']['message'] ?? "HTTP {$r['code']}";
            throw new \RuntimeException('Grok error: ' . esc_html( $msg ));
        }
        return self::parse_llm_response( $data['choices'][0]['message']['content'] ?? '[]', $items );
    }

    // =========================================================================
    // Provider: OpenRouter
    // =========================================================================
    private static function call_openrouter( string $api_key, array $items ) : array {
        $body = wp_json_encode( [
            'model'       => self::get_openrouter_model(),
            'messages'    => [[
                'role'    => 'system',
                'content' => self::system_prompt(),
            ], [
                'role'    => 'user',
                'content' => self::build_prompt( $items ),
            ]],
            'max_tokens'  => 4096,
            'temperature' => 0,
        ] );
        $r = self::http_post(
            'https://openrouter.ai/api/v1/chat/completions',
            [
                'Authorization: Bearer ' . $api_key,
                'Content-Type: application/json',
                'HTTP-Referer: ' . home_url(),
                'X-Title: Spellmatic'
            ],
            $body,
            'OpenRouter'
        );
        $data = json_decode( $r['body'], true );
        if ( $r['code'] !== 200 ) {
            $msg = $data['error']['message'] ?? $data['message'] ?? "HTTP {$r['code']}";
            throw new \RuntimeException('OpenRouter error: ' . esc_html( $msg ));
        }
        return self::parse_llm_response( $data['choices'][0]['message']['content'] ?? '[]', $items );
    }

    // =========================================================================
    // Prompt helpers
    // =========================================================================
    private static function system_prompt() : string {
        return 'You are a professional proofreader for WordPress websites. ' . 'Find contextual spelling and grammar errors that basic spell-checkers miss — ' . 'wrong homophones (their/there/they\'re), misused words, clear grammatical mistakes. ' . 'Do NOT flag: proper nouns, brand names, technical terms, URLs, shortcodes, HTML, ' . 'or intentional stylistic choices. ' . 'Respond ONLY with a JSON object: {"errors": [...]}. ' . 'Each error object must have: post_id (integer), post_type (string), field (string), ' . 'original (the exact wrong text, max 10 words), suggestion (the correction), ' . 'context (surrounding text, max 100 chars), error_type ("spelling" or "grammar"). ' . 'If no errors are found, return {"errors": []}.';
    }

    private static function build_prompt( array $items ) : string {
        $lines = [];
        foreach ( $items as $i => $item ) {
            $lines[] = sprintf(
                "Item %d — post_id:%d post_type:%s field:%s\nText: %s",
                $i + 1,
                $item['post_id'],
                $item['post_type'],
                $item['field'],
                $item['text']
            );
        }
        return "Check these WordPress content items for contextual errors:\n\n" . implode( "\n\n", $lines );
    }

    /** Strip markdown fences, parse JSON, and validate the error structure. */
    private static function parse_llm_response( string $content, array $items ) : array {
        // Strip markdown code fences if the model wrapped its JSON.
        $content = preg_replace( '/^```(?:json)?\\s*/m', '', $content );
        $content = preg_replace( '/\\s*```\\s*$/m', '', $content );
        $content = trim( $content );
        $decoded = json_decode( $content, true );
        // Accept {"errors":[...]} or bare [...] formats.
        if ( isset( $decoded['errors'] ) && is_array( $decoded['errors'] ) ) {
            $raw = $decoded['errors'];
        } elseif ( is_array( $decoded ) && (empty( $decoded ) || array_key_exists( 0, $decoded )) ) {
            $raw = $decoded;
        } else {
            return [];
        }
        // Build lookup for valid item metadata.
        $item_map = [];
        foreach ( $items as $item ) {
            $item_map[$item['post_id']] = $item;
        }
        $valid = [];
        foreach ( $raw as $err ) {
            if ( empty( $err['original'] ) || empty( $err['suggestion'] ) ) {
                continue;
            }
            $pid = absint( $err['post_id'] ?? 0 );
            $source = $item_map[$pid] ?? (( !empty( $items ) ? $items[0] : [] ));
            $valid[] = [
                'post_id'    => ( $pid ?: $source['post_id'] ?? 0 ),
                'post_type'  => sanitize_key( $err['post_type'] ?? $source['post_type'] ?? 'post' ),
                'field'      => sanitize_key( $err['field'] ?? $source['field'] ?? 'post_content' ),
                'original'   => sanitize_text_field( $err['original'] ),
                'suggestion' => sanitize_text_field( $err['suggestion'] ),
                'context'    => sanitize_text_field( $err['context'] ?? '' ),
                'error_type' => ( in_array( $err['error_type'] ?? '', ['spelling', 'grammar'], true ) ? $err['error_type'] : 'grammar' ),
            ];
        }
        return $valid;
    }

    // =========================================================================
    // HTTP transport — native PHP cURL
    // =========================================================================
    /**
     * POST JSON to an AI API endpoint via WordPress HTTP API.
     *
     * @param  string   $url
     * @param  string[] $headers  "Name: Value" format.
     * @param  string   $body     JSON-encoded body.
     * @param  string   $label    Provider name for error messages.
     * @return array    ['code' => int, 'body' => string]
     * @throws \RuntimeException on connection failure.
     */
    private static function http_post(
        string $url,
        array $headers,
        string $body,
        string $label
    ) : array {
        // Convert headers from "Name: Value" format to array format
        $headers_array = [];
        foreach ( $headers as $header ) {
            if ( strpos( $header, ':' ) !== false ) {
                [$name, $value] = explode( ':', $header, 2 );
                $headers_array[trim( $name )] = trim( $value );
            }
        }
        $args = [
            'method'    => 'POST',
            'headers'   => $headers_array,
            'body'      => $body,
            'timeout'   => 60,
            'sslverify' => self::ssl_verify(),
        ];
        $response = wp_remote_post( $url, $args );
        if ( is_wp_error( $response ) ) {
            throw new \RuntimeException(esc_html( $label . ' request failed: ' . $response->get_error_message() ));
        }
        $http_code = (int) wp_remote_retrieve_response_code( $response );
        $body_str = wp_remote_retrieve_body( $response );
        return [
            'code' => $http_code,
            'body' => $body_str,
        ];
    }

    // =========================================================================
    // Helpers
    // =========================================================================
    /**
     * SSL verification for outbound requests.
     * Defaults to true. Local dev override: define( 'SPELLMATIC_BYOK_SSLVERIFY', false );
     */
    private static function ssl_verify() : bool {
        if ( defined( 'SPELLMATIC_BYOK_SSLVERIFY' ) ) {
            return (bool) SPELLMATIC_BYOK_SSLVERIFY;
        }
        return true;
    }

    private static function encrypt( string $plaintext ) : string {
        if ( !function_exists( 'openssl_encrypt' ) ) {
            return base64_encode( $plaintext );
        }
        $key = hash( 'sha256', AUTH_KEY . SECURE_AUTH_KEY );
        $iv = openssl_random_pseudo_bytes( 16 );
        $cipher = openssl_encrypt(
            $plaintext,
            'AES-256-CBC',
            $key,
            0,
            $iv
        );
        return base64_encode( $iv . '::' . $cipher );
    }

    private static function decrypt( string $encoded ) : string {
        if ( !function_exists( 'openssl_decrypt' ) ) {
            return base64_decode( $encoded );
        }
        $decoded = base64_decode( $encoded );
        if ( !str_contains( $decoded, '::' ) ) {
            return $decoded;
        }
        [$iv, $cipher] = explode( '::', $decoded, 2 );
        $key = hash( 'sha256', AUTH_KEY . SECURE_AUTH_KEY );
        return ( openssl_decrypt(
            $cipher,
            'AES-256-CBC',
            $key,
            0,
            $iv
        ) ?: '' );
    }

    private static function mask( string $key ) : string {
        $len = strlen( $key );
        if ( $len <= 10 ) {
            return str_repeat( '•', $len );
        }
        return substr( $key, 0, 6 ) . str_repeat( '•', max( 4, $len - 10 ) ) . substr( $key, -4 );
    }

}
