<?php
/** Shared contract for verified per-post Astra design overrides. */

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

/** @return array<string,array{meta:string,values:list<string>}> */
function wpagent_astra_post_field_catalog(): array {
	$inherit_disabled = array( 'inherit', 'disabled' );
	return array(
		'sidebar_layout'     => array( 'meta' => 'site-sidebar-layout', 'values' => array( 'inherit', 'no-sidebar', 'left-sidebar', 'right-sidebar' ) ),
		'content_layout'     => array( 'meta' => 'ast-site-content-layout', 'values' => array( 'inherit', 'normal-width-container', 'narrow-width-container', 'full-width-container' ) ),
		'content_style'      => array( 'meta' => 'site-content-style', 'values' => array( 'inherit', 'unboxed', 'boxed' ) ),
		'sidebar_style'      => array( 'meta' => 'site-sidebar-style', 'values' => array( 'inherit', 'unboxed', 'boxed' ) ),
		'header'             => array( 'meta' => 'ast-global-header-display', 'values' => $inherit_disabled ),
		'above_header'       => array( 'meta' => 'ast-hfb-above-header-display', 'values' => $inherit_disabled ),
		'primary_header'     => array( 'meta' => 'ast-main-header-display', 'values' => $inherit_disabled ),
		'below_header'       => array( 'meta' => 'ast-hfb-below-header-display', 'values' => $inherit_disabled ),
		'mobile_header'      => array( 'meta' => 'ast-hfb-mobile-header-display', 'values' => $inherit_disabled ),
		'footer'             => array( 'meta' => 'footer-sml-layout', 'values' => $inherit_disabled ),
		'title'              => array( 'meta' => 'site-post-title', 'values' => $inherit_disabled ),
		'banner'             => array( 'meta' => 'ast-banner-title-visibility', 'values' => $inherit_disabled ),
		'breadcrumbs'        => array( 'meta' => 'ast-breadcrumbs-content', 'values' => $inherit_disabled ),
		'featured_image'     => array( 'meta' => 'ast-featured-img', 'values' => $inherit_disabled ),
		'related_posts'      => array( 'meta' => 'ast-disable-related-posts', 'values' => $inherit_disabled ),
		'transparent_header' => array( 'meta' => 'theme-transparent-header-meta', 'values' => array( 'inherit', 'enabled', 'disabled' ) ),
	);
}

