<?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
 * geschrieben wird; das Ergebnis wird per Re-Read über get_option()
 * verifiziert, nicht über den Rückgabewert von update_option() (das WP-Kern-
 * Verhalten "false bei unverändertem Wert" ist kein Fehler-Signal, analog zum
 * dokumentierten Verhalten in wp-agent-power/option-write.php).
 *
 * 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 ändert sich NICHT wirksam durch einen rohen
 *     update_option()-Aufruf allein: WordPress braucht dafür den Umweg über
 *     $wp_rewrite->set_permalink_structure() (das intern selbst
 *     update_option() aufruft) gefolgt von flush_rewrite_rules(), sonst bleiben
 *     die tatsächlichen Rewrite-Regeln auf dem alten Stand, obwohl die Option
 *     längst den neuen Wert zeigt.
 *
 * @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, Permalink-Struktur, Zeitzone, Datums-/Zeitformat, Standard-Kategorie und mehr). Prüft Enum-/Typ-Werte, die Konsistenz von show_on_front/page_on_front/page_for_posts und baut bei einer Permalink-Änderung die Rewrite-Regeln neu auf. Verifiziert per Re-Read über get_option().', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'name'  => array(
					'type'        => 'string',
					'description' => __( 'Options-Name aus der beschreibbaren Allowlist.', 'wp-agent-companion' ),
				),
				'value' => array(
					'type'        => array( 'string', 'number', 'integer' ),
					'description' => __( 'Neuer Wert. Typ hängt vom jeweiligen Namen ab (String, Ganzzahl oder Enum-String).', 'wp-agent-companion' ),
				),
			),
			'required'   => array( 'name', 'value' ),
		),
		'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' ),
				),
			),
		),
		'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'  => true,
			),
		),
	);
}

/**
 * @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 )
		);
	}

	$sanitized = wpagent_sanitize_setting_value( $name, $input['value'] );
	if ( is_wp_error( $sanitized ) ) {
		return $sanitized;
	}

	$consistency = wpagent_check_front_page_consistency( $name, $sanitized );
	if ( is_wp_error( $consistency ) ) {
		return $consistency;
	}

	$actions = array();

	if ( 'permalink_structure' === $name ) {
		global $wp_rewrite;
		if ( ! ( $wp_rewrite instanceof WP_Rewrite ) ) {
			return new WP_Error( 'wpagent_no_rewrite', __( 'WP_Rewrite ist nicht verfügbar.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		// set_permalink_structure() persistiert selbst per update_option() und
		// feuert nur bei einer echten Änderung; flush_rewrite_rules() baut
		// danach die tatsächlichen Rewrite-Regeln aus dem neuen Wert neu auf.
		// Ohne diesen zweiten Schritt zeigt die Option zwar den neuen Wert, aber
		// die aktiven Rewrite-Regeln bleiben auf dem alten Stand.
		$wp_rewrite->set_permalink_structure( $sanitized );
		flush_rewrite_rules( false );
		$actions[] = 'flush_rewrite_rules(false)';
	} else {
		update_option( $name, $sanitized );
	}

	$fresh    = get_option( $name );
	$verified = ( (string) $fresh === (string) $sanitized );

	return array(
		'name'     => $name,
		'value'    => $fresh,
		'verified' => $verified,
		'actions'  => $actions,
	);
}

/**
 * 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':
		case 'blogdescription':
			return (string) $raw;

		case 'posts_per_page':
			if ( ! is_numeric( $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 ) {
				return new WP_Error( 'wpagent_bad_setting_value', __( 'posts_per_page muss größer als 0 sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return $posts_per_page;

		case 'show_on_front':
			$show_on_front = (string) $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 (string) $raw;

		case 'timezone_string':
			$timezone_string = (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 ( ! is_numeric( $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 = (string) $raw;
			if ( '' === trim( $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 ( ! is_numeric( $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 ( ! is_numeric( $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;
}
