<?php
/**
 * Ability: wp-agent/upsert-pattern
 *
 * Legt eine Landing-Page (post_type 'page') aus einer deklarativen Sektionsliste
 * an oder findet sie über slug oder post_id wieder. Bestehende Seiten werden nur
 * mit einem frischen vollständigen content_state_hash geändert. Das Block-Markup
 * wird serverseitig als valide Gutenberg-Core-Blocks erzeugt, damit WordPress nie
 * still auf den Classic Editor (core/freeform) zurückfällt. Vor dem Speichern
 * wird mit parse_blocks verifiziert, dass 0 freeform-Blöcke entstehen.
 *
 * Design: Jede Sektion wird zu einem vollbreiten core/group mit Farb-, Abstands-
 * und Rahmen-Tokens aus theme.json. Feature-Karten liegen in einem responsiven
 * core/group mit Grid-Layout (statt N zerquetschten Spalten). Die HTML-Klassen
 * und Inline-Styles entsprechen exakt der Gutenberg-Serialisierung, damit der
 * Editor die Blöcke als gültig akzeptiert (keine Block-Recovery). Farb-, Abstands-
 * und Schriftgrössen-Slugs werden gegen die aktive theme.json-Skala aufgelöst:
 * unbekannte Farb-Defaults werden weggelassen, ein fehlender Abstands-Slug auf den
 * numerisch nächsten vorhandenen abgebildet und eine fehlende Schriftgrösse auf die
 * nächste Standardgrösse, statt tote has-*-Klassen in ein fremdes Theme zu schreiben.
 * So hängt der Builder nicht mehr an der Twenty-Twenty-Five-Skala.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_upsert_pattern(): array {
	return array(
		'label'               => __( 'Landing-Page aus Sektionen anlegen/aktualisieren', 'wp-agent-companion' ),
		'description'         => __( 'Baut eine Landing-Page (Seite) aus einer deklarativen Sektionsliste (hero, features, media-text, logos, cta, pricing, testimonials, faq, stats, gallery, steps, comparison) als professionell gestaltete, valide Gutenberg-Core-Blöcke mit theme.json-Tokens (Farben, Abstände, Grid-Karten, Split-Layouts, Karten-Bilder, Hero-Hintergrundbild via core/cover, Bildergalerien via core/gallery, Akkordeons, KPI-Grids, nummerierte Prozess-Schritte, Vergleichstabellen via core/table) und legt sie idempotent über slug oder post_id an bzw. aktualisiert sie. Erzeugt garantiert 0 freeform.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'properties'           => array(
				'title'    => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 200,
					'description' => __( 'Seitentitel.', 'wp-agent-companion' ),
				),
				'slug'     => array(
					'type'        => 'string',
					'maxLength'   => 200,
					'description' => __( 'Seiten-Slug; wird für die idempotente Zuordnung genutzt, wenn keine post_id gesetzt ist.', 'wp-agent-companion' ),
				),
				'post_id'  => array(
					'type'        => 'integer',
					'minimum'     => 1,
					'description' => __( 'Wenn gesetzt, wird genau diese Seite aktualisiert (höchste Priorität vor slug).', 'wp-agent-companion' ),
				),
				'status'   => array(
					'type'    => 'string',
					'enum'    => array( 'draft', 'publish', 'pending', 'private' ),
					'default' => 'draft',
				),
				'expected_state_hash' => array(
					'type'        => 'string',
					'pattern'     => '^[a-f0-9]{64}$',
					'minLength'   => 64,
					'maxLength'   => 64,
					'description' => __( 'Vollständiger content_state_hash eines frischen get-post-Reads. Bei Updates verpflichtend.', 'wp-agent-companion' ),
				),
				'sections' => array(
					'type'        => 'array',
					'minItems'    => 1,
					'maxItems'    => 50,
					'description' => __( 'Geordnete Sektionsliste. Jede Sektion hat ein type-Feld (hero|features|media-text|logos|cta|pricing|testimonials|faq|stats|gallery|steps|comparison) plus die zugehörigen Felder.', 'wp-agent-companion' ),
					'items'       => array(
						'type'                 => 'object',
						'additionalProperties' => false,
						'properties'           => array(
							'type'            => array(
								'type' => 'string',
								'enum' => array( 'hero', 'features', 'media-text', 'logos', 'cta', 'pricing', 'testimonials', 'faq', 'stats', 'gallery', 'steps', 'comparison' ),
							),
							'heading'         => array( 'type' => 'string', 'maxLength' => 500 ),
							'text'            => array( 'type' => 'string', 'maxLength' => 10000 ),
							'subheading'      => array(
								'type'        => 'string',
								'maxLength'   => 4000,
								'description' => __( 'Optionale Unterüberschrift unter dem H2 (gedämpft, mittig).', 'wp-agent-companion' ),
							),
							'backgroundColor' => array(
								'type'        => 'string',
								'maxLength'   => 64,
								'description' => __( 'Optionaler theme.json-Farb-Slug für den Sektions-Hintergrund (z. B. accent-3). Ohne Angabe wählt der Builder einen sinnvollen Standard.', 'wp-agent-companion' ),
							),
							'textColor'       => array(
								'type'        => 'string',
								'maxLength'   => 64,
								'description' => __( 'Optionaler theme.json-Farb-Slug für die Sektions-Textfarbe.', 'wp-agent-companion' ),
							),
							'cta_label'       => array( 'type' => 'string', 'maxLength' => 200 ),
							'cta_url'         => array( 'type' => 'string', 'maxLength' => 2048 ),
							'button_label'    => array(
								'type'        => 'string',
								'maxLength'   => 200,
								'description' => __( 'Alternative zu cta_label. hero und cta akzeptieren BEIDE Schreibweisen (Fallback), damit kein Button still verschluckt wird.', 'wp-agent-companion' ),
							),
							'button_url'      => array( 'type' => 'string', 'maxLength' => 2048 ),
							'image_url'       => array(
								'type'        => 'string',
								'maxLength'   => 2048,
								'description' => __( 'media-text: Bild-URL des Split-Layouts (Pflicht für diese Sektion). hero: optionales Hintergrundbild; mit ihm wird der Hero als core/cover (Bild + Farb-Overlay) statt als farbiger Block gerendert.', 'wp-agent-companion' ),
							),
							'image_alt'       => array(
								'type'        => 'string',
								'maxLength'   => 500,
								'description' => __( 'media-text/hero: Alt-Text des Sektions- bzw. Hintergrundbildes (darf leer sein).', 'wp-agent-companion' ),
							),
							'dimRatio'        => array(
								'type'        => 'integer',
								'minimum'     => 0,
								'maximum'     => 100,
								'description' => __( 'Nur hero mit image_url: Deckkraft des Farb-Overlays über dem Hintergrundbild (0..100, Standard 50). Wird auf 0..100 geklemmt.', 'wp-agent-companion' ),
							),
							'mediaPosition'   => array(
								'type'        => 'string',
								'enum'        => array( 'left', 'right' ),
								'description' => __( 'Nur media-text: Bildseite. Ohne Angabe wechseln aufeinanderfolgende media-text-Sektionen automatisch die Seite (erste links).', 'wp-agent-companion' ),
							),
							'columns'         => array(
								'type'        => 'array',
								'maxItems'    => 12,
								'items'       => array( 'type' => 'string', 'maxLength' => 200 ),
								'description' => __( 'Nur comparison: Spalten-Überschriften der Vergleichstabelle. Die erste Spalte ist das Zeilen-Label, die weiteren die Vergleichsobjekte (z. B. ["Merkmal","WPAgently","Andere"]).', 'wp-agent-companion' ),
							),
							'items'           => array(
								'type'        => 'array',
								'maxItems'    => 50,
								'description' => __( 'Karten/Logos/Preis-Pläne/Stimmen/FAQ-Fragen/KPI-Kacheln/Prozess-Schritte/Tabellenzeilen, je nach Sektionstyp.', 'wp-agent-companion' ),
								'items'       => array(
									'type'                 => 'object',
									'additionalProperties' => false,
									'properties'           => array(
										'title'        => array( 'type' => 'string', 'maxLength' => 500 ),
										'text'         => array( 'type' => 'string', 'maxLength' => 4000 ),
										'subheading'   => array(
											'type'        => 'string',
											'maxLength'   => 1000,
											'description' => __( 'Nur stats: optionaler kleiner Zusatz unter dem Label einer KPI-Kachel.', 'wp-agent-companion' ),
										),
										'badge'        => array(
											'type'        => 'string',
											'maxLength'   => 200,
											'description' => __( 'Optionales kleines, farbiges Label oben in der Karte.', 'wp-agent-companion' ),
										),
										'icon'         => array(
											'type'        => 'string',
											'maxLength'   => 64,
											'description' => __( 'Nur features/stats/steps: optionaler Icon-Name aus der mitgelieferten Phosphor-Allowlist. Wird als kleines, natives core/image oben in der Karte/Kachel über Titel bzw. Nummer gerendert. Erlaubt: check, check-circle, star, rocket, shield-check, lightning, heart, gear-six, chart-line-up, users-three, lock-key, magic-wand, clock, globe, sparkle, trophy, thumbs-up, wrench, plugs-connected, gauge. Unbekannter oder leerer Name: kein Icon (Sektion bleibt valide).', 'wp-agent-companion' ),
										),
										'label'        => array(
											'type'        => 'string',
											'maxLength'   => 500,
											'description' => __( 'Alias für badge. Bei comparison zusätzlich das Zeilen-Label (erste Spalte) einer Tabellenzeile.', 'wp-agent-companion' ),
										),
										'price'        => array(
											'type'        => 'string',
											'maxLength'   => 200,
											'description' => __( 'Nur pricing: hervorgehobene Preiszeile (z. B. "49 €/Monat").', 'wp-agent-companion' ),
										),
										'role'         => array(
											'type'        => 'string',
											'maxLength'   => 500,
											'description' => __( 'Nur testimonials: Rolle/Firma unter dem Namen.', 'wp-agent-companion' ),
										),
										'featured'     => array(
											'type'        => 'boolean',
											'description' => __( 'Nur pricing: hebt die Karte farbig hervor.', 'wp-agent-companion' ),
										),
										'image_url'    => array(
											'type'        => 'string',
											'maxLength'   => 2048,
											'description' => __( 'logos: Bild-URL statt Text-Logo. features/pricing: optionales Karten-Bild oben in der Karte. gallery: Bild-URL einer Galerie-Kachel (Pflicht je Kachel).', 'wp-agent-companion' ),
										),
										'image_alt'    => array(
											'type'        => 'string',
											'maxLength'   => 500,
											'description' => __( 'features/pricing/gallery: Alt-Text des Bildes (Fallback: title).', 'wp-agent-companion' ),
										),
										'cta_label'    => array( 'type' => 'string', 'maxLength' => 200 ),
										'cta_url'      => array( 'type' => 'string', 'maxLength' => 2048 ),
										'button_label' => array( 'type' => 'string', 'maxLength' => 200 ),
										'button_url'   => array( 'type' => 'string', 'maxLength' => 2048 ),
										'values'       => array(
											'type'        => 'array',
											'maxItems'    => 12,
											'items'       => array( 'type' => 'string', 'maxLength' => 2000 ),
											'description' => __( 'Nur comparison: Zellwerte einer Tabellenzeile OHNE das Zeilen-Label (label ist die erste Spalte). Sollte columns.length - 1 Werte enthalten; fehlende Werte werden zu leeren Zellen, überzählige abgeschnitten.', 'wp-agent-companion' ),
										),
									),
								),
							),
						),
						'required'   => array( 'type' ),
					),
				),
			),
			'required'             => array( 'title', 'sections' ),
		),
		'output_schema'       => array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'properties'           => array(
				'post_id'        => array( 'type' => 'integer', 'minimum' => 1 ),
				'url'            => array( 'type' => 'string', 'maxLength' => 2048 ),
				'status'         => array( 'type' => 'string', 'enum' => array( 'draft', 'publish', 'pending', 'private' ) ),
				'content_hash'   => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'content_state_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'block_count'    => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 20000 ),
				'freeform_count' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 20000 ),
				'block_types'    => array(
					'type'  => 'array',
					'maxItems' => 200,
					'items' => array( 'type' => 'string', 'maxLength' => 128 ),
				),
				'updated'        => array( 'type' => 'boolean' ),
			),
			'required'             => array( 'post_id', 'url', 'status', 'content_hash', 'content_state_hash', 'block_count', 'freeform_count', 'block_types', 'updated' ),
		),
		'execute_callback'    => 'wpagent_exec_upsert_pattern',
		'permission_callback' => 'wpagent_perm_edit_pages',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
				// New pages are verified after insertion. Existing pages use the
				// bounded post-row CAS below, including a fresh full-state hash.
				'remote_create' => true,
				'remote_update' => true,
			),
		),
	);
}

/** @return WP_Error */
function wpagent_pattern_input_error( string $message ): WP_Error {
	return new WP_Error( 'wpagent_invalid_pattern_input', $message, array( 'status' => 400 ) );
}

