<?php
/**
 * Bounded, redacted and tamper-evident ledger for Companion write attempts.
 *
 * Every mutable Companion ability is instantiated as a narrowly scoped
 * WP_Ability subclass. The subclass writes a recovery_required event before
 * validation, then appends the terminal outcome. A process that stops before
 * that second append remains visibly recovery_required instead of being
 * inferred as successful. The option stores only fixed metadata and HMACs.
 *
 * @package WPAgentCompanion
 */

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

define( 'WPAGENT_COMPANION_ACTIVITY_LOG_OPTION', 'wp_agent_companion_activity_log' );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_VERSION', 2 );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_LIMIT', 100 );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_EVENT_LIMIT', 200 );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_MAX_BYTES', 131072 );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_MAX_INPUT_BYTES', 32768 );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_MAX_INPUT_NODES', 512 );
define( 'WPAGENT_COMPANION_ACTIVITY_LOG_RESET_CONFIRMATION', 'reset-activity-log' );

/** @return array{version:int,blog_id:int,sequence:int,anchor_hash:string,head_hash:string,entries:array<int,array<string,mixed>>} */
function wpagent_companion_activity_log_empty_state( int $blog_id ): array {
	$anchor = str_repeat( '0', 64 );
	return array(
		'version'     => WPAGENT_COMPANION_ACTIVITY_LOG_VERSION,
		'blog_id'     => $blog_id,
		'sequence'    => 0,
		'anchor_hash' => $anchor,
		'head_hash'   => $anchor,
		'entries'     => array(),
	);
}

function wpagent_companion_activity_log_valid_hash( $value ): bool {
	return is_string( $value ) && 1 === preg_match( '/^[a-f0-9]{64}$/', $value );
}

function wpagent_companion_activity_log_valid_timestamp( $value ): bool {
	if ( ! is_string( $value ) || 1 !== preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $value ) ) {
		return false;
	}
	$timestamp = strtotime( $value );
	return false !== $timestamp && gmdate( 'Y-m-d\TH:i:s\Z', $timestamp ) === $value;
}

function wpagent_companion_activity_log_key( int $blog_id ): string {
	return hash_hmac( 'sha256', "wpagently-operation-ledger-v2\0" . $blog_id, wp_salt( 'auth' ), true );
}

/**
 * Builds a bounded canonical byte string only for a local HMAC operation. It
 * is never stored or returned, so it may cover private write intent safely.
 *
 * @param mixed $value
 */
function wpagent_companion_activity_log_canonical_input( $value, int &$remaining, int &$nodes, int $depth = 0 ): ?string {
	if ( $depth > 12 || ++$nodes > WPAGENT_COMPANION_ACTIVITY_LOG_MAX_INPUT_NODES || $remaining < 1 ) {
		return null;
	}
	if ( null === $value ) {
		return 'n';
	}
	if ( is_bool( $value ) ) {
		return $value ? 'b:1' : 'b:0';
	}
	if ( is_int( $value ) ) {
		return 'i:' . $value;
	}
	if ( is_float( $value ) ) {
		if ( ! is_finite( $value ) ) {
			return null;
		}
		$encoded = wp_json_encode( $value, JSON_PRESERVE_ZERO_FRACTION );
		return is_string( $encoded ) ? 'f:' . $encoded : null;
	}
	if ( is_string( $value ) ) {
		$length = strlen( $value );
		if ( $length > $remaining ) {
			return null;
		}
		$remaining -= $length;
		return 's:' . $length . ':' . $value;
	}
	if ( ! is_array( $value ) || count( $value ) > 128 ) {
		return null;
	}
	$is_list = array_is_list( $value );
	$keys    = array_keys( $value );
	if ( ! $is_list ) {
		sort( $keys, SORT_STRING );
	}
	$parts = array( $is_list ? 'l[' : 'a[' );
	foreach ( $keys as $key ) {
		if ( ! is_int( $key ) && ! is_string( $key ) ) {
			return null;
		}
		$key_text = is_int( $key ) ? 'i:' . $key : 's:' . strlen( $key ) . ':' . $key;
		if ( strlen( $key_text ) > $remaining ) {
			return null;
		}
		$remaining -= strlen( $key_text );
		$child = wpagent_companion_activity_log_canonical_input( $value[ $key ], $remaining, $nodes, $depth + 1 );
		if ( null === $child ) {
			return null;
		}
		$parts[] = $key_text . '=' . $child . ';';
	}
	$parts[] = ']';
	return implode( '', $parts );
}

