<?php
/** Shared contract for verified per-post GeneratePress layout overrides. */

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

define( 'WPAGENT_GENERATEPRESS_POST_LOCK_PREFIX', 'wp_agent_generatepress_post_lock_' );

/** @return array<string,array{meta:string,values:list<string>,stored:array<string,string>}> */
function wpagent_generatepress_post_field_catalog(): array {
	return array(
		'sidebar_layout' => array(
			'meta'   => '_generate-sidebar-layout-meta',
			'values' => array( 'inherit', 'right-sidebar', 'left-sidebar', 'no-sidebar', 'both-sidebars', 'both-left', 'both-right' ),
			'stored' => array(),
		),
		'footer_widgets' => array(
			'meta'   => '_generate-footer-widget-meta',
			'values' => array( 'inherit', '0', '1', '2', '3', '4', '5' ),
			'stored' => array(),
		),
		'content_container' => array(
			'meta'   => '_generate-full-width-content',
			'values' => array( 'inherit', 'full-width', 'contained' ),
			'stored' => array( 'full-width' => 'true' ),
		),
		'title_visibility' => array(
			'meta'   => '_generate-disable-headline',
			'values' => array( 'inherit', 'hidden' ),
			'stored' => array( 'hidden' => 'true' ),
		),
	);
}

function wpagent_generatepress_post_override_schema( bool $complete = false ): array {
	$properties = array();
	foreach ( wpagent_generatepress_post_field_catalog() as $field => $definition ) {
		$properties[ $field ] = array( 'type' => 'string', 'enum' => $definition['values'] );
	}
	$schema = array(
		'type'                 => 'object',
		'properties'           => $properties,
		'additionalProperties' => false,
	);
	if ( $complete ) {
		$schema['required'] = array_keys( $properties );
	} else {
		$schema['minProperties'] = 1;
		$schema['maxProperties'] = count( $properties );
	}
	return $schema;
}

