<?php
/** Transactional management for MB Relationships definitions and connections. */

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

/** @return array<string,mixed> */
function wpagent_metabox_relationship_ability( string $label, string $description, array $input, array $output, string $callback, bool $readonly = false, bool $destructive = false ): array {
	return array(
		'label' => $label,
		'description' => $description,
		'category' => 'wp-agent',
		'input_schema' => $input,
		'output_schema' => $output,
		'execute_callback' => $callback,
		'permission_callback' => 'wpagent_perm_manage_options',
		'meta' => array(
			'show_in_rest' => true,
			'annotations' => array( 'readonly' => $readonly, 'destructive' => $destructive, 'idempotent' => $readonly ),
		),
	);
}

/** @return array<string,mixed> */
function wpagent_ability_list_meta_box_relationships(): array {
	return wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Beziehungen auflisten', 'wp-agent-companion' ),
		__( 'Listet persistente MB-Relationships-Definitionen mit Konflikthash.', 'wp-agent-companion' ),
		array( 'type' => 'object', 'properties' => array( 'search' => array( 'type' => 'string', 'maxLength' => 100 ), 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 50 ), 'offset' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 10000, 'default' => 0 ) ), 'additionalProperties' => false ),
		array( 'type' => 'object', 'properties' => array( 'relationships' => array( 'type' => 'array', 'maxItems' => 100, 'items' => wpagent_metabox_relationship_state_schema() ), 'total' => array( 'type' => 'integer', 'minimum' => 0 ) ), 'required' => array( 'relationships', 'total' ), 'additionalProperties' => false ),
		'wpagent_metabox_list_relationships',
		true
	);
}

/** @return array<string,mixed> */
function wpagent_ability_get_meta_box_relationship(): array {
	return wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Beziehung lesen', 'wp-agent-companion' ),
		__( 'Liest eine persistente MB-Relationships-Definition mit Konflikthash.', 'wp-agent-companion' ),
		array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'integer', 'minimum' => 1 ) ), 'required' => array( 'id' ), 'additionalProperties' => false ),
		wpagent_metabox_relationship_state_schema(),
		'wpagent_metabox_get_relationship',
		true
	);
}

/** @return array<string,mixed> */
function wpagent_ability_create_meta_box_relationship(): array {
	return wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Beziehung erstellen', 'wp-agent-companion' ),
		__( 'Erstellt eine geprüfte MB-Relationships-Definition und verifiziert beide Providerzustände.', 'wp-agent-companion' ),
		array( 'type' => 'object', 'properties' => array( 'title' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 200 ), 'settings' => wpagent_metabox_relationship_settings_schema() ), 'required' => array( 'title', 'settings' ), 'additionalProperties' => false ),
		wpagent_metabox_relationship_state_schema(),
		'wpagent_metabox_create_relationship'
	);
}

/** @return array<string,mixed> */
function wpagent_ability_update_meta_box_relationship(): array {
	$definition = wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Beziehung ändern: manuell erforderlich', 'wp-agent-companion' ),
		__( 'Meta Box bietet für Beziehungsdefinitionen keine speicheratomare Vergleichsoperation. Diese registrierte Schreib-Ability lehnt Änderungen deshalb ab, damit Dashboard-Änderungen nicht überschrieben werden.', 'wp-agent-companion' ),
		array(
			'type' => 'object',
			'properties' => array(
				'id' => array( 'type' => 'integer', 'minimum' => 1 ),
				'expected_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'title' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 200 ),
				'settings' => wpagent_metabox_relationship_settings_schema(),
			),
			'required' => array( 'id', 'expected_hash', 'settings' ),
			'additionalProperties' => false,
		),
		array( 'type' => 'object', 'properties' => array( 'manual_action_required' => array( 'type' => 'boolean' ) ), 'required' => array( 'manual_action_required' ), 'additionalProperties' => false ),
		'wpagent_metabox_update_relationship'
	);
	$definition['meta']['annotations']['manual_only'] = true;
	return $definition;
}

