<?php
/**
 * Sensitive data redaction.
 *
 * Single source of truth for deciding which option/setting keys hold secrets
 * (passwords, API keys, tokens, license keys, encryption material, etc.) and
 * for scrubbing those values out of diagnostics, CLI output, and exports.
 *
 * Two layers are combined:
 *   1. A key-name heuristic (regex) that catches the common secret naming
 *      conventions used across modules.
 *   2. An explicit allow-list of known secret field names for the cases the
 *      heuristic cannot infer (e.g. "aws_access_key_id"). Explicit metadata is
 *      preferred over pure name matching, per the CLI security requirements.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.7
 */

namespace WP_PowerSuite\Core;

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

/**
 * Reusable secret detection and redaction helper.
 *
 * @since 1.0.7
 */
final class SensitiveDataRedactor {

	/**
	 * Placeholder written in place of a redacted value when masking (as opposed
	 * to dropping the key entirely).
	 *
	 * @var string
	 */
	public const MASK = '••••••••';

	/**
	 * Explicit secret field names that the key-name heuristic would otherwise
	 * miss. Kept lowercase for case-insensitive comparison.
	 *
	 * @var list<string>
	 */
	private const EXPLICIT_SECRET_KEYS = [
		'aws_access_key_id',
		'aws_secret_access_key',
		'consumer_key',
		'consumer_secret',
		'turbo_smtp_consumer_key',
		'turbo_smtp_consumer_secret',
		'app_secret',
		'salt',
		'tl_secret_key',
	];

	/**
	 * Whether an option/setting key indicates a secret value.
	 *
	 * @since 1.0.7
	 * @param string $key Option or setting key (any case).
	 * @return bool True when the key should be treated as sensitive.
	 */
	public static function isSensitiveKey( string $key ): bool {
		if ( $key === '' ) {
			return false;
		}

		if ( in_array( strtolower( $key ), self::EXPLICIT_SECRET_KEYS, true ) ) {
			return true;
		}

		$pattern = '/(?:'
			. 'pass(?:word|wd|phrase)?'
			. '|secret'
			. '|token'
			. '|api[_-]?key'
			. '|access[_-]?key'
			. '|private[_-]?key'
			. '|encrypt(?:ion)?[_-]?key'
			. '|signing[_-]?secret'
			. '|client[_-]?secret'
			. '|refresh[_-]?token'
			. '|access[_-]?token'
			. '|authoriz'
			. '|credential'
			. '|license(?:[_-]?key)?'
			. '|webhook[_-]?secret'
			. ')/i';

		return (bool) preg_match( $pattern, $key );
	}

	/**
	 * Recursively mask sensitive values in an array.
	 *
	 * Scalars whose key is sensitive are replaced with {@see self::MASK}. Arrays
	 * are walked recursively so nested secrets (e.g. per-module settings blobs)
	 * are also masked. The structure is preserved so callers can still show
	 * which settings exist without leaking their values.
	 *
	 * @since 1.0.7
	 * @param array<int|string, mixed> $data        Data to redact.
	 * @param string                   $replacement Replacement for masked values.
	 * @return array<int|string, mixed> Redacted copy.
	 */
	public static function redact( array $data, string $replacement = self::MASK ): array {
		$out = [];

		foreach ( $data as $key => $value ) {
			$key_string = is_string( $key ) ? $key : (string) $key;

			if ( self::isSensitiveKey( $key_string ) ) {
				$out[ $key ] = is_array( $value ) ? array_fill_keys( array_keys( $value ), $replacement ) : $replacement;
				continue;
			}

			$out[ $key ] = is_array( $value )
				? self::redact( $value, $replacement )
				: $value;
		}

		return $out;
	}
}
