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

/**
 * APIキーの暗号化/復号化
 *
 * WordPressのAUTH_KEYと SECURE_AUTH_KEY を鍵としてAES-256-CBCで暗号化し、
 * wp_optionsに平文APIキーを保存しない。
 *
 * セキュリティ:
 *  - 暗号文には固定プレフィックス 'aiap_v1:' を付与し、平文/暗号文を明確に判別
 *  - AUTH_KEY が未定義の環境では暗号化を行わない(平文保存。AUTH_KEY 設定を促す)
 */
class AIAP_Crypto {

    const METHOD          = 'aes-256-cbc';
    const CIPHER_PREFIX   = 'aiap_v1:';

    /**
     * 暗号化キーを取得 (WordPress の AUTH_KEY 由来)
     *
     * @return string|false 32バイトの暗号化キー、または AUTH_KEY 未定義時 false
     */
    private static function get_key() {
        if ( ! defined( 'AUTH_KEY' ) || AUTH_KEY === '' ) {
            return false;
        }
        // SECURE_AUTH_KEY も併用してエントロピーを増やす(あれば)
        $salt = defined( 'SECURE_AUTH_KEY' ) && SECURE_AUTH_KEY !== '' ? SECURE_AUTH_KEY : 'aiap_salt';
        return hash( 'sha256', AUTH_KEY . '|' . $salt . '|aiap_v1', true );
    }

    /**
     * 文字列を暗号化
     *
     * 暗号化に失敗した場合は false を返す(平文を返さない)。
     * 呼び出し側は false を受けたら API キーを保存しない判断をすること。
     *
     * @param string $plain 平文
     * @return string|false 暗号文 (プレフィックス + Base64)、空入力時は ''、暗号化失敗時は false
     */
    public static function encrypt( $plain ) {
        if ( $plain === '' ) {
            return '';
        }
        if ( ! function_exists( 'openssl_encrypt' ) ) {
            // openssl 拡張が無い: 暗号化不可。平文保存は明示的に拒否する。
            if ( class_exists( 'AIAP_Logger' ) ) {
                AIAP_Logger::add( 'error', 'log_crypto_encrypt_failed' );
            }
            return false;
        }

        $key = self::get_key();
        if ( $key === false ) {
            // AUTH_KEY が未定義: 暗号化不可。平文保存は明示的に拒否する。
            if ( class_exists( 'AIAP_Logger' ) ) {
                AIAP_Logger::add( 'error', 'log_crypto_encrypt_failed' );
            }
            return false;
        }

        $iv_len = openssl_cipher_iv_length( self::METHOD );
        // PHP 7+ では random_bytes が暗号学的に強い乱数を保証するためこちらを優先。
        // 何らかの理由で利用不能な環境では openssl_random_pseudo_bytes にフォールバック。
        try {
            $iv = random_bytes( $iv_len );
        } catch ( \Exception $e ) {
            $iv = openssl_random_pseudo_bytes( $iv_len );
        }

        $encrypted = openssl_encrypt( $plain, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );

        if ( $encrypted === false ) {
            if ( class_exists( 'AIAP_Logger' ) ) {
                AIAP_Logger::add( 'error', 'log_crypto_encrypt_failed' );
            }
            return false;
        }

        // プレフィックス + base64(iv + ciphertext) として明確に判別可能にする
        return self::CIPHER_PREFIX . base64_encode( $iv . $encrypted );
    }

    /**
     * 暗号文を復号化
     *
     * @param string $value 暗号文または平文
     * @return string 平文(復号成功時)、空文字(復号失敗時)、または入力値そのまま(暗号文でない時)
     */
    public static function decrypt( $value ) {
        if ( $value === '' || ! function_exists( 'openssl_decrypt' ) ) {
            return $value;
        }

        // 旧形式 (1.0.0 リリース前の内部形式) の暗号文を検出
        // 旧形式は base64( "iv::ciphertext" ) で、復号できないため再入力を促す
        if ( self::looks_like_legacy_cipher( $value ) ) {
            if ( class_exists( 'AIAP_Logger' ) ) {
                AIAP_Logger::add( 'error', 'log_crypto_decrypt_failed' );
            }
            return '';
        }

        // プレフィックスがない = 平文として保存されている(暗号化前のレガシーデータ含む)
        if ( strpos( $value, self::CIPHER_PREFIX ) !== 0 ) {
            return $value;
        }

        $key = self::get_key();
        if ( $key === false ) {
            // 暗号文があるのに AUTH_KEY なし = 復号不能。空文字で「キー未設定」扱い
            if ( class_exists( 'AIAP_Logger' ) ) {
                AIAP_Logger::add( 'error', 'log_crypto_decrypt_failed' );
            }
            return '';
        }

        $payload = substr( $value, strlen( self::CIPHER_PREFIX ) );
        $decoded = base64_decode( $payload, true );
        if ( $decoded === false ) {
            return '';
        }

        $iv_len = openssl_cipher_iv_length( self::METHOD );
        if ( strlen( $decoded ) <= $iv_len ) {
            return '';
        }

        $iv         = substr( $decoded, 0, $iv_len );
        $ciphertext = substr( $decoded, $iv_len );

        $decrypted = openssl_decrypt( $ciphertext, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );

        if ( $decrypted === false ) {
            // 復号失敗: AUTH_KEY が変更された (WP 移転など) 可能性が高い
            if ( class_exists( 'AIAP_Logger' ) ) {
                AIAP_Logger::add( 'error', 'log_crypto_decrypt_failed' );
            }
            return '';
        }

        return $decrypted;
    }

