<?php
/** Begrenztes, konfliktgeschütztes Site-Memory für Agent-Läufe. */

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

define( 'WPAGENT_COMPANION_MEMORY_SCHEMA_VERSION', 1 );
define( 'WPAGENT_COMPANION_MEMORY_AUDIT_LIMIT', 100 );
define( 'WPAGENT_COMPANION_MEMORY_WIPE_CONFIRMATION', 'ALLE ERINNERUNGEN LÖSCHEN' );
define( 'WPAGENT_COMPANION_MEMORY_ID_MAX_LENGTH', 64 );
define( 'WPAGENT_COMPANION_MEMORY_TITLE_MAX_LENGTH', 150 );
define( 'WPAGENT_COMPANION_MEMORY_CONTENT_MAX_LENGTH', 10000 );
define( 'WPAGENT_COMPANION_MEMORY_TAG_MAX_LENGTH', 64 );
define( 'WPAGENT_COMPANION_MEMORY_TAG_LIMIT', 20 );
define( 'WPAGENT_COMPANION_MEMORY_UPDATED_AT_MAX_LENGTH', 100 );
define( 'WPAGENT_COMPANION_MEMORY_SEARCH_MAX_LENGTH', 200 );
define( 'WPAGENT_COMPANION_MEMORY_LIST_LIMIT', 100 );

function wpagent_memory_schema(): array {
	return array(
		'type'                 => 'object',
		'additionalProperties' => false,
		'required'             => array( 'id', 'title', 'content', 'tags', 'updated_at' ),
		'properties'           => array(
			'id'         => array( 'type' => 'string', 'pattern' => '^[a-z0-9][a-z0-9_-]{0,63}$', 'maxLength' => WPAGENT_COMPANION_MEMORY_ID_MAX_LENGTH ),
			'title'      => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_TITLE_MAX_LENGTH ),
			'content'    => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_CONTENT_MAX_LENGTH ),
			'tags'       => array( 'type' => 'array', 'maxItems' => WPAGENT_COMPANION_MEMORY_TAG_LIMIT, 'items' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_TAG_MAX_LENGTH ) ),
			'updated_at' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_UPDATED_AT_MAX_LENGTH ),
		),
	);
}

/** @return string|WP_Error */
function wpagent_memory_clean_tag( $value ) {
	if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
		return new WP_Error( 'wpagent_memory_bad_tag', __( 'Memory-Tags müssen Text sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$text = wpagent_state_text( $value, WPAGENT_COMPANION_MEMORY_TAG_MAX_LENGTH, 'tag' );
	if ( is_wp_error( $text ) ) {
		return new WP_Error( 'wpagent_memory_bad_tag', $text->get_error_message(), array( 'status' => 400 ) );
	}
	return sanitize_key( $text );
}

function wpagent_memory_ability_definition( string $label, string $callback, bool $readonly, array $input, array $output, array $required = array(), bool $destructive = false ): array {
	$input_schema = array( 'type' => 'object', 'additionalProperties' => false, 'properties' => $input );
	if ( $required ) {
		$input_schema['required'] = $required;
	}
	return array(
		'label' => $label, 'description' => __( 'Verwaltet explizite, begrenzte Erinnerungen dieser Website. Es werden keine Gespräche automatisch gespeichert.', 'wp-agent-companion' ), 'category' => 'wp-agent',
		'input_schema' => $input_schema, 'output_schema' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => $output, 'required' => array_keys( $output ) ),
		'execute_callback' => $callback, 'permission_callback' => 'wpagent_perm_edit_posts',
		'meta' => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => $readonly, 'destructive' => $destructive, 'idempotent' => $readonly ) ),
	);
}

function wpagent_memory_state_hash_schema(): array {
	return array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$', 'minLength' => 64, 'maxLength' => 64 );
}

