<?php
declare(strict_types=1);
namespace ConfManager\Security;

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


use ConfManager\Traits\Singleton;

/**
 * AES-256-GCM 暗号化/復号エンジン
 *
 * メールアドレス・電話番号・住所等の個人情報を暗号化して保存する。
 * マスターキーは wp-config.php の CONF_MANAGER_ENCRYPTION_KEY 定数から取得。
 *
 * 暗号化フォーマット: base64( nonce(12bytes) + ciphertext + tag(16bytes) )
 *
 * @since 1.0.0
 */
class Encryption {

	use Singleton;

	private const CIPHER = 'aes-256-gcm';
	private const NONCE_LENGTH = 12;
	private const TAG_LENGTH = 16;

	/**
	 * HKDF の info 文字列
	 *
	 * **絶対に変更しないこと。** 変更するとすべての暗号化済みデータが
	 * 復号不能になる。同じ文字列を複数箇所に書くと片方だけ直す事故が
	 * 起こり得るので、導出はこのクラスの derive_key() に集約する。
	 */
	private const HKDF_INFO = 'conf-manager-encryption';

	/**
	 * 導出済みの暗号化キー（バイナリ）
	 */
	private string $derived_key;

	/**
	 * 初期化
	 *
	 * @throws \RuntimeException マスターキーが未定義の場合
	 */
	protected function init(): void {
		$master_key = $this->get_master_key();
		// HKDF でマスターキーから256bit鍵を導出
		$this->derived_key = self::derive_key( $master_key );
	}

	/**
	 * 文字列を暗号化する
	 *
	 * @param string $plaintext 平文
	 * @return string Base64エンコードされた暗号文（nonce + ciphertext + tag）
	 * @throws \RuntimeException 暗号化に失敗した場合
	 */
	public function encrypt( string $plaintext ): string {
		if ( '' === $plaintext ) {
			return '';
		}

		$nonce = random_bytes( self::NONCE_LENGTH );
		$tag   = '';

		$ciphertext = openssl_encrypt(
			$plaintext,
			self::CIPHER,
			$this->derived_key,
			OPENSSL_RAW_DATA,
			$nonce,
			$tag,
			'',
			self::TAG_LENGTH
		);

		if ( false === $ciphertext ) {
			throw new \RuntimeException( esc_html( 'Encryption failed: ' . openssl_error_string() ) );
		}

		// nonce + ciphertext + tag をbase64エンコード
		return base64_encode( $nonce . $ciphertext . $tag );
	}

	/**
	 * 暗号文を復号する
	 *
	 * @param string $encoded Base64エンコードされた暗号文
	 * @return string 復号された平文
	 * @throws \RuntimeException 復号に失敗した場合
	 */
	public function decrypt( string $encoded ): string {
		if ( '' === $encoded ) {
			return '';
		}

		$raw = base64_decode( $encoded, true );
		if ( false === $raw ) {
			throw new \RuntimeException( 'Decryption failed: invalid base64 encoding.' );
		}

		if ( strlen( $raw ) < self::NONCE_LENGTH + self::TAG_LENGTH + 1 ) {
			throw new \RuntimeException( 'Decryption failed: data too short.' );
		}

		$nonce      = substr( $raw, 0, self::NONCE_LENGTH );
		$tag        = substr( $raw, -self::TAG_LENGTH );
		$ciphertext = substr( $raw, self::NONCE_LENGTH, -self::TAG_LENGTH );

		$plaintext = openssl_decrypt(
			$ciphertext,
			self::CIPHER,
			$this->derived_key,
			OPENSSL_RAW_DATA,
			$nonce,
			$tag
		);

		if ( false === $plaintext ) {
			throw new \RuntimeException( 'Decryption failed: authentication tag mismatch or corrupted data.' );
		}

		return $plaintext;
	}