/** @return true|WP_Error */
function wpagent_pattern_validate_text( array $value, string $field, int $maximum, bool $required = false ) {
	if ( ! array_key_exists( $field, $value ) ) {
		return $required ? wpagent_pattern_input_error( __( 'Ein erforderliches Pattern-Feld fehlt.', 'wp-agent-companion' ) ) : true;
	}
	$text = $value[ $field ];
	$length = is_string( $text ) && function_exists( 'mb_strlen' ) ? mb_strlen( $text, 'UTF-8' ) : ( is_string( $text ) ? strlen( $text ) : 0 );
	if ( ! is_string( $text ) || $length > $maximum || str_contains( $text, "\0" ) || ( $required && '' === trim( $text ) ) ) {
		return wpagent_pattern_input_error( __( 'Ein Pattern-Textfeld hat einen ungültigen Typ, Inhalt oder Umfang.', 'wp-agent-companion' ) );
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_pattern_validate_url( array $value, string $field, bool $image = false ) {
	$valid = wpagent_pattern_validate_text( $value, $field, 2048 );
	if ( is_wp_error( $valid ) || ! array_key_exists( $field, $value ) || '' === $value[ $field ] ) {
		return $valid;
	}
	$url = $value[ $field ];
	$relative = str_starts_with( $url, '/' ) && ! str_starts_with( $url, '//' );
	$fragment = ! $image && str_starts_with( $url, '#' );
	$protocols = $image ? array( 'http', 'https' ) : array( 'http', 'https', 'mailto', 'tel' );
	if ( ! $relative && ! $fragment && esc_url_raw( $url, $protocols ) !== $url ) {
		return wpagent_pattern_input_error( __( 'Eine Pattern-URL ist ungültig oder verwendet ein nicht erlaubtes Protokoll.', 'wp-agent-companion' ) );
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_pattern_validate_item( $item ) {
	if ( ! is_array( $item ) || array_is_list( $item ) ) {
		return wpagent_pattern_input_error( __( 'Jedes Pattern-Item muss ein Objekt sein.', 'wp-agent-companion' ) );
	}
	$allowed = array( 'title', 'text', 'subheading', 'badge', 'icon', 'label', 'price', 'role', 'featured', 'image_url', 'image_alt', 'cta_label', 'cta_url', 'button_label', 'button_url', 'values' );
	foreach ( array_keys( $item ) as $key ) {
		if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
			return wpagent_pattern_input_error( __( 'Ein Pattern-Item enthält ein unbekanntes Feld.', 'wp-agent-companion' ) );
		}
	}
	foreach ( array( 'title' => 500, 'text' => 4000, 'subheading' => 1000, 'badge' => 200, 'icon' => 64, 'label' => 500, 'price' => 200, 'role' => 500, 'image_alt' => 500, 'cta_label' => 200, 'button_label' => 200 ) as $field => $maximum ) {
		$valid = wpagent_pattern_validate_text( $item, $field, $maximum );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	foreach ( array( 'image_url' => true, 'cta_url' => false, 'button_url' => false ) as $field => $image ) {
		$valid = wpagent_pattern_validate_url( $item, $field, $image );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	if ( array_key_exists( 'featured', $item ) && ! is_bool( $item['featured'] ) ) {
		return wpagent_pattern_input_error( __( 'Das Pattern-Feld featured muss boolesch sein.', 'wp-agent-companion' ) );
	}
	if ( array_key_exists( 'values', $item ) ) {
		if ( ! is_array( $item['values'] ) || ! array_is_list( $item['values'] ) || count( $item['values'] ) > 12 ) {
			return wpagent_pattern_input_error( __( 'Pattern-Vergleichswerte müssen eine begrenzte Liste sein.', 'wp-agent-companion' ) );
		}
		foreach ( $item['values'] as $value ) {
			if ( ! is_string( $value ) || ( function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value ) ) > 2000 || str_contains( $value, "\0" ) ) {
				return wpagent_pattern_input_error( __( 'Ein Pattern-Vergleichswert ist ungültig oder zu lang.', 'wp-agent-companion' ) );
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_pattern_validate_input( $input ) {
	if ( ! is_array( $input ) || array_is_list( $input ) ) {
		return wpagent_pattern_input_error( __( 'Die Pattern-Eingabe muss ein Objekt sein.', 'wp-agent-companion' ) );
	}
	$allowed = array( 'title', 'slug', 'post_id', 'status', 'expected_state_hash', 'sections' );
	foreach ( array_keys( $input ) as $key ) {
		if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
			return wpagent_pattern_input_error( __( 'Die Pattern-Eingabe enthält ein unbekanntes Feld.', 'wp-agent-companion' ) );
		}
	}
	foreach ( array( 'title' => 200, 'slug' => 200 ) as $field => $maximum ) {
		$valid = wpagent_pattern_validate_text( $input, $field, $maximum, 'title' === $field );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	if ( array_key_exists( 'post_id', $input ) && ( ! is_int( $input['post_id'] ) || $input['post_id'] < 1 ) ) {
		return wpagent_pattern_input_error( __( 'post_id muss eine positive Ganzzahl sein.', 'wp-agent-companion' ) );
	}
	if ( array_key_exists( 'status', $input ) && ( ! is_string( $input['status'] ) || ! in_array( $input['status'], array( 'draft', 'publish', 'pending', 'private' ), true ) ) ) {
		return wpagent_pattern_input_error( __( 'Der Pattern-Status ist ungültig.', 'wp-agent-companion' ) );
	}
	if ( array_key_exists( 'expected_state_hash', $input ) && ( ! is_string( $input['expected_state_hash'] ) || 1 !== preg_match( '/^[a-f0-9]{64}$/D', $input['expected_state_hash'] ) ) ) {
		return wpagent_pattern_input_error( __( 'expected_state_hash muss ein vollständiger content_state_hash sein.', 'wp-agent-companion' ) );
	}
	if ( ! is_array( $input['sections'] ?? null ) || ! array_is_list( $input['sections'] ) || count( $input['sections'] ) < 1 || count( $input['sections'] ) > 50 ) {
		return wpagent_pattern_input_error( __( 'sections muss eine nicht leere, begrenzte Liste sein.', 'wp-agent-companion' ) );
	}
	$section_allowed = array( 'type', 'heading', 'text', 'subheading', 'backgroundColor', 'textColor', 'cta_label', 'cta_url', 'button_label', 'button_url', 'image_url', 'image_alt', 'dimRatio', 'mediaPosition', 'columns', 'items' );
	foreach ( $input['sections'] as $section ) {
		if ( ! is_array( $section ) || array_is_list( $section ) ) {
			return wpagent_pattern_input_error( __( 'Jede Pattern-Sektion muss ein Objekt sein.', 'wp-agent-companion' ) );
		}
		foreach ( array_keys( $section ) as $key ) {
			if ( ! is_string( $key ) || ! in_array( $key, $section_allowed, true ) ) {
				return wpagent_pattern_input_error( __( 'Eine Pattern-Sektion enthält ein unbekanntes Feld.', 'wp-agent-companion' ) );
			}
		}
		if ( ! is_string( $section['type'] ?? null ) || ! in_array( $section['type'], array( 'hero', 'features', 'media-text', 'logos', 'cta', 'pricing', 'testimonials', 'faq', 'stats', 'gallery', 'steps', 'comparison' ), true ) ) {
			return wpagent_pattern_input_error( __( 'Der Pattern-Sektionstyp ist ungültig.', 'wp-agent-companion' ) );
		}
		foreach ( array( 'heading' => 500, 'text' => 10000, 'subheading' => 4000, 'backgroundColor' => 64, 'textColor' => 64, 'cta_label' => 200, 'button_label' => 200, 'image_alt' => 500 ) as $field => $maximum ) {
			$valid = wpagent_pattern_validate_text( $section, $field, $maximum );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
		foreach ( array( 'image_url' => true, 'cta_url' => false, 'button_url' => false ) as $field => $image ) {
			$valid = wpagent_pattern_validate_url( $section, $field, $image );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
		if ( array_key_exists( 'dimRatio', $section ) && ( ! is_int( $section['dimRatio'] ) || $section['dimRatio'] < 0 || $section['dimRatio'] > 100 ) ) {
			return wpagent_pattern_input_error( __( 'dimRatio muss eine Ganzzahl zwischen 0 und 100 sein.', 'wp-agent-companion' ) );
		}
		if ( array_key_exists( 'mediaPosition', $section ) && ( ! is_string( $section['mediaPosition'] ) || ! in_array( $section['mediaPosition'], array( 'left', 'right' ), true ) ) ) {
			return wpagent_pattern_input_error( __( 'mediaPosition muss left oder right sein.', 'wp-agent-companion' ) );
		}
		if ( array_key_exists( 'columns', $section ) ) {
			if ( ! is_array( $section['columns'] ) || ! array_is_list( $section['columns'] ) || count( $section['columns'] ) > 12 ) {
				return wpagent_pattern_input_error( __( 'Pattern-Spalten müssen eine begrenzte Liste sein.', 'wp-agent-companion' ) );
			}
			foreach ( $section['columns'] as $column ) {
				if ( ! is_string( $column ) || ( function_exists( 'mb_strlen' ) ? mb_strlen( $column, 'UTF-8' ) : strlen( $column ) ) > 200 || str_contains( $column, "\0" ) ) {
					return wpagent_pattern_input_error( __( 'Eine Pattern-Spalte ist ungültig oder zu lang.', 'wp-agent-companion' ) );
				}
			}
		}
		if ( array_key_exists( 'items', $section ) ) {
			if ( ! is_array( $section['items'] ) || ! array_is_list( $section['items'] ) || count( $section['items'] ) > 50 ) {
				return wpagent_pattern_input_error( __( 'Pattern-Items müssen eine begrenzte Liste sein.', 'wp-agent-companion' ) );
			}
			foreach ( $section['items'] as $item ) {
				$valid = wpagent_pattern_validate_item( $item );
				if ( is_wp_error( $valid ) ) {
					return $valid;
				}
			}
		}
	}
	return true;
}

if ( ! defined( 'WPAGENT_PATTERN_POST_LOCK_TTL' ) ) {
	define( 'WPAGENT_PATTERN_POST_LOCK_TTL', 120 );
}

function wpagent_pattern_post_lock_option( int $post_id ): string {
	return 'wp_agent_pattern_post_lock_' . $post_id;
}

/** @return string|false */
function wpagent_pattern_post_acquire_lock( int $post_id ) {
	return wpagent_companion_acquire_option_lock( wpagent_pattern_post_lock_option( $post_id ), WPAGENT_PATTERN_POST_LOCK_TTL );
}

function wpagent_pattern_post_release_lock( int $post_id, string $token ): void {
	wpagent_companion_release_option_lock( wpagent_pattern_post_lock_option( $post_id ), $token );
}

/** @return true|WP_Error */
function wpagent_pattern_write_guard( int $post_id, string $token ) {
	if ( ! current_user_can( 'edit_post', $post_id ) || ! wpagent_companion_refresh_option_lock( wpagent_pattern_post_lock_option( $post_id ), $token, WPAGENT_PATTERN_POST_LOCK_TTL ) || ! wpagent_companion_option_lock_is_owned( wpagent_pattern_post_lock_option( $post_id ), $token ) ) {
		return new WP_Error( 'wpagent_pattern_post_lock_lost', __( 'Die exklusive Pattern-Sperre oder Berechtigung ging verloren. Der Schreibvorgang wurde nicht fortgesetzt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_pattern_update_post_row_cas( int $post_id, array $before, array $expected ) {
	global $wpdb;
	if ( ! isset( $before['post'] ) || ! is_array( $before['post'] ) ) {
		return new WP_Error( 'wpagent_pattern_post_state_unavailable', __( 'Der bisherige Seitenzustand konnte nicht sicher verglichen werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	$mutable = array( 'post_title', 'post_content', 'post_status', 'post_type', 'post_name', 'post_modified', 'post_modified_gmt' );
	$sets = array();
	$params = array();
	foreach ( $mutable as $field ) {
		if ( array_key_exists( $field, $expected ) && (string) ( $before['post'][ $field ] ?? '' ) !== (string) $expected[ $field ] ) {
			$sets[] = "`{$field}` = %s";
			$params[] = (string) $expected[ $field ];
		}
	}
	if ( ! $sets ) {
		return true;
	}
	$where = array( 'ID = %d' );
	$params[] = $post_id;
	foreach ( $before['post'] as $field => $value ) {
		// Core exposes the primary-key column as the uppercase `ID`; all other
		// selected post columns are lowercase snake case. Keep the identifier
		// validation closed while accepting that canonical core exception.
		if ( ! is_string( $field ) || ! preg_match( '/^[A-Za-z_]+$/', $field ) ) {
			return new WP_Error( 'wpagent_pattern_post_state_unavailable', __( 'Der bisherige Seitenzustand enthält ein ungültiges Speicherfeld.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
		}
		$where[] = "HEX(CAST(`{$field}` AS CHAR)) = HEX(CAST(%s AS CHAR))";
		$params[] = (string) $value;
	}
	$query = "UPDATE {$wpdb->posts} SET " . implode( ', ', $sets ) . ' WHERE ' . implode( ' AND ', $where );
	$written = $wpdb->query( $wpdb->prepare( $query, ...$params ) );
	if ( false === $written ) {
		return new WP_Error( 'wpagent_pattern_post_write_failed', __( 'Die Seite konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	if ( 1 !== $written ) {
		return new WP_Error( 'wpagent_pattern_post_conflict', __( 'Die Seite wurde zwischen Lesen und Speichern verändert. Lies content_state_hash neu.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	clean_post_cache( $post_id );
	wp_cache_delete( $post_id, 'post_meta' );
	return true;
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_upsert_pattern( $input ) {
	$valid_input = wpagent_pattern_validate_input( $input );
	if ( is_wp_error( $valid_input ) ) {
		return $valid_input;
	}
	$title    = trim( $input['title'] );
	$sections = $input['sections'];

	// Idempotente Zuordnung und Berechtigungen werden vor der vergleichsweise
	// teuren Block-Erzeugung geprüft. post_id darf niemals einen Beitrag still
	// durch Setzen von post_type=page in eine Seite umwandeln.
	$post_id = $input['post_id'] ?? 0;
	$slug    = isset( $input['slug'] ) ? sanitize_title( $input['slug'] ) : '';
	// A slug lookup can otherwise reveal whether a foreign page exists: a
	// caller allowed to create pages but not edit others would receive a
	// different result for a free versus occupied slug. Resolve it only for a
	// caller allowed to edit arbitrary pages. A concrete ID uses WordPress'
	// object-specific meta capability before loading that page.
	if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für diese Seite.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( $post_id <= 0 && '' !== $slug && ! current_user_can( wpagent_post_type_cap( 'page', 'edit_others_posts', 'edit_others_pages' ) ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, vorhandene Seiten per Slug zuzuordnen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( $post_id <= 0 && '' !== $slug ) {
		$existing_by_slug = get_page_by_path( $slug, OBJECT, 'page' );
		if ( $existing_by_slug instanceof WP_Post ) {
			$post_id = (int) $existing_by_slug->ID;
		}
	}

	$existing_post = null;
	if ( $post_id > 0 ) {
		$existing_post = get_post( $post_id );
		if ( ! $existing_post ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für diese Seite.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( 'page' !== $existing_post->post_type ) {
			return new WP_Error( 'wpagent_not_page', __( 'upsert-pattern darf ausschließlich bestehende Seiten aktualisieren.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( '' !== $slug && ! hash_equals( $existing_post->post_name, $slug ) ) {
			return new WP_Error( 'wpagent_pattern_slug_change_unavailable', __( 'Das Ändern eines Seiten-Slugs ist über Pattern-Updates nicht verfügbar, weil WordPress dafür keinen mehrzeiligen bedingten Schreibvorgang anbietet.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! is_string( $input['expected_state_hash'] ?? null ) ) {
			return new WP_Error( 'wpagent_expected_state_hash_required', __( 'Updates benötigen einen vollständigen content_state_hash aus einem frischen get-post-Read.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	} elseif ( ! current_user_can( wpagent_post_type_create_cap( 'page' ) ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, Seiten anzulegen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! current_user_can( wpagent_post_type_edit_cap( 'page' ) ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, Seiten zu bearbeiten.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}

	$status = $input['status'] ?? 'draft';
	$status_cap = wpagent_check_post_status_cap( 'page', $status );
	if ( is_wp_error( $status_cap ) ) {
		return $status_cap;
	}

	// Sektionsliste in valides Core-Block-Markup übersetzen. Jede Sektion liefert
	// genau einen vollbreiten Top-Level-core/group.
	$parts            = array();
	$features_index   = 0; // Für den abwechselnden Hintergrund der Feature-Bänder.
	$media_text_index = 0; // Für die abwechselnde Bildseite der media-text-Sektionen.
	foreach ( $sections as $index => $section ) {
		if ( ! is_array( $section ) ) {
			/* translators: %d: zero-based section index. */
			return new WP_Error( 'wpagent_bad_section', sprintf( __( 'Sektion %d ist kein Objekt.', 'wp-agent-companion' ), (int) $index ), array( 'status' => 400 ) );
		}
		$type = (string) ( $section['type'] ?? '' );
		switch ( $type ) {
			case 'hero':
				$parts[] = wpagent_pattern_build_hero( $section );
				break;
			case 'features':
				$parts[] = wpagent_pattern_build_features( $section, $features_index );
				$features_index++;
				break;
			case 'media-text':
				$parts[] = wpagent_pattern_build_media_text( $section, $media_text_index );
				$media_text_index++;
				break;
			case 'logos':
				$parts[] = wpagent_pattern_build_logos( $section );
				break;
			case 'cta':
				$parts[] = wpagent_pattern_build_cta( $section );
				break;
			case 'pricing':
				$parts[] = wpagent_pattern_build_pricing( $section );
				break;
			case 'testimonials':
				$parts[] = wpagent_pattern_build_testimonials( $section );
				break;
			case 'faq':
				$parts[] = wpagent_pattern_build_faq( $section );
				break;
			case 'stats':
				$parts[] = wpagent_pattern_build_stats( $section );
				break;
			case 'gallery':
				$parts[] = wpagent_pattern_build_gallery( $section );
				break;
			case 'steps':
				$parts[] = wpagent_pattern_build_steps( $section );
				break;
			case 'comparison':
				$parts[] = wpagent_pattern_build_comparison( $section );
				break;
			default:
				return new WP_Error(
					'wpagent_bad_section_type',
					/* translators: %s: submitted landing-page section type. */
					sprintf( __( 'Unbekannter Sektionstyp "%s" (erlaubt: hero, features, media-text, logos, cta, pricing, testimonials, faq, stats, gallery, steps, comparison).', 'wp-agent-companion' ), $type ),
					array( 'status' => 400 )
				);
		}
	}

	$content = implode( "\n\n", array_filter( $parts, static fn( $p ) => '' !== $p ) );
	if ( strlen( $content ) > 500000 ) {
		return wpagent_pattern_input_error( __( 'Das erzeugte Pattern-Markup überschreitet die sichere Umfangsgrenze.', 'wp-agent-companion' ) );
	}

	// Selbst-Verifikation VOR dem Speichern: nur die explizite Core-Allowlist.
	$report    = wpagent_block_report( $content );
	$validated = wpagent_validate_generated_core_blocks( $content );
	if ( is_wp_error( $validated ) ) {
		return $validated;
	}

	$postarr = array(
		'post_title'   => $title,
		'post_content' => $content,
		'post_status'  => $status,
		'post_type'    => 'page',
	);
	if ( '' !== $slug ) {
		$postarr['post_name'] = $slug;
	}

	$expected = array(
		'post_title'   => $title,
		'post_content' => $content,
		'post_status'  => $status,
		'post_type'    => 'page',
	);
	if ( '' !== $slug ) {
		$expected['post_name'] = $slug;
	}
	$updated   = $existing_post instanceof WP_Post;
	$lock      = '';
	$candidate = null;
	try {
		if ( $updated ) {
			$lock = wpagent_pattern_post_acquire_lock( $post_id );
			if ( ! is_string( $lock ) || '' === $lock ) {
				return new WP_Error( 'wpagent_pattern_post_busy', __( 'Die Seite wird gerade durch einen anderen Pattern-Schreibvorgang bearbeitet.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			$guard = wpagent_pattern_write_guard( $post_id, $lock );
			if ( is_wp_error( $guard ) ) {
				return $guard;
			}
			$before = wpagent_markdown_post_full_state( $post_id );
			if ( is_wp_error( $before ) ) {
				return $before;
			}
			if ( ! hash_equals( $input['expected_state_hash'], wpagent_markdown_post_state_hash( $before ) ) ) {
				return new WP_Error( 'wpagent_pattern_post_conflict', __( 'Die Seite wurde seit dem Read verändert. Lies content_state_hash neu.', 'wp-agent-companion' ), array( 'status' => 409, 'current_content_state_hash' => wpagent_markdown_post_state_hash( $before ) ) );
			}
			$current = get_post( $post_id );
			if ( ! $current instanceof WP_Post || 'page' !== $current->post_type || ! current_user_can( 'edit_post', $post_id ) ) {
				return new WP_Error( 'wpagent_forbidden', __( 'Die Seite oder Berechtigung änderte sich während der Schreibvorbereitung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
			}
			$status_cap = wpagent_check_post_status_cap( 'page', $status );
			if ( is_wp_error( $status_cap ) ) {
				return $status_cap;
			}
			// The row-CAS deliberately avoids wp_update_post(), but WordPress
			// callers still need a truthful modification timestamp. Bind both
			// values into the same conditional row write and subsequent readback.
			$expected['post_modified']     = current_time( 'mysql' );
			$expected['post_modified_gmt'] = current_time( 'mysql', true );
			$guard = wpagent_pattern_write_guard( $post_id, $lock );
			if ( is_wp_error( $guard ) ) {
				return $guard;
			}
			$prewrite = wpagent_markdown_post_full_state( $post_id );
			if ( is_wp_error( $prewrite ) || ! hash_equals( $input['expected_state_hash'], wpagent_markdown_post_state_hash( $prewrite ) ) ) {
				return new WP_Error( 'wpagent_pattern_post_conflict', __( 'Die Seite wurde vor dem Speichern verändert. Lies content_state_hash neu.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			do_action( 'wpagent_pattern_before_post_row_cas', array( 'post_id' => $post_id, 'content_state_hash' => wpagent_markdown_post_state_hash( $prewrite ) ) );
			$cas = wpagent_pattern_update_post_row_cas( $post_id, $prewrite, $expected );
			if ( is_wp_error( $cas ) ) {
				return 'wpagent_pattern_post_conflict' === $cas->get_error_code() ? $cas : wpagent_markdown_post_recovery_required( $cas, $post_id, 'pattern_post_row_cas_failed' );
			}
			$candidate = wpagent_markdown_post_full_state( $post_id );
			if ( is_wp_error( $candidate ) || ! wpagent_markdown_post_expected_state_matches( $candidate, $expected ) || ! wpagent_markdown_post_unmanaged_state_matches( $prewrite, $candidate ) || is_wp_error( wpagent_pattern_write_guard( $post_id, $lock ) ) ) {
				return wpagent_markdown_post_recovery_required( new WP_Error( 'wpagent_post_write_unverified', __( 'Die Pattern-Seite konnte nicht exakt zurückgelesen werden.', 'wp-agent-companion' ), array( 'status' => 502 ) ), $post_id, 'pattern_update_readback_unverified' );
			}
		} else {
			$res = wp_insert_post( wp_slash( $postarr ), true );
			if ( is_wp_error( $res ) ) {
				return $res;
			}
			$post_id = (int) $res;
			$lock = wpagent_pattern_post_acquire_lock( $post_id );
			if ( ! is_string( $lock ) || '' === $lock ) {
				return wpagent_markdown_post_recovery_required( new WP_Error( 'wpagent_pattern_post_busy', __( 'Die exklusive Sperre für die neue Seite konnte nicht bezogen werden.', 'wp-agent-companion' ), array( 'status' => 409 ) ), $post_id, 'pattern_created_post_lock_unavailable' );
			}
			$guard = wpagent_pattern_write_guard( $post_id, $lock );
			$candidate = wpagent_markdown_post_full_state( $post_id );
			if ( is_wp_error( $guard ) || is_wp_error( $candidate ) || ! wpagent_markdown_post_expected_state_matches( $candidate, $expected ) ) {
				return wpagent_content_write_recovery_required( new WP_Error( 'wpagent_post_write_unverified', __( 'Die neue Pattern-Seite konnte nicht exakt zurückgelesen werden.', 'wp-agent-companion' ), array( 'status' => 502 ) ), $post_id );
			}
		}
	} finally {
		if ( '' !== $lock ) {
			wpagent_pattern_post_release_lock( $post_id, $lock );
		}
	}

	return array(
		'post_id'        => $post_id,
		'url'            => get_permalink( $post_id ),
		'status'         => get_post_status( $post_id ),
		'content_hash'   => hash( 'sha256', $content ),
		'content_state_hash' => wpagent_markdown_post_state_hash( $candidate ),
		'block_count'    => $report['block_count'],
		'freeform_count' => $report['freeform_count'],
		'block_types'    => $report['block_types'],
		'updated'        => $updated,
	);
}