function wpagent_ability_list_memory(): array {
	$definition = wpagent_memory_ability_definition( __( 'Memory durchsuchen', 'wp-agent-companion' ), 'wpagent_exec_list_memory', true, array( 'search' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_SEARCH_MAX_LENGTH ), 'tag' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_TAG_MAX_LENGTH ), 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => WPAGENT_COMPANION_MEMORY_LIST_LIMIT, 'default' => 50 ) ), array( 'items' => array( 'type' => 'array', 'maxItems' => WPAGENT_COMPANION_MEMORY_LIST_LIMIT, 'items' => wpagent_memory_schema() ), 'total' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 250 ), 'state_hash' => wpagent_memory_state_hash_schema(), 'blog_id' => array( 'type' => 'integer', 'minimum' => 1 ) ) );
	$definition['meta']['mcp'] = array( 'uri' => 'wpagently://site/memory', 'annotations' => array( 'audience' => array( 'assistant' ) ) );
	$definition['input_schema']['default'] = array();
	return $definition;
}

function wpagent_ability_save_memory(): array {
	return wpagent_memory_ability_definition( __( 'Memory speichern', 'wp-agent-companion' ), 'wpagent_exec_save_memory', false, array( 'id' => array( 'type' => 'string', 'pattern' => '^[a-z0-9][a-z0-9_-]{0,63}$', 'maxLength' => WPAGENT_COMPANION_MEMORY_ID_MAX_LENGTH ), 'title' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_TITLE_MAX_LENGTH ), 'content' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_CONTENT_MAX_LENGTH ), 'tags' => array( 'type' => 'array', 'maxItems' => WPAGENT_COMPANION_MEMORY_TAG_LIMIT, 'items' => array( 'type' => 'string', 'maxLength' => WPAGENT_COMPANION_MEMORY_TAG_MAX_LENGTH ) ), 'expected_state_hash' => wpagent_memory_state_hash_schema() ), array( 'memory' => wpagent_memory_schema(), 'state_hash' => wpagent_memory_state_hash_schema(), 'verified' => array( 'type' => 'boolean' ) ), array( 'title', 'content' ) );
}

function wpagent_ability_delete_memory(): array {
	return wpagent_memory_ability_definition( __( 'Memory löschen', 'wp-agent-companion' ), 'wpagent_exec_delete_memory', false, array( 'id' => array( 'type' => 'string', 'pattern' => '^[a-z0-9][a-z0-9_-]{0,63}$', 'maxLength' => WPAGENT_COMPANION_MEMORY_ID_MAX_LENGTH ), 'expected_state_hash' => wpagent_memory_state_hash_schema() ), array( 'id' => array( 'type' => 'string', 'pattern' => '^[a-z0-9][a-z0-9_-]{0,63}$', 'maxLength' => WPAGENT_COMPANION_MEMORY_ID_MAX_LENGTH ), 'deleted' => array( 'type' => 'boolean' ), 'state_hash' => wpagent_memory_state_hash_schema(), 'verified' => array( 'type' => 'boolean' ) ), array( 'id', 'expected_state_hash' ), true );
}

