<?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 {
	$value = get_option( $name, array() );
	return is_array( $value ) ? $value : array();
}

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

/** @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'    => maybe_unserialize( $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'] ) );
}

/**
 * Restores one exact option row. The normal API path runs first so caches and
 * hooks remain coherent. A direct database fallback is used only when a
 * third-party option filter prevents that compensation from taking effect.
 *
 * @param array{readable:bool,exists:bool,value:mixed,raw:string,autoload:string} $snapshot
 */
function wpagent_state_restore_option_snapshot( string $name, array $snapshot ): bool {
	try {
		if ( $snapshot['exists'] ) {
			$autoload = in_array( $snapshot['autoload'], array( 'yes', 'on', 'auto-on', 'auto' ), true );
			update_option( $name, $snapshot['value'], $autoload );
		} else {
			delete_option( $name );
		}
	} catch ( Throwable $error ) {
		// Verify below. Some hooks throw after WordPress has already persisted.
	}
	if ( wpagent_state_option_matches_snapshot( $name, $snapshot ) ) {
		return true;
	}

	global $wpdb;
	if ( $snapshot['exists'] ) {
		$exists = null !== $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $name ) );
		$result = $exists
			? $wpdb->update(
				$wpdb->options,
				array( 'option_value' => $snapshot['raw'], 'autoload' => $snapshot['autoload'] ),
				array( 'option_name' => $name ),
				array( '%s', '%s' ),
				array( '%s' )
			)
			: $wpdb->insert(
				$wpdb->options,
				array( 'option_name' => $name, 'option_value' => $snapshot['raw'], 'autoload' => $snapshot['autoload'] ),
				array( '%s', '%s', '%s' )
			);
	} else {
		$result = $wpdb->delete( $wpdb->options, array( 'option_name' => $name ), array( '%s' ) );
		if ( 0 === $result ) {
			$result = true;
		}
	}
	wp_cache_delete( $name, 'options' );
	wp_cache_delete( 'alloptions', 'options' );
	wp_cache_delete( 'notoptions', 'options' );
	return false !== $result && wpagent_state_option_matches_snapshot( $name, $snapshot );
}

/** @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 one or more already validated state options as one compensated
 * transaction. Any failed or transformed write restores every original row.
 *
 * @param array<string,mixed> $updates
 * @return true|WP_Error
 */
function wpagent_state_persist_options( array $updates, string $error_code, string $error_message ) {
	$lock = wpagent_state_acquire_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$snapshots = array();
		foreach ( $updates as $name => $value ) {
			$snapshot = wpagent_state_option_snapshot( $name );
			if ( ! $snapshot['readable'] ) {
				return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'rolled_back' => true ) );
			}
			$snapshots[ $name ] = $snapshot;
		}

		$failed = false;
		foreach ( $updates as $name => $value ) {
			try {
				update_option( $name, $value, false );
				$fresh   = wpagent_state_option_snapshot( $name );
				$missing = new stdClass();
				$visible = get_option( $name, $missing );
				$failed  = ! $fresh['readable']
					|| ! $fresh['exists']
					|| $fresh['raw'] !== maybe_serialize( $value )
					|| $missing === $visible
					|| $visible !== $value;
			} catch ( Throwable $error ) {
				$failed = true;
			}
			if ( $failed ) {
				break;
			}
		}
		if ( ! $failed ) {
			return true;
		}

		$rolled_back = true;
		foreach ( array_reverse( $snapshots, true ) as $name => $snapshot ) {
			$rolled_back = wpagent_state_restore_option_snapshot( $name, $snapshot ) && $rolled_back;
		}
		return new WP_Error( $error_code, $error_message, array( 'status' => 500, 'rolled_back' => $rolled_back ) );
	} finally {
		wpagent_state_release_lock( $lock );
	}
}