/** @return array<string,mixed> */
function wpagent_ability_delete_meta_box_relationship(): array {
	$definition = wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Beziehung löschen: manuell erforderlich', 'wp-agent-companion' ),
		__( 'Meta Box bietet für Beziehungsdefinitionen keine speicheratomare Compare-and-Delete-Operation. Diese registrierte Schreib-Ability ändert oder verschiebt deshalb keine Definition.', 'wp-agent-companion' ),
		array(
			'type' => 'object',
			'properties' => array(
				'id' => array( 'type' => 'integer', 'minimum' => 1 ),
				'expected_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required' => array( 'id', 'expected_hash' ),
			'additionalProperties' => false,
		),
		array( 'type' => 'object', 'properties' => array( 'manual_action_required' => array( 'type' => 'boolean' ) ), 'required' => array( 'manual_action_required' ), 'additionalProperties' => false ),
		'wpagent_metabox_delete_relationship',
		false,
		true
	);
	$definition['meta']['annotations']['manual_only'] = true;
	return $definition;
}

/** @return array<string,mixed> */
function wpagent_ability_list_meta_box_connections(): array {
	return wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Verbindungen auflisten', 'wp-agent-companion' ),
		__( 'Listet begrenzt die Verbindungen einer freigegebenen Relationship-Seite auf.', 'wp-agent-companion' ),
		array(
			'type' => 'object',
			'properties' => array(
				'relationship_id' => array( 'type' => 'string', 'pattern' => '^[a-z][a-z0-9_-]{0,43}$', 'maxLength' => 44 ),
				'direction' => array( 'type' => 'string', 'enum' => array( 'from', 'to' ) ),
				'object_id' => array( 'type' => 'integer', 'minimum' => 1 ),
				'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 50 ),
				'offset' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 10000, 'default' => 0 ),
			),
			'required' => array( 'relationship_id', 'direction', 'object_id' ),
			'additionalProperties' => false,
		),
		array( 'type' => 'object', 'properties' => array( 'connections' => array( 'type' => 'array', 'maxItems' => 100, 'items' => wpagent_metabox_connection_schema() ), 'total' => array( 'type' => 'integer', 'minimum' => 0 ) ), 'required' => array( 'connections', 'total' ), 'additionalProperties' => false ),
		'wpagent_metabox_list_connections',
		true
	);
}

/** @return array<string,mixed> */
function wpagent_ability_add_meta_box_connection(): array {
	$properties = array(
		'relationship_id' => array( 'type' => 'string', 'pattern' => '^[a-z][a-z0-9_-]{0,43}$', 'maxLength' => 44 ),
		'from' => array( 'type' => 'integer', 'minimum' => 1 ),
		'to' => array( 'type' => 'integer', 'minimum' => 1 ),
		'order_from' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 2147483647, 'default' => 1 ),
		'order_to' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 2147483647, 'default' => 1 ),
	);
	return wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Verbindung hinzufügen', 'wp-agent-companion' ),
		__( 'Fügt zwei gültige Objekte über die offizielle MB-Relationships-API zusammen und prüft den Datenbankzustand.', 'wp-agent-companion' ),
		array( 'type' => 'object', 'properties' => $properties, 'required' => array( 'relationship_id', 'from', 'to' ), 'additionalProperties' => false ),
		wpagent_metabox_connection_schema(),
		'wpagent_metabox_add_connection'
	);
}