/** @return true|WP_Error */
function wpagent_memory_validate_input( $input, array $allowed ) {
	if ( ! is_array( $input ) ) {
		return new WP_Error( 'wpagent_memory_bad_input', __( 'Die Memory-Eingabe muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( array_keys( $input ) as $key ) {
		if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
			return new WP_Error( 'wpagent_memory_bad_input', __( 'Die Memory-Eingabe enthält ein nicht erlaubtes Feld.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	return true;
}

/** @return string|WP_Error */
function wpagent_memory_input_string( array $input, string $key, string $default = '' ) {
	if ( ! array_key_exists( $key, $input ) ) {
		return $default;
	}
	if ( ! is_string( $input[ $key ] ) ) {
		return new WP_Error( 'wpagent_memory_bad_input', __( 'Die Memory-Eingabe enthält einen ungültigen Textwert.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $input[ $key ];
}

/** @return array<string,mixed>|null */
function wpagent_normalize_stored_memory( $value ): ?array {
	if ( ! is_array( $value ) ) {
		return null;
	}
	$id = wpagent_state_id( $value['id'] ?? '' );
	if ( is_wp_error( $id ) ) {
		return null;
	}
	$title = wpagent_state_text( $value['title'] ?? '', WPAGENT_COMPANION_MEMORY_TITLE_MAX_LENGTH, 'title' );
	$content = wpagent_state_text( $value['content'] ?? '', WPAGENT_COMPANION_MEMORY_CONTENT_MAX_LENGTH, 'content' );
	$updated_at = wpagent_state_text( $value['updated_at'] ?? '', WPAGENT_COMPANION_MEMORY_UPDATED_AT_MAX_LENGTH, 'updated_at' );
	if ( is_wp_error( $title ) || is_wp_error( $content ) || is_wp_error( $updated_at ) || '' === $title || '' === $content ) {
		return null;
	}
	$raw_tags = $value['tags'] ?? array();
	if ( ! is_array( $raw_tags ) || count( $raw_tags ) > WPAGENT_COMPANION_MEMORY_TAG_LIMIT ) {
		return null;
	}
	$tags = array();
	foreach ( $raw_tags as $tag ) {
		$clean = wpagent_memory_clean_tag( $tag );
		if ( is_wp_error( $clean ) ) {
			return null;
		}
		if ( '' !== $clean ) {
			$tags[] = $clean;
		}
	}
	return array( 'id' => $id, 'title' => $title, 'content' => $content, 'tags' => array_values( array_unique( $tags ) ), 'updated_at' => $updated_at );
}

/** @param array<string,array<string,mixed>> $memories */
function wpagent_memory_state_hash( int $blog_id, array $memories ): string {
	ksort( $memories, SORT_STRING );
	$canonical = array();
	foreach ( $memories as $id => $memory ) {
		$canonical[ $id ] = array(
			'id' => $memory['id'], 'title' => $memory['title'], 'content' => $memory['content'],
			'tags' => array_values( $memory['tags'] ), 'updated_at' => $memory['updated_at'],
		);
	}
	return hash( 'sha256', wp_json_encode( array( 'schema' => WPAGENT_COMPANION_MEMORY_SCHEMA_VERSION, 'blog_id' => $blog_id, 'memories' => $canonical ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
}

/** @return array<string,mixed>|null */
function wpagent_memory_normalize_audit_entry( $value, int $blog_id ): ?array {
	if ( ! is_array( $value ) || ! in_array( $value['action'] ?? '', array( 'save', 'delete', 'wipe' ), true ) ) {
		return null;
	}
	$at = wpagent_state_text( $value['at'] ?? '', 100, 'at' );
	$before = is_string( $value['before_hash'] ?? null ) ? $value['before_hash'] : '';
	$after = is_string( $value['after_hash'] ?? null ) ? $value['after_hash'] : '';
	if ( is_wp_error( $at ) || '' === $at || ! preg_match( '/^[a-f0-9]{64}$/', $before ) || ! preg_match( '/^[a-f0-9]{64}$/', $after ) || (int) ( $value['blog_id'] ?? 0 ) !== $blog_id ) {
		return null;
	}
	$id = '';
	if ( isset( $value['memory_id'] ) && '' !== $value['memory_id'] ) {
		$id = wpagent_state_id( $value['memory_id'] );
		if ( is_wp_error( $id ) ) {
			return null;
		}
	}
	return array( 'action' => $value['action'], 'at' => $at, 'blog_id' => $blog_id, 'actor_id' => max( 0, (int) ( $value['actor_id'] ?? 0 ) ), 'memory_id' => $id, 'before_hash' => $before, 'after_hash' => $after, 'total' => max( 0, min( 250, (int) ( $value['total'] ?? 0 ) ) ) );
}

/**
 * Distinguishes the structured state from a legacy map without reserving a
 * legacy Memory id such as "version". A structural envelope field cannot
 * itself be a valid Memory record.
 */
function wpagent_memory_is_envelope( array $stored ): bool {
	foreach ( array( 'version', 'blog_id', 'memories', 'audit', 'state_hash' ) as $key ) {
		if ( array_key_exists( $key, $stored ) && null === wpagent_normalize_stored_memory( $stored[ $key ] ) ) {
			return true;
		}
	}
	return false;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_memory_state_from_snapshot( array $snapshot ) {
	if ( ! $snapshot['readable'] ) {
		return new WP_Error( 'wpagent_memory_persist_failed', __( 'Der Memory-Speicher konnte nicht sicher gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	$blog_id = max( 1, (int) get_current_blog_id() );
	if ( ! $snapshot['exists'] ) {
		return array( 'blog_id' => $blog_id, 'memories' => array(), 'audit' => array(), 'state_hash' => wpagent_memory_state_hash( $blog_id, array() ), 'snapshot' => $snapshot );
	}
	$stored = $snapshot['value'];
	if ( ! is_array( $stored ) ) {
		return new WP_Error( 'wpagent_memory_state_invalid', __( 'Der gespeicherte Memory-Zustand ist ungültig und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	$envelope = wpagent_memory_is_envelope( $stored );
	if ( $envelope ) {
		if ( WPAGENT_COMPANION_MEMORY_SCHEMA_VERSION !== (int) ( $stored['version'] ?? 0 ) || $blog_id !== (int) ( $stored['blog_id'] ?? 0 ) || ! is_array( $stored['memories'] ?? null ) || ! is_array( $stored['audit'] ?? null ) ) {
			$code = $blog_id !== (int) ( $stored['blog_id'] ?? 0 ) ? 'wpagent_memory_site_mismatch' : 'wpagent_memory_state_invalid';
			return new WP_Error( $code, __( 'Der gespeicherte Memory-Zustand passt nicht zu dieser Website und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 409, 'recovery_required' => true ) );
		}
		$raw_memories = $stored['memories'];
		$raw_audit = $stored['audit'];
	} else {
		$raw_memories = $stored;
		$raw_audit = array();
	}
	if ( count( $raw_memories ) > 250 || count( $raw_audit ) > WPAGENT_COMPANION_MEMORY_AUDIT_LIMIT ) {
		return new WP_Error( 'wpagent_memory_state_invalid', __( 'Der gespeicherte Memory-Zustand überschreitet seine sichere Grenze und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	$memories = array();
	foreach ( $raw_memories as $key => $stored_memory ) {
		$memory = wpagent_normalize_stored_memory( $stored_memory );
		if ( null === $memory ) {
			if ( $envelope ) {
				return new WP_Error( 'wpagent_memory_state_invalid', __( 'Der gespeicherte Memory-Zustand ist ungültig und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
			}
			continue;
		}
		if ( $envelope && ( ! is_string( $key ) || ! hash_equals( $key, $memory['id'] ) ) ) {
			return new WP_Error( 'wpagent_memory_state_invalid', __( 'Der gespeicherte Memory-Zustand ist ungültig und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
		}
		$memories[ $memory['id'] ] = $memory;
	}
	ksort( $memories, SORT_STRING );
	$state_hash = wpagent_memory_state_hash( $blog_id, $memories );
	$audit = array();
	foreach ( $raw_audit as $entry ) {
		$normalized = wpagent_memory_normalize_audit_entry( $entry, $blog_id );
		if ( null === $normalized ) {
			return new WP_Error( 'wpagent_memory_state_invalid', __( 'Das gespeicherte Memory-Audit ist ungültig und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
		}
		$audit[] = $normalized;
	}
	if ( $envelope && ( ! is_string( $stored['state_hash'] ?? null ) || ! hash_equals( $state_hash, $stored['state_hash'] ) ) ) {
		return new WP_Error( 'wpagent_memory_state_invalid', __( 'Der gespeicherte Memory-Zustand stimmt nicht mit seiner Prüfsumme überein und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	return array( 'blog_id' => $blog_id, 'memories' => $memories, 'audit' => $audit, 'state_hash' => $state_hash, 'snapshot' => $snapshot );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_memory_current_state() {
	return wpagent_memory_state_from_snapshot( wpagent_state_option_snapshot( WPAGENT_COMPANION_OPT_MEMORY ) );
}

/** @return array<string,array<string,mixed>> */
function wpagent_normalized_memory(): array {
	$state = wpagent_memory_current_state();
	return is_wp_error( $state ) ? array() : $state['memories'];
}

/** @return array<int,array<string,mixed>> */
function wpagent_memory_audit_entries(): array {
	$state = wpagent_memory_current_state();
	return is_wp_error( $state ) ? array() : $state['audit'];
}

/** @return true|WP_Error */
function wpagent_memory_expected_hash( array $input, array $state, bool $required ) {
	if ( ! array_key_exists( 'expected_state_hash', $input ) ) {
		return $required ? new WP_Error( 'wpagent_memory_state_hash_required', __( 'Für diese Memory-Änderung ist der aktuelle state_hash erforderlich.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $state['state_hash'] ) ) : true;
	}
	$expected = $input['expected_state_hash'];
	if ( ! is_string( $expected ) || ! preg_match( '/^[a-f0-9]{64}$/', $expected ) ) {
		return new WP_Error( 'wpagent_memory_state_hash_invalid', __( 'Der state_hash muss eine gültige Prüfsumme sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return hash_equals( $state['state_hash'], $expected ) ? true : new WP_Error( 'wpagent_memory_conflict', __( 'Das Memory wurde seit dem Lesen verändert. Bitte neu lesen und den aktuellen state_hash verwenden.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $state['state_hash'] ) );
}

/** @param array<string,array<string,mixed>> $memories @param array<int,array<string,mixed>> $audit */
function wpagent_memory_storage( int $blog_id, array $memories, array $audit ): array {
	ksort( $memories, SORT_STRING );
	return array( 'version' => WPAGENT_COMPANION_MEMORY_SCHEMA_VERSION, 'blog_id' => $blog_id, 'memories' => $memories, 'audit' => array_slice( $audit, -WPAGENT_COMPANION_MEMORY_AUDIT_LIMIT ), 'state_hash' => wpagent_memory_state_hash( $blog_id, $memories ) );
}

/** @return array<string,mixed> */
function wpagent_memory_audit_record( string $action, string $memory_id, array $before, array $after ): array {
	return array( 'action' => $action, 'at' => wpagent_state_now(), 'blog_id' => $after['blog_id'], 'actor_id' => max( 0, get_current_user_id() ), 'memory_id' => $memory_id, 'before_hash' => $before['state_hash'], 'after_hash' => $after['state_hash'], 'total' => count( $after['memories'] ) );
}

/** @return true|WP_Error */
function wpagent_memory_compare_and_swap( array $before, array $next ) {
	global $wpdb;
	$next_raw = maybe_serialize( $next );
	$snapshot = $before['snapshot'];
	$old_value = $snapshot['exists'] ? $snapshot['value'] : false;
	try {
		$filtered = apply_filters( 'pre_update_option_' . WPAGENT_COMPANION_OPT_MEMORY, $next, $old_value, WPAGENT_COMPANION_OPT_MEMORY );
		$filtered = apply_filters( 'pre_update_option', $filtered, WPAGENT_COMPANION_OPT_MEMORY, $old_value );
	} catch ( Throwable $error ) {
		return new WP_Error( 'wpagent_memory_persist_failed', __( 'Memory konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	if ( $filtered !== $next ) {
		return new WP_Error( 'wpagent_memory_persist_failed', __( 'Memory konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	if ( $snapshot['exists'] ) {
		$updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND HEX(option_value) = HEX(%s)", $next_raw, WPAGENT_COMPANION_OPT_MEMORY, $snapshot['raw'] ) );
		$write_error = $wpdb->last_error;
	} else {
		$updated = wpagent_companion_add_option_create_only( WPAGENT_COMPANION_OPT_MEMORY, $next, '', false ) ? 1 : 0;
		$write_error = $wpdb->last_error;
	}
	wpagent_companion_forget_option_cache( WPAGENT_COMPANION_OPT_MEMORY, $snapshot['exists'] && in_array( $snapshot['autoload'], wp_autoload_values_to_autoload(), true ) );
	if ( false === $updated || ( 0 === $updated && '' !== $write_error ) ) {
		return new WP_Error( 'wpagent_memory_persist_failed', __( 'Memory konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	if ( 1 !== $updated ) {
		$fresh = wpagent_memory_current_state();
		return is_wp_error( $fresh ) ? new WP_Error( 'wpagent_memory_persist_failed', __( 'Memory konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) ) : new WP_Error( 'wpagent_memory_conflict', __( 'Das Memory wurde parallel verändert. Bitte neu lesen und erneut versuchen.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $fresh['state_hash'] ) );
	}
	$fresh = wpagent_memory_current_state();
	if ( is_wp_error( $fresh ) || ! hash_equals( $next['state_hash'], $fresh['state_hash'] ) || maybe_serialize( $next ) !== maybe_serialize( wpagent_memory_storage( $fresh['blog_id'], $fresh['memories'], $fresh['audit'] ) ) ) {
		return new WP_Error( 'wpagent_memory_persist_failed', __( 'Memory konnte nicht exakt verifiziert gespeichert werden. Eine manuelle Prüfung ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	return true;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_memory_commit( string $lock, array $before, array $memories, string $action, string $memory_id ) {
	$prototype = wpagent_memory_storage( $before['blog_id'], $memories, $before['audit'] );
	$next = wpagent_memory_storage( $before['blog_id'], $memories, array_merge( $before['audit'], array( wpagent_memory_audit_record( $action, $memory_id, $before, $prototype ) ) ) );
	do_action( 'wpagent_memory_before_cas', array( 'id' => $memory_id, 'action' => $action, 'state_hash' => $before['state_hash'] ) );
	if ( ! wpagent_companion_refresh_option_lock( WPAGENT_COMPANION_OPT_AGENT_STATE_LOCK, $lock, WPAGENT_COMPANION_AGENT_STATE_LOCK_TTL ) ) {
		return new WP_Error( 'wpagent_memory_persist_failed', __( 'Die Memory-Sperre konnte nicht sicher erneuert werden. Eine manuelle Prüfung ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	$persisted = wpagent_memory_compare_and_swap( $before, $next );
	if ( is_wp_error( $persisted ) ) {
		return $persisted;
	}
	return wpagent_memory_current_state();
}

/** @return array|WP_Error */
function wpagent_exec_list_memory( $input = array() ) {
	$valid_input = wpagent_memory_validate_input( $input, array( 'search', 'tag', 'limit' ) );
	if ( is_wp_error( $valid_input ) ) {
		return $valid_input;
	}
	$search_value = wpagent_memory_input_string( $input, 'search' );
	$tag_value = wpagent_memory_input_string( $input, 'tag' );
	if ( is_wp_error( $search_value ) || is_wp_error( $tag_value ) ) {
		return is_wp_error( $search_value ) ? $search_value : $tag_value;
	}
	$search = wpagent_state_text( $search_value, WPAGENT_COMPANION_MEMORY_SEARCH_MAX_LENGTH, 'search' );
	$tag = wpagent_memory_clean_tag( $tag_value );
	if ( is_wp_error( $search ) || is_wp_error( $tag ) ) {
		return is_wp_error( $search ) ? $search : $tag;
	}
	$limit = 50;
	if ( array_key_exists( 'limit', $input ) ) {
		if ( ! is_int( $input['limit'] ) || $input['limit'] < 1 || $input['limit'] > WPAGENT_COMPANION_MEMORY_LIST_LIMIT ) {
			return new WP_Error( 'wpagent_memory_bad_input', __( 'Das Memory-Limit muss zwischen 1 und 100 liegen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$limit = $input['limit'];
	}
	$state = wpagent_memory_current_state();
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	$items = array_values( array_filter( $state['memories'], static function ( array $item ) use ( $search, $tag ): bool {
		return ( '' === $tag || in_array( $tag, $item['tags'], true ) ) && ( '' === $search || false !== stripos( $item['title'] . "\n" . $item['content'], $search ) );
	} ) );
	usort( $items, static fn( array $a, array $b ): int => strcmp( $b['updated_at'], $a['updated_at'] ) );
	return array( 'items' => array_slice( $items, 0, $limit ), 'total' => count( $items ), 'state_hash' => $state['state_hash'], 'blog_id' => $state['blog_id'] );
}

/** @return array|WP_Error */
function wpagent_exec_save_memory( $input ) {
	$valid_input = wpagent_memory_validate_input( $input, array( 'id', 'title', 'content', 'tags', 'expected_state_hash' ) );
	if ( is_wp_error( $valid_input ) ) {
		return $valid_input;
	}
	$id_value = wpagent_memory_input_string( $input, 'id' );
	$title_value = wpagent_memory_input_string( $input, 'title' );
	$content_value = wpagent_memory_input_string( $input, 'content' );
	if ( is_wp_error( $id_value ) || is_wp_error( $title_value ) || is_wp_error( $content_value ) ) {
		return is_wp_error( $id_value ) ? $id_value : ( is_wp_error( $title_value ) ? $title_value : $content_value );
	}
	$has_id = '' !== $id_value;
	$id = $has_id ? wpagent_state_id( $id_value ) : wp_generate_uuid4();
	$title = wpagent_state_text( $title_value, WPAGENT_COMPANION_MEMORY_TITLE_MAX_LENGTH, 'title' );
	$content = wpagent_state_text( $content_value, WPAGENT_COMPANION_MEMORY_CONTENT_MAX_LENGTH, 'content' );
	if ( is_wp_error( $id ) || is_wp_error( $title ) || is_wp_error( $content ) ) {
		return is_wp_error( $id ) ? $id : ( is_wp_error( $title ) ? $title : $content );
	}
	if ( '' === $title || '' === $content ) {
		return new WP_Error( 'wpagent_memory_missing_content', __( 'title und content müssen gültig sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_key_exists( 'tags', $input ) && ! is_array( $input['tags'] ) ) {
		return new WP_Error( 'wpagent_memory_bad_tag', __( 'Memory-Tags müssen eine Liste sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$raw_tags = $input['tags'] ?? array();
	if ( count( $raw_tags ) > WPAGENT_COMPANION_MEMORY_TAG_LIMIT ) {
		return new WP_Error( 'wpagent_memory_tag_limit', __( 'Ein Memory darf höchstens 20 Tags haben.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$tags = array();
	foreach ( $raw_tags as $raw_tag ) {
		if ( ! is_string( $raw_tag ) ) {
			return new WP_Error( 'wpagent_memory_bad_tag', __( 'Memory-Tags müssen Text sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$tag = wpagent_memory_clean_tag( $raw_tag );
		if ( is_wp_error( $tag ) ) {
			return $tag;
		}
		if ( '' !== $tag ) {
			$tags[] = $tag;
		}
	}
	return wpagent_state_with_lock( static function ( string $lock ) use ( $input, $has_id, $id, $title, $content, $tags ) {
		$before = wpagent_memory_current_state();
		if ( is_wp_error( $before ) ) {
			return $before;
		}
		$existing = isset( $before['memories'][ $id ] );
		$expected = wpagent_memory_expected_hash( $input, $before, $has_id && $existing );
		if ( is_wp_error( $expected ) ) {
			return $expected;
		}
		if ( ! $existing && count( $before['memories'] ) >= 250 ) {
			return new WP_Error( 'wpagent_memory_limit', __( 'Es können höchstens 250 Memories gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$memories = $before['memories'];
		$memories[ $id ] = array( 'id' => $id, 'title' => $title, 'content' => $content, 'tags' => array_values( array_unique( $tags ) ), 'updated_at' => wpagent_state_now() );
		$after = wpagent_memory_commit( $lock, $before, $memories, 'save', $id );
		return is_wp_error( $after ) ? $after : array( 'memory' => $after['memories'][ $id ], 'state_hash' => $after['state_hash'], 'verified' => true );
	}, 'wpagent_memory_persist_failed', __( 'Memory konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ) );
}

/** @return array|WP_Error */
function wpagent_exec_delete_memory( $input ) {
	$valid_input = wpagent_memory_validate_input( $input, array( 'id', 'expected_state_hash' ) );
	if ( is_wp_error( $valid_input ) ) {
		return $valid_input;
	}
	$id_value = wpagent_memory_input_string( $input, 'id' );
	if ( is_wp_error( $id_value ) ) {
		return $id_value;
	}
	$id = wpagent_state_id( $id_value );
	if ( is_wp_error( $id ) ) {
		return $id;
	}
	return wpagent_state_with_lock( static function ( string $lock ) use ( $input, $id ) {
		$before = wpagent_memory_current_state();
		if ( is_wp_error( $before ) ) {
			return $before;
		}
		$expected = wpagent_memory_expected_hash( $input, $before, true );
		if ( is_wp_error( $expected ) ) {
			return $expected;
		}
		if ( ! isset( $before['memories'][ $id ] ) ) {
			return array( 'id' => $id, 'deleted' => false, 'state_hash' => $before['state_hash'], 'verified' => true );
		}
		$memories = $before['memories'];
		unset( $memories[ $id ] );
		$after = wpagent_memory_commit( $lock, $before, $memories, 'delete', $id );
		return is_wp_error( $after ) ? $after : array( 'id' => $id, 'deleted' => true, 'state_hash' => $after['state_hash'], 'verified' => true );
	}, 'wpagent_memory_persist_failed', __( 'Memory konnte nicht sicher gelöscht werden.', 'wp-agent-companion' ) );
}

/** @return array|WP_Error */
function wpagent_exec_wipe_memory( $input ) {
	$valid_input = wpagent_memory_validate_input( $input, array( 'confirmation', 'expected_state_hash' ) );
	if ( is_wp_error( $valid_input ) ) {
		return $valid_input;
	}
	if ( ! current_user_can( 'manage_options' ) ) {
		return new WP_Error( 'wpagent_memory_wipe_forbidden', __( 'Nur Administratoren dürfen alle Memories löschen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! is_string( $input['confirmation'] ?? null ) || ! hash_equals( WPAGENT_COMPANION_MEMORY_WIPE_CONFIRMATION, trim( $input['confirmation'] ) ) ) {
		return new WP_Error( 'wpagent_memory_wipe_confirmation_required', __( 'Die ausdrückliche Bestätigung zum Löschen aller Memories fehlt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_state_with_lock( static function ( string $lock ) use ( $input ) {
		$before = wpagent_memory_current_state();
		if ( is_wp_error( $before ) ) {
			return $before;
		}
		$expected = wpagent_memory_expected_hash( $input, $before, true );
		if ( is_wp_error( $expected ) ) {
			return $expected;
		}
		if ( ! $before['memories'] ) {
			return array( 'deleted' => 0, 'state_hash' => $before['state_hash'], 'verified' => true );
		}
		$after = wpagent_memory_commit( $lock, $before, array(), 'wipe', '' );
		return is_wp_error( $after ) ? $after : array( 'deleted' => count( $before['memories'] ), 'state_hash' => $after['state_hash'], 'verified' => true );
	}, 'wpagent_memory_persist_failed', __( 'Memories konnten nicht sicher gelöscht werden.', 'wp-agent-companion' ) );
}