	/**
	 * 指定キーで復号する（キーローテーション時に使用）
	 *
	 * @param string $encoded    Base64エンコードされた暗号文
	 * @param string $key_binary 32バイトの暗号化キー
	 * @return string 復号された平文
	 * @throws \RuntimeException 復号に失敗した場合
	 */
	public function decrypt_with_key( string $encoded, string $key_binary ): string {
		if ( '' === $encoded ) {
			return '';
		}

		$raw = base64_decode( $encoded, true );
		if ( false === $raw || strlen( $raw ) < self::NONCE_LENGTH + self::TAG_LENGTH + 1 ) {
			throw new \RuntimeException( 'Decryption failed: invalid data.' );
		}

		$nonce      = substr( $raw, 0, self::NONCE_LENGTH );
		$tag        = substr( $raw, -self::TAG_LENGTH );
		$ciphertext = substr( $raw, self::NONCE_LENGTH, -self::TAG_LENGTH );

		$plaintext = openssl_decrypt(
			$ciphertext,
			self::CIPHER,
			$key_binary,
			OPENSSL_RAW_DATA,
			$nonce,
			$tag
		);

		if ( false === $plaintext ) {
			throw new \RuntimeException( 'Decryption failed with provided key.' );
		}

		return $plaintext;
	}

	/**
	 * メールアドレスの照合用SHA-256ハッシュを生成
	 *
	 * @param string $email メールアドレス（平文）
	 * @return string SHA-256ハッシュ（16進数64文字）
	 */
	public static function hash_email( string $email ): string {
		return hash( 'sha256', strtolower( trim( $email ) ) );
	}

	/**
	 * 現在の導出済みキーを取得（キーローテーション時に使用）
	 *
	 * @return string 32バイトのバイナリキー
	 */
	public function get_current_key(): string {
		return $this->derived_key;
	}

	/**
	 * 現在のキーのハッシュを取得（キー識別用）
	 *
	 * @return string SHA-256ハッシュ
	 */
	public function get_current_key_hash(): string {
		return hash( 'sha256', $this->derived_key );
	}

	/**
	 * 任意のマスターキーから暗号化キーを導出する
	 *
	 * 現在有効なキーだけでなく、旧キーや「候補のキー」を扱う場面
	 * （ローテーション・鍵の差し替わり検知）でも同じ導出手順が要る。
	 *
	 * @param string $master_key マスターキー（平文）
	 * @return string 32バイトのバイナリキー
	 */
	public static function derive_key( string $master_key ): string {
		return hash_hkdf( 'sha256', $master_key, 32, self::HKDF_INFO );
	}

	/**
	 * 任意のマスターキーに対応するキーハッシュを返す
	 *
	 * ハッシュ同士を比べれば、**キーそのものを表示・保存せずに**
	 * 「そのキーが目的のキーかどうか」を判定できる。
	 *
	 * @param string $master_key マスターキー（平文）
	 * @return string SHA-256ハッシュ（16進数64文字）
	 */
	public static function hash_of_master_key( string $master_key ): string {
		return hash( 'sha256', self::derive_key( $master_key ) );
	}

	/** @var string キーの取得元（'config' | 'database'） */
	private string $key_source = 'config';

	/**
	 * キーの取得元を返す
	 *
	 * @return string 'config'（wp-config.php）または 'database'（wp_options自動生成）
	 */
	public function get_key_source(): string {
		return $this->key_source;
	}

	/**
	 * マスターキーを取得する（ハイブリッド方式）
	 *
	 * 優先順位:
	 * 1. wp-config.php の CONF_MANAGER_ENCRYPTION_KEY 定数（最も安全）
	 * 2. wp_options の conf_manager_encryption_key（自動生成）
	 * 3. いずれもなければ自動生成して wp_options に保存
	 *
	 * @return string マスターキー（16文字以上）
	 */
	private function get_master_key(): string {
		// 1. wp-config.php 定数を最優先
		if ( defined( 'CONF_MANAGER_ENCRYPTION_KEY' ) && strlen( CONF_MANAGER_ENCRYPTION_KEY ) >= 16 ) {
			$this->key_source = 'config';
			return CONF_MANAGER_ENCRYPTION_KEY;
		}

		// 2. wp_options から取得
		$db_key = get_option( 'conf_manager_encryption_key', '' );
		if ( ! empty( $db_key ) && strlen( $db_key ) >= 16 ) {
			$this->key_source = 'database';
			return $db_key;
		}

		// 3. 自動生成して wp_options に保存
		$generated_key = bin2hex( random_bytes( 32 ) ); // 64文字の16進数
		update_option( 'conf_manager_encryption_key', $generated_key, false ); // autoload=false
		$this->key_source = 'database';

		return $generated_key;
	}
}
