<?php
/**
 * Secure API Key Management System
 *
 * This file contains the INFYP_API_Key_Manager class which provides centralized,
 * secure management of API keys for multiple AI providers. It implements encryption,
 * validation, and secure access patterns to protect sensitive API credentials.
 *
 * Security Features:
 * - Encryption at rest using WordPress salts
 * - In-memory caching (cleared on shutdown)
 * - Validation of API key formats
 * - Secure migration from plaintext to encrypted keys
 * - Provider-based key organization
 *
 * Supported Providers:
 * - OpenAI (GPT models)
 * - Google Gemini (Gemini models)
 * - Anthropic (Claude models)
 *
 * @package    AI_Image_SEO
 * @subpackage Core
 * @since 1.0.0
 * @author     InfyPress
 * @license    GPL-2.0+
 */

namespace IPAIS\Core;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * API Key Manager for Secure Credential Storage
 *
 * Implements singleton pattern to provide centralized API key management with
 * encryption, validation, and secure access controls. All API keys are encrypted
 * before storage and decrypted only when needed, with in-memory caching for
 * performance.
 *
 * Design Pattern: Singleton
 * Ensures only one instance manages API keys, providing consistent encryption
 * keys and cache state throughout the request lifecycle.
 *
 * Encryption Strategy:
 * API keys are encrypted using WordPress salts (AUTH_KEY, SECURE_AUTH_KEY, etc.)
 * which are unique per installation. This provides encryption at rest while
 * allowing decryption when needed for API calls.
 *
 * Key Storage Structure:
 * ```
 * infyp_api_keys => [
 *     'openai' => 'encrypted_key_string',
 *     'google' => 'encrypted_key_string',
 *     'anthropic' => 'encrypted_key_string'
 * ]
 * ```
 *
 * Usage Example:
 * ```php
 * $manager = INFYP_API_Key_Manager::get_instance();
 *
 * // Store encrypted API key
 * $manager->set_api_key( 'openai', 'sk-...' );
 *
 * // Retrieve decrypted key
 * $api_key = $manager->get_api_key( 'openai' );
 *
 * // Check if key exists
 * if ( $manager->has_api_key( 'google' ) ) {
 *     // Use Gemini API
 * }
 * ```
 *
 * @package    AI_Image_SEO
 * @subpackage Core
 * @since 1.0.0
 *
 * @see INFYP_Encryption_Service For encryption implementation
 * @see get_option() For retrieving stored keys
 * @see update_option() For storing encrypted keys
 */
class INFYP_API_Key_Manager {
    
    /**
     * Singleton instance
     *
     * @since 1.0.0
     * @var    INFYP_API_Key_Manager|null
     * @access private
     */
    private static $instance = null;

    /**
     * Encryption service instance
     *
     * @since 1.0.0
     * @var    INFYP_Encryption_Service
     * @access private
     */
    private $encryption;

    /**
     * In-memory cache for decrypted API keys
     *
     * Stores decrypted keys temporarily during request to avoid repeated
     * decryption operations. Cleared on shutdown for security.
     *
     * @since 1.0.0
     * @var    array
     * @access private
     */
    private $key_cache = [];

    /**
     * Built-in API provider slugs.
     *
     * @since 1.0.0
     * @var   array
     */
    const BUILTIN_PROVIDERS = ['openai', 'google', 'anthropic'];

    /**
     * Returns the list of allowed API key provider slugs.
     *
     * Defaults to the three built-in providers but can be extended
     * via the `infyp_allowed_api_key_providers` filter so that
     * add-ons registering custom AI providers can also store API keys.
     *
     * @since 1.0.0
     * @return array Provider slug strings.
     */
    public static function get_allowed_providers() {
        /** This filter is documented in INFYP_Settings::sanitize_api_keys() */
        return apply_filters( 'infyp_allowed_api_key_providers', self::BUILTIN_PROVIDERS );
    }
    
    /**
     * Get the singleton instance.
     *
     * @since 1.0.0
     *
     * @return INFYP_API_Key_Manager The singleton instance.
     */
    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Private constructor.
     *
     * Initializes the encryption service and registers shutdown and migration hooks.
     *
     * @since 1.0.0
     * @access private
     */
    private function __construct() {
        $this->encryption = INFYP_Encryption_Service::get_instance();
        
        // Clear cache on shutdown
        add_action('shutdown', [$this, 'clear_cache']);
    }
    