/** @param mixed $input */
function wpagent_companion_activity_log_intent_hash( string $ability_name, string $action, int $actor_id, $input, int $blog_id ): string {
	$remaining = WPAGENT_COMPANION_ACTIVITY_LOG_MAX_INPUT_BYTES;
	$nodes     = 0;
	$canonical = wpagent_companion_activity_log_canonical_input( $input, $remaining, $nodes );
	if ( null === $canonical ) {
		$canonical = 'unavailable';
	}
	return hash_hmac(
		'sha256',
		"intent\0" . $blog_id . "\0" . $actor_id . "\0" . $ability_name . "\0" . $action . "\0" . $canonical,
		wpagent_companion_activity_log_key( $blog_id )
	);
}

/** @param array{blog_id:int,actor_id:int,ability:string,action:string,started_at:string,intent_hash:string} $context */
function wpagent_companion_activity_log_operation_hash( array $context ): string {
	return hash_hmac(
		'sha256',
		"operation\0" . $context['blog_id'] . "\0" . $context['actor_id'] . "\0" . $context['ability'] . "\0" . $context['action'] . "\0" . $context['started_at'] . "\0" . wp_generate_uuid4(),
		wpagent_companion_activity_log_key( $context['blog_id'] )
	);
}

function wpagent_companion_activity_log_valid_status( $value ): bool {
	return in_array( $value, array( 'succeeded', 'failed', 'rejected', 'recovery_required' ), true );
}

/** @param array<string,mixed> $entry */
function wpagent_companion_activity_log_entry_material( array $entry ): string {
	return implode(
		"\0",
		array(
			(string) $entry['sequence'],
			$entry['started_at'],
			(string) ( $entry['completed_at'] ?? '' ),
			(string) $entry['blog_id'],
			(string) $entry['actor_id'],
			$entry['ability'],
			$entry['action'],
			$entry['operation_hash'],
			$entry['intent_hash'],
			$entry['status'],
		)
	);
}

/** @param array<string,mixed> $entry */
function wpagent_companion_activity_log_entry_hash( string $previous_hash, array $entry, int $blog_id ): string {
	return hash_hmac( 'sha256', "entry\0" . $previous_hash . "\0" . wpagent_companion_activity_log_entry_material( $entry ), wpagent_companion_activity_log_key( $blog_id ) );
}

/** @param mixed $entry */
function wpagent_companion_activity_log_valid_entry( $entry, int $blog_id, int $previous_sequence, string $previous_hash ): bool {
	if ( ! is_array( $entry ) ) {
		return false;
	}
	$keys     = array_keys( $entry );
	$expected = array( 'sequence', 'started_at', 'completed_at', 'blog_id', 'actor_id', 'ability', 'action', 'operation_hash', 'intent_hash', 'status', 'entry_hash' );
	sort( $keys, SORT_STRING );
	sort( $expected, SORT_STRING );
	if ( $keys !== $expected
		|| ! is_int( $entry['sequence'] ?? null )
		|| $entry['sequence'] <= $previous_sequence
		|| $blog_id !== ( $entry['blog_id'] ?? 0 )
		|| ! is_int( $entry['actor_id'] ?? null )
		|| $entry['actor_id'] < 0
		|| ! wpagent_companion_activity_log_valid_timestamp( $entry['started_at'] ?? null )
		|| ! is_string( $entry['ability'] ?? null )
		|| 1 !== preg_match( '#^wp-agent/[a-z0-9-]+$#', $entry['ability'] )
		|| ! in_array( $entry['action'] ?? null, array( 'read', 'write' ), true )
		|| ! wpagent_companion_activity_log_valid_hash( $entry['operation_hash'] ?? null )
		|| ! wpagent_companion_activity_log_valid_hash( $entry['intent_hash'] ?? null )
		|| ! wpagent_companion_activity_log_valid_status( $entry['status'] ?? null )
		|| ! wpagent_companion_activity_log_valid_hash( $entry['entry_hash'] ?? null )
	) {
		return false;
	}
	$completed_at = $entry['completed_at'];
	if ( 'recovery_required' === $entry['status'] ) {
		if ( null !== $completed_at ) {
			return false;
		}
	} elseif ( ! wpagent_companion_activity_log_valid_timestamp( $completed_at ) || $completed_at < $entry['started_at'] ) {
		return false;
	}
	return hash_equals( wpagent_companion_activity_log_entry_hash( $previous_hash, $entry, $blog_id ), $entry['entry_hash'] );
}