/** @return WP_Post|WP_Error */
function wpagent_generatepress_post_target( int $post_id ) {
	// Keep the object-bound authorization ahead of every post lookup. Otherwise
	// inaccessible IDs reveal their existence or GeneratePress eligibility.
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$post = get_post( $post_id );
	if ( ! $post || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$post_type = get_post_type_object( $post->post_type );
	if ( 'attachment' === $post->post_type || ! $post_type || ! $post_type->public ) {
		return new WP_Error( 'wpagent_generatepress_post_unsupported', __( 'GeneratePress unterstützt für diesen Inhaltstyp keine individuellen Layoutvorgaben.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( 'page' === $post->post_type && $post_id === absint( get_option( 'page_for_posts', 0 ) ) ) {
		return new WP_Error( 'wpagent_generatepress_post_blog_page', __( 'GeneratePress verwendet für die Beitragsübersicht das globale Blog-Layout und keine individuellen Seitenvorgaben.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $post;
}

/** @return list<string> */
function wpagent_generatepress_post_stored_values( array $definition ): array {
	$values = array();
	foreach ( $definition['values'] as $value ) {
		if ( 'inherit' !== $value ) {
			$values[] = $definition['stored'][ $value ] ?? $value;
		}
	}
	return array_values( array_unique( $values ) );
}

/**
 * @return array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>}|WP_Error
 */
function wpagent_generatepress_post_snapshot( WP_Post $post ) {
	$fresh_post = get_post( $post->ID );
	if ( ! $fresh_post instanceof WP_Post || $fresh_post->post_type !== $post->post_type ) {
		return new WP_Error( 'wpagent_generatepress_post_changed', __( 'Der GeneratePress-Beitrag ist nicht mehr im gelesenen Zustand vorhanden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$meta = array();
	foreach ( wpagent_generatepress_post_field_catalog() as $definition ) {
		$key    = $definition['meta'];
		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,
				$key
			),
			ARRAY_A
		);
		$rows   = is_array( $rows ) ? $rows : array();
		$values = wpagent_generatepress_post_stored_values( $definition );
		if ( count( $rows ) > 1 ) {
			return new WP_Error( 'wpagent_generatepress_post_duplicate_state', __( 'Der Beitrag enthält doppelte GeneratePress-Einzelmetadaten und kann nicht sicher bearbeitet werden.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$exists = 1 === count( $rows );
		$value  = $exists ? (string) ( $rows[0]['meta_value'] ?? '' ) : '';
		if ( $exists && ( ! is_string( $value ) || strlen( $value ) > 64 || ( '' !== $value && ! in_array( $value, $values, true ) ) ) ) {
			return new WP_Error( 'wpagent_generatepress_post_invalid_state', __( 'Der Beitrag enthält einen ungültigen GeneratePress-Einstellungsstand.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$meta[ $key ] = array(
			'exists'    => $exists,
			'value'     => $value,
			'meta_id'   => $exists ? (int) $rows[0]['meta_id'] : 0,
			'raw_value' => $value,
		);
	}
	return array( 'post_id' => (int) $fresh_post->ID, 'post_type' => $fresh_post->post_type, 'meta' => $meta );
}

/** @return string */
function wpagent_generatepress_post_public_to_stored( string $value, array $definition ): string {
	return $definition['stored'][ $value ] ?? $value;
}

/** @return string|WP_Error */
function wpagent_generatepress_post_stored_to_public( string $value, array $definition ) {
	if ( '' === $value ) {
		return 'inherit';
	}
	foreach ( $definition['values'] as $public ) {
		if ( 'inherit' !== $public && wpagent_generatepress_post_public_to_stored( $public, $definition ) === $value ) {
			return $public;
		}
	}
	return new WP_Error( 'wpagent_generatepress_post_invalid_state', __( 'Der Beitrag enthält einen ungültigen GeneratePress-Einstellungsstand.', 'wp-agent-companion' ), array( 'status' => 502 ) );
}

/**
 * @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string}>} $snapshot
 * @return array<string,string>|WP_Error
 */
function wpagent_generatepress_post_normalize( array $snapshot ) {
	$overrides = array();
	foreach ( wpagent_generatepress_post_field_catalog() as $field => $definition ) {
		$entry = $snapshot['meta'][ $definition['meta'] ];
		$value = ! $entry['exists'] ? 'inherit' : wpagent_generatepress_post_stored_to_public( $entry['value'], $definition );
		if ( is_wp_error( $value ) ) {
			return $value;
		}
		$overrides[ $field ] = $value;
	}
	return $overrides;
}

/**
 * @param array{exists:bool,value:string,meta_id:int,raw_value:string} $previous
 * @return array{exists:bool,value:string,meta_id:int,raw_value:string}
 */
function wpagent_generatepress_post_expected_entry( array $previous, bool $exists, string $value ): array {
	return array(
		'exists'    => $exists,
		'value'     => $exists ? $value : '',
		'meta_id'   => $exists && $previous['exists'] ? $previous['meta_id'] : 0,
		'raw_value' => $exists ? $value : '',
	);
}

/**
 * @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $snapshot
 * @param array<string,string> $settings
 * @return array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>}
 */
function wpagent_generatepress_post_expected_snapshot( array $snapshot, array $settings ): array {
	$expected = $snapshot;
	$catalog  = wpagent_generatepress_post_field_catalog();
	foreach ( $settings as $field => $value ) {
		$definition = $catalog[ $field ];
		$key = $definition['meta'];
		$expected['meta'][ $key ] = wpagent_generatepress_post_expected_entry(
			$snapshot['meta'][ $key ],
			'inherit' !== $value,
			wpagent_generatepress_post_public_to_stored( $value, $definition )
		);
	}
	return $expected;
}

/** @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $snapshot */
function wpagent_generatepress_post_state_hash( array $snapshot ): string {
	$parent = wp_get_theme( get_template() );
	$child  = wp_get_theme( get_stylesheet() );
	$state  = get_template() . '@' . $parent->get( 'Version' ) . '|' . get_stylesheet() . '@' . $child->get( 'Version' );
	$state .= "\0" . $snapshot['post_id'] . "\0" . $snapshot['post_type'];
	foreach ( $snapshot['meta'] as $key => $entry ) {
		$state .= "\0" . $key . "\0" . ( $entry['exists'] ? '1' : '0' ) . "\0" . $entry['meta_id'] . "\0" . $entry['raw_value'];
	}
	return hash( 'sha256', $state );
}

/** @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $expected @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $actual */
function wpagent_generatepress_post_matches_expected( array $expected, array $actual ): bool {
	if ( $expected['post_id'] !== $actual['post_id'] || $expected['post_type'] !== $actual['post_type'] ) {
		return false;
	}
	foreach ( $expected['meta'] as $key => $entry ) {
		$current = $actual['meta'][ $key ] ?? null;
		if ( ! is_array( $current ) || $entry['exists'] !== $current['exists'] || $entry['value'] !== $current['value'] ) {
			return false;
		}
	}
	return true;
}

/** @return array{sidebar_layout:string,footer_widgets:string,content_container:string,title_visibility:string}|WP_Error */
function wpagent_generatepress_post_runtime_state( int $post_id ) {
	foreach ( array( 'generate_get_layout', 'generate_get_footer_widgets', 'generate_show_title', 'generate_body_classes' ) as $function ) {
		if ( ! function_exists( $function ) ) {
			return new WP_Error( 'wpagent_generatepress_post_runtime_missing', __( 'Die aktive GeneratePress-Version stellt den geprüften Beitragslayout-Weg nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
	}
	$target = get_post( $post_id );
	if ( ! $target instanceof WP_Post ) {
		return new WP_Error( 'wpagent_generatepress_post_changed', __( 'Der GeneratePress-Beitrag ist nicht mehr im gelesenen Zustand vorhanden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	global $post, $wp_query;
	$previous_post  = $post;
	$previous_query = $wp_query;
	try {
		$post                        = $target;
		$wp_query                    = new WP_Query();
		$wp_query->is_singular       = true;
		$wp_query->is_page           = 'page' === $target->post_type;
		$wp_query->is_single         = 'page' !== $target->post_type;
		$wp_query->queried_object    = $target;
		$wp_query->queried_object_id = $target->ID;
		$classes = generate_body_classes( array() );
		return array(
			'sidebar_layout'   => (string) generate_get_layout(),
			'footer_widgets'   => (string) generate_get_footer_widgets(),
			'content_container' => in_array( 'full-width-content', $classes, true )
				? 'full-width'
				: ( in_array( 'contained-content', $classes, true ) ? 'contained' : 'inherit' ),
			'title_visibility' => generate_show_title() ? 'inherit' : 'hidden',
		);
	} catch ( Throwable $error ) {
		return new WP_Error( 'wpagent_generatepress_post_runtime_failed', __( 'GeneratePress konnte das individuelle Beitragslayout nicht auswerten.', 'wp-agent-companion' ), array( 'status' => 502 ) );
	} finally {
		$post     = $previous_post;
		$wp_query = $previous_query;
	}
}

/** @return string|WP_Error */
function wpagent_generatepress_post_acquire_lock( int $post_id ) {
	$option = WPAGENT_GENERATEPRESS_POST_LOCK_PREFIX . $post_id;
	$token  = wpagent_companion_acquire_option_lock( $option, WPAGENT_GENERATEPRESS_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_generatepress_post_locked', __( 'Das GeneratePress-Beitragslayout wird gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_generatepress_post_release_lock( int $post_id, string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_GENERATEPRESS_POST_LOCK_PREFIX . $post_id, $token );
}

function wpagent_generatepress_post_lock_is_owned( int $post_id, string $token ): bool {
	return wpagent_companion_option_lock_is_owned( WPAGENT_GENERATEPRESS_POST_LOCK_PREFIX . $post_id, $token );
}

function wpagent_generatepress_post_refresh_lock( int $post_id, string $token ): bool {
	return wpagent_companion_refresh_option_lock( WPAGENT_GENERATEPRESS_POST_LOCK_PREFIX . $post_id, $token, WPAGENT_GENERATEPRESS_LOCK_TTL );
}

/** @return true|WP_Error */
function wpagent_generatepress_post_checkpoint( int $post_id, string $token, bool $write_started = false ) {
	if ( current_user_can( 'edit_post', $post_id ) && wpagent_generatepress_post_refresh_lock( $post_id, $token ) && wpagent_generatepress_post_lock_is_owned( $post_id, $token ) ) {
		return true;
	}
	return new WP_Error(
		$write_started ? 'wpagent_generatepress_post_lock_lost_after_write' : 'wpagent_generatepress_post_lock_lost',
		__( 'Die GeneratePress-Beitragsänderung hat ihre exklusive Sperre verloren.', 'wp-agent-companion' ),
		array( 'status' => $write_started ? 500 : 409, 'write_started' => $write_started )
	);
}

/** @param array{exists:bool,value:string,meta_id:int,raw_value:string} $left @param array{exists:bool,value:string,meta_id:int,raw_value:string} $right */
function wpagent_generatepress_post_entry_equals( array $left, array $right ): bool {
	return $left['exists'] === $right['exists'] && $left['meta_id'] === $right['meta_id'] && hash_equals( $left['raw_value'], $right['raw_value'] );
}

/** @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $candidate */
function wpagent_generatepress_post_candidate_owned( array $candidate ): bool {
	$post  = get_post( $candidate['post_id'] );
	$fresh = $post instanceof WP_Post ? wpagent_generatepress_post_snapshot( $post ) : null;
	if ( ! is_array( $fresh ) || $fresh['post_type'] !== $candidate['post_type'] ) {
		return false;
	}
	foreach ( $candidate['meta'] as $key => $entry ) {
		if ( ! isset( $fresh['meta'][ $key ] ) || ! wpagent_generatepress_post_entry_equals( $entry, $fresh['meta'][ $key ] ) ) {
			return false;
		}
	}
	return true;
}

/** @param array{exists:bool,value:string,meta_id:int,raw_value:string} $candidate */
function wpagent_generatepress_post_candidate_entry_owned( int $post_id, string $key, array $candidate ): bool {
	$post  = get_post( $post_id );
	$fresh = $post instanceof WP_Post ? wpagent_generatepress_post_snapshot( $post ) : null;
	return is_array( $fresh ) && isset( $fresh['meta'][ $key ] ) && wpagent_generatepress_post_entry_equals( $candidate, $fresh['meta'][ $key ] );
}

/** @param array{exists:bool,value:string,meta_id:int,raw_value:string} $previous @param array{exists:bool,value:string,meta_id:int,raw_value:string} $next */
function wpagent_generatepress_post_write_meta_cas( int $post_id, string $key, array $previous, array $next ): bool {
	global $wpdb;
	if ( wpagent_generatepress_post_entry_equals( $previous, $next ) ) {
		return true;
	}
	if ( ! $previous['exists'] ) {
		return ! $next['exists'] || false !== add_post_meta( $post_id, $key, $next['raw_value'], true );
	}
	if ( $next['exists'] ) {
		$changed = $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)", $next['raw_value'], $previous['meta_id'], $post_id, $key, $previous['raw_value'] ) );
	} else {
		$changed = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_id = %d AND post_id = %d AND meta_key = %s AND HEX(meta_value) = HEX(%s)", $previous['meta_id'], $post_id, $key, $previous['raw_value'] ) );
	}
	if ( 1 !== $changed ) {
		return false;
	}
	wp_cache_delete( $post_id, 'post_meta' );
	return true;
}

/** @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $previous @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $expected @return true|WP_Error */
function wpagent_generatepress_post_apply_expected( array $previous, array $expected, string $token ) {
	$changed_any = false;
	foreach ( $expected['meta'] as $key => $next ) {
		$checkpoint = wpagent_generatepress_post_checkpoint( $previous['post_id'], $token, $changed_any );
		if ( is_wp_error( $checkpoint ) ) {
			return $checkpoint;
		}
		if ( wpagent_generatepress_post_entry_equals( $previous['meta'][ $key ], $next ) ) {
			continue;
		}
		if ( ! wpagent_generatepress_post_write_meta_cas( $previous['post_id'], $key, $previous['meta'][ $key ], $next ) ) {
			return new WP_Error( 'wpagent_generatepress_post_write_conflict', __( 'Der GeneratePress-Beitragszustand wurde während der Änderung extern verändert. Es wurde kein Rollback ausgeführt.', 'wp-agent-companion' ), array( 'status' => $changed_any ? 502 : 409, 'recovery_required' => $changed_any, 'write_started' => $changed_any ) );
		}
		$changed_any = true;
	}
	clean_post_cache( $previous['post_id'] );
	return true;
}

/** @return WP_Error */
function wpagent_generatepress_post_recovery_required( int $post_id ) {
	$data  = array( 'status' => 502, 'rollback_verified' => false, 'recovery_required' => true );
	$post  = get_post( $post_id );
	$fresh = $post instanceof WP_Post ? wpagent_generatepress_post_snapshot( $post ) : null;
	if ( is_array( $fresh ) ) {
		$data['current_hash'] = wpagent_generatepress_post_state_hash( $fresh );
	}
	return new WP_Error( 'wpagent_generatepress_post_recovery_required', __( 'GeneratePress hat die Beitragsänderung nicht vollständig bestätigt. Der aktuelle Zustand bleibt erhalten, weil eine fremde Änderung nicht sicher ausgeschlossen werden kann. Wiederhole den Vorgang nicht automatisch und prüfe den Beitrag manuell.', 'wp-agent-companion' ), $data );
}

/**
 * Restores only exactly owned candidate rows, with raw meta_id/value CAS.
 *
 * @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $snapshot
 * @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $candidate
 * @return true|WP_Error
 */
function wpagent_generatepress_post_rollback( array $snapshot, array $candidate, string $expected_hash, string $token ) {
	if ( is_wp_error( wpagent_generatepress_post_checkpoint( $snapshot['post_id'], $token, true ) ) || ! wpagent_generatepress_post_candidate_owned( $candidate ) ) {
		return wpagent_generatepress_post_recovery_required( $snapshot['post_id'] );
	}
	global $wpdb;
	foreach ( $snapshot['meta'] as $key => $previous ) {
		$candidate_entry = $candidate['meta'][ $key ];
		if ( wpagent_generatepress_post_entry_equals( $previous, $candidate_entry ) ) {
			continue;
		}
		if ( is_wp_error( wpagent_generatepress_post_checkpoint( $snapshot['post_id'], $token, true ) ) || ! wpagent_generatepress_post_candidate_entry_owned( $snapshot['post_id'], $key, $candidate_entry ) ) {
			return wpagent_generatepress_post_recovery_required( $snapshot['post_id'] );
		}
		if ( $previous['exists'] && ! $candidate_entry['exists'] ) {
			$changed = $wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->postmeta} (meta_id, post_id, meta_key, meta_value) SELECT %d, %d, %s, %s WHERE NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} WHERE meta_id = %d) AND NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s)", $previous['meta_id'], $snapshot['post_id'], $key, $previous['raw_value'], $previous['meta_id'], $snapshot['post_id'], $key ) );
		} elseif ( ! $previous['exists'] ) {
			$changed = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_id = %d AND post_id = %d AND meta_key = %s AND HEX(meta_value) = HEX(%s)", $candidate_entry['meta_id'], $snapshot['post_id'], $key, $candidate_entry['raw_value'] ) );
		} else {
			$changed = $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)", $previous['raw_value'], $candidate_entry['meta_id'], $snapshot['post_id'], $key, $candidate_entry['raw_value'] ) );
		}
		if ( 1 !== $changed ) {
			return wpagent_generatepress_post_recovery_required( $snapshot['post_id'] );
		}
		wp_cache_delete( $snapshot['post_id'], 'post_meta' );
	}
	clean_post_cache( $snapshot['post_id'] );
	$post  = get_post( $snapshot['post_id'] );
	$fresh = $post instanceof WP_Post ? wpagent_generatepress_post_snapshot( $post ) : null;
	if ( ! is_array( $fresh ) || ! hash_equals( $expected_hash, wpagent_generatepress_post_state_hash( $fresh ) ) ) {
		return wpagent_generatepress_post_recovery_required( $snapshot['post_id'] );
	}
	return true;
}

/** @return WP_Error */
function wpagent_generatepress_post_fail_after_rollback( array $snapshot, ?array $candidate, string $previous_hash, string $token ) {
	if ( is_array( $candidate ) ) {
		$rollback = wpagent_generatepress_post_rollback( $snapshot, $candidate, $previous_hash, $token );
		if ( ! is_wp_error( $rollback ) ) {
			return new WP_Error( 'wpagent_generatepress_post_write_unverified', __( 'GeneratePress hat die Beitragsänderung nicht exakt bestätigt. Der vorherige Zustand wurde eigentumsgeprüft wiederhergestellt.', 'wp-agent-companion' ), array( 'status' => 502, 'rollback_verified' => true ) );
		}
	}
	return wpagent_generatepress_post_recovery_required( $snapshot['post_id'] );
}