    /**
     * Get API key for a specific provider
     *
     * @param string $provider The API provider (openai, google, anthropic)
     * @return string|null Decrypted API key or null if not found
     */
    public function get_api_key($provider) {
        if (!in_array($provider, self::get_allowed_providers(), true)) {
            return null;
        }
        
        // Check memory cache first
        if (isset($this->key_cache[$provider])) {
            return $this->key_cache[$provider];
        }
        
        // Get encrypted keys from database
        $encrypted_keys = get_option('infyp_api_keys', []);
        
        if (!isset($encrypted_keys[$provider]) || empty($encrypted_keys[$provider])) {
            return null;
        }
        
        // Decrypt the key
        $decrypted_key = $this->encryption->decrypt($encrypted_keys[$provider]);
        
        if ($decrypted_key === false) {
            return null;
        }
        
        // Cache in memory for this request
        $this->key_cache[$provider] = $decrypted_key;
        
        return $decrypted_key;
    }
    
    /**
     * Set API key for a specific provider
     *
     * @param string $provider The API provider
     * @param string $key The API key to store
     * @return bool True on success
     */
    public function set_api_key($provider, $key) {
        if (!in_array($provider, self::get_allowed_providers(), true)) {
            return false;
        }
        
        // Get current keys
        $api_keys = get_option('infyp_api_keys', []);
        
        // Encrypt and store the key
        if (!empty($key)) {
            $encrypted_key = $this->encryption->encrypt($key);
            if ($encrypted_key === false) {
                return false;
            }
            $api_keys[$provider] = $encrypted_key;
        } else {
            // Remove empty keys
            unset($api_keys[$provider]);
        }
        
        // Update cache
        $this->key_cache[$provider] = $key;
        
        // Save to database
        return update_option('infyp_api_keys', $api_keys);
    }
    
    /**
     * Set multiple API keys at once
     *
     * @param array $keys Array of provider => key pairs
     * @return bool True on success
     */
    public function set_api_keys($keys) {
        if (!is_array($keys)) {
            return false;
        }
        
        $api_keys = get_option('infyp_api_keys', []);
        
        foreach (self::get_allowed_providers() as $provider) {
            if (isset($keys[$provider])) {
                // Minimal sanitization for API keys - just trim whitespace and remove any HTML
                // API keys contain various characters that should not be modified
                $key = trim( wp_strip_all_tags( $keys[$provider] ) );

                if (!empty($key)) {
                    $encrypted_key = $this->encryption->encrypt($key);
                    if ($encrypted_key !== false) {
                        $api_keys[$provider] = $encrypted_key;
                        $this->key_cache[$provider] = $key;
                    }
                } else {
                    unset($api_keys[$provider]);
                    unset($this->key_cache[$provider]);
                }
            }
        }
        
        return update_option('infyp_api_keys', $api_keys);
    }
    
    /**
     * Get all API keys (decrypted)
     *
     * @param bool $mask Whether to mask the keys for display
     * @return array Array of provider => key pairs
     */
    public function get_all_api_keys($mask = false) {
        $encrypted_keys = get_option('infyp_api_keys', []);
        $decrypted_keys = [];
        
        foreach (self::get_allowed_providers() as $provider) {
            if (isset($encrypted_keys[$provider]) && !empty($encrypted_keys[$provider])) {
                $decrypted = $this->encryption->decrypt($encrypted_keys[$provider]);
                if ($decrypted !== false) {
                    $decrypted_keys[$provider] = $mask ? $this->encryption->get_masked_key($decrypted) : $decrypted;
                }
            } else {
                $decrypted_keys[$provider] = '';
            }
        }
        
        return $decrypted_keys;
    }
    
    /**
     * Check if API key exists for a provider
     *
     * @param string $provider The API provider
     * @return bool True if key exists
     */
    public function has_api_key($provider) {
        $key = $this->get_api_key($provider);
        return !empty($key);
    }
    
    /**
     * Get API key status for all providers
     *
     * @return array Array of provider => bool pairs
     */
    public function get_api_key_status() {
        $status = [];
        
        foreach (self::get_allowed_providers() as $provider) {
            $status[$provider] = $this->has_api_key($provider);
        }
        
        return $status;
    }
    