function wpagent_astra_post_override_schema( bool $complete = false ): array {
	$properties = array();
	foreach ( wpagent_astra_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_astra_post_target( int $post_id ) {
	// A post's existence, type and Astra eligibility are target state. Resolve
	// the concrete meta-cap first so foreign and absent IDs fail identically.
	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 );
	$excluded  = class_exists( 'Astra_Meta_Boxes' ) ? Astra_Meta_Boxes::get_excluded_meta_post_types() : array( 'sureforms_form' );
	if ( 'attachment' === $post->post_type || 'fl-theme-layout' === $post->post_type || ! $post_type || ! $post_type->public || in_array( $post->post_type, $excluded, true ) ) {
		return new WP_Error( 'wpagent_astra_post_unsupported', __( 'Astra unterstützt für diesen Inhaltstyp keine individuellen Designvorgaben.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $post;
}

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

/** @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_astra_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_astra_post_changed', __( 'Der Astra-Beitrag ist nicht mehr im gelesenen Zustand vorhanden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$allowed = array();
	foreach ( wpagent_astra_post_field_catalog() as $definition ) {
		$provider_values = array_values( array_diff( $definition['values'], array( 'inherit' ) ) );
		$allowed[ $definition['meta'] ] = array_merge( array( '', 'default' ), $provider_values );
	}
	$allowed['astra-migrate-meta-layouts'] = array( '', 'default', 'set' );
	$allowed['site-content-layout']        = array( '', 'default', 'boxed-container', 'content-boxed-container', 'plain-container', 'page-builder', 'narrow-container' );

	$meta = array();
	foreach ( $allowed as $key => $values ) {
		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();
		if ( count( $rows ) > 1 ) {
			return new WP_Error( 'wpagent_astra_post_duplicate_state', __( 'Der Beitrag enthält doppelte Astra-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 || ! in_array( $value, $values, true ) ) ) {
			return new WP_Error( 'wpagent_astra_post_invalid_state', __( 'Der Beitrag enthält einen ungültigen Astra-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 );
}

/**
 * Calculate the exact persisted state expected after a validated partial update.
 *
 * @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_astra_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_astra_post_expected_snapshot( array $snapshot, array $settings ): array {
	$expected = $snapshot;
	$catalog  = wpagent_astra_post_field_catalog();
	foreach ( $settings as $field => $value ) {
		$key = $catalog[ $field ]['meta'];
		if ( 'content_layout' === $field ) {
			$expected['meta']['site-content-layout'] = wpagent_astra_post_expected_entry( $snapshot['meta']['site-content-layout'], false, '' );
		}
		$expected['meta'][ $key ] = wpagent_astra_post_expected_entry( $snapshot['meta'][ $key ], 'inherit' !== $value, $value );
	}
	if ( array_key_exists( 'content_layout', $settings ) ) {
		$expected['meta']['astra-migrate-meta-layouts'] = wpagent_astra_post_expected_entry(
			$snapshot['meta']['astra-migrate-meta-layouts'],
			'inherit' !== $settings['content_layout'],
			'set'
		);
	}
	return $expected;
}

function wpagent_astra_post_legacy_layout_to_modern( string $value ): string {
	$layouts = array(
		'boxed-container'         => 'normal-width-container',
		'content-boxed-container' => 'normal-width-container',
		'plain-container'         => 'normal-width-container',
		'page-builder'            => 'full-width-container',
		'narrow-container'        => 'narrow-width-container',
	);
	return $layouts[ $value ] ?? 'inherit';
}

/** @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_astra_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_astra_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;
}

/** @param array{post_id:int,post_type:string,meta:array<string,array{exists:bool,value:string,meta_id:int,raw_value:string}>} $snapshot @return array<string,string> */
function wpagent_astra_post_normalize( array $snapshot ): array {
	$overrides = array();
	foreach ( wpagent_astra_post_field_catalog() as $field => $definition ) {
		if ( 'content_layout' === $field ) {
			$legacy    = $snapshot['meta']['site-content-layout'];
			$migration = $snapshot['meta']['astra-migrate-meta-layouts'];
			if ( $legacy['exists'] && 'set' !== $migration['value'] ) {
				$legacy_value = wpagent_astra_post_legacy_layout_to_modern( $legacy['value'] );
				if ( 'inherit' !== $legacy_value ) {
					$overrides[ $field ] = $legacy_value;
					continue;
				}
			}
		}
		$entry = $snapshot['meta'][ $definition['meta'] ];
		$value = $entry['exists'] ? $entry['value'] : '';
		$overrides[ $field ] = '' === $value || 'default' === $value ? 'inherit' : $value;
	}
	return $overrides;
}

/** @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_astra_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_astra_post_candidate_owned( array $candidate ): bool {
	$post  = get_post( $candidate['post_id'] );
	$fresh = $post instanceof WP_Post ? wpagent_astra_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_astra_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_astra_post_candidate_entry_owned( int $post_id, string $key, array $candidate ): bool {
	$post  = get_post( $post_id );
	$fresh = $post instanceof WP_Post ? wpagent_astra_post_snapshot( $post ) : null;
	return is_array( $fresh ) && isset( $fresh['meta'][ $key ] ) && wpagent_astra_post_entry_equals( $candidate, $fresh['meta'][ $key ] );
}

/**
 * Applies one meta transition with an exact meta_id/raw-old-value condition.
 *
 * @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_astra_post_write_meta_cas( int $post_id, string $key, array $previous, array $next ): bool {
	global $wpdb;
	if ( wpagent_astra_post_entry_equals( $previous, $next ) ) {
		return true;
	}
	if ( ! $previous['exists'] ) {
		if ( ! $next['exists'] ) {
			return true;
		}
		return 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_astra_post_apply_expected( array $previous, array $expected, string $token ) {
	$changed_any = false;
	foreach ( $expected['meta'] as $key => $next ) {
		$checkpoint = wpagent_astra_post_checkpoint( $previous['post_id'], $token, $changed_any );
		if ( is_wp_error( $checkpoint ) ) {
			return $checkpoint;
		}
		if ( wpagent_astra_post_entry_equals( $previous['meta'][ $key ], $next ) ) {
			continue;
		}
		if ( ! wpagent_astra_post_write_meta_cas( $previous['post_id'], $key, $previous['meta'][ $key ], $next ) ) {
			return new WP_Error( 'wpagent_astra_post_write_conflict', __( 'Der Astra-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_astra_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_astra_post_snapshot( $post ) : null;
	if ( is_array( $fresh ) ) {
		$data['current_hash'] = wpagent_astra_post_state_hash( $fresh );
	}
	return new WP_Error( 'wpagent_astra_post_recovery_required', __( 'Astra 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 rows which still exactly match the candidate we created. The
 * caller must already have demonstrated a semantically exact candidate.
 *
 * @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_astra_post_rollback( array $snapshot, array $candidate, string $expected_hash, string $token ) {
	if ( is_wp_error( wpagent_astra_post_checkpoint( $snapshot['post_id'], $token, true ) ) || ! wpagent_astra_post_candidate_owned( $candidate ) ) {
		return wpagent_astra_post_recovery_required( $snapshot['post_id'] );
	}
	global $wpdb;
	foreach ( $snapshot['meta'] as $key => $previous ) {
		$candidate_entry = $candidate['meta'][ $key ];
		if ( wpagent_astra_post_entry_equals( $previous, $candidate_entry ) ) {
			continue;
		}
		if ( is_wp_error( wpagent_astra_post_checkpoint( $snapshot['post_id'], $token, true ) ) || ! wpagent_astra_post_candidate_entry_owned( $snapshot['post_id'], $key, $candidate_entry ) ) {
			return wpagent_astra_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_astra_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_astra_post_snapshot( $post ) : null;
	if ( ! is_array( $fresh ) || ! hash_equals( $expected_hash, wpagent_astra_post_state_hash( $fresh ) ) ) {
		return wpagent_astra_post_recovery_required( $snapshot['post_id'] );
	}
	return true;
}

/** @return WP_Error */
function wpagent_astra_post_fail_after_rollback( array $snapshot, ?array $candidate, string $previous_hash, string $token ) {
	if ( is_array( $candidate ) ) {
		$rollback = wpagent_astra_post_rollback( $snapshot, $candidate, $previous_hash, $token );
		if ( ! is_wp_error( $rollback ) ) {
			return new WP_Error( 'wpagent_astra_post_write_unverified', __( 'Astra 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_astra_post_recovery_required( $snapshot['post_id'] );
}