    /**
     * 旧形式の暗号文(base64 encoded "iv::ciphertext")を判定
     *
     * 1.0.0 リリース前の内部形式。実害はないが復号できないため、
     * 検出してユーザに再入力を促す目的で判定する。
     */
    private static function looks_like_legacy_cipher( $value ) {
        // base64 文字以外があったら違う
        if ( ! preg_match( '/^[A-Za-z0-9+\/=]+$/', $value ) ) return false;
        // 短すぎは違う
        if ( strlen( $value ) < 40 ) return false;
        $decoded = base64_decode( $value, true );
        if ( $decoded === false ) return false;
        // 旧形式は中に '::' が必ずあった
        if ( strpos( $decoded, '::' ) === false ) return false;
        // 念のため、新形式プレフィックスがあれば違う
        if ( strpos( $value, self::CIPHER_PREFIX ) === 0 ) return false;
        return true;
    }

    /**
     * 暗号化が利用可能な環境かを判定する。
     *
     * AUTH_KEY が定義されており、かつ openssl 拡張が利用可能な環境では暗号化/復号化が可能。
     * いずれかが欠けている環境では encrypt() / decrypt() が失敗する。呼び出し側はこのヘルパで
     * 早期判定し、API キー fields の既存値を温存する経路に分岐するのに使う。
     *
     * Bug fix (audit r9): AUTH_KEY 欠落環境で update_settings を呼ぶと、decrypt 失敗で空に
     * なった API キー fields が encrypt_keys でも空のまま素通りし、既存暗号文を空で上書きして
     * しまう問題を、呼び出し側で is_available() を見て分岐することで回避する。
     *
     * @return bool true = 暗号化/復号化可能
     */
    public static function is_available() {
        if ( ! defined( 'AUTH_KEY' ) || AUTH_KEY === '' ) {
            return false;
        }
        if ( ! function_exists( 'openssl_encrypt' ) || ! function_exists( 'openssl_decrypt' ) ) {
            return false;
        }
        return true;
    }

    /**
     * 暗号化済みかどうか判定
     */
    public static function is_encrypted( $value ) {
        if ( ! is_string( $value ) || $value === '' ) return false;
        return strpos( $value, self::CIPHER_PREFIX ) === 0;
    }

    /**
     * 設定配列内のAPIキーフィールドを暗号化
     *
     * @param array $settings   設定配列
     * @param array $key_fields 暗号化対象のフィールド名リスト
     * @return array 暗号化された設定配列
     */
    /**
     * 設定配列内の API キーを暗号化する。
     *
     * 暗号化に失敗した field は、ユーザー入力(平文)を保存させず、
     * 空文字に置き換える。これにより:
     *  - 平文 API キーが DB に書き込まれない
     *  - update_option() 後に当該 field は空 = ユーザーが再入力する必要があると分かる
     *
     * @param array $settings   設定配列
     * @param array $key_fields 暗号化対象のフィールド名リスト
     * @return array 暗号化された設定配列(失敗 field は '' に置換)
     */
    public static function encrypt_keys( $settings, $key_fields ) {
        foreach ( $key_fields as $field ) {
            if ( isset( $settings[ $field ] ) && $settings[ $field ] !== '' ) {
                // 既に暗号化済みなら再暗号化しない
                if ( ! self::is_encrypted( $settings[ $field ] ) ) {
                    $encrypted = self::encrypt( $settings[ $field ] );
                    if ( $encrypted === false ) {
                        // 暗号化失敗: 平文を保存させず空文字化する。ログは encrypt() 内で記録済み。
                        $settings[ $field ] = '';
                    } else {
                        $settings[ $field ] = $encrypted;
                    }
                }
            }
        }
        return $settings;
    }

    /**
     * 設定配列内のAPIキーフィールドを復号化
     *
     * @param array $settings   設定配列
     * @param array $key_fields 復号化対象のフィールド名リスト
     * @return array 復号化された設定配列
     */
    public static function decrypt_keys( $settings, $key_fields ) {
        foreach ( $key_fields as $field ) {
            if ( isset( $settings[ $field ] ) && $settings[ $field ] !== '' ) {
                $settings[ $field ] = self::decrypt( $settings[ $field ] );
            }
        }
        return $settings;
    }
}
