<?php
/**
 * Ability: wp-agent/update-setting
 *
 * Setzt genau eine WordPress-Kern-Einstellung aus der festen, beschreibbaren
 * Allowlist (WPAGENT_SETTINGS_ALLOWLIST aus includes/settings-allowlist.php:
 * 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). Jeder Name durchläuft eine
 * eigene Typ-/Enum-/Existenz-Prüfung (wpagent_sanitize_setting_value()), bevor
 * sie per bedingtem Raw-Options-Update geschrieben wird. Das Ergebnis wird
 * anschließend aus der Optionszeile und über get_option() zurückgelesen.
 *
 * Zwei bekannte WordPress-Kern-Fallstricke werden explizit behandelt:
 *
 *  1. show_on_front/page_on_front/page_for_posts hängen fachlich zusammen:
 *     show_on_front=page ohne eine bereits gesetzte page_on_front-Seite ist
 *     eine Sackgasse (leere Startseite), und page_on_front darf nie dieselbe
 *     Seite wie page_for_posts sein. Beides wird vor dem Schreiben geprüft
 *     (wpagent_check_front_page_consistency()).
 *  2. permalink_structure verlangt zusätzlich eine Rewrite-Regel-Änderung.
 *     Der WordPress-Kern bietet dafür keine bedingte Provider-Operation, die
 *     an die vorher gelesene Optionszeile gebunden werden kann. Sie bleibt
 *     daher bis zu einer solchen Provider-Schnittstelle manuell.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_update_setting(): array {
	return array(
		'label'               => __( 'Einstellung aktualisieren', 'wp-agent-companion' ),
		'description'         => __( 'Setzt eine WordPress-Kern-Einstellung aus einer festen, beschreibbaren Allowlist (Website-Titel, Anzeige-Optionen, Zeitzone, Datums-/Zeitformat, Standard-Kategorie und mehr) per konfliktgeschütztem Options-Row-CAS. Prüft Enum-/Typ-Werte und die Konsistenz von show_on_front/page_on_front/page_for_posts. Permalink-Strukturen bleiben manuell, bis WordPress dafür eine bedingte Rewrite-Operation anbietet.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'name'  => array(
					'type'        => 'string',
					'enum'        => WPAGENT_SETTINGS_ALLOWLIST,
					'description' => __( 'Options-Name aus der beschreibbaren Allowlist.', 'wp-agent-companion' ),
				),
				'value' => array(
					'type'        => array( 'string', 'integer' ),
					'maxLength'   => 500,
					'description' => __( 'Neuer Wert. Typ hängt vom jeweiligen Namen ab (String, Ganzzahl oder Enum-String).', 'wp-agent-companion' ),
				),
				'expected_hash' => array(
					'type'    => 'string',
					'pattern' => '^[a-f0-9]{64}$',
				),
			),
			'required'              => array( 'name', 'value', 'expected_hash' ),
			'additionalProperties'  => false,
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'name'     => array( 'type' => 'string' ),
				'value'    => array(
					'type'        => array( 'string', 'integer' ),
					'description' => __( 'Der tatsächlich gespeicherte, per Re-Read gelesene Wert.', 'wp-agent-companion' ),
				),
				'verified' => array( 'type' => 'boolean' ),
				'actions'  => array(
					'type'  => 'array',
					'items' => array( 'type' => 'string' ),
				),
				'previous_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'state_hash'    => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required'              => array( 'name', 'value', 'verified', 'actions', 'previous_hash', 'state_hash' ),
			'additionalProperties'  => false,
		),
		'execute_callback'    => 'wpagent_exec_update_setting',
		'permission_callback' => 'wpagent_perm_manage_options',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
			),
		),
	);
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_update_setting( $input ) {
	$input = is_array( $input ) ? $input : array();
	$name  = (string) ( $input['name'] ?? '' );

	if ( '' === trim( $name ) ) {
		return new WP_Error( 'wpagent_missing_name', __( 'Options-Name fehlt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! array_key_exists( 'value', $input ) ) {
		return new WP_Error( 'wpagent_missing_value', __( 'value fehlt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! wpagent_setting_is_allowed( $name ) ) {
		return new WP_Error(
			'wpagent_setting_not_allowed',
			sprintf(
				/* translators: %s: Options-Name. */
				__( 'Die Einstellung "%s" ist nicht in der Allowlist beschreibbarer Einstellungen.', 'wp-agent-companion' ),
				$name
			),
			array( 'status' => 400 )
		);
	}
	$expected_hash = wpagent_core_mutator_expected_hash( $input );
	if ( is_wp_error( $expected_hash ) ) {
		return $expected_hash;
	}

	$sanitized = wpagent_sanitize_setting_value( $name, $input['value'] );
	if ( is_wp_error( $sanitized ) ) {
		return $sanitized;
	}
	if ( 'permalink_structure' === $name ) {
		// Core's rewrite API has no compare-and-swap input and may touch rewrite
		// rules after an unbound option write. Until that provider operation can
		// prove the same raw option row, keep it operator-only.
		return new WP_Error( 'wpagent_permalink_manual_update_required', __( 'Die Permalink-Struktur kann derzeit nicht konfliktgeschützt aktualisiert werden. Ändere sie manuell in WordPress.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}

	$before = wpagent_core_mutator_setting_state( $name );
	if ( ! hash_equals( $before['hash'], $expected_hash ) ) {
		return new WP_Error( 'wpagent_setting_conflict', __( 'Die Einstellung wurde seit dem Lesen verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $before['hash'] ) );
	}
	$lock_identity = wpagent_core_mutator_setting_lock_identity( $name );
	$lock = wpagent_core_mutator_acquire_lock( 'setting', $lock_identity );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}

	try {
		if ( ! wpagent_core_mutator_refresh_lock( 'setting', $lock_identity, $lock ) || ! current_user_can( 'manage_options' ) ) {
			return new WP_Error( 'wpagent_setting_lock_lost', __( 'Die exklusive Sperre oder Berechtigung ging vor dem Schreiben verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$before = wpagent_core_mutator_setting_state( $name );
		if ( ! hash_equals( $before['hash'], $expected_hash ) ) {
			return new WP_Error( 'wpagent_setting_conflict', __( 'Die Einstellung wurde während der Sperrübernahme verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $before['hash'] ) );
		}
		$consistency = wpagent_check_front_page_consistency( $name, $sanitized );
		if ( is_wp_error( $consistency ) ) {
			return $consistency;
		}
		if ( ! $before['exists'] || ! is_string( $before['raw'] ) || ! is_string( $before['autoload'] ) ) {
			return wpagent_core_mutator_recovery_required( 'wpagent_setting_storage_unavailable', __( 'Der aktuelle Optionsdatensatz konnte nicht exakt gelesen werden. Es wurde nichts geändert.', 'wp-agent-companion' ), $before['hash'] );
		}
		$before_row = array( 'raw' => $before['raw'], 'autoload' => $before['autoload'] );
		$actions = array();
		if ( wpagent_core_mutator_setting_values_equal( $name, $before['value'], $sanitized ) ) {
			if ( ! wpagent_core_mutator_lock_is_owned( 'setting', $lock_identity, $lock ) || ! current_user_can( 'manage_options' ) ) {
				return new WP_Error( 'wpagent_setting_lock_lost', __( 'Die exklusive Sperre oder Berechtigung ging während der No-op-Prüfung verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			return array(
				'name'          => $name,
				'value'         => $before['value'],
				'verified'      => true,
				'actions'       => $actions,
				'previous_hash' => $before['hash'],
				'state_hash'    => $before['hash'],
			);
		}
		$candidate_raw = maybe_serialize( $sanitized );
		if ( ! is_scalar( $candidate_raw ) ) {
			return wpagent_core_mutator_recovery_required( 'wpagent_setting_candidate_unserializable', __( 'Der neue Einstellungswert konnte nicht als exakter Optionswert gebunden werden. Es wurde nichts geändert.', 'wp-agent-companion' ), $before['hash'] );
		}
		if ( ! wpagent_core_mutator_refresh_lock( 'setting', $lock_identity, $lock ) || ! current_user_can( 'manage_options' ) ) {
			return new WP_Error( 'wpagent_setting_lock_lost', __( 'Die exklusive Sperre oder Berechtigung ging unmittelbar vor dem Schreiben verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! wpagent_core_mutator_replace_option_if_current( $name, $before_row, (string) $candidate_raw ) ) {
			$fresh_after_failed_cas = wpagent_core_mutator_setting_state( $name );
			if ( hash_equals( $before['hash'], $fresh_after_failed_cas['hash'] ) ) {
				return new WP_Error( 'wpagent_setting_write_failed', __( 'Die Einstellung konnte nicht geschrieben werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => false ) );
			}
			return wpagent_core_mutator_recovery_required( 'wpagent_setting_recovery_required', __( 'Die Einstellung änderte sich während des bedingten Schreibens. Prüfe ihren aktuellen Zustand manuell.', 'wp-agent-companion' ), $fresh_after_failed_cas['hash'] );
		}

		$fresh         = wpagent_core_mutator_setting_state( $name );
		$fresh_row     = wpagent_core_mutator_option_row( $name );
		$verified      = $fresh['exists'] && is_array( $fresh_row ) && wpagent_core_mutator_setting_values_equal( $name, $fresh['value'], $sanitized ) && hash_equals( (string) $candidate_raw, $fresh_row['raw'] ) && hash_equals( $before_row['autoload'], $fresh_row['autoload'] );
		$owned         = wpagent_core_mutator_lock_is_owned( 'setting', $lock_identity, $lock ) && current_user_can( 'manage_options' );
		if ( ! $verified || ! $owned ) {
			// Core options have no ownership marker. Even a byte-equal candidate
			// may have been independently written by another actor, so an automatic
			// rollback would be unsafe. The compare-and-swap restore helper remains
			// available for future marker-backed scopes, but is intentionally not
			// used for this ambiguous shared Core state.
			return wpagent_core_mutator_recovery_required( 'wpagent_setting_recovery_required', __( 'Die Einstellungsänderung konnte nicht vollständig verifiziert werden. Es wurde kein automatisches Rollback ausgeführt, weil ein fremder gleichzeitiger Schreibvorgang nicht sicher auszuschließen ist.', 'wp-agent-companion' ), $fresh['hash'] );
		}

		return array(
			'name'          => $name,
			'value'         => $fresh['value'],
			'verified'      => true,
			'actions'       => $actions,
			'previous_hash' => $before['hash'],
			'state_hash'    => $fresh['hash'],
		);
	} finally {
		wpagent_core_mutator_release_lock( 'setting', $lock_identity, $lock );
	}
}

/**
 * Validiert und normalisiert den neuen Wert für einen konkreten,
 * allowlisted Options-Namen. Jede Einstellung hat eigene Typ-/Enum-/
 * Existenz-Regeln.
 *
 * @param string $name Bereits gegen die Allowlist geprüfter Options-Name.
 * @param mixed  $raw  Roher Eingabewert.
 * @return mixed|WP_Error Sanitisierter Wert oder WP_Error.
 */
function wpagent_sanitize_setting_value( string $name, $raw ) {
	switch ( $name ) {
		case 'blogname':
			return wpagent_core_mutator_bounded_text( $raw, 200, true );

		case 'blogdescription':
			return wpagent_core_mutator_bounded_text( $raw, 500, true );

		case 'posts_per_page':
			if ( ! wpagent_core_mutator_is_strict_integer( $raw ) ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'posts_per_page muss eine Zahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$posts_per_page = (int) $raw;
			if ( $posts_per_page <= 0 || $posts_per_page > 1000 ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'posts_per_page muss zwischen 1 und 1000 liegen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return $posts_per_page;

		case 'show_on_front':
			$show_on_front = is_string( $raw ) ? $raw : '';
			if ( ! in_array( $show_on_front, array( 'posts', 'page' ), true ) ) {
				return new WP_Error(
					'wpagent_bad_setting_value',
					sprintf(
						/* translators: %s: übergebener Wert. */
						__( 'show_on_front muss "posts" oder "page" sein, war "%s".', 'wp-agent-companion' ),
						$show_on_front
					),
					array( 'status' => 400 )
				);
			}
			return $show_on_front;

		case 'page_on_front':
		case 'page_for_posts':
			return wpagent_sanitize_front_page_id( $name, $raw );

		case 'permalink_structure':
			return wpagent_core_mutator_bounded_text( $raw, 255, true );

		case 'timezone_string':
			if ( ! is_string( $raw ) || strlen( $raw ) > 64 || 1 === preg_match( '/[\x00-\x1F\x7F]/', $raw ) ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'timezone_string enthält ungültige Steuerzeichen oder ist zu lang.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$timezone_string = $raw;
			if ( '' !== $timezone_string && ! in_array( $timezone_string, DateTimeZone::listIdentifiers(), true ) ) {
				return new WP_Error(
					'wpagent_bad_setting_value',
					sprintf(
						/* translators: %s: übergebener Wert. */
						__( 'timezone_string ist keine gültige Zeitzonen-ID: "%s".', 'wp-agent-companion' ),
						$timezone_string
					),
					array( 'status' => 400 )
				);
			}
			return $timezone_string;

		case 'start_of_week':
			if ( ! wpagent_core_mutator_is_strict_integer( $raw ) ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'start_of_week muss eine Zahl von 0 (Sonntag) bis 6 (Samstag) sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$start_of_week = (int) $raw;
			if ( $start_of_week < 0 || $start_of_week > 6 ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'start_of_week muss zwischen 0 und 6 liegen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return $start_of_week;

		case 'date_format':
		case 'time_format':
			$format_value = wpagent_core_mutator_bounded_text( $raw, 100 );
			if ( is_wp_error( $format_value ) ) {
				return new WP_Error(
					'wpagent_bad_setting_value',
					sprintf(
						/* translators: %s: Options-Name. */
						__( '%s darf nicht leer sein.', 'wp-agent-companion' ),
						$name
					),
					array( 'status' => 400 )
				);
			}
			return $format_value;

		case 'default_category':
			if ( ! wpagent_core_mutator_is_strict_integer( $raw ) ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'default_category muss eine Kategorie-ID (Ganzzahl) sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$term_id = (int) $raw;
			$term    = get_term( $term_id, 'category' );
			if ( ! $term || is_wp_error( $term ) ) {
				return new WP_Error(
					'wpagent_bad_setting_value',
					sprintf(
						/* translators: %d: Kategorie-ID. */
						__( 'default_category verweist auf keine bestehende Kategorie (ID %d).', 'wp-agent-companion' ),
						$term_id
					),
					array( 'status' => 400 )
				);
			}
			return $term_id;

		default:
			// Sollte durch wpagent_setting_is_allowed() bereits ausgeschlossen sein;
			// dieser Zweig ist ein reines Sicherheitsnetz.
			return new WP_Error(
				'wpagent_setting_not_allowed',
				sprintf(
					/* translators: %s: Options-Name. */
					__( 'Die Einstellung "%s" ist nicht beschreibbar.', 'wp-agent-companion' ),
					$name
				),
				array( 'status' => 400 )
			);
	}
}

/**
 * Validiert page_on_front/page_for_posts: 0 (kein Wert gesetzt) ist erlaubt,
 * jede positive ID muss auf eine bestehende Seite (post_type page) verweisen.
 *
 * @param string $name Entweder "page_on_front" oder "page_for_posts".
 * @param mixed  $raw  Roher Eingabewert.
 * @return int|WP_Error
 */
function wpagent_sanitize_front_page_id( string $name, $raw ) {
	if ( ! wpagent_core_mutator_is_strict_integer( $raw ) ) {
		return new WP_Error(
			'wpagent_bad_setting_value',
			sprintf(
				/* translators: %s: Options-Name. */
				__( '%s muss eine Seiten-ID (Ganzzahl) sein.', 'wp-agent-companion' ),
				$name
			),
			array( 'status' => 400 )
		);
	}

	$page_id = (int) $raw;
	if ( $page_id < 0 ) {
		return new WP_Error(
			'wpagent_bad_setting_value',
			sprintf(
				/* translators: %s: Options-Name. */
				__( '%s darf nicht negativ sein.', 'wp-agent-companion' ),
				$name
			),
			array( 'status' => 400 )
		);
	}

	if ( $page_id > 0 ) {
		$page = get_post( $page_id );
		if ( ! $page || 'page' !== $page->post_type ) {
			return new WP_Error(
				'wpagent_bad_setting_value',
				sprintf(
					/* translators: 1: Options-Name, 2: Seiten-ID. */
					__( '%1$s verweist auf keine bestehende Seite (ID %2$d).', 'wp-agent-companion' ),
					$name,
					$page_id
				),
				array( 'status' => 400 )
			);
		}
	}

	return $page_id;
}

/**
 * Prüft die fachliche Konsistenz von show_on_front/page_on_front/
 * page_for_posts, bezogen auf den Zustand NACH dieser einen Aktualisierung
 * (die beiden nicht betroffenen Werte kommen aus der Datenbank).
 *
 *  - show_on_front wird gerade auf "page" gesetzt: es muss dann bereits eine
 *    page_on_front-Seite gesetzt sein (sonst zeigt die Startseite auf keine
 *    Seite).
 *  - Ist show_on_front (neu oder bereits bestehend) "page": page_on_front und
 *    page_for_posts dürfen nicht dieselbe Seite sein.
 *
 * @param string $name      Der gerade aktualisierte Options-Name.
 * @param mixed  $sanitized Der bereits sanitisierte neue Wert dieses Namens.
 * @return true|WP_Error
 */
function wpagent_check_front_page_consistency( string $name, $sanitized ) {
	if ( ! in_array( $name, array( 'show_on_front', 'page_on_front', 'page_for_posts' ), true ) ) {
		return true;
	}

	$show_on_front  = ( 'show_on_front' === $name ) ? $sanitized : (string) get_option( 'show_on_front', 'posts' );
	$page_on_front  = ( 'page_on_front' === $name ) ? (int) $sanitized : (int) get_option( 'page_on_front', 0 );
	$page_for_posts = ( 'page_for_posts' === $name ) ? (int) $sanitized : (int) get_option( 'page_for_posts', 0 );

	if ( 'page' !== $show_on_front ) {
		return true;
	}

	if ( 'show_on_front' === $name && $page_on_front <= 0 ) {
		return new WP_Error(
			'wpagent_inconsistent_front_page',
			__( 'show_on_front=page erfordert eine bereits gesetzte page_on_front-Seite. Erst page_on_front setzen, dann show_on_front=page.', 'wp-agent-companion' ),
			array( 'status' => 400 )
		);
	}

	if ( $page_on_front > 0 && $page_for_posts > 0 && $page_on_front === $page_for_posts ) {
		return new WP_Error(
			'wpagent_inconsistent_front_page',
			__( 'page_on_front und page_for_posts dürfen nicht dieselbe Seite sein.', 'wp-agent-companion' ),
			array( 'status' => 400 )
		);
	}

	return true;
}
