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

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


use ConfManager\Traits\Singleton;

/**
 * 暗号化キーのローテーション管理
 *
 * wp_conf_security_keys テーブルでキー履歴を管理し、
 * ローテーション時に既存データを新キーで再暗号化する。
 *
 * @since 1.0.0
 */
class KeyRotation {

	use Singleton;

	private const TABLE = 'conf_security_keys';

	/**
	 * 暗号化カラムの定義（テーブル名 => カラム名の配列）
	 */
	private const ENCRYPTED_COLUMNS = [
		'conf_members'       => [ 'email', 'phone', 'org_address' ],
		'conf_group_members' => [ 'email' ],
		'conf_coauthors'     => [ 'email' ],
		'conf_sponsors'      => [ 'contact_email', 'contact_phone' ],
		'conf_sponsor_staff' => [ 'email' ],
	];

	/**
	 * 初期化
	 */
	protected function init(): void {}

	/**
	 * 現在のキーをセキュリティキーテーブルに登録する
	 *
	 * プラグイン有効化時や初回起動時に呼び出す。
	 */
	public function register_current_key(): void {
		global $wpdb;

		$encryption = Encryption::instance();
		$key_hash   = $encryption->get_current_key_hash();
		$table      = $wpdb->prefix . self::TABLE;

		// 既に登録済みか確認
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$exists = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$table} WHERE key_hash = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$key_hash
			)
		);

		if ( $exists > 0 ) {
			return;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table query.
		$wpdb->insert(
			$table,
			[
				'key_hash'      => $key_hash,
				'purpose'       => 'data_encryption',
				'encrypted_key' => '', // マスターキー由来のため保存不要
				'active_from'   => current_time( 'mysql', true ),
				'active_to'     => null,
			],
			[ '%s', '%s', '%s', '%s', null ]
		);
	}

	/**
	 * キーローテーションを実行する
	 *
	 * 1. 旧キーのactive_toを設定
	 * 2. 新キーを登録
	 * 3. 全暗号化データを新キーで再暗号化
	 *
	 * @param string $old_master_key 旧マスターキー（平文）
	 * @return array{re_encrypted: int, errors: int} 処理結果
	 */
	public function rotate( string $old_master_key ): array {
		global $wpdb;

		$table      = $wpdb->prefix . self::TABLE;
		$encryption = Encryption::instance();

		// 旧キーを導出
		$old_derived = hash_hkdf( 'sha256', $old_master_key, 32, 'conf-manager-encryption' );
		$old_hash    = hash( 'sha256', $old_derived );

		// 旧キーのactive_toを設定
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$wpdb->update(
			$table,
			[
				'active_to'  => current_time( 'mysql', true ),
				'rotated_at' => current_time( 'mysql', true ),
			],
			[ 'key_hash' => $old_hash ],
			[ '%s', '%s' ],
			[ '%s' ]
		);

		// 新キーを登録
		$this->register_current_key();

		// 全暗号化データを再暗号化
		return $this->re_encrypt_all_data( $old_derived, $encryption );
	}

	/**
	 * 全暗号化データを新キーで再暗号化
	 *
	 * @param string     $old_key    旧導出済みキー（32バイトバイナリ）
	 * @param Encryption $encryption 新キーのEncryptionインスタンス
	 * @return array{re_encrypted: int, errors: int}
	 */
	private function re_encrypt_all_data( string $old_key, Encryption $encryption ): array {
		global $wpdb;

		$re_encrypted = 0;
		$errors       = 0;

		foreach ( self::ENCRYPTED_COLUMNS as $table_suffix => $columns ) {
			$table = $wpdb->prefix . $table_suffix;

			// テーブルが存在するか確認
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
			$table_exists = $wpdb->get_var(
				$wpdb->prepare( 'SHOW TABLES LIKE %s', $table )
			);
			if ( ! $table_exists ) {
				continue;
			}

			// 全レコードを取得（バッチ処理）
			$batch_size = 100;
			$offset     = 0;

			while ( true ) {
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and column list from plugin-defined whitelist; values bound via prepare().
				$rows = $wpdb->get_results(
					$wpdb->prepare(
						"SELECT id, " . implode( ', ', $columns ) . " FROM {$table} WHERE status != %s LIMIT %d OFFSET %d",
						'deleted',
						$batch_size,
						$offset
					)
				);
				// phpcs:enable

				if ( empty( $rows ) ) {
					break;
				}

				foreach ( $rows as $row ) {
					$update_data = [];

					foreach ( $columns as $col ) {
						$encrypted_value = $row->$col ?? '';
						if ( '' === $encrypted_value ) {
							continue;
						}

						try {
							// 旧キーで復号
							$plaintext = $encryption->decrypt_with_key( $encrypted_value, $old_key );
							// 新キーで暗号化
							$update_data[ $col ] = $encryption->encrypt( $plaintext );
						} catch ( \RuntimeException $e ) {
							$errors++;
							continue;
						}
					}

					if ( ! empty( $update_data ) ) {
						// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
						$wpdb->update(
							$table,
							$update_data,
							[ 'id' => $row->id ],
							array_fill( 0, count( $update_data ), '%s' ),
							[ '%d' ]
						);
						$re_encrypted++;
					}
				}

				$offset += $batch_size;
			}
		}

		return [
			're_encrypted' => $re_encrypted,
			'errors'       => $errors,
		];
	}

	/**
	 * キー履歴を取得
	 *
	 * @return array キー履歴の配列
	 */
	public function get_key_history(): array {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from plugin-defined prefix; no user values in this query.
		$history = $wpdb->get_results(
			"SELECT key_hash, purpose, active_from, active_to, rotated_at, created_at
			 FROM {$table}
			 WHERE purpose = 'data_encryption'
			 ORDER BY active_from DESC",
			ARRAY_A
		);
		// phpcs:enable
		return $history;
	}
}