/** @return WP_Error */
function wpagent_companion_activity_log_invalid_state_error(): WP_Error {
	return new WP_Error( 'wpagent_activity_log_invalid_state', __( 'Das Aktivitätsprotokoll ist beschädigt und bleibt unverändert.', 'wp-agent-companion' ), array( 'status' => 500 ) );
}

function wpagent_companion_activity_log_v1_key( int $blog_id ): string {
	return hash_hmac( 'sha256', "wpagently-activity-log-v1\0" . $blog_id, wp_salt( 'auth' ), true );
}

/** @param array<string,mixed> $entry */
function wpagent_companion_activity_log_v1_entry_material( array $entry ): string {
	return implode( "\0", array( (string) $entry['sequence'], $entry['at'], (string) $entry['blog_id'], (string) $entry['actor_id'], $entry['ability'], $entry['status'], $entry['mode'], $entry['intent_hash'] ) );
}

/** @param array<string,mixed> $entry */
function wpagent_companion_activity_log_v1_entry_hash( string $previous_hash, array $entry, int $blog_id ): string {
	return hash_hmac( 'sha256', "entry\0" . $previous_hash . "\0" . wpagent_companion_activity_log_v1_entry_material( $entry ), wpagent_companion_activity_log_v1_key( $blog_id ) );
}

/** @param mixed $entry */
function wpagent_companion_activity_log_valid_v1_entry( $entry, int $blog_id, int $expected_sequence, string $previous_hash ): bool {
	if ( ! is_array( $entry ) ) {
		return false;
	}
	$keys     = array_keys( $entry );
	$expected = array( 'sequence', 'at', 'blog_id', 'actor_id', 'ability', 'status', 'mode', 'intent_hash', 'entry_hash' );
	sort( $keys, SORT_STRING );
	sort( $expected, SORT_STRING );
	return $keys === $expected
		&& is_int( $entry['sequence'] ?? null )
		&& $expected_sequence === $entry['sequence']
		&& $blog_id === ( $entry['blog_id'] ?? 0 )
		&& is_int( $entry['actor_id'] ?? null )
		&& $entry['actor_id'] >= 0
		&& wpagent_companion_activity_log_valid_timestamp( $entry['at'] ?? null )
		&& is_string( $entry['ability'] ?? null )
		&& 1 === preg_match( '#^wp-agent/[a-z0-9-]+$#', $entry['ability'] )
		&& 'succeeded' === ( $entry['status'] ?? '' )
		&& in_array( $entry['mode'] ?? null, array( 'readonly', 'mutating' ), true )
		&& wpagent_companion_activity_log_valid_hash( $entry['intent_hash'] ?? null )
		&& wpagent_companion_activity_log_valid_hash( $entry['entry_hash'] ?? null )
		&& hash_equals( wpagent_companion_activity_log_v1_entry_hash( $previous_hash, $entry, $blog_id ), $entry['entry_hash'] );
}

/** @param mixed $stored @return array<string,mixed>|WP_Error */
function wpagent_companion_activity_log_normalize_v1_state( $stored, int $blog_id ) {
	if ( ! is_array( $stored ) ) {
		return wpagent_companion_activity_log_invalid_state_error();
	}
	$keys     = array_keys( $stored );
	$expected = array( 'version', 'blog_id', 'sequence', 'anchor_hash', 'head_hash', 'entries' );
	sort( $keys, SORT_STRING );
	sort( $expected, SORT_STRING );
	if ( $keys !== $expected || 1 !== ( $stored['version'] ?? null ) || $blog_id !== ( $stored['blog_id'] ?? null ) || ! is_int( $stored['sequence'] ?? null ) || $stored['sequence'] < 0 || ! wpagent_companion_activity_log_valid_hash( $stored['anchor_hash'] ?? null ) || ! wpagent_companion_activity_log_valid_hash( $stored['head_hash'] ?? null ) || ! is_array( $stored['entries'] ?? null ) || ! array_is_list( $stored['entries'] ) || count( $stored['entries'] ) > WPAGENT_COMPANION_ACTIVITY_LOG_LIMIT ) {
		return wpagent_companion_activity_log_invalid_state_error();
	}
	if ( empty( $stored['entries'] ) ) {
		$initial = str_repeat( '0', 64 );
		return 0 === $stored['sequence'] && hash_equals( $initial, $stored['anchor_hash'] ) && hash_equals( $initial, $stored['head_hash'] ) ? $stored : wpagent_companion_activity_log_invalid_state_error();
	}
	$sequence = $stored['sequence'] - count( $stored['entries'] ) + 1;
	$previous = $stored['anchor_hash'];
	if ( $sequence < 1 ) {
		return wpagent_companion_activity_log_invalid_state_error();
	}
	foreach ( $stored['entries'] as $entry ) {
		if ( ! wpagent_companion_activity_log_valid_v1_entry( $entry, $blog_id, $sequence, $previous ) ) {
			return wpagent_companion_activity_log_invalid_state_error();
		}
		$previous = $entry['entry_hash'];
		++$sequence;
	}
	return hash_equals( $previous, $stored['head_hash'] ) ? $stored : wpagent_companion_activity_log_invalid_state_error();
}

