<?php
/**
 * Save settings use case.
 *
 * Phase-8 slice 8c-1 Application-layer use case backing the
 * §13 Settings tabs' Save action. The use case persists a
 * caller-supplied {@see PluginSettings} snapshot through the
 * configured {@see SettingsRepository}, applying the
 * "回転入力" (rotating-input) rule documented in
 * `specifications/security-and-compliance.md` §6.3 verbatim:
 *
 *   > 編集時は**新値を入力**して保存する方式（回転入力）に
 *   > 統一する。
 *   > 空欄保存は「変更なし」として扱い、現値を保持する。
 *   > 現値を削除したい場合は専用の「削除」操作を用意する。
 *
 * Concretely, for each provider in the supplied snapshot's
 * {@see PluginSettings::providerCredentials()} map, every
 * secret value that is the empty string after `trim()` is
 * REPLACED with the corresponding secret from the currently
 * persisted snapshot's same provider, key-by-key. Non-empty
 * secret values from the new snapshot win; the `enabled`
 * provider-toggle flag from the new snapshot ALWAYS wins
 * (rotation only protects secret values, never the toggle).
 *
 * Edge cases:
 *
 *   - Non-existent provider key in the currently persisted
 *     snapshot: every empty value from the new snapshot is
 *     persisted as the empty string. The caller is responsible
 *     for probing {@see ProviderCredentials::isConfigured()}
 *     after the save and surfacing a "not yet configured"
 *     state in the §13.10 保存 UX response.
 *
 *   - Whitespace-only value (e.g. `"   "`): treated as empty
 *     per §6.3 because the admin form's empty-string detection
 *     happens after sanitization trims whitespace.
 *
 *   - Non-secret VOs ({@see RenderingDefaults},
 *     {@see LinkDefaults}, {@see SyncSettings},
 *     {@see CacheSettings}, and the §13.9 Debug Mode flag)
 *     replace the persisted values WHOLESALE; they do not
 *     carry secret material so the rotation rule does not
 *     apply.
 *
 * Authorization is NOT enforced by this use case. The §13
 * admin page renderer (slice 8c-2) and the §13.2
 * `POST /affilun/v1/settings` REST controller (later slice) are
 * the canonical capability-check seams; this use case is the
 * Application-layer primitive both surfaces drive once
 * authorization has already passed.
 *
 * Field-level validation is also NOT performed here. Each
 * sub-VO ({@see ProviderCredentials},
 * {@see RenderingDefaults}, etc.) enforces its own
 * structural invariants in its constructor; by the time a
 * {@see PluginSettings} value reaches this use case those
 * invariants have already held, so an additional validation
 * pass would be redundant.
 *
 * @package Affilun\Application\Settings\UseCase
 */

declare(strict_types=1);

namespace Affilun\Application\Settings\UseCase;

use Affilun\Application\Settings\OpenAiSettings;
use Affilun\Application\Settings\PluginSettings;
use Affilun\Application\Settings\ProviderCredentials;
use Affilun\Application\Settings\SettingsRepository;

/**
 * Persist a {@see PluginSettings} snapshot, applying the §6.3
 * rotating-input rule to every provider's secret values.
 *
 * See the file-level docblock for the rotation rule, the edge
 * cases (missing current provider, whitespace-only values),
 * and the rationale for not enforcing authorization or
 * field-level validation here.
 */
final class SaveSettingsUseCase {

	/**
	 * Backing settings repository.
	 */
	private SettingsRepository $repository;

	/**
	 * Construct the use case with the repository it delegates
	 * to.
	 *
	 * @param SettingsRepository $repository Backing settings repository.
	 */
	public function __construct( SettingsRepository $repository ) {
		$this->repository = $repository;
	}

	/**
	 * Persist the supplied settings snapshot, applying the
	 * §6.3 rotation rule to every provider's secret values.
	 *
	 * Returns the merged snapshot the repository now holds so
	 * callers can echo the canonical state back to the §13.10
	 * 保存 UX response without a follow-up
	 * {@see SettingsRepository::load()} round-trip.
	 *
	 * @param PluginSettings $newSettings Caller-supplied snapshot.
	 */
	public function execute( PluginSettings $newSettings ): PluginSettings {
		$current = $this->repository->load();

		$newOpenAi = OpenAiSettings::defaults();

		$newCloud = $newSettings->cloud();
		if ( trim( $newCloud->apiKey() ) === '' && $current->cloud()->apiKey() !== '' ) {
			$newCloud = $newCloud->withApiKey( $current->cloud()->apiKey() );
		}

		$merged = $newSettings
			->withRendering( $newSettings->rendering() )
			->withLinks( $newSettings->links() )
			->withSync( $newSettings->sync() )
			->withCache( $newSettings->cache() )
			->withDebugMode( $newSettings->debugMode() )
			->withOpenAi( $newOpenAi )
			->withCloud( $newCloud );

		foreach ( $newSettings->providerCredentials() as $providerKey => $newCreds ) {
			$mergedCreds = self::mergeProviderCredentials(
				$newCreds,
				$current->provider( $providerKey )
			);
			$merged      = $merged->withProviderCredentials( $mergedCreds );
		}

		$this->repository->save( $merged );

		return $merged;
	}

	/**
	 * Merge a single provider's new credentials with its
	 * currently persisted credentials per the §6.3 rotation
	 * rule.
	 *
	 * For each secret key in `$newCreds`:
	 *
	 *   - If `trim($newValue) === ''`, the corresponding value
	 *     from `$currentCreds` is preserved (when present).
	 *   - Otherwise, the new value wins.
	 *
	 * The `enabled` flag from `$newCreds` always wins; the
	 * resulting credentials carry the new snapshot's toggle
	 * state regardless of whether secrets were preserved.
	 *
	 * @param ProviderCredentials      $newCreds     New credentials supplied by the caller.
	 * @param ProviderCredentials|null $currentCreds Currently persisted credentials, or null when
	 *                                               the provider key is absent from the persisted
	 *                                               snapshot.
	 */
	private static function mergeProviderCredentials(
		ProviderCredentials $newCreds,
		?ProviderCredentials $currentCreds
	): ProviderCredentials {
		$mergedSecrets = array();
		foreach ( $newCreds->secrets() as $secretKey => $newValue ) {
			if ( trim( $newValue ) === '' && $currentCreds !== null ) {
				$preserved                   = $currentCreds->secret( $secretKey );
				$mergedSecrets[ $secretKey ] = $preserved ?? $newValue;
				continue;
			}

			$mergedSecrets[ $secretKey ] = $newValue;
		}

		return new ProviderCredentials(
			$newCreds->providerKey(),
			$newCreds->enabled(),
			$mergedSecrets
		);
	}
}
