<?php
/**
 * Settings Manager
 *
 * Manages integration settings with optional encryption for sensitive data.
 * Automatic encryption/decryption for API keys and passwords.
 *
 * @package FleekDash\Integration
 * @since 2.3.0
 */

namespace FleekDash\Integration;

defined('ABSPATH') || exit;

class SettingsManager
{
    /**
     * Encryption method
     */
    const ENCRYPTION_METHOD = 'AES-256-CBC';

    /** REST/API error when Registry has no integration row for settings operations. */
    public const ERROR_INTEGRATION_NOT_FOUND = 'Integration not found';

    /**
     * Get setting value
     *
     * Automatically decrypts if encrypted.
     *
     * @param string $integration_id Integration ID
     * @param string $key Setting key
     * @param mixed $default Default value
     * @return mixed Setting value
     */
    public static function get(string $integration_id, string $key, $default = null)
    {
        global $wpdb;

        $table = $wpdb->prefix . 'fleekdash_integration_settings';

        $setting = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM `{$table}` WHERE `integration_id` = %s AND `setting_key` = %s",
                $integration_id,
                $key
            ),
            ARRAY_A
        );

        if (!$setting) {
            return $default;
        }

        $value = $setting['setting_value'];

        // Decrypt if encrypted
        if ($setting['is_encrypted']) {
            $value = self::decrypt($value);
        }

        // Type casting
        return self::castValue($value, $setting['setting_type']);
    }

    /**
     * Set setting value
     *
     * Automatically encrypts if sensitive.
     *
     * @param string $integration_id Integration ID
     * @param string $key Setting key
     * @param mixed $value Setting value
     * @param array $options Setting options (type, encrypted, etc)
     * @return bool Success status
     */
    public static function set(string $integration_id, string $key, $value, array $options = []): bool
    {
        global $wpdb;

        $table = $wpdb->prefix . 'fleekdash_integration_settings';
        [$type, $is_encrypted, $is_sensitive, $is_required] = self::resolveSetOptionFlags($key, $options);
        $store_value = self::buildStoreValueForPersistence($value, $is_encrypted);

        $exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM `{$table}`
                 WHERE `integration_id` = %s AND `setting_key` = %s",
                $integration_id,
                $key
            )
        );

        $row = self::buildSettingRowPayload($store_value, $type, $is_encrypted, $is_sensitive, $is_required);

        $result = $exists
            ? self::updateIntegrationSettingRow($wpdb, $table, $integration_id, $key, $row)
            : self::insertIntegrationSettingRow($wpdb, $table, $integration_id, $key, $row);

        return $result !== false;
    }

    /**
     * @return array{0: string, 1: bool, 2: bool, 3: bool}
     */
    private static function resolveSetOptionFlags(string $key, array $options): array
    {
        $type = $options['type'] ?? 'string';
        $is_encrypted = $options['encrypted'] ?? false;
        $is_sensitive = $options['sensitive'] ?? false;
        $is_required = $options['required'] ?? false;

        if (in_array($key, ['api_key', 'secret', 'password', 'token', 'private_key'], true)) {
            $is_encrypted = true;
            $is_sensitive = true;
        }

        return [$type, $is_encrypted, $is_sensitive, $is_required];
    }

    /**
     * Serialize then optionally encrypt the canonical payload (never encrypt before JSON for structured values).
     *
     * @param mixed $value
     * @return mixed
     */
    private static function buildStoreValueForPersistence($value, bool $is_encrypted)
    {
        if (is_array($value) || is_object($value)) {
            $encoded = \wp_json_encode($value);
            if ($encoded === false) {
                $encoded = '';
            }
            $store_value = ($is_encrypted && !empty($value))
                ? self::encrypt($encoded)
                : $encoded;
        } elseif ($is_encrypted && !empty($value)) {
            $store_value = self::encrypt((string) $value);
        } else {
            $store_value = $value;
        }

        return $store_value;
    }

    /**
     * @param mixed $store_value
     * @return array<string, mixed>
     */
    private static function buildSettingRowPayload(
        $store_value,
        string $type,
        bool $is_encrypted,
        bool $is_sensitive,
        bool $is_required
    ): array {
        return [
            'setting_value' => $store_value,
            'setting_type' => $type,
            'is_encrypted' => $is_encrypted ? 1 : 0,
            'is_sensitive' => $is_sensitive ? 1 : 0,
            'is_required' => $is_required ? 1 : 0,
        ];
    }

    /**
     * @param array<string, mixed> $data
     */
    private static function updateIntegrationSettingRow(
        \wpdb $wpdb,
        string $table,
        string $integration_id,
        string $key,
        array $data
    ) {
        return $wpdb->update(
            $table,
            $data,
            [
                'integration_id' => $integration_id,
                'setting_key' => $key,
            ],
            ['%s', '%s', '%d', '%d', '%d'],
            ['%s', '%s']
        );
    }

    /**
     * @param array<string, mixed> $data
     */
    private static function insertIntegrationSettingRow(
        \wpdb $wpdb,
        string $table,
        string $integration_id,
        string $key,
        array $data
    ) {
        return $wpdb->insert(
            $table,
            array_merge(
                [
                    'integration_id' => $integration_id,
                    'setting_key' => $key,
                ],
                $data
            ),
            ['%s', '%s', '%s', '%s', '%d', '%d', '%d']
        );
    }

    /**
     * Get all settings for integration
     *
     * @param string $integration_id Integration ID
     * @param bool $include_sensitive Include sensitive values
     * @return array All settings
     */
    public static function getAll(string $integration_id, bool $include_sensitive = false): array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'fleekdash_integration_settings';

        $sql = $wpdb->prepare(
            "SELECT * FROM `{$table}` WHERE `integration_id` = %s",
            $integration_id
        );

        if (!$include_sensitive) {
            $sql .= " AND `is_sensitive` = 0";
        }

        $results = $wpdb->get_results($sql, ARRAY_A);

        $settings = [];
        foreach ($results as $setting) {
            $value = $setting['setting_value'];

            // Decrypt if encrypted
            if ($setting['is_encrypted']) {
                $value = self::decrypt($value);
            }

            // Cast to correct type
            $value = self::castValue($value, $setting['setting_type']);

            $settings[$setting['setting_key']] = $value;
        }

        return $settings;
    }

    /**
     * Delete setting
     *
     * @param string $integration_id Integration ID
     * @param string $key Setting key
     * @return bool Success status
     */
    public static function delete(string $integration_id, string $key): bool
    {
        global $wpdb;

        $table = $wpdb->prefix . 'fleekdash_integration_settings';

        $result = $wpdb->delete(
            $table,
            [
                'integration_id' => $integration_id,
                'setting_key' => $key,
            ],
            ['%s', '%s']
        );

        return $result !== false;
    }

    /**
     * Encrypt value
     *
     * @param string $value Value to encrypt
     * @return string Encrypted value
     */
    private static function encrypt(string $value): string
    {
        $key = self::getEncryptionKey();
        $iv_length = openssl_cipher_iv_length(self::ENCRYPTION_METHOD);
        $iv = openssl_random_pseudo_bytes($iv_length);

        $encrypted = openssl_encrypt(
            $value,
            self::ENCRYPTION_METHOD,
            $key,
            0,
            $iv
        );

        // Store IV with encrypted data
        return base64_encode($iv . '::' . $encrypted);
    }

    /**
     * Decrypt value
     *
     * @param string $encrypted Encrypted value
     * @return string Decrypted value
     */
    private static function decrypt(string $encrypted): string
    {
        $key = self::getEncryptionKey();
        $data = base64_decode($encrypted);

        list($iv, $encrypted_data) = explode('::', $data, 2);

        return openssl_decrypt(
            $encrypted_data,
            self::ENCRYPTION_METHOD,
            $key,
            0,
            $iv
        );
    }

    /**
     * Get encryption key
     *
     * Uses WordPress salts for key derivation.
     *
     * @return string Encryption key
     */
    private static function getEncryptionKey(): string
    {
        // Use WordPress auth key and salt
        $key_source = AUTH_KEY . AUTH_SALT . SECURE_AUTH_KEY . SECURE_AUTH_SALT;
        return hash('sha256', $key_source, true);
    }

    /**
     * Cast value to correct type
     *
     * @param mixed $value Value
     * @param string $type Type
     * @return mixed Casted value
     */
    private static function castValue($value, string $type)
    {
        return match ($type) {
            'integer', 'number' => intval($value),
            'boolean' => filter_var($value, FILTER_VALIDATE_BOOLEAN),
            'array', 'json' => is_string($value) ? json_decode($value, true) : $value,
            'float', 'double' => floatval($value),
            'string' => (string) $value,
            default => (string) $value,
        };
    }

    /**
     * Validate settings against schema
     *
     * @param string $integration_id Integration ID
     * @param array $settings Settings to validate
     * @return array Validation result
     */
    public static function validate(string $integration_id, array $settings): array
    {
        $integration = Registry::get($integration_id);

        if (!$integration) {
            return [
                'success' => false,
                'error' => self::ERROR_INTEGRATION_NOT_FOUND,
            ];
        }

        if (!isset($integration['manifest']['settings_schema'])) {
            return [
                'success' => true,
                'settings' => $settings,
            ];
        }

        return self::validateAgainstSettingsSchema(
            $integration['manifest']['settings_schema'],
            $settings
        );
    }

    /**
     * @param array<string, mixed> $schema
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private static function validateAgainstSettingsSchema(array $schema, array $settings): array
    {
        $errors = [];
        $validated = [];

        foreach ($schema as $key => $field) {
            $value = $settings[$key] ?? ($field['default'] ?? null);

            if (!empty($field['required']) && empty($value)) {
                $errors[] = sprintf('Field "%s" is required', $key);
                continue;
            }

            $validated[$key] = ManifestValidator::sanitizeSettingValue($value, $field['type']);
        }

        if (!empty($errors)) {
            return [
                'success' => false,
                'errors' => $errors,
            ];
        }

        return [
            'success' => true,
            'settings' => $validated,
        ];
    }
}