/** @param array<string,mixed> $state @return array<string,mixed> */
function wpagent_companion_activity_log_rechain( array $state, string $anchor_hash ): array {
	$previous = $anchor_hash;
	foreach ( $state['entries'] as $index => $entry ) {
		$entry['entry_hash']          = wpagent_companion_activity_log_entry_hash( $previous, $entry, $state['blog_id'] );
		$state['entries'][ $index ]   = $entry;
		$previous                     = $entry['entry_hash'];
	}
	$state['anchor_hash'] = $anchor_hash;
	$state['head_hash']   = $previous;
	return $state;
}

/** @param mixed $stored @return array<string,mixed>|WP_Error */
function wpagent_companion_activity_log_convert_v1_state( $stored, int $blog_id ) {
	$legacy = wpagent_companion_activity_log_normalize_v1_state( $stored, $blog_id );
	if ( is_wp_error( $legacy ) ) {
		return $legacy;
	}
	if ( empty( $legacy['entries'] ) ) {
		return wpagent_companion_activity_log_empty_state( $blog_id );
	}
	$state = wpagent_companion_activity_log_empty_state( $blog_id );
	$state['sequence'] = $legacy['sequence'];
	foreach ( $legacy['entries'] as $legacy_entry ) {
		$action            = 'readonly' === $legacy_entry['mode'] ? 'read' : 'write';
		$state['entries'][] = array(
			'sequence'       => $legacy_entry['sequence'],
			'started_at'     => $legacy_entry['at'],
			'completed_at'   => $legacy_entry['at'],
			'blog_id'        => $legacy_entry['blog_id'],
			'actor_id'       => $legacy_entry['actor_id'],
			'ability'        => $legacy_entry['ability'],
			'action'         => $action,
			'operation_hash' => hash_hmac( 'sha256', "legacy-operation\0" . $legacy_entry['blog_id'] . "\0" . $legacy_entry['actor_id'] . "\0" . $legacy_entry['ability'] . "\0" . $action . "\0" . $legacy_entry['intent_hash'] . "\0" . $legacy_entry['entry_hash'], wpagent_companion_activity_log_key( $blog_id ) ),
			'intent_hash'    => $legacy_entry['intent_hash'],
			'status'         => 'succeeded',
			'entry_hash'     => str_repeat( '0', 64 ),
		);
	}
	$anchor = hash_hmac( 'sha256', "legacy-anchor\0" . $legacy['blog_id'] . "\0" . $legacy['sequence'] . "\0" . $legacy['anchor_hash'] . "\0" . $legacy['head_hash'], wpagent_companion_activity_log_key( $blog_id ) );
	return wpagent_companion_activity_log_rechain( $state, $anchor );
}

/** @param mixed $stored @return array<string,mixed>|WP_Error */
function wpagent_companion_activity_log_normalize_v2_state( $stored, int $blog_id ) {
	if ( ! is_array( $stored ) ) {
		return wpagent_companion_activity_log_invalid_state_error();
	}
	$keys     = array_keys( $stored );
	$expected = array( 'version', 'blog_id', 'sequence', 'anchor_hash', 'head_hash', 'entries' );
	sort( $keys, SORT_STRING );
	sort( $expected, SORT_STRING );
	if ( $keys !== $expected || WPAGENT_COMPANION_ACTIVITY_LOG_VERSION !== ( $stored['version'] ?? null ) || $blog_id !== ( $stored['blog_id'] ?? null ) || ! is_int( $stored['sequence'] ?? null ) || $stored['sequence'] < 0 || ! wpagent_companion_activity_log_valid_hash( $stored['anchor_hash'] ?? null ) || ! wpagent_companion_activity_log_valid_hash( $stored['head_hash'] ?? null ) || ! is_array( $stored['entries'] ?? null ) || ! array_is_list( $stored['entries'] ) || count( $stored['entries'] ) > WPAGENT_COMPANION_ACTIVITY_LOG_EVENT_LIMIT ) {
		return wpagent_companion_activity_log_invalid_state_error();
	}
	if ( empty( $stored['entries'] ) ) {
		$initial = str_repeat( '0', 64 );
		return 0 === $stored['sequence'] && hash_equals( $initial, $stored['anchor_hash'] ) && hash_equals( $initial, $stored['head_hash'] ) ? $stored : wpagent_companion_activity_log_invalid_state_error();
	}
	$previous_hash     = $stored['anchor_hash'];
	$previous_sequence = 0;
	foreach ( $stored['entries'] as $entry ) {
		if ( ! wpagent_companion_activity_log_valid_entry( $entry, $blog_id, $previous_sequence, $previous_hash ) ) {
			return wpagent_companion_activity_log_invalid_state_error();
		}
		$previous_hash     = $entry['entry_hash'];
		$previous_sequence = $entry['sequence'];
	}
	return $stored['sequence'] === $previous_sequence && hash_equals( $previous_hash, $stored['head_hash'] ) ? $stored : wpagent_companion_activity_log_invalid_state_error();
}

/** @param mixed $stored @return array<string,mixed>|WP_Error */
function wpagent_companion_activity_log_normalize_state( $stored, int $blog_id ) {
	if ( is_array( $stored ) && 1 === ( $stored['version'] ?? null ) ) {
		return wpagent_companion_activity_log_convert_v1_state( $stored, $blog_id );
	}
	return wpagent_companion_activity_log_normalize_v2_state( $stored, $blog_id );
}

/**
 * The ledger option is read directly to support optimistic compare-and-swap.
 * Decode only its own array format, never attacker-controlled PHP objects.
 *
 * @return array<string,mixed>|null
 */
