<?php
/** Begrenzte und validierte Speicherhelfer für Agent-Kontext. */

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

define( 'WPAGENT_COMPANION_OPT_SITE_CONTEXT', 'wp_agent_site_context' );
define( 'WPAGENT_COMPANION_OPT_SKILLS', 'wp_agent_saved_skills' );
define( 'WPAGENT_COMPANION_OPT_MEMORY', 'wp_agent_memory' );
define( 'WPAGENT_COMPANION_OPT_DESIGN_PROFILE', 'wp_agent_design_profile' );
define( 'WPAGENT_COMPANION_OPT_DESIGN_DIRECTIONS', 'wp_agent_design_directions' );
define( 'WPAGENT_COMPANION_OPT_ACTIVE_DESIGN_DIRECTION', 'wp_agent_active_design_direction' );
define( 'WPAGENT_COMPANION_OPT_DESIGN_DIRECTIONS_LOCK', 'wp_agent_design_directions_lock' );
define( 'WPAGENT_COMPANION_OPT_AGENT_STATE_LOCK', 'wp_agent_state_lock' );
define( 'WPAGENT_COMPANION_AGENT_STATE_LOCK_TTL', 30 );

/** @return string|WP_Error */
function wpagent_state_text( $value, int $max_length, string $field ) {
	if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
		/* translators: %s: machine-readable field name. */
		return new WP_Error( 'wpagent_state_bad_type', sprintf( __( '%s muss Text sein.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
	}
	$text   = trim( str_replace( array( "\r\n", "\r" ), "\n", wp_strip_all_tags( (string) $value ) ) );
	$length = function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text );
	if ( $length > $max_length ) {
		/* translators: 1: machine-readable field name, 2: maximum character count. */
		return new WP_Error( 'wpagent_state_too_long', sprintf( __( '%1$s darf höchstens %2$d Zeichen enthalten.', 'wp-agent-companion' ), $field, $max_length ), array( 'status' => 400 ) );
	}
	return $text;
}

/** @return string|WP_Error */
function wpagent_state_id( $value ) {
	$raw = is_string( $value ) ? $value : '';
	if ( ! preg_match( '/^[a-z0-9][a-z0-9_-]{0,63}$/', $raw ) || sanitize_key( $raw ) !== $raw ) {
		return new WP_Error( 'wpagent_state_bad_id', __( 'id muss aus 1-64 Kleinbuchstaben, Zahlen, Bindestrichen oder Unterstrichen bestehen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $raw;
}

/** @return array<mixed> */
function wpagent_state_array_option( string $name ): array {
	$snapshot = wpagent_state_option_snapshot( $name );
	$value = $snapshot['readable'] ? $snapshot['value'] : null;
	return is_array( $value ) ? $value : array();
}

function wpagent_state_now(): string {
	return gmdate( 'c' );
}

/**
 * Decode a raw state option without allowing a stored object to instantiate
 * or run a magic method while the state is merely being inspected.
 *
 * Agent State persists only arrays and scalar values. An object is therefore
 * intentionally returned as an incomplete object and rejected by each state
 * reader, rather than being revived through WordPress' generic option helper.
 *
 * @return mixed
 */
function wpagent_state_decode_option_value( string $raw ) {
	if ( ! is_serialized( $raw, false ) ) {
		return $raw;
	}
	try {
		return @unserialize( $raw, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Agent State reads its raw option for CAS and must reject object hydration.
	} catch ( Throwable $error ) {
		return false;
	}
}

/** @return array{readable:bool,exists:bool,value:mixed,raw:string,autoload:string} */
function wpagent_state_option_snapshot( string $name ): array {
	global $wpdb;
	$row = $wpdb->get_row(
		$wpdb->prepare(
			"SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
			$name
		),
		ARRAY_A
	);
	if ( '' !== $wpdb->last_error ) {
		return array( 'readable' => false, 'exists' => false, 'value' => null, 'raw' => '', 'autoload' => 'off' );
	}
	if ( ! is_array( $row ) ) {
		return array( 'readable' => true, 'exists' => false, 'value' => null, 'raw' => '', 'autoload' => 'off' );
	}
	return array(
		'readable' => true,
		'exists'   => true,
		'value'    => wpagent_state_decode_option_value( (string) $row['option_value'] ),
		'raw'      => (string) $row['option_value'],
		'autoload' => (string) $row['autoload'],
	);
}

/** @param array{readable:bool,exists:bool,value:mixed,raw:string,autoload:string} $snapshot */
function wpagent_state_option_matches_snapshot( string $name, array $snapshot ): bool {
	$current = wpagent_state_option_snapshot( $name );
	return $snapshot['readable'] && $current['readable']
		&& $current['exists'] === $snapshot['exists']
		&& ( ! $snapshot['exists'] || ( $current['raw'] === $snapshot['raw'] && $current['autoload'] === $snapshot['autoload'] ) );
}

/**
 * Returns a byte-sensitive hash for one or more option rows.  Values and
 * autoload flags both matter: a WordPress admin edit can change either while
 * an MCP client is preparing a mutation.
 *
 * @param array<string,array{readable:bool,exists:bool,value:mixed,raw:string,autoload:string}> $snapshots
 */
function wpagent_state_snapshot_hash( array $snapshots ): string {
	ksort( $snapshots, SORT_STRING );
	$state = array();
	foreach ( $snapshots as $name => $snapshot ) {
		$state[ $name ] = array(
			'exists'   => ! empty( $snapshot['exists'] ),
			'raw'      => (string) ( $snapshot['raw'] ?? '' ),
			'autoload' => (string) ( $snapshot['autoload'] ?? '' ),
		);
	}
	return hash( 'sha256', (string) wp_json_encode( $state ) );
}

/** @return string|WP_Error */
function wpagent_state_hash_for_options( array $names ) {
	$snapshots = array();
	foreach ( array_values( array_unique( $names ) ) as $name ) {
		$snapshot = wpagent_state_option_snapshot( (string) $name );
		if ( ! $snapshot['readable'] ) {
			return new WP_Error( 'wpagent_state_read_failed', __( 'Der aktuelle Agent-State konnte nicht sicher gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
		}
		$snapshots[ (string) $name ] = $snapshot;
	}
	return wpagent_state_snapshot_hash( $snapshots );
}

/** @return true|WP_Error */
function wpagent_state_require_expected_hash( $input, string $current_hash ) {
	if ( ! is_array( $input ) || ! array_key_exists( 'expected_state_hash', $input ) ) {
		return new WP_Error( 'wpagent_state_hash_required', __( 'expected_state_hash ist für Änderungen am Agent-State erforderlich. Lade den aktuellen State neu.', 'wp-agent-companion' ), array( 'status' => 409, 'state_hash' => $current_hash ) );
	}
	$expected = $input['expected_state_hash'];
	if ( ! is_string( $expected ) || ! preg_match( '/^[a-f0-9]{64}$/', $expected ) ) {
		return new WP_Error( 'wpagent_state_hash_invalid', __( 'expected_state_hash muss ein SHA-256-Hash sein.', 'wp-agent-companion' ), array( 'status' => 400, 'state_hash' => $current_hash ) );
	}
	if ( ! hash_equals( $current_hash, $expected ) ) {
		return new WP_Error( 'wpagent_state_conflict', __( 'Der Agent-State wurde zwischenzeitlich geändert. Lade ihn neu und wiederhole die Änderung.', 'wp-agent-companion' ), array( 'status' => 409, 'state_hash' => $current_hash ) );
	}
	return true;
}

function wpagent_state_scoped_lock_option( string $scope ): string {
	if ( 'design-directions' === $scope ) {
		return WPAGENT_COMPANION_OPT_DESIGN_DIRECTIONS_LOCK;
	}
	return 'wp_agent_state_lock_' . substr( hash( 'sha256', $scope ), 0, 32 );
}

/** @return string|WP_Error */
function wpagent_state_acquire_scoped_lock( string $scope ) {
	global $wpdb;
	$option = wpagent_state_scoped_lock_option( $scope );
	$token  = wpagent_companion_acquire_option_lock( $option, WPAGENT_COMPANION_AGENT_STATE_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	if ( '' !== $wpdb->last_error ) {
		return new WP_Error( 'wpagent_state_lock_failed', __( 'Die Agent-State-Sperre konnte nicht sicher erzeugt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	if ( 'design-directions' === $scope ) {
		return new WP_Error( 'wpagent_design_direction_busy', __( 'Eine andere Designrichtungsänderung läuft bereits. Versuche es gleich erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return new WP_Error( 'wpagent_state_locked', __( 'Der Agent-State wird gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_state_refresh_scoped_lock( string $scope, string $token ): bool {
	return wpagent_companion_refresh_option_lock( wpagent_state_scoped_lock_option( $scope ), $token, WPAGENT_COMPANION_AGENT_STATE_LOCK_TTL );
}

function wpagent_state_scoped_lock_is_owned( string $scope, string $token ): bool {
	return wpagent_companion_option_lock_is_owned( wpagent_state_scoped_lock_option( $scope ), $token );
}

function wpagent_state_release_scoped_lock( string $scope, string $token ): void {
	wpagent_companion_release_option_lock( wpagent_state_scoped_lock_option( $scope ), $token );
}

/** @return mixed|WP_Error */
function wpagent_state_with_scoped_lock( string $scope, callable $operation, string $error_code, string $error_message ) {
	$lock = wpagent_state_acquire_scoped_lock( $scope );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		if ( ! wpagent_state_refresh_scoped_lock( $scope, $lock ) || ! wpagent_state_scoped_lock_is_owned( $scope, $lock ) ) {
			return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
		}
		return $operation( $lock );
	} finally {
		wpagent_state_release_scoped_lock( $scope, $lock );
	}
}

/** @return array{readable:bool,exists:bool,value:mixed,raw:string,autoload:string}|WP_Error */
function wpagent_state_compare_and_swap_option( string $name, array $before, $next, string $error_code, string $error_message ) {
	if ( ! $before['readable'] ) {
		return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
	}
	try {
		$filtered = apply_filters( "pre_update_option_{$name}", $next, $before['value'], $name );
		$filtered = apply_filters( 'pre_update_option', $filtered, $name, $before['value'] );
	} catch ( Throwable $error ) {
		return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
	}
	if ( $filtered !== $next ) {
		return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
	}

	$next_raw = maybe_serialize( $next );
	if ( $before['exists'] && $before['raw'] === $next_raw ) {
		return $before;
	}
	global $wpdb;
	if ( $before['exists'] ) {
		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s, autoload = %s WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND autoload = %s",
				$next_raw,
				$before['autoload'],
				$name,
				$before['raw'],
				$before['autoload']
			)
		);
	} else {
		$updated = wpagent_companion_add_option_create_only( $name, $next, '', false ) ? 1 : 0;
	}
	wpagent_companion_forget_option_cache( $name, $before['exists'] && in_array( $before['autoload'], wp_autoload_values_to_autoload(), true ) );
	$fresh = wpagent_state_option_snapshot( $name );
	if ( 1 === $updated && $fresh['readable'] && $fresh['exists'] && $fresh['raw'] === $next_raw ) {
		return $fresh;
	}
	$changed_since_snapshot = $fresh['readable'] && ( $fresh['exists'] !== $before['exists'] || ( $fresh['exists'] && ( $fresh['raw'] !== $before['raw'] || $fresh['autoload'] !== $before['autoload'] ) ) );
	if ( 0 === $updated && $changed_since_snapshot ) {
		return new WP_Error( 'wpagent_state_conflict', __( 'Der Agent-State wurde zwischenzeitlich geändert. Lade ihn neu und wiederhole die Änderung.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
}

/** @return bool */
function wpagent_state_compare_and_restore_option( string $name, array $after, array $before ): bool {
	global $wpdb;
	if ( ! $after['exists'] || ! $before['readable'] ) {
		return false;
	}
	if ( $before['exists'] ) {
		$result = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s, autoload = %s WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND autoload = %s",
				$before['raw'],
				$before['autoload'],
				$name,
				$after['raw'],
				$after['autoload']
			)
		);
	} else {
		$result = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->options} WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND autoload = %s",
				$name,
				$after['raw'],
				$after['autoload']
			)
		);
	}
	wpagent_companion_forget_option_cache( $name, in_array( $after['autoload'], wp_autoload_values_to_autoload(), true ) );
	return 1 === $result && wpagent_state_option_matches_snapshot( $name, $before );
}

/** @return true|WP_Error */
function wpagent_state_commit_options( array $updates, array $snapshots, string $scope, string $token, string $error_code, string $error_message ) {
	$written = array();
	$guard_names = array_diff( array_keys( $snapshots ), array_keys( $updates ) );
	$recover = static function ( WP_Error $failure ) use ( &$written, $snapshots, $error_code, $error_message ) {
		$recovered = true;
		foreach ( array_reverse( $written, true ) as $written_name => $after ) {
			$recovered = wpagent_state_compare_and_restore_option( $written_name, $after, $snapshots[ $written_name ] ) && $recovered;
		}
		if ( ! $recovered ) {
			return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
		}
		$data = $failure->get_error_data();
		$failure->add_data( array_merge( is_array( $data ) ? $data : array(), array( 'rolled_back' => true ) ) );
		return $failure;
	};
	foreach ( $updates as $name => $value ) {
		if ( ! wpagent_state_refresh_scoped_lock( $scope, $token ) || ! wpagent_state_scoped_lock_is_owned( $scope, $token ) ) {
			return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
		}
		foreach ( $guard_names as $guard_name ) {
			if ( ! wpagent_state_option_matches_snapshot( $guard_name, $snapshots[ $guard_name ] ) ) {
				return $recover( new WP_Error( 'wpagent_state_conflict', __( 'Der Agent-State wurde zwischenzeitlich geändert. Lade ihn neu und wiederhole die Änderung.', 'wp-agent-companion' ), array( 'status' => 409 ) ) );
			}
		}
		$result = wpagent_state_compare_and_swap_option( $name, $snapshots[ $name ], $value, $error_code, $error_message );
		if ( ! is_wp_error( $result ) ) {
			$written[ $name ] = $result;
			continue;
		}
		return $recover( $result );
	}
	if ( ! wpagent_state_refresh_scoped_lock( $scope, $token ) || ! wpagent_state_scoped_lock_is_owned( $scope, $token ) ) {
		return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
	}
	foreach ( $guard_names as $guard_name ) {
		if ( ! wpagent_state_option_matches_snapshot( $guard_name, $snapshots[ $guard_name ] ) ) {
			return $recover( new WP_Error( 'wpagent_state_conflict', __( 'Der Agent-State wurde zwischenzeitlich geändert. Lade ihn neu und wiederhole die Änderung.', 'wp-agent-companion' ), array( 'status' => 409 ) ) );
		}
	}
	foreach ( $written as $name => $after ) {
		if ( ! wpagent_state_option_matches_snapshot( $name, $after ) ) {
			return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
		}
	}
	return true;
}

/** @return string|WP_Error */
function wpagent_state_acquire_lock() {
	global $wpdb;
	$token = wpagent_companion_acquire_option_lock( WPAGENT_COMPANION_OPT_AGENT_STATE_LOCK, WPAGENT_COMPANION_AGENT_STATE_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	if ( '' !== $wpdb->last_error ) {
		return new WP_Error( 'wpagent_state_lock_failed', __( 'Die Agent-State-Sperre konnte nicht sicher erzeugt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return new WP_Error( 'wpagent_state_locked', __( 'Der Agent-State wird gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_state_release_lock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_COMPANION_OPT_AGENT_STATE_LOCK, $token );
}

/**
 * Runs a short state mutation while a create-only, renewable lock is owned.
 * Callers that read-modify-write an option must read its snapshot inside this
 * callback. The lock owner is passed through so long operations can renew it
 * immediately before their compare-and-swap and verified read-back.
 *
 * @return mixed|WP_Error
 */
function wpagent_state_with_lock( callable $operation, string $error_code, string $error_message ) {
	$lock = wpagent_state_acquire_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		if ( ! wpagent_companion_refresh_option_lock( WPAGENT_COMPANION_OPT_AGENT_STATE_LOCK, $lock, WPAGENT_COMPANION_AGENT_STATE_LOCK_TTL ) ) {
			return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
		}
		return $operation( $lock );
	} finally {
		wpagent_state_release_lock( $lock );
	}
}

/**
 * Persists already validated state options as one conditional transaction.
 * Compensation itself is compare-and-swap based, so a foreign write is never
 * overwritten during recovery.
 *
 * @param array<string,mixed> $updates
 * @return true|WP_Error
 */
function wpagent_state_persist_options( array $updates, string $error_code, string $error_message, ?string $expected_state_hash = null ) {
	if ( null === $expected_state_hash ) {
		return new WP_Error( 'wpagent_state_hash_required', __( 'expected_state_hash ist für Änderungen am Agent-State erforderlich. Lade den aktuellen State neu.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return wpagent_state_with_scoped_lock(
		'combined-admin-state',
		static function ( string $token ) use ( $updates, $error_code, $error_message, $expected_state_hash ) {
			$snapshots = array();
			foreach ( array_keys( $updates ) as $name ) {
				$snapshot = wpagent_state_option_snapshot( $name );
				if ( ! $snapshot['readable'] ) {
					return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'recovery_required' => true ) );
				}
				$snapshots[ $name ] = $snapshot;
			}
			$required = wpagent_state_require_expected_hash( array( 'expected_state_hash' => $expected_state_hash ), wpagent_state_snapshot_hash( $snapshots ) );
			if ( is_wp_error( $required ) ) {
				return $required;
			}
			return wpagent_state_commit_options( $updates, $snapshots, 'combined-admin-state', $token, $error_code, $error_message );
		},
		$error_code,
		$error_message
	);
}