    /**
     * Validate API key format
     *
     * @param string $provider The API provider
     * @param string $key The API key to validate
     * @return bool|WP_Error True if valid, WP_Error otherwise
     */
    public function validate_api_key($provider, $key) {
        if (empty($key)) {
            return new \WP_Error('empty_key', esc_html__('API key cannot be empty', 'infy-ai-image-seo-optimizer'));
        }

        // Basic format validation per provider
        switch ($provider) {
            case 'openai':
                // OpenAI keys start with 'sk-' and are typically 40-60 characters
                if (strpos($key, 'sk-') !== 0) {
                    return new \WP_Error('invalid_format', esc_html__('OpenAI API keys should start with "sk-"', 'infy-ai-image-seo-optimizer'));
                }
                if (strlen($key) < 20) {
                    return new \WP_Error('invalid_length', esc_html__('OpenAI API key appears too short', 'infy-ai-image-seo-optimizer'));
                }
                break;

            case 'anthropic':
                // Anthropic keys start with 'sk-ant-' and are typically 90+ characters
                if (strpos($key, 'sk-ant-') !== 0) {
                    return new \WP_Error('invalid_format', esc_html__('Anthropic API keys should start with "sk-ant-"', 'infy-ai-image-seo-optimizer'));
                }
                if (strlen($key) < 40) {
                    return new \WP_Error('invalid_length', esc_html__('Anthropic API key appears too short', 'infy-ai-image-seo-optimizer'));
                }
                break;

            case 'google':
                // Gemini/Google AI keys start with 'AIza' and are typically 39 characters
                if (strpos($key, 'AIza') !== 0) {
                    return new \WP_Error('invalid_format', esc_html__('Gemini API keys should start with "AIza"', 'infy-ai-image-seo-optimizer'));
                }
                if (strlen($key) < 30) {
                    return new \WP_Error('invalid_length', esc_html__('Gemini API key appears too short', 'infy-ai-image-seo-optimizer'));
                }
                break;

            default:
                // Unknown provider - just check minimum length
                if (strlen($key) < 10) {
                    return new \WP_Error('invalid_length', esc_html__('API key appears too short', 'infy-ai-image-seo-optimizer'));
                }
        }

        return true;
    }
    
    /**
     * Delete API key for a provider
     *
     * @param string $provider The API provider
     * @return bool True on success
     */
    public function delete_api_key($provider) {
        if (!in_array($provider, self::get_allowed_providers(), true)) {
            return false;
        }
        
        $api_keys = get_option('infyp_api_keys', []);
        unset($api_keys[$provider]);
        unset($this->key_cache[$provider]);
        
        return update_option('infyp_api_keys', $api_keys);
    }
    
    /**
     * Clear the in-memory key cache.
     *
     * Called on shutdown to prevent decrypted keys from persisting in memory
     * longer than necessary.
     *
     * @since 1.0.0
     *
     * @return void
     */
    public function clear_cache() {
        $this->key_cache = [];
    }
    
    /**
     * Resolves a model identifier to its provider slug.
     *
     * Checks built-in model name prefixes first, then consults the
     * Model Config Registry for custom models registered via
     * `infyp_register_model_configs`. Falls back to the
     * `infyp_provider_for_model` filter for edge cases.
     *
     * @since 1.0.0
     * @param string $model The AI model identifier (e.g. 'gemini-3.1-flash-lite-preview', 'gpt-4.1-mini').
     * @return string|null Provider slug or null if unresolvable.
     */
    public function get_provider_for_model($model) {
        // Built-in prefix matching for core providers.
        if (strpos($model, 'gemini') === 0) {
            return 'google';
        } elseif (strpos($model, 'gpt') === 0) {
            return 'openai';
        } elseif (strpos($model, 'claude') === 0) {
            return 'anthropic';
        }

        // Fallback: consult the Model Config Registry for custom models.
        $config = \IPAIS\Core\AI\INFYP_Model_Config_Registry::get_instance()->get_config($model);
        if ($config && !empty($config['provider'])) {
            return $config['provider'];
        }

        /**
         * Filters the provider slug resolved for a model identifier.
         *
         * Fires only when built-in prefix matching and the Model Config
         * Registry both fail to resolve the provider. Extensions can use
         * this to map custom model names to their provider slug.
         *
         * @since 1.0.0
         * @param string|null $provider Resolved provider slug (null at this point).
         * @param string      $model    The model identifier being resolved.
         */
        return apply_filters('infyp_provider_for_model', null, $model);
    }
}