<?php
/**
 * Geteilte Infrastruktur der Ability-Gruppe "Einstellungen"
 * (wp-agent/get-setting, wp-agent/update-setting, wp-agent/list-settings).
 *
 * Eine einzige Allowlist speist alle drei Abilities: was gelesen werden darf
 * (get-setting, list-settings), darf hier auch geschrieben werden
 * (update-setting). Das vermeidet zwei getrennte, potenziell auseinander-
 * laufende Listen für denselben Zweck.
 *
 * Zusätzlich zur Allowlist greift IMMER eine harte Denylist (siehe
 * wpagent_setting_denial_reason()): Salts/Keys, siteurl, home, active_plugins,
 * wp_user_roles, users_can_register, admin_email und alles mit "secret" im
 * Namen sind nie lesbar oder schreibbar, selbst wenn ein solcher Name
 * versehentlich in die Allowlist oben aufgenommen würde.
 *
 * WICHTIG (Integrationsphase): Diese Datei muss per require_once geladen
 * werden, BEVOR die drei zugehörigen Ability-Dateien
 * (includes/abilities/get-setting.php, update-setting.php, list-settings.php)
 * geladen werden, analog zum bestehenden require_once für includes/helpers.php
 * in wp-agent-companion.php. Alle drei neuen Ability-Dateien rufen Funktionen
 * aus dieser Datei auf.
 *
 * @package WPAgentCompanion
 */

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

/**
 * Die einzige Allowlist der Gruppe: lesbar UND (bis auf die Denylist-Prüfung)
 * schreibbar. Reihenfolge entspricht der fachlichen Spezifikation.
 */
const WPAGENT_SETTINGS_ALLOWLIST = array(
	'blogname',
	'blogdescription',
	'posts_per_page',
	'show_on_front',
	'page_on_front',
	'page_for_posts',
	'permalink_structure',
	'timezone_string',
	'start_of_week',
	'date_format',
	'time_format',
	'default_category',
);

/**
 * Public schema for every value exposed by the small core-settings surface.
 *
 * WordPress stores these twelve options as database strings (or, briefly
 * after an in-request update, integers). An absent row is represented by
 * null. Arrays, objects, booleans and floating-point values never belong to
 * this allowlist and must not cross the public Ability boundary.
 */
function wpagent_setting_public_value_schema(): array {
	return array(
		'type'      => array( 'string', 'integer', 'null' ),
		'maxLength' => 500,
		'minimum'   => 0,
		'maximum'   => 2147483647,
	);
}

/**
 * Validates a provider value before it is returned through a read ability.
 *
 * Read results are rejected, not truncated. Returning a partial setting would
 * make an agent believe it had received the complete current configuration.
 *
 * @param string $name Allowlisted option name.
 * @param array{exists:bool,value:mixed} $state Current option state.
 * @return string|int|null|WP_Error
 */
function wpagent_setting_public_value( string $name, array $state ) {
	if ( ! wpagent_setting_is_allowed( $name ) ) {
		return new WP_Error( 'wpagent_setting_not_allowed', __( 'Die Einstellung ist nicht für die öffentliche Ausgabe freigegeben.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	if ( empty( $state['exists'] ) ) {
		return null;
	}
	$value = $state['value'] ?? null;
	if ( is_string( $value ) && strlen( $value ) <= 500 ) {
		return $value;
	}
	if ( is_int( $value ) && $value >= 0 && $value <= 2147483647 ) {
		return $value;
	}
	return new WP_Error(
		'wpagent_setting_output_invalid',
		/* translators: %s: option name. */
		sprintf( __( 'Die Einstellung "%s" enthält keinen zulässigen begrenzten Skalarwert und wurde nicht ausgegeben.', 'wp-agent-companion' ), $name ),
		array( 'status' => 500, 'recovery_required' => false )
	);
}

/**
 * Permission-Callback: der Aufrufer muss die Site administrieren dürfen.
 *
 * Der Bot-User ist als Editor vorgesehen (nie Admin, siehe
 * wpagent_perm_edit_posts in helpers.php) und hat manage_options nicht.
 * Einstellungen sind Site-weite Konfiguration, keine Content-Bearbeitung,
 * deshalb bewusst nicht auf eine Content-Capability wie edit_posts gegated.
 *
 * @param mixed $input Unbenutzt.
 * @return bool
 */
function wpagent_perm_manage_options( $input = null ): bool {
	return current_user_can( 'manage_options' );
}

/**
 * Prüft, ob ein Options-Name auf der harten Denylist steht. Liefert den
 * Grund oder einen Leerstring, wenn nichts greift.
 *
 * Diese Prüfung ist bewusst UNABHÄNGIG von der Allowlist und wird
 * zusätzlich zu ihr aufgerufen (siehe wpagent_setting_is_allowed()): sollte
 * die Allowlist oben jemals versehentlich einen dieser Namen enthalten, greift
 * die Denylist trotzdem.
 *
 * @param string $name Options-Name.
 * @return string Grund oder Leerstring.
 */
function wpagent_setting_denial_reason( string $name ): string {
	$lower = strtolower( trim( $name ) );
	if ( '' === $lower ) {
		return '';
	}

	// Exakte Namen: Salts/Keys liegen in wp-config.php-Konstanten, nicht in
	// wp_options, könnten aber über gleichnamige Plugin-Options auftauchen;
	// die restlichen sechs sind echte wp_options mit Eskalations- bzw.
	// Umgebungserkennungs-Risiko (siteurl/home für Umgebungserkennung,
	// active_plugins/wp_user_roles/users_can_register für Rechte-Eskalation,
	// admin_email für Account-Wiederherstellung).
	$exact = array(
		'siteurl',
		'home',
		'active_plugins',
		'wp_user_roles',
		'users_can_register',
		'admin_email',
	);
	if ( in_array( $lower, $exact, true ) ) {
		return 'protected-core-option';
	}

	// Substring-Treffer: deckt Salts/Keys/Secrets generisch ab, auch wenn sie
	// unter einem Plugin-spezifischen Options-Namen liegen (z. B.
	// "some_plugin_api_secret" oder "auth_key").
	foreach ( array( 'salt', 'key', 'secret' ) as $needle ) {
		if ( false !== strpos( $lower, $needle ) ) {
			return 'salt-key-or-secret-like-name';
		}
	}

	return '';
}

/**
 * Ist ein Options-Name für diese Ability-Gruppe lesbar/schreibbar?
 *
 * @param string $name Options-Name.
 * @return bool
 */
function wpagent_setting_is_allowed( string $name ): bool {
	return in_array( $name, WPAGENT_SETTINGS_ALLOWLIST, true ) && '' === wpagent_setting_denial_reason( $name );
}

/**
 * Liest eine Option über get_option() und unterscheidet dabei sauber
 * zwischen "existiert nicht" und "existiert, Wert ist leer/false/0": ein
 * zufälliges Sentinel als Default, kommt es zurück, existiert die Option
 * nicht. Muster übernommen aus wp-agent-power/get-option.php.
 *
 * @param string $name Options-Name.
 * @return array{exists:bool,value:mixed}
 */
function wpagent_setting_read_value( string $name ): array {
	$sentinel = '__wpagent_setting_absent__' . wp_generate_password( 8, false );
	$value    = get_option( $name, $sentinel );
	if ( $sentinel === $value ) {
		return array(
			'exists' => false,
			'value'  => null,
		);
	}
	return array(
		'exists' => true,
		'value'  => $value,
	);
}