function wpagent_companion_activity_log_decode_state( string $raw ): ?array {
	if ( strlen( $raw ) > WPAGENT_COMPANION_ACTIVITY_LOG_MAX_BYTES || ! is_serialized( $raw, false ) ) {
		return null;
	}
	$state = @unserialize( $raw, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Raw option decode is object-safe and required for byte-exact compare-and-swap.
	return is_array( $state ) ? $state : null;
}

/** @return array{state:array<string,mixed>,exists:bool,raw:string,autoload:string}|WP_Error */
function wpagent_companion_activity_log_snapshot() {
	$blog_id = (int) get_current_blog_id();
	if ( $blog_id < 1 ) {
		return new WP_Error( 'wpagent_activity_log_invalid_site', __( 'Das Aktivitätsprotokoll konnte dieser Website nicht sicher zugeordnet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$row = wpagent_companion_read_option_row_direct( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION );
	if ( ! is_array( $row ) ) {
		return array( 'state' => wpagent_companion_activity_log_empty_state( $blog_id ), 'exists' => false, 'raw' => '', 'autoload' => '' );
	}
	$state = wpagent_companion_activity_log_normalize_state( wpagent_companion_activity_log_decode_state( $row['raw'] ), $blog_id );
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	return array( 'state' => $state, 'exists' => true, 'raw' => $row['raw'], 'autoload' => $row['autoload'] );
}

/**
 * @param array{state:array<string,mixed>,exists:bool,raw:string,autoload:string} $snapshot
 * @param array<string,mixed> $next
 * @return bool|null Null means the value was written but a concurrent autoload
 *                   change invalidated final verification. Do not retry that
 *                   append, because doing so would duplicate a real event.
 */
function wpagent_companion_activity_log_compare_and_swap( array $snapshot, array $next ): ?bool {
	global $wpdb;
	$next_raw = maybe_serialize( $next );
	if ( strlen( $next_raw ) > WPAGENT_COMPANION_ACTIVITY_LOG_MAX_BYTES ) {
		return false;
	}
	if ( $snapshot['exists'] ) {
		$updated = $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core options table name is selected by WordPress for the active site.
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND HEX(autoload) = HEX(%s)",
				$next_raw,
				WPAGENT_COMPANION_ACTIVITY_LOG_OPTION,
				$snapshot['raw'],
				$snapshot['autoload']
			)
		);
		if ( 1 !== $updated ) {
			return false;
		}
		wpagent_companion_forget_option_cache( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION, in_array( $snapshot['autoload'], wp_autoload_values_to_autoload(), true ) );
	} elseif ( ! wpagent_companion_add_option_create_only( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION, $next, '', false ) ) {
		return false;
	}
	$row = wpagent_companion_read_option_row_direct( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION );
	if ( ! is_array( $row ) || ! hash_equals( $next_raw, $row['raw'] ) ) {
		return false;
	}
	if ( $snapshot['exists'] && ! hash_equals( $snapshot['autoload'], $row['autoload'] ) ) {
		return null;
	}
	return true;
}

/** @return array{blog_id:int,raw:string,autoload:string}|WP_Error */
function wpagent_companion_activity_log_recovery_snapshot() {
	$blog_id = (int) get_current_blog_id();
	if ( $blog_id < 1 ) {
		return new WP_Error( 'wpagent_activity_log_invalid_site', __( 'Das Aktivitätsprotokoll konnte dieser Website nicht sicher zugeordnet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$row = wpagent_companion_read_option_row_direct( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION );
	if ( ! is_array( $row ) ) {
		return new WP_Error( 'wpagent_activity_log_reset_not_required', __( 'Das Aktivitätsprotokoll ist gültig und benötigt keinen Recovery-Reset.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$state = wpagent_companion_activity_log_normalize_state( wpagent_companion_activity_log_decode_state( $row['raw'] ), $blog_id );
	if ( ! is_wp_error( $state ) ) {
		return new WP_Error( 'wpagent_activity_log_reset_not_required', __( 'Das Aktivitätsprotokoll ist gültig und benötigt keinen Recovery-Reset.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return array(
		'blog_id'  => $blog_id,
		'raw'      => $row['raw'],
		'autoload' => $row['autoload'],
	);
}

/** @param array{blog_id:int,raw:string,autoload:string} $snapshot */
function wpagent_companion_activity_log_reset_snapshot( array $snapshot ): bool {
	global $wpdb;
	if ( (int) get_current_blog_id() !== ( $snapshot['blog_id'] ?? 0 )
		|| ! is_string( $snapshot['raw'] ?? null )
		|| ! is_string( $snapshot['autoload'] ?? null )
	) {
		return false;
	}
	if ( ! is_wp_error( wpagent_companion_activity_log_normalize_state( wpagent_companion_activity_log_decode_state( $snapshot['raw'] ), $snapshot['blog_id'] ) ) ) {
		return false;
	}
	$next          = wpagent_companion_activity_log_empty_state( $snapshot['blog_id'] );
	$next_raw      = maybe_serialize( $next );
	$next_autoload = wp_determine_option_autoload_value( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION, $next, $next_raw, false );
	$updated       = $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core options table name is selected by WordPress for the active site.
		$wpdb->prepare(
			"UPDATE {$wpdb->options} SET option_value = %s, autoload = %s WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND HEX(autoload) = HEX(%s)",
			$next_raw,
			$next_autoload,
			WPAGENT_COMPANION_ACTIVITY_LOG_OPTION,
			$snapshot['raw'],
			$snapshot['autoload']
		)
	);
	if ( 1 !== $updated ) {
		return false;
	}
	wpagent_companion_forget_option_cache( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION, in_array( $snapshot['autoload'], wp_autoload_values_to_autoload(), true ) );
	$row = wpagent_companion_read_option_row_direct( WPAGENT_COMPANION_ACTIVITY_LOG_OPTION );
	return is_array( $row ) && hash_equals( $next_raw, $row['raw'] ) && hash_equals( $next_autoload, $row['autoload'] );
}

/** @return array{blog_id:int,reset:bool}|WP_Error */
function wpagent_companion_activity_log_reset() {
	$snapshot = wpagent_companion_activity_log_recovery_snapshot();
	if ( is_wp_error( $snapshot ) ) {
		return $snapshot;
	}
	if ( ! wpagent_companion_activity_log_reset_snapshot( $snapshot ) ) {
		return new WP_Error( 'wpagent_activity_log_reset_conflict', __( 'Das Aktivitätsprotokoll wurde gleichzeitig geändert. Der Recovery-Reset wurde nicht ausgeführt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return array( 'blog_id' => $snapshot['blog_id'], 'reset' => true );
}

/** @param array<string,mixed> $state @return array<string,mixed> */
function wpagent_companion_activity_log_prune_operations( array $state ): array {
	while ( count( $state['entries'] ) > WPAGENT_COMPANION_ACTIVITY_LOG_EVENT_LIMIT
		|| count( wpagent_companion_activity_log_project_operations( $state['entries'] ) ) > WPAGENT_COMPANION_ACTIVITY_LOG_LIMIT
		|| strlen( maybe_serialize( $state ) ) > WPAGENT_COMPANION_ACTIVITY_LOG_MAX_BYTES
	) {
		if ( empty( $state['entries'] ) ) {
			break;
		}
		$operation_hash = $state['entries'][0]['operation_hash'];
		$dropped        = array();
		$entries        = array();
		foreach ( $state['entries'] as $entry ) {
			if ( hash_equals( $operation_hash, $entry['operation_hash'] ) ) {
				$dropped[] = $entry['entry_hash'];
			} else {
				$entries[] = $entry;
			}
		}
		if ( empty( $dropped ) || empty( $entries ) ) {
			break;
		}
		$state['entries'] = $entries;
		$anchor           = hash_hmac( 'sha256', "prune\0" . $state['anchor_hash'] . "\0" . implode( "\0", $dropped ), wpagent_companion_activity_log_key( $state['blog_id'] ) );
		$state            = wpagent_companion_activity_log_rechain( $state, $anchor );
	}
	return $state;
}

/** @param array{blog_id:int,actor_id:int,ability:string,action:string,started_at:string,intent_hash:string,operation_hash:string} $context */
function wpagent_companion_activity_log_append( array $context, string $status ): bool {
	if ( ! wpagent_companion_activity_log_valid_status( $status ) ) {
		return false;
	}
	for ( $attempt = 0; $attempt < 3; ++$attempt ) {
		$snapshot = wpagent_companion_activity_log_snapshot();
		if ( is_wp_error( $snapshot ) || (int) $snapshot['state']['blog_id'] !== $context['blog_id'] ) {
			return false;
		}
		$state = $snapshot['state'];
		if ( $state['sequence'] >= PHP_INT_MAX ) {
			return false;
		}
		$entry = array(
			'sequence'       => $state['sequence'] + 1,
			'started_at'     => $context['started_at'],
			'completed_at'   => 'recovery_required' === $status ? null : gmdate( 'Y-m-d\TH:i:s\Z' ),
			'blog_id'        => $context['blog_id'],
			'actor_id'       => $context['actor_id'],
			'ability'        => $context['ability'],
			'action'         => $context['action'],
			'operation_hash' => $context['operation_hash'],
			'intent_hash'    => $context['intent_hash'],
			'status'         => $status,
		);
		$entry['entry_hash'] = wpagent_companion_activity_log_entry_hash( $state['head_hash'], $entry, $context['blog_id'] );
		$entries             = $state['entries'];
		$entries[]           = $entry;
		$next                = $state;
		$next['sequence'] = $entry['sequence'];
		$next['head_hash'] = $entry['entry_hash'];
		$next['entries'] = $entries;
		$next            = wpagent_companion_activity_log_prune_operations( $next );
		$written         = wpagent_companion_activity_log_compare_and_swap( $snapshot, $next );
		if ( true === $written ) {
			return true;
		}
		if ( null === $written ) {
			return false;
		}
	}
	return false;
}

/** @return array<string,array{group:string,readonly:bool,chat:bool}> */
function wpagent_companion_activity_log_catalog(): array {
	static $catalog = null;
	if ( null === $catalog ) {
		$catalog = wpagent_companion_ability_catalog();
	}
	return $catalog;
}

/** @param mixed $input @return array{blog_id:int,actor_id:int,ability:string,action:string,started_at:string,intent_hash:string,operation_hash:string}|null */
function wpagent_companion_activity_log_begin( string $ability_name, $input ): ?array {
	try {
		$blog_id = (int) get_current_blog_id();
		if ( $blog_id < 1 || ! isset( wpagent_companion_activity_log_catalog()[ $ability_name ] ) ) {
			return null;
		}
		$context = array(
			'blog_id'    => $blog_id,
			'actor_id'   => max( 0, (int) get_current_user_id() ),
			'ability'    => $ability_name,
			'action'     => 'write',
			'started_at' => gmdate( 'Y-m-d\TH:i:s\Z' ),
		);
		$context['intent_hash']    = wpagent_companion_activity_log_intent_hash( $ability_name, $context['action'], $context['actor_id'], $input, $blog_id );
		$context['operation_hash'] = wpagent_companion_activity_log_operation_hash( $context );
		return wpagent_companion_activity_log_append( $context, 'recovery_required' ) ? $context : null;
	} catch ( Throwable $error ) {
		// Ledger collection is observational and never changes an ability result.
		return null;
	}
}

/** @param array{blog_id:int,actor_id:int,ability:string,action:string,started_at:string,intent_hash:string,operation_hash:string}|null $context */
function wpagent_companion_activity_log_settle( ?array $context, string $status ): void {
	if ( null === $context || ! in_array( $status, array( 'succeeded', 'failed', 'rejected', 'recovery_required' ), true ) ) {
		return;
	}
	try {
		if ( (int) get_current_blog_id() === $context['blog_id'] ) {
			wpagent_companion_activity_log_append( $context, $status );
		}
	} catch ( Throwable $error ) {
		// Ledger collection is observational and never changes an ability result.
	}
}

require_once __DIR__ . '/activity-log-execution.php';

/** @param array<int,array<string,mixed>> $entries @return array<int,array<string,mixed>> */
function wpagent_companion_activity_log_project_operations( array $entries ): array {
	$operations = array();
	foreach ( $entries as $entry ) {
		$operations[ $entry['operation_hash'] ] = $entry;
	}
	return array_values( $operations );
}

/** @return array{blog_id:int,total:int,event_total:int,oldest_sequence:int,latest_sequence:int,entries:array<int,array<string,mixed>>}|WP_Error */
function wpagent_companion_activity_log_read( int $limit = 50 ) {
	$limit    = max( 1, min( WPAGENT_COMPANION_ACTIVITY_LOG_LIMIT, $limit ) );
	$snapshot = wpagent_companion_activity_log_snapshot();
	if ( is_wp_error( $snapshot ) ) {
		return $snapshot;
	}
	$state      = $snapshot['state'];
	$operations = wpagent_companion_activity_log_project_operations( $state['entries'] );
	$entries    = array_reverse( array_slice( $operations, -$limit ) );
	return array(
		'blog_id'         => $state['blog_id'],
		'total'           => count( $operations ),
		'event_total'     => count( $state['entries'] ),
		'oldest_sequence' => empty( $state['entries'] ) ? 0 : $state['entries'][0]['sequence'],
		'latest_sequence' => $state['sequence'],
		'entries'         => $entries,
	);
}

function wpagent_companion_activity_log_register_rest_route(): void {
	register_rest_route(
		'wp-agent/v1',
		'/activity-log',
		array(
			'methods'             => 'GET',
			'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
			'args'                => array(
				'limit' => array(
					'default'           => 50,
					'sanitize_callback' => static fn( $value ): int => absint( $value ),
					'validate_callback' => static fn( $value ): bool => is_scalar( $value ) && 1 === preg_match( '/^[1-9][0-9]{0,2}$/', (string) $value ) && (int) $value <= WPAGENT_COMPANION_ACTIVITY_LOG_LIMIT,
				),
			),
			'callback'            => static function ( WP_REST_Request $request ) {
				$result = wpagent_companion_activity_log_read( (int) $request->get_param( 'limit' ) );
				return is_wp_error( $result ) ? $result : rest_ensure_response( $result );
			},
		)
	);
	register_rest_route(
		'wp-agent/v1',
		'/activity-log/reset',
		array(
			'methods'             => 'POST',
			'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
			'args'                => array(
				'confirm' => array(
					'required'          => true,
					'sanitize_callback' => static fn( $value ): string => is_scalar( $value ) ? trim( sanitize_text_field( (string) $value ) ) : '',
					'validate_callback' => static fn( $value ): bool => is_string( $value ) && hash_equals( WPAGENT_COMPANION_ACTIVITY_LOG_RESET_CONFIRMATION, $value ),
				),
			),
			'callback'            => static function ( WP_REST_Request $request ) {
				$result = wpagent_companion_activity_log_reset();
				return is_wp_error( $result ) ? $result : rest_ensure_response( $result );
			},
		)
	);
}
add_action( 'rest_api_init', 'wpagent_companion_activity_log_register_rest_route' );
