<?php
/**
 * Ability: wp-agent/set-featured-image
 *
 * Sets a pre-existing attachment as a post thumbnail with a post-state CAS,
 * an exact `_thumbnail_id` snapshot and the shared Core-post lock. A failed
 * provider call may have persisted the meta row. Because a later foreign write
 * cannot be distinguished from a locally persisted candidate, every uncertain
 * result fails closed as recovery-required instead of attempting a rollback.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_set_featured_image(): array {
	return array(
		'label'               => __( 'Featured Image setzen', 'wp-agent-companion' ),
		'description'         => __( 'Setzt einen vorhandenen Medienbeitrag konfliktgeschützt als Featured Image. Benötigt den state_hash aus get-post und verifiziert Meta-Zustand und Core-Postzustand exakt.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'                 => 'object',
			'properties'           => array(
				'post_id'             => array( 'type' => 'integer', 'minimum' => 1 ),
				'attachment_id'       => array( 'type' => 'integer', 'minimum' => 1 ),
				'expected_state_hash' => array(
					'type'        => 'string',
					'pattern'     => '^[a-f0-9]{64}$',
					'description' => __( 'Exakter state_hash aus get-post. Bindet auch das bestehende Featured Image.', 'wp-agent-companion' ),
				),
			),
			'required'             => array( 'post_id', 'attachment_id', 'expected_state_hash' ),
			'additionalProperties' => false,
		),
		'output_schema'       => array(
			'type'                 => 'object',
			'properties'           => array(
				'post_id'             => array( 'type' => 'integer', 'minimum' => 1 ),
				'attachment_id'       => array( 'type' => 'integer', 'minimum' => 1 ),
				'verified'            => array( 'type' => 'boolean' ),
				'previous_state_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'state_hash'          => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required'             => array( 'post_id', 'attachment_id', 'verified', 'previous_state_hash', 'state_hash' ),
			'additionalProperties' => false,
		),
		'execute_callback'    => 'wpagent_exec_set_featured_image',
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
				// The target post is changed through an exact state-bound CAS.
				'remote_update' => true,
			),
		),
	);
}

/** @return array{exists:bool,meta_id:int,raw:string,id:int}|WP_Error */
function wpagent_featured_image_snapshot( int $post_id ) {
	global $wpdb;
	$rows = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s ORDER BY meta_id ASC",
			$post_id,
			'_thumbnail_id'
		),
		ARRAY_A
	);
	if ( '' !== (string) $wpdb->last_error || ! is_array( $rows ) || count( $rows ) > 1 ) {
		return new WP_Error( 'wpagent_featured_image_ambiguous', __( 'Der Featured-Image-Metazustand ist nicht eindeutig. Die Änderung wurde nicht ausgeführt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( array() === $rows ) {
		return array( 'exists' => false, 'meta_id' => 0, 'raw' => '', 'id' => 0 );
	}
	$row = $rows[0];
	$raw = $row['meta_value'] ?? null;
	$meta_id = (int) ( $row['meta_id'] ?? 0 );
	if ( ! is_string( $raw ) || $meta_id < 1 || ! ctype_digit( $raw ) || (string) (int) $raw !== $raw ) {
		return new WP_Error( 'wpagent_featured_image_ambiguous', __( 'Der vorhandene Featured-Image-Metawert ist nicht kanonisch. Die Änderung wurde nicht ausgeführt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return array( 'exists' => true, 'meta_id' => $meta_id, 'raw' => $raw, 'id' => (int) $raw );
}

/** @return array{post:WP_Post,snapshot:array{exists:bool,meta_id:int,raw:string,id:int}}|WP_Error */
function wpagent_featured_image_current( int $post_id ) {
	clean_post_cache( $post_id );
	wp_cache_delete( $post_id, 'post_meta' );
	$post = get_post( $post_id );
	if ( ! $post instanceof WP_Post ) {
		return new WP_Error( 'wpagent_no_post', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$snapshot = wpagent_featured_image_snapshot( $post_id );
	if ( is_wp_error( $snapshot ) ) {
		return $snapshot;
	}
	if ( (int) get_post_thumbnail_id( $post_id ) !== $snapshot['id'] ) {
		return new WP_Error( 'wpagent_featured_image_ambiguous', __( 'Der effektive Featured-Image-Zustand stimmt nicht mit seinen Metadaten überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return array( 'post' => $post, 'snapshot' => $snapshot );
}

/** @param array{post:WP_Post,snapshot:array{exists:bool,meta_id:int,raw:string,id:int}} $current */
function wpagent_featured_image_matches( array $current, int $attachment_id, int $meta_id, string $candidate_hash ): bool {
	return $current['snapshot']['id'] === $attachment_id
		&& $current['snapshot']['meta_id'] === $meta_id
		&& hash_equals( $candidate_hash, wpagent_post_state_hash( $current['post'] ) );
}

/** @param array{post:WP_Post,snapshot:array{exists:bool,meta_id:int,raw:string,id:int}}|null $current */
function wpagent_featured_image_recovery_required( int $post_id, string $previous_hash, ?array $current, string $reason ): WP_Error {
	return new WP_Error(
		'wpagent_featured_image_recovery_required',
		__( 'Das Featured Image konnte nicht vollständig verifiziert werden. Prüfe den Beitrag manuell und lies seinen state_hash neu.', 'wp-agent-companion' ),
		array(
			'status'              => 500,
			'post_id'             => $post_id,
			'previous_state_hash' => $previous_hash,
			'current_state_hash'  => is_array( $current ) ? wpagent_post_state_hash( $current['post'] ) : '',
			'rolled_back'         => false,
			'rollback_verified'   => false,
			'recovery_required'   => true,
			'reason'              => $reason,
		)
	);
}

/**
 * Replaces an existing thumbnail row only while its exact row identity and raw
 * old value still match. This makes the persistence step an atomic CAS.
 *
 * @param array{exists:bool,meta_id:int,raw:string,id:int} $before
 */
function wpagent_featured_image_replace_if_current( int $post_id, array $before, int $candidate_id ): bool {
	global $wpdb;
	if ( ! $before['exists'] || $before['meta_id'] < 1 ) {
		return false;
	}
	$updated = $wpdb->query(
		$wpdb->prepare(
			"UPDATE {$wpdb->postmeta} SET meta_value = %s WHERE meta_id = %d AND post_id = %d AND meta_key = %s AND HEX(meta_value) = HEX(%s)",
			(string) $candidate_id,
			$before['meta_id'],
			$post_id,
			'_thumbnail_id',
			$before['raw']
		)
	);
	if ( 1 !== $updated ) {
		return false;
	}
	wp_cache_delete( $post_id, 'post_meta' );
	return true;
}

/**
 * Adds a thumbnail only from an absent state and returns the exact new row ID.
 * The immediate caller must read it back and compare this ID, otherwise a
 * same-key racing insert is treated as recovery-required.
 *
 * @return int|false
 */
function wpagent_featured_image_insert_if_absent( int $post_id, int $candidate_id ): int|false {
	$meta_id = add_post_meta( $post_id, '_thumbnail_id', (string) $candidate_id, true );
	if ( ! is_int( $meta_id ) || $meta_id < 1 ) {
		return false;
	}
	wp_cache_delete( $post_id, 'post_meta' );
	return $meta_id;
}

/**
 * @param array{exists:bool,meta_id:int,raw:string,id:int} $before
 * @return WP_Error
 */
function wpagent_featured_image_fail_after_uncertain_write( int $post_id, string $previous_hash, array $before ): WP_Error {
	$current = wpagent_featured_image_current( $post_id );
	if ( is_wp_error( $current ) ) {
		return wpagent_featured_image_recovery_required( $post_id, $previous_hash, null, 'readback_failed' );
	}
	if ( hash_equals( $previous_hash, wpagent_post_state_hash( $current['post'] ) ) ) {
		return new WP_Error( 'wpagent_featured_failed', __( 'Featured Image konnte nicht gesetzt werden.', 'wp-agent-companion' ), array( 'status' => 500, 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => false ) );
	}
	return wpagent_featured_image_recovery_required( $post_id, $previous_hash, $current, 'conditional_write_not_confirmed' );
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_set_featured_image( $input ) {
	$input         = is_array( $input ) ? $input : array();
	$post_id       = (int) ( $input['post_id'] ?? 0 );
	$attachment_id = (int) ( $input['attachment_id'] ?? 0 );
	// Both objects are explicit targets. Check both meta capabilities before
	// loading either one so a readable post cannot be used to probe a foreign
	// attachment (or vice versa).
	if ( ! current_user_can( 'edit_post', $post_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$post = get_post( $post_id );
	if ( ! $post ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$attachment = get_post( $attachment_id );
	if ( ! $attachment ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( 'attachment' !== $attachment->post_type ) {
		return new WP_Error( 'wpagent_no_media', __( 'Medienbeitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$expected_state_hash = is_string( $input['expected_state_hash'] ?? null ) ? strtolower( $input['expected_state_hash'] ) : '';
	if ( 1 !== preg_match( '/^[a-f0-9]{64}$/D', $expected_state_hash ) ) {
		return new WP_Error( 'wpagent_post_expected_hash', __( 'expected_state_hash muss der vollständige state_hash aus get-post sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$lock = wpagent_update_post_acquire_lock( $post_id );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$current = wpagent_featured_image_current( $post_id );
		if ( is_wp_error( $current ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( ! current_user_can( 'edit_post', $post_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$previous_hash = wpagent_post_state_hash( $current['post'] );
		if ( ! hash_equals( $expected_state_hash, $previous_hash ) ) {
			return new WP_Error( 'wpagent_post_conflict', __( 'Der Beitrag wurde seit dem Lesen verändert. Lies ihn neu und wiederhole den Vorgang mit dem aktuellen state_hash.', 'wp-agent-companion' ), array( 'status' => 409, 'current_state_hash' => $previous_hash ) );
		}
		$attachment = get_post( $attachment_id );
		if ( ! $attachment ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( 'attachment' !== $attachment->post_type ) {
			return new WP_Error( 'wpagent_no_media', __( 'Medienbeitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		}
		if ( ! wp_get_attachment_image( $attachment_id, 'thumbnail' ) ) {
			return new WP_Error( 'wpagent_not_featured_image', __( 'Medienbeitrag kann nicht als Featured Image gerendert werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( ! wpagent_update_post_refresh_lock( $post_id, $lock ) ) {
			return new WP_Error( 'wpagent_post_busy', __( 'Die exklusive Beitragssperre ging vor dem Schreiben verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( $current['snapshot']['id'] === $attachment_id ) {
			$confirm = wpagent_featured_image_current( $post_id );
			if ( is_wp_error( $confirm ) || ! hash_equals( $expected_state_hash, is_array( $confirm ) ? wpagent_post_state_hash( $confirm['post'] ) : '' ) || ! wpagent_update_post_lock_is_owned( $post_id, $lock ) ) {
				return new WP_Error( 'wpagent_post_conflict', __( 'Der Beitrag änderte sich während der No-op-Prüfung.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			return array( 'post_id' => $post_id, 'attachment_id' => $attachment_id, 'verified' => true, 'previous_state_hash' => $previous_hash, 'state_hash' => $previous_hash );
		}
		if ( ! current_user_can( 'edit_post', $post_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für Beitrag oder Medienbeitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( ! wpagent_update_post_refresh_lock( $post_id, $lock ) ) {
			return new WP_Error( 'wpagent_post_busy', __( 'Die exklusive Beitragssperre ging vor dem Schreiben verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$written_meta_id = $current['snapshot']['exists']
			? ( wpagent_featured_image_replace_if_current( $post_id, $current['snapshot'], $attachment_id ) ? $current['snapshot']['meta_id'] : false )
			: wpagent_featured_image_insert_if_absent( $post_id, $attachment_id );
		if ( false === $written_meta_id ) {
			return wpagent_featured_image_fail_after_uncertain_write( $post_id, $previous_hash, $current['snapshot'] );
		}
		$fresh = wpagent_featured_image_current( $post_id );
		$candidate_hash = wpagent_post_featured_state_hash( is_array( $fresh ) ? $fresh['post'] : $current['post'], $attachment_id );
		if ( is_wp_error( $fresh ) || ! wpagent_featured_image_matches( $fresh, $attachment_id, $written_meta_id, $candidate_hash ) || ! wpagent_update_post_lock_is_owned( $post_id, $lock ) || ! current_user_can( 'edit_post', $post_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
			return wpagent_featured_image_recovery_required( $post_id, $previous_hash, is_array( $fresh ) ? $fresh : null, 'write_readback_or_lock_failed' );
		}
		return array( 'post_id' => $post_id, 'attachment_id' => $attachment_id, 'verified' => true, 'previous_state_hash' => $previous_hash, 'state_hash' => wpagent_post_state_hash( $fresh['post'] ) );
	} finally {
		wpagent_update_post_release_lock( $post_id, $lock );
	}
}