/** @return array<string,mixed> */
function wpagent_ability_delete_meta_box_connection(): array {
	$definition = wpagent_metabox_relationship_ability(
		__( 'Meta-Box-Verbindung löschen: manuell erforderlich', 'wp-agent-companion' ),
		__( 'Meta Box bietet für Verbindungen keine speicheratomare Compare-and-Delete-Operation. Diese registrierte Schreib-Ability entfernt deshalb keine Verbindung.', 'wp-agent-companion' ),
		array(
			'type' => 'object',
			'properties' => array(
				'relationship_id' => array( 'type' => 'string', 'pattern' => '^[a-z][a-z0-9_-]{0,43}$', 'maxLength' => 44 ),
				'from' => array( 'type' => 'integer', 'minimum' => 1 ),
				'to' => array( 'type' => 'integer', 'minimum' => 1 ),
				'expected_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required' => array( 'relationship_id', 'from', 'to', 'expected_hash' ),
			'additionalProperties' => false,
		),
		array( 'type' => 'object', 'properties' => array( 'manual_action_required' => array( 'type' => 'boolean' ) ), 'required' => array( 'manual_action_required' ), 'additionalProperties' => false ),
		'wpagent_metabox_delete_connection',
		false,
		true
	);
	$definition['meta']['annotations']['manual_only'] = true;
	return $definition;
}

/** @return mixed */
function wpagent_metabox_canonicalize( $value ) {
	if ( ! is_array( $value ) ) {
		return $value;
	}
	if ( ! array_is_list( $value ) ) {
		ksort( $value, SORT_STRING );
	}
	foreach ( $value as $key => $item ) {
		$value[ $key ] = wpagent_metabox_canonicalize( $item );
	}
	return $value;
}

function wpagent_metabox_hash( $value ): string {
	return hash( 'sha256', (string) wp_json_encode( wpagent_metabox_canonicalize( $value ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
}

function wpagent_metabox_relationship_post_exists( int $post_id ): bool {
	global $wpdb;
	return (bool) $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM {$wpdb->posts} WHERE `ID` = %d LIMIT 1", $post_id ) );
}

function wpagent_metabox_relationship_table_available(): bool {
	global $wpdb;
	$table = (string) ( $wpdb->mb_relationships ?? '' );
	return '' !== $table && $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_relationship_state( int $post_id ) {
	$post = get_post( $post_id );
	if ( ! $post instanceof WP_Post || 'mb-relationship' !== $post->post_type ) {
		return new WP_Error( 'wpagent_metabox_relationship_not_found', __( 'Meta-Box-Beziehung nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$settings     = get_post_meta( $post_id, 'settings', true );
	$relationship = get_post_meta( $post_id, 'relationship', true );
	if ( ! is_array( $settings ) || ! is_array( $relationship ) ) {
		return new WP_Error( 'wpagent_metabox_relationship_invalid', __( 'Die gespeicherte Meta-Box-Beziehung ist unvollständig.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$normalized = wpagent_metabox_validate_relationship_settings( $settings );
	if ( is_wp_error( $normalized ) || ! is_string( $normalized['settings']['id'] ?? null ) ) {
		return new WP_Error( 'wpagent_metabox_relationship_invalid', __( 'Die gespeicherte Meta-Box-Beziehung entspricht nicht der begrenzten Providerdefinition.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$title      = wpagent_metabox_relationship_text( $post->post_title, 'title', 200, false );
	$expected_slug = $normalized['settings']['id'];
	$valid_slug    = 'trash' === $post->post_status
		? 1 === preg_match( '/^' . preg_quote( $expected_slug, '/' ) . '__trashed(?:-[1-9][0-9]{0,3})?$/', $post->post_name )
		: hash_equals( $expected_slug, $post->post_name );
	if (
		is_wp_error( $title ) ||
		! preg_match( '/^[a-z][a-z0-9_-]{0,43}(?:__trashed(?:-[1-9][0-9]{0,3})?)?$/', $post->post_name ) ||
		! $valid_slug ||
		! in_array( $post->post_status, array( 'publish', 'draft', 'trash' ), true ) ||
		wpagent_metabox_canonicalize( $relationship ) !== wpagent_metabox_canonicalize( $normalized['relationship'] )
	) {
		return new WP_Error( 'wpagent_metabox_relationship_invalid', __( 'Die gespeicherte Meta-Box-Beziehung entspricht nicht der begrenzten Providerdefinition.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$state = array(
		'id' => $post_id,
		'title' => (string) $post->post_title,
		'slug' => (string) $post->post_name,
		'status' => (string) $post->post_status,
		'settings' => $settings,
		'relationship' => $relationship,
	);
	$state['hash'] = wpagent_metabox_hash( $state );
	return $state;
}

/** @return array<string,mixed> */
function wpagent_metabox_relationship_create_candidate_state( int $post_id, string $title, array $normalized, string $status = 'publish' ): array {
	$state = array(
		'id'           => $post_id,
		'title'        => $title,
		'slug'         => (string) $normalized['settings']['id'],
		'status'       => $status,
		'settings'     => $normalized['settings'],
		'relationship' => $normalized['relationship'],
	);
	$state['hash'] = wpagent_metabox_hash( $state );
	return $state;
}

/** @param array<string,mixed>|WP_Error $fresh @param array<string,mixed> $candidate */
function wpagent_metabox_relationship_owned_candidate( $fresh, array $candidate ): bool {
	if ( ! is_array( $fresh ) || ! is_string( $fresh['hash'] ?? null ) || ! is_string( $candidate['hash'] ?? null ) ) {
		return false;
	}
	$candidate_id   = absint( $candidate['id'] ?? 0 );
	$relationship_id = (string) ( $candidate['settings']['id'] ?? '' );
	return $candidate_id > 0
		&& $candidate_id === absint( $fresh['id'] ?? 0 )
		&& '' !== $relationship_id
		&& hash_equals( $relationship_id, (string) ( $fresh['slug'] ?? '' ) )
		&& hash_equals( $relationship_id, (string) ( $fresh['settings']['id'] ?? '' ) )
		&& hash_equals( $candidate['hash'], $fresh['hash'] );
}

/** @param array<string,mixed>|WP_Error $fresh */
function wpagent_metabox_relationship_create_recovery_required( int $post_id, $fresh ): WP_Error {
	$data = array( 'status' => 409, 'recovery_required' => true, 'created_id' => $post_id );
	if ( is_array( $fresh ) && is_string( $fresh['hash'] ?? null ) ) {
		$data['current_hash'] = $fresh['hash'];
	}
	return new WP_Error(
		'wpagent_metabox_relationship_create_recovery_required',
		__( 'Die neu angelegte Meta-Box-Beziehung konnte nicht exakt verifiziert werden. Sie bleibt unverändert erhalten. Wiederhole den Vorgang nicht automatisch und prüfe sie manuell.', 'wp-agent-companion' ),
		$data
	);
}

function wpagent_metabox_relationship_lock_name( string $identity ): string {
	return 'wp_agent_metabox_relationship_lock_' . hash( 'sha256', $identity );
}

/** @return string|WP_Error */
function wpagent_metabox_relationship_acquire_lock( string $identity ) {
	$option = wpagent_metabox_relationship_lock_name( $identity );
	$token  = wpagent_companion_acquire_option_lock( $option, 300 );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_metabox_relationship_busy', __( 'Diese Meta-Box-Beziehung wird gerade geändert. Versuche es gleich erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_metabox_relationship_release_lock( string $identity, string $token ): void {
	wpagent_companion_release_option_lock( wpagent_metabox_relationship_lock_name( $identity ), $token );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_save_relationship( WP_Post $post, string $title, array $normalized, string $status = 'publish' ) {
	$settings     = $normalized['settings'];
	$relationship = $normalized['relationship'];
	$id           = (string) $settings['id'];
	$result = wp_update_post(
		array( 'ID' => $post->ID, 'post_title' => $title, 'post_name' => $id, 'post_status' => $status ),
		true
	);
	if ( is_wp_error( $result ) ) {
		return $result;
	}
	update_post_meta( $post->ID, 'settings', $settings );
	update_post_meta( $post->ID, 'relationship', $relationship );
	$fresh = wpagent_metabox_relationship_state( $post->ID );
	if (
		is_wp_error( $fresh ) ||
		$fresh['title'] !== $title ||
		$fresh['slug'] !== $id ||
		$status !== $fresh['status'] ||
		wpagent_metabox_canonicalize( $fresh['settings'] ) !== wpagent_metabox_canonicalize( $settings ) ||
		wpagent_metabox_canonicalize( $fresh['relationship'] ) !== wpagent_metabox_canonicalize( $relationship )
	) {
		return new WP_Error( 'wpagent_metabox_relationship_readback_failed', __( 'Die Meta-Box-Beziehung weicht nach dem Speichern vom erwarteten Zustand ab.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return $fresh;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_list_relationships( array $input ) {
	$limit  = min( 100, max( 1, absint( $input['limit'] ?? 50 ) ) );
	$offset = min( 10000, absint( $input['offset'] ?? 0 ) );
	$args = array( 'post_type' => 'mb-relationship', 'post_status' => array( 'publish', 'draft', 'trash' ), 'posts_per_page' => $limit, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => false );
	$search = sanitize_text_field( (string) ( $input['search'] ?? '' ) );
	if ( '' !== $search ) {
		$args['s'] = $search;
	}
	$query  = new WP_Query( $args );
	$states = array();
	foreach ( $query->posts as $post ) {
		$state = wpagent_metabox_relationship_state( $post->ID );
		if ( is_wp_error( $state ) ) {
			return $state;
		}
		$states[] = $state;
	}
	return array( 'relationships' => $states, 'total' => (int) $query->found_posts );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_get_relationship( array $input ) {
	return wpagent_metabox_relationship_state( absint( $input['id'] ?? 0 ) );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_create_relationship( array $input ) {
	$title = wpagent_metabox_relationship_text( $input['title'] ?? null, 'title', 200, false );
	if ( is_wp_error( $title ) ) {
		return $title;
	}
	$normalized = wpagent_metabox_validate_relationship_settings( is_array( $input['settings'] ?? null ) ? $input['settings'] : array() );
	if ( is_wp_error( $normalized ) ) {
		return $normalized;
	}
	$relationship_id = (string) $normalized['settings']['id'];
	$identity = 'relationship:' . $relationship_id;
	$lock = wpagent_metabox_relationship_acquire_lock( $identity );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$existing = get_page_by_path( $relationship_id, OBJECT, 'mb-relationship' );
		if ( $existing instanceof WP_Post ) {
			return new WP_Error( 'wpagent_metabox_relationship_exists', __( 'Eine Meta-Box-Beziehung mit dieser ID existiert bereits.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( MB_Relationships_API::get_relationship( $relationship_id ) ) {
			return new WP_Error( 'wpagent_metabox_relationship_runtime_collision', __( 'Eine per PHP registrierte Meta-Box-Beziehung verwendet diese ID bereits. Sie wurde nicht überschrieben.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$post_id = wp_insert_post( array( 'post_type' => 'mb-relationship', 'post_title' => $title, 'post_name' => $relationship_id, 'post_status' => 'publish' ), true );
		if ( is_wp_error( $post_id ) ) {
			return $post_id;
		}
		$created_post = get_post( $post_id );
		$saved = $created_post instanceof WP_Post
			? wpagent_metabox_save_relationship( $created_post, $title, $normalized )
			: new WP_Error( 'wpagent_metabox_relationship_save_failed', __( 'Die neu angelegte Meta-Box-Beziehung ist vor der Verifikation verschwunden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		if ( is_wp_error( $saved ) ) {
			$candidate = wpagent_metabox_relationship_create_candidate_state( $post_id, $title, $normalized );
			$fresh     = wpagent_metabox_relationship_state( $post_id );
			if ( ! wpagent_metabox_relationship_owned_candidate( $fresh, $candidate ) ) {
				return wpagent_metabox_relationship_create_recovery_required( $post_id, $fresh );
			}
			$deleted = wp_delete_post( $post_id, true );
			if ( ! $deleted || wpagent_metabox_relationship_post_exists( $post_id ) ) {
				return wpagent_metabox_relationship_create_recovery_required( $post_id, wpagent_metabox_relationship_state( $post_id ) );
			}
			return new WP_Error( 'wpagent_metabox_relationship_save_failed', __( 'Die Meta-Box-Beziehung konnte nicht exakt verifiziert werden und wurde zurückgerollt.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		return $saved;
	} finally {
		wpagent_metabox_relationship_release_lock( $identity, $lock );
	}
}

/** @return WP_Error */
function wpagent_metabox_relationship_manual_write_only() {
	return new WP_Error(
		'wpagent_metabox_relationship_mutation_manual_only',
		__( 'Meta Box stellt für Beziehungsdefinitionen und Verbindungen keine speicheratomare Compare-and-Set- oder Compare-and-Delete-Operation bereit. WPAgently ändert oder entfernt sie deshalb nicht. Nimm die Änderung manuell in WordPress vor und lies den Zustand anschließend neu.', 'wp-agent-companion' ),
		array( 'status' => 409, 'manual_action_required' => true )
	);
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_update_relationship( array $input ) {
	return wpagent_metabox_relationship_manual_write_only();
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_delete_relationship( array $input ) {
	return wpagent_metabox_relationship_manual_write_only();
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_relationship_by_slug( string $relationship_id ) {
	if ( ! preg_match( '/^[a-z][a-z0-9_-]{0,43}$/', $relationship_id ) ) {
		return new WP_Error( 'wpagent_metabox_relationship_id_invalid', __( 'Die Beziehungs-ID ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$post = get_page_by_path( $relationship_id, OBJECT, 'mb-relationship' );
	if ( ! $post instanceof WP_Post || 'publish' !== $post->post_status ) {
		return new WP_Error( 'wpagent_metabox_relationship_not_found', __( 'Aktive Meta-Box-Beziehung nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	if ( ! wpagent_metabox_relationship_table_available() ) {
		return new WP_Error( 'wpagent_metabox_relationship_table_missing', __( 'Die MB-Relationships-Datenbanktabelle fehlt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$state = wpagent_metabox_relationship_state( $post->ID );
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	$runtime_relationship = MB_Relationships_API::get_relationship( $relationship_id );
	$runtime_settings     = MB_Relationships_API::get_relationship_settings( $relationship_id );
	if ( ! $runtime_relationship || ! is_array( $runtime_settings ) ) {
		return new WP_Error( 'wpagent_metabox_relationship_not_registered', __( 'Meta Box hat diese veröffentlichte Beziehung nicht registriert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$normalized = wpagent_metabox_validate_relationship_settings( $state['settings'] );
	if (
		is_wp_error( $normalized ) ||
		wpagent_metabox_canonicalize( $normalized['relationship'] ) !== wpagent_metabox_canonicalize( $state['relationship'] ) ||
		wpagent_metabox_relationship_runtime_signature( $runtime_settings ) !== wpagent_metabox_relationship_runtime_signature( $normalized['settings'] )
	) {
		return new WP_Error( 'wpagent_metabox_relationship_provider_mismatch', __( 'Die gespeicherte Meta-Box-Beziehung stimmt nicht mit der in diesem Request registrierten Providerdefinition überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $state;
}

/** @return true|WP_Error */
function wpagent_metabox_validate_relationship_object( array $side, int $object_id ) {
	$type = (string) ( $side['object_type'] ?? 'post' );
	if ( 'post' === $type ) {
		$post = get_post( $object_id );
		$allowed = $side['field']['post_type'] ?? $side['post_type'] ?? '';
		$allowed = is_array( $allowed ) ? $allowed : array( $allowed );
		return $post instanceof WP_Post && in_array( $post->post_type, $allowed, true ) ? true : new WP_Error( 'wpagent_metabox_relationship_object_not_found', __( 'Ein Beitragsobjekt passt nicht zur Beziehungsdefinition.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	if ( 'term' === $type ) {
		$term = get_term( $object_id );
		$taxonomy = (string) ( $side['field']['taxonomy'] ?? $side['taxonomy'] ?? '' );
		return $term instanceof WP_Term && $term->taxonomy === $taxonomy ? true : new WP_Error( 'wpagent_metabox_relationship_object_not_found', __( 'Ein Begriffsobjekt passt nicht zur Beziehungsdefinition.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return get_user_by( 'id', $object_id ) instanceof WP_User ? true : new WP_Error( 'wpagent_metabox_relationship_object_not_found', __( 'Ein Benutzerobjekt passt nicht zur Beziehungsdefinition.', 'wp-agent-companion' ), array( 'status' => 404 ) );
}

/** @return array<string,mixed>|null */
function wpagent_metabox_connection_state( string $relationship_id, int $from, int $to ): ?array {
	global $wpdb;
	$row = $wpdb->get_row( $wpdb->prepare( "SELECT `from`, `to`, `order_from`, `order_to` FROM {$wpdb->mb_relationships} WHERE `type` = %s AND `from` = %d AND `to` = %d LIMIT 1", $relationship_id, $from, $to ), ARRAY_A );
	if ( ! is_array( $row ) ) {
		return null;
	}
	$state = array( 'relationship_id' => $relationship_id, 'from' => (int) $row['from'], 'to' => (int) $row['to'], 'order_from' => (int) $row['order_from'], 'order_to' => (int) $row['order_to'], 'exists' => true );
	$state['hash'] = wpagent_metabox_hash( $state );
	return $state;
}

/** @return array<string,mixed> */
function wpagent_metabox_connection_candidate_state( string $relationship_id, int $from, int $to, int $order_from, int $order_to ): array {
	$state = array( 'relationship_id' => $relationship_id, 'from' => $from, 'to' => $to, 'order_from' => $order_from, 'order_to' => $order_to, 'exists' => true );
	$state['hash'] = wpagent_metabox_hash( $state );
	return $state;
}

/** @param array<string,mixed>|null $fresh @param array<string,mixed> $candidate */
function wpagent_metabox_connection_owned_candidate( ?array $fresh, array $candidate ): bool {
	if ( ! is_array( $fresh ) || ! is_string( $fresh['hash'] ?? null ) || ! is_string( $candidate['hash'] ?? null ) ) {
		return false;
	}
	return hash_equals( (string) $candidate['relationship_id'], (string) ( $fresh['relationship_id'] ?? '' ) )
		&& (int) $candidate['from'] === (int) ( $fresh['from'] ?? 0 )
		&& (int) $candidate['to'] === (int) ( $fresh['to'] ?? 0 )
		&& hash_equals( $candidate['hash'], $fresh['hash'] );
}

/** @param array<string,mixed>|null $fresh */
function wpagent_metabox_connection_create_recovery_required( string $relationship_id, int $from, int $to, ?array $fresh ): WP_Error {
	$data = array( 'status' => 409, 'recovery_required' => true, 'relationship_id' => $relationship_id, 'from' => $from, 'to' => $to );
	if ( is_array( $fresh ) && is_string( $fresh['hash'] ?? null ) ) {
		$data['current_hash'] = $fresh['hash'];
	}
	return new WP_Error(
		'wpagent_metabox_connection_create_recovery_required',
		__( 'Die neu angelegte Meta-Box-Verbindung konnte nicht exakt verifiziert werden. Sie bleibt unverändert erhalten. Wiederhole den Vorgang nicht automatisch und prüfe sie manuell.', 'wp-agent-companion' ),
		$data
	);
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_list_connections( array $input ) {
	$relationship_id = (string) ( $input['relationship_id'] ?? '' );
	$definition = wpagent_metabox_relationship_by_slug( $relationship_id );
	if ( is_wp_error( $definition ) ) {
		return $definition;
	}
	$direction = (string) ( $input['direction'] ?? '' );
	if ( ! in_array( $direction, array( 'from', 'to' ), true ) ) {
		return new WP_Error( 'wpagent_metabox_relationship_direction', __( 'direction muss from oder to sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$object_id = absint( $input['object_id'] ?? 0 );
	$valid = wpagent_metabox_validate_relationship_object( $definition['relationship'][ $direction ], $object_id );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	$limit  = min( 100, max( 1, absint( $input['limit'] ?? 50 ) ) );
	$offset = min( 10000, absint( $input['offset'] ?? 0 ) );
	global $wpdb;
	$total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->mb_relationships} WHERE `type` = %s AND `{$direction}` = %d", $relationship_id, $object_id ) );
	$rows = $wpdb->get_results( $wpdb->prepare( "SELECT `from`, `to`, `order_from`, `order_to` FROM {$wpdb->mb_relationships} WHERE `type` = %s AND `{$direction}` = %d ORDER BY `ID` ASC LIMIT %d OFFSET %d", $relationship_id, $object_id, $limit, $offset ), ARRAY_A );
	$connections = array();
	foreach ( $rows as $row ) {
		$state = array( 'relationship_id' => $relationship_id, 'from' => (int) $row['from'], 'to' => (int) $row['to'], 'order_from' => (int) $row['order_from'], 'order_to' => (int) $row['order_to'], 'exists' => true );
		$state['hash'] = wpagent_metabox_hash( $state );
		$connections[] = $state;
	}
	return array( 'connections' => $connections, 'total' => $total );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_add_connection( array $input ) {
	$relationship_id = (string) ( $input['relationship_id'] ?? '' );
	$preflight = wpagent_metabox_relationship_by_slug( $relationship_id );
	if ( is_wp_error( $preflight ) ) {
		return $preflight;
	}
	$from = absint( $input['from'] ?? 0 );
	$to   = absint( $input['to'] ?? 0 );
	$identity = 'relationship:' . $relationship_id;
	$lock = wpagent_metabox_relationship_acquire_lock( $identity );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$definition = wpagent_metabox_relationship_by_slug( $relationship_id );
		if ( is_wp_error( $definition ) ) {
			return $definition;
		}
		foreach ( array( 'from' => $from, 'to' => $to ) as $side => $object_id ) {
			$valid = wpagent_metabox_validate_relationship_object( $definition['relationship'][ $side ], $object_id );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
		if ( wpagent_metabox_connection_state( $relationship_id, $from, $to ) ) {
			return new WP_Error( 'wpagent_metabox_connection_exists', __( 'Diese Meta-Box-Verbindung existiert bereits.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		global $wpdb;
		$from_has_one = ! empty( $definition['relationship']['from']['has_one_relationship'] );
		$to_has_one   = ! empty( $definition['relationship']['to']['has_one_relationship'] );
		$from_count   = $from_has_one ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->mb_relationships} WHERE `type` = %s AND `from` = %d", $relationship_id, $from ) ) : 0;
		$to_count     = $to_has_one ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->mb_relationships} WHERE `type` = %s AND `to` = %d", $relationship_id, $to ) ) : 0;
		if ( $from_count > 0 || $to_count > 0 ) {
			return new WP_Error( 'wpagent_metabox_connection_limit', __( 'Die Beziehungsdefinition erlaubt auf dieser Seite nur eine Verbindung.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$order_from = min( 2147483647, max( 0, (int) ( $input['order_from'] ?? 1 ) ) );
		$order_to   = min( 2147483647, max( 0, (int) ( $input['order_to'] ?? 1 ) ) );
		$added = MB_Relationships_API::add( $from, $to, $relationship_id, $order_from, $order_to );
		$fresh = wpagent_metabox_connection_state( $relationship_id, $from, $to );
		if ( $added && $fresh && $fresh['order_from'] === $order_from && $fresh['order_to'] === $order_to ) {
			return $fresh;
		}
		$candidate = wpagent_metabox_connection_candidate_state( $relationship_id, $from, $to, $order_from, $order_to );
		if ( ! wpagent_metabox_connection_owned_candidate( $fresh, $candidate ) ) {
			return wpagent_metabox_connection_create_recovery_required( $relationship_id, $from, $to, $fresh );
		}
		$deleted = MB_Relationships_API::delete( $from, $to, $relationship_id );
		if ( ! $deleted || wpagent_metabox_connection_state( $relationship_id, $from, $to ) ) {
			return wpagent_metabox_connection_create_recovery_required( $relationship_id, $from, $to, wpagent_metabox_connection_state( $relationship_id, $from, $to ) );
		}
		return new WP_Error( 'wpagent_metabox_connection_save_failed', __( 'Die Meta-Box-Verbindung konnte nicht exakt verifiziert werden und wurde zurückgerollt.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	} finally {
		wpagent_metabox_relationship_release_lock( $identity, $lock );
	}
}

/** @return array<string,mixed>|WP_Error */
function wpagent_metabox_delete_connection( array $input ) {
	return wpagent_metabox_relationship_manual_write_only();
}
