<?php
/**
 * Ability: wp-agent/create-post-from-markdown
 *
 * The only content write path. Converts Markdown to valid Gutenberg blocks
 * server-side and creates or updates a post, returning a block-validation
 * report so the pipeline can gate on freeform fallback.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_create_post_from_markdown(): array {
	return array(
		'label'               => __( 'Post aus Markdown erstellen', 'wp-agent-companion' ),
		'description'         => __( 'Konvertiert Markdown serverseitig in valide Gutenberg-Blöcke und legt einen neuen Beitrag an. Das ferngesteuerte Aktualisieren bestehender Beiträge ist nicht verfügbar, weil WordPress keinen atomaren Core-Compare-and-Set-Schreibvorgang mit vollständigen Prewrite-Hooks bietet. Verhindert den stillen core/freeform-Fallback.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'properties' => array(
				'markdown' => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 500000,
					'description' => __( 'Artikeltext in Markdown.', 'wp-agent-companion' ),
				),
				'title'    => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 200,
					'description' => __( 'Beitragstitel.', 'wp-agent-companion' ),
				),
				'status'   => array(
					'type'    => 'string',
					'enum'    => array( 'draft', 'publish', 'pending', 'private' ),
					'default' => 'draft',
				),
				'slug'     => array( 'type' => 'string', 'maxLength' => 200 ),
				'excerpt'  => array( 'type' => 'string', 'maxLength' => 10000 ),
				'category' => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 191,
					'description' => __( 'Kategoriename; wird bei Bedarf angelegt und exklusiv gesetzt.', 'wp-agent-companion' ),
				),
				'post_id'  => array(
					'type'        => 'integer',
					'minimum'     => 1,
					'description' => __( 'Kennzeichnet einen Update-Versuch. Remote-Updates sind nicht verfügbar und enden ohne Datenmutation mit manual_only.', 'wp-agent-companion' ),
				),
				'post_type' => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 20,
					'description' => __( 'Zieltyp für neue Inhalte; muss ein registrierter, öffentlicher Post-Type sein (z. B. post, page).', 'wp-agent-companion' ),
				),
				'expected_state_hash' => array(
					'type'        => 'string',
					'pattern'     => '^[a-f0-9]{64}$',
					'description' => __( 'Vollständiger content_state_hash eines frischen get-post-Reads. Der Wert ist für externe Update-Planung reserviert; Remote-Updates sind nicht verfügbar.', 'wp-agent-companion' ),
				),
			),
			'required'   => array( 'markdown', 'title' ),
		),
		'output_schema'       => array(
			'type'                 => 'object',
			'additionalProperties' => false,
			'properties' => array(
				'post_id'        => array( 'type' => 'integer' ),
				'url'            => array( 'type' => 'string' ),
				'status'         => array( 'type' => 'string' ),
				'content_hash'   => array( 'type' => 'string' ),
				'content_state_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'block_count'    => array( 'type' => 'integer' ),
				'freeform_count' => array( 'type' => 'integer' ),
				'block_types'    => array(
					'type'  => 'array',
					'items' => array( 'type' => 'string' ),
				),
				'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_create_post_from_markdown',
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
				'remote_create'      => true,
				'manual_only_update' => true,
			),
		),
	);
}

/**
 * Capture the fields a content write can change, including dates WordPress may
 * alter as a side effect of a status transition.
 *
 * @param WP_Post $post
 * @return array<string,int|string>
 */
function wpagent_content_write_snapshot( WP_Post $post ): array {
	return array(
		'post_title'    => $post->post_title,
		'post_content'  => $post->post_content,
		'post_excerpt'  => $post->post_excerpt,
		'post_status'   => $post->post_status,
		'post_name'     => $post->post_name,
		'post_type'     => $post->post_type,
		'post_date'     => $post->post_date,
		'post_date_gmt' => $post->post_date_gmt,
	);
}

/**
 * @param WP_Post                  $post
 * @param array<string,int|string> $snapshot
 */
function wpagent_content_write_snapshot_matches( WP_Post $post, array $snapshot ): bool {
	foreach ( $snapshot as $field => $expected ) {
		if ( ! property_exists( $post, $field ) || (string) $post->{$field} !== (string) $expected ) {
			return false;
		}
	}
	return true;
}

/**
 * Restore an updated post or permanently remove a post created by the failed
 * operation. The latter deliberately also covers already-published partials.
 *
 * @param int                           $post_id
 * @param array<string,int|string>|null $snapshot Null means the post was new.
 */
function wpagent_compensate_content_write( int $post_id, ?array $snapshot ): bool {
	if ( null === $snapshot ) {
		return null === get_post( $post_id );
	}
	$current = get_post( $post_id );
	return $current instanceof WP_Post && wpagent_content_write_snapshot_matches( $current, $snapshot );
}

/** @return WP_Error */
function wpagent_content_write_recovery_required( WP_Error $failure, int $post_id ): WP_Error {
	$data = $failure->get_error_data();
	$data = is_array( $data ) ? $data : array();
	return new WP_Error(
		'wpagent_post_recovery_required',
		__( 'Der Inhaltsschreibvorgang wurde nicht vollständig bestätigt. Es wurde kein automatisches Rollback ausgeführt. Prüfe den aktuellen Beitrag manuell und wiederhole den Vorgang nicht automatisch.', 'wp-agent-companion' ),
		array_merge(
			$data,
			array(
				'status'             => 500,
				'post_id'            => $post_id,
				'rolled_back'        => false,
				'rollback_verified'  => false,
				'recovery_required'  => true,
				'original_error'     => $failure->get_error_code(),
			)
		)
	);
}

/**
 * @param WP_Error                      $failure
 * @param int                           $post_id
 * @param array<string,int|string>|null $snapshot
 * @return WP_Error
 */
function wpagent_content_write_failure_after_compensation( WP_Error $failure, int $post_id, ?array $snapshot ): WP_Error {
	if ( wpagent_compensate_content_write( $post_id, $snapshot ) ) {
		$data = $failure->get_error_data();
		$data = is_array( $data ) ? $data : array();
		$data['rollback_verified'] = true;
		$failure->add_data( $data );
		return $failure;
	}

	return wpagent_content_write_recovery_required( $failure, $post_id );
}

/**
 * @param WP_Post              $post
 * @param array<string,mixed>  $expected
 */
function wpagent_content_write_matches( WP_Post $post, array $expected ): bool {
	foreach ( $expected as $field => $value ) {
		if ( ! property_exists( $post, $field ) || (string) $post->{$field} !== (string) $value ) {
			return false;
		}
	}
	return true;
}

/**
 * Return only field names and hashes, never persisted content itself.
 *
 * @param WP_Post             $post
 * @param array<string,mixed> $expected
 * @return string[]
 */
function wpagent_content_write_mismatches( WP_Post $post, array $expected ): array {
	$mismatches = array();
	foreach ( $expected as $field => $value ) {
		if ( ! property_exists( $post, $field ) || (string) $post->{$field} !== (string) $value ) {
			$mismatches[] = $field;
		}
	}
	return $mismatches;
}

/**
 * @param WP_Error                      $failure
 * @param int                           $post_id
 * @param array<string,int|string>|null $snapshot
 * @param int[]|null                    $category_snapshot Null when no category write was requested.
 */
function wpagent_markdown_failure_after_compensation( WP_Error $failure, int $post_id, ?array $snapshot, ?array $category_snapshot, int $created_category_id ): WP_Error {
	return wpagent_content_write_recovery_required( $failure, $post_id );
}

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

function wpagent_markdown_post_lock_option( int $post_id ): string {
	return 'wp_agent_markdown_post_lock_' . $post_id;
}

/** @return string|false */
function wpagent_markdown_post_acquire_lock( int $post_id ) {
	return wpagent_companion_acquire_option_lock( wpagent_markdown_post_lock_option( $post_id ), WPAGENT_MARKDOWN_POST_LOCK_TTL );
}

function wpagent_markdown_post_release_lock( int $post_id, string $token ): void {
	wpagent_companion_release_option_lock( wpagent_markdown_post_lock_option( $post_id ), $token );
}

/** @return true|WP_Error */
function wpagent_markdown_post_guard( int $post_id, string $token, bool $deleting = false ) {
	$capability = $deleting ? 'delete_post' : 'edit_post';
	if ( ! current_user_can( $capability, $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Die Berechtigung für diesen Beitrag ist nicht mehr vorhanden.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$option = wpagent_markdown_post_lock_option( $post_id );
	if ( ! wpagent_companion_refresh_option_lock( $option, $token, WPAGENT_MARKDOWN_POST_LOCK_TTL ) || ! wpagent_companion_option_lock_is_owned( $option, $token ) ) {
		return new WP_Error( 'wpagent_markdown_post_lock_lost', __( 'Die exklusive Beitragssperre ging verloren. Der Schreibvorgang wurde nicht fortgesetzt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return true;
}

/**
 * The full Markdown-write state is intentionally a direct, deterministic
 * database snapshot. It binds all core columns plus every meta and term row,
 * while returning only a hash to the caller.
 *
 * @return array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>}|WP_Error
 */
function wpagent_markdown_post_full_state( int $post_id ) {
	global $wpdb;
	$post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ), ARRAY_A );
	if ( ! is_array( $post ) ) {
		return new WP_Error( 'wpagent_no_post', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	ksort( $post, SORT_STRING );
	$meta = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT meta_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d ORDER BY meta_id ASC",
			$post_id
		),
		ARRAY_A
	);
	$terms = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT tr.term_taxonomy_id, tr.term_order, tt.taxonomy, tt.term_id FROM {$wpdb->term_relationships} tr INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = %d ORDER BY tt.taxonomy ASC, tr.term_taxonomy_id ASC, tr.term_order ASC",
			$post_id
		),
		ARRAY_A
	);
	if ( ! is_array( $meta ) || ! is_array( $terms ) ) {
		return new WP_Error( 'wpagent_markdown_post_state_unavailable', __( 'Der vollständige Beitragszustand konnte nicht gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return array(
		'post'  => $post,
		'meta'  => array_values( $meta ),
		'terms' => array_values( $terms ),
	);
}

/** @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $state */
function wpagent_markdown_post_state_hash( array $state ): string {
	return hash( 'sha256', "wpagently-markdown-post-state-v1\0" . serialize( $state ) );
}

/** @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $state @param array<string,mixed> $expected */
function wpagent_markdown_post_expected_state_matches( array $state, array $expected ): bool {
	foreach ( $expected as $field => $value ) {
		if ( ! array_key_exists( $field, $state['post'] ) || (string) $state['post'][ $field ] !== (string) $value ) {
			return false;
		}
	}
	return true;
}

/** @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $before @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $after */
function wpagent_markdown_post_unmanaged_state_matches( array $before, array $after ): bool {
	return $before['meta'] === $after['meta'] && $before['terms'] === $after['terms'];
}

/** @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $state @return int[] */
function wpagent_markdown_post_category_ids( array $state ): array {
	$ids = array();
	foreach ( $state['terms'] as $term ) {
		if ( 'category' === (string) ( $term['taxonomy'] ?? '' ) ) {
			$ids[] = (int) $term['term_id'];
		}
	}
	sort( $ids, SORT_NUMERIC );
	return $ids;
}

/** @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $before @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $after */
function wpagent_markdown_post_category_write_matches( array $before, array $after, int $term_id ): bool {
	if ( $before['post'] !== $after['post'] || $before['meta'] !== $after['meta'] || wpagent_markdown_post_category_ids( $after ) !== array( $term_id ) ) {
		return false;
	}
	$before_other = array_values( array_filter( $before['terms'], static fn( array $term ): bool => 'category' !== (string) ( $term['taxonomy'] ?? '' ) ) );
	$after_other  = array_values( array_filter( $after['terms'], static fn( array $term ): bool => 'category' !== (string) ( $term['taxonomy'] ?? '' ) ) );
	return $before_other === $after_other;
}

/**
 * @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>}|null $attempt_state
 * @return WP_Error
 */
function wpagent_markdown_post_recovery_required( WP_Error $failure, int $post_id, string $reason, ?array $attempt_state = null ) {
	$current = wpagent_markdown_post_full_state( $post_id );
	$data    = $failure->get_error_data();
	$data    = is_array( $data ) ? $data : array();
	$data    = array_merge(
		$data,
		array(
			'status'            => 500,
			'post_id'           => $post_id,
			'rolled_back'       => false,
			'rollback_verified' => false,
			'recovery_required' => true,
			'original_error'    => $failure->get_error_code(),
			'reason'            => $reason,
		)
	);
	if ( is_array( $current ) ) {
		$data['current_content_state_hash'] = wpagent_markdown_post_state_hash( $current );
	}
	if ( is_array( $attempt_state ) ) {
		$data['attempt_content_state_hash'] = wpagent_markdown_post_state_hash( $attempt_state );
	}
	return new WP_Error(
		'wpagent_markdown_post_recovery_required',
		__( 'Der Markdown-Beitrag wurde nicht vollständig verifiziert. Es wurde kein automatisches Rollback ausgeführt. Prüfe den aktuellen Zustand manuell und wiederhole den Vorgang nicht automatisch.', 'wp-agent-companion' ),
		$data
	);
}

/**
 * Fail closed for created-post compensation while retaining the immutable,
 * by-value candidate snapshot for the failure report.
 *
 * WordPress has no atomic conditional permanent deletion covering the post
 * row, metadata and term relations. Even a fresh comparison protected by the
 * Companion lock leaves a native-editor race before deletion, so this helper
 * intentionally never mutates the candidate. The caller reports manual
 * recovery instead.
 *
 * @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>} $candidate
 */
function wpagent_markdown_compensate_created_post( int $post_id, array $candidate, string $token ): bool {
	// A read followed by permanent deletion could still erase a concurrent
	// native-editor change. Uncertain creates remain available for explicit
	// manual recovery instead of being removed remotely.
	return false;
}

/**
 * @param array{post:array<string,string>,meta:array<int,array<string,string>>,terms:array<int,array<string,string>>}|null $candidate
 * @return WP_Error
 */
function wpagent_markdown_failure_after_write( WP_Error $failure, int $post_id, ?array $candidate, string $reason ): WP_Error {
	// A created post may have reached any Core or provider hook before a later
	// check fails. No remote compare-and-delete primitive can prove that a
	// cleanup still belongs to this request, so recovery is the only safe path.
	return wpagent_markdown_post_recovery_required( $failure, $post_id, $reason, $candidate );
}

/** @return WP_Error */
function wpagent_markdown_post_update_manual_only(): WP_Error {
	return new WP_Error(
		'wpagent_markdown_post_update_manual_only',
		__( 'Das ferngesteuerte Aktualisieren bestehender Markdown-Beiträge ist nicht verfügbar. WordPress bietet keinen atomaren Core-Compare-and-Set-Schreibvorgang, der zugleich alle Prewrite-Filter und Seiteneffekte bewahrt. Ändere den Beitrag lokal in einer beaufsichtigten Administration und lies ihn anschließend neu.', 'wp-agent-companion' ),
		array( 'status' => 409, 'manual_only' => true, 'recovery_required' => false )
	);
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_create_post_from_markdown( $input ) {
	if ( ! is_array( $input ) ) {
		return new WP_Error( 'wpagent_invalid_markdown_input', __( 'Die Markdown-Eingabe muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$allowed = array( 'markdown', 'title', 'status', 'slug', 'excerpt', 'category', 'post_id', 'post_type', 'expected_state_hash' );
	foreach ( array_keys( $input ) as $key ) {
		if ( ! in_array( $key, $allowed, true ) ) {
			return new WP_Error( 'wpagent_invalid_markdown_input', __( 'Die Markdown-Eingabe enthält ein unbekanntes Feld.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	$length = static function ( string $value ): int {
		return function_exists( 'mb_strlen' ) ? (int) mb_strlen( $value, 'UTF-8' ) : strlen( $value );
	};
	foreach ( array( 'markdown' => 500000, 'title' => 200, 'slug' => 200, 'excerpt' => 10000, 'category' => 191, 'post_type' => 20 ) as $field => $maximum ) {
		if ( array_key_exists( $field, $input ) && ! is_string( $input[ $field ] ) ) {
			return new WP_Error( 'wpagent_invalid_markdown_input', __( 'Ein Markdown-Feld hat einen ungültigen Typ.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( is_string( $input[ $field ] ?? null ) && ( $length( $input[ $field ] ) > $maximum || str_contains( $input[ $field ], "\0" ) ) ) {
			return new WP_Error( 'wpagent_invalid_markdown_input', __( 'Ein Markdown-Feld ist zu lang oder enthält ein ungültiges Steuerzeichen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	if ( ! is_string( $input['markdown'] ?? null ) || '' === $input['markdown'] || ! is_string( $input['title'] ?? null ) || '' === trim( $input['title'] ) ) {
		return new WP_Error( 'wpagent_invalid_markdown_input', __( 'Markdown und Titel müssen nicht leer sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_key_exists( 'post_id', $input ) && ( ! is_int( $input['post_id'] ) || $input['post_id'] < 1 ) ) {
		return new WP_Error( 'wpagent_invalid_markdown_input', __( 'post_id muss eine positive Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}

	$markdown = $input['markdown'];
	$title    = trim( $input['title'] );
	$post_id  = (int) ( $input['post_id'] ?? 0 );
	if ( $post_id > 0 ) {
		return wpagent_markdown_post_update_manual_only();
	}
	$status   = (string) ( $input['status'] ?? 'draft' );
	if ( ! in_array( $status, array( 'draft', 'publish', 'pending', 'private' ), true ) ) {
		return new WP_Error( 'wpagent_invalid_markdown_input', __( 'Der Beitragsstatus ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$post_type_provided = array_key_exists( 'post_type', $input ) && '' !== trim( (string) $input['post_type'] );
	$post_type          = $post_type_provided ? (string) $input['post_type'] : 'post';
	$type_valid = wpagent_validate_post_type( $post_type );
	if ( is_wp_error( $type_valid ) ) {
		return $type_valid;
	}
	if ( ! current_user_can( wpagent_post_type_create_cap( $post_type ) ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, Inhalte für diesen Post-Type zu erstellen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! current_user_can( wpagent_post_type_edit_cap( $post_type ) ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für diesen Post-Type.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$status_cap = wpagent_check_post_status_cap( $post_type, $status );
	if ( is_wp_error( $status_cap ) ) {
		return $status_cap;
	}

	$category_requested = array_key_exists( 'category', $input );
	$category_name      = $category_requested ? trim( (string) $input['category'] ) : '';
	$term_id            = 0;
	$category_assign_cap = 'do_not_allow';
	$category_manage_cap = 'do_not_allow';
	if ( $category_requested ) {
		if ( '' === $category_name || ! is_object_in_taxonomy( $post_type, 'category' ) ) {
			return new WP_Error( 'wpagent_category_not_supported', __( 'Der gewählte Post-Type unterstützt keine Kategorien.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$category            = get_taxonomy( 'category' );
		$category_assign_cap = ( $category && ! empty( $category->cap->assign_terms ) ) ? (string) $category->cap->assign_terms : 'do_not_allow';
		$category_manage_cap = ( $category && ! empty( $category->cap->manage_terms ) ) ? (string) $category->cap->manage_terms : 'do_not_allow';
		if ( ! current_user_can( $category_assign_cap ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, Kategorien zuzuordnen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$term = get_term_by( 'name', $category_name, 'category' );
		if ( $term instanceof WP_Term ) {
			$term_id = (int) $term->term_id;
		} elseif ( ! current_user_can( $category_manage_cap ) ) {
			return new WP_Error( 'wpagent_forbidden_manage_terms', __( 'Die Kategorie existiert noch nicht. Zum automatischen Anlegen ist die manage_terms-Berechtigung dieser Taxonomie erforderlich.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
	}

	$converter = new WPAgent_Markdown_To_Blocks();
	$content   = $converter->convert( $markdown );
	$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'    => $post_type,
	);
	if ( '' !== (string) ( $input['slug'] ?? '' ) ) {
		$postarr['post_name'] = sanitize_title( (string) $input['slug'] );
	}
	if ( array_key_exists( 'excerpt', $input ) ) {
		$postarr['post_excerpt'] = (string) $input['excerpt'];
	}
	$expected = array(
		'post_title'   => $title,
		'post_content' => $content,
		'post_status'  => $status,
		'post_type'    => $post_type,
	);
	if ( isset( $postarr['post_name'] ) ) {
		$expected['post_name'] = $postarr['post_name'];
	}
	if ( array_key_exists( 'post_excerpt', $postarr ) ) {
		$expected['post_excerpt'] = $postarr['post_excerpt'];
	}

	$lock      = '';
	$candidate = null;
	try {
		$result = wp_insert_post( wp_slash( $postarr ), true );

		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$post_id = (int) $result;
		$lock = wpagent_markdown_post_acquire_lock( $post_id );
		if ( ! is_string( $lock ) || '' === $lock ) {
			return wpagent_markdown_post_recovery_required( new WP_Error( 'wpagent_markdown_post_lock_busy', __( 'Die exklusive Sperre für den neuen Beitrag konnte nicht bezogen werden.', 'wp-agent-companion' ), array( 'status' => 409 ) ), $post_id, 'created_post_lock_unavailable' );
		}
		$guard = wpagent_markdown_post_guard( $post_id, $lock );
		if ( is_wp_error( $guard ) ) {
			return wpagent_markdown_failure_after_write( $guard, $post_id, $candidate, 'lock_lost_after_write' );
		}
		$candidate = wpagent_markdown_post_full_state( $post_id );
		if ( is_wp_error( $candidate ) || ! wpagent_markdown_post_expected_state_matches( $candidate, $expected ) ) {
			return wpagent_markdown_failure_after_write( new WP_Error( 'wpagent_post_write_unverified', __( 'WordPress hat den angeforderten Beitragsinhalt nicht exakt gespeichert.', 'wp-agent-companion' ), array( 'status' => 502 ) ), $post_id, is_array( $candidate ) ? $candidate : null, 'post_readback_unverified' );
		}

		if ( $category_requested && $term_id <= 0 ) {
			$guard = wpagent_markdown_post_guard( $post_id, $lock );
			if ( is_wp_error( $guard ) || ! current_user_can( $category_manage_cap ) || ! current_user_can( $category_assign_cap ) ) {
				return wpagent_markdown_failure_after_write( is_wp_error( $guard ) ? $guard : new WP_Error( 'wpagent_forbidden_manage_terms', __( 'Die Berechtigung zum Anlegen oder Zuordnen der Kategorie ist nicht mehr vorhanden.', 'wp-agent-companion' ), array( 'status' => 403 ) ), $post_id, $candidate, 'category_capability_or_lock_lost' );
			}
			$pre_category = wpagent_markdown_post_full_state( $post_id );
			if ( is_wp_error( $pre_category ) || ! hash_equals( wpagent_markdown_post_state_hash( $candidate ), wpagent_markdown_post_state_hash( $pre_category ) ) ) {
				return wpagent_markdown_failure_after_write( new WP_Error( 'wpagent_markdown_post_conflict', __( 'Der Beitrag wurde vor der Kategoriezuordnung verändert.', 'wp-agent-companion' ), array( 'status' => 409 ) ), $post_id, $candidate, 'drift_before_category_create' );
			}
			$new = wp_insert_term( $category_name, 'category' );
			if ( is_wp_error( $new ) ) {
				$existing_id = 'term_exists' === $new->get_error_code() ? (int) $new->get_error_data() : 0;
				if ( $existing_id <= 0 ) {
					return wpagent_markdown_failure_after_write( $new, $post_id, $candidate, 'category_create_failed' );
				}
				$term_id = $existing_id;
			} else {
				$term_id = (int) $new['term_id'];
			}
		}

		if ( $category_requested ) {
			$guard = wpagent_markdown_post_guard( $post_id, $lock );
			if ( is_wp_error( $guard ) || ! current_user_can( $category_assign_cap ) ) {
				return wpagent_markdown_failure_after_write( is_wp_error( $guard ) ? $guard : new WP_Error( 'wpagent_forbidden', __( 'Die Berechtigung zum Zuordnen der Kategorie ist nicht mehr vorhanden.', 'wp-agent-companion' ), array( 'status' => 403 ) ), $post_id, $candidate, 'category_assign_capability_or_lock_lost' );
			}
			$pre_assignment = wpagent_markdown_post_full_state( $post_id );
			if ( is_wp_error( $pre_assignment ) || ! hash_equals( wpagent_markdown_post_state_hash( $candidate ), wpagent_markdown_post_state_hash( $pre_assignment ) ) ) {
				return wpagent_markdown_failure_after_write( new WP_Error( 'wpagent_markdown_post_conflict', __( 'Der Beitrag wurde vor der Kategoriezuordnung verändert.', 'wp-agent-companion' ), array( 'status' => 409 ) ), $post_id, $candidate, 'drift_before_category_assignment' );
			}
			$assigned = wp_set_post_terms( $post_id, array( $term_id ), 'category', false );
			if ( is_wp_error( $assigned ) ) {
				return wpagent_markdown_failure_after_write( $assigned, $post_id, $candidate, 'category_assignment_failed' );
			}
			$after_assignment = wpagent_markdown_post_full_state( $post_id );
			$read_back        = wp_get_post_terms( $post_id, 'category', array( 'fields' => 'ids' ) );
			$assigned_ids     = is_wp_error( $read_back ) ? array() : array_map( 'intval', $read_back );
			sort( $assigned_ids, SORT_NUMERIC );
			if ( is_wp_error( $after_assignment ) || is_wp_error( $read_back ) || array( $term_id ) !== $assigned_ids || ! wpagent_markdown_post_category_write_matches( $candidate, $after_assignment, $term_id ) ) {
				return wpagent_markdown_failure_after_write( new WP_Error( 'wpagent_category_unverified', __( 'Die angeforderte Kategorie konnte nicht bestätigt werden.', 'wp-agent-companion' ), array( 'status' => 502 ) ), $post_id, is_array( $after_assignment ) ? $after_assignment : $candidate, 'category_readback_unverified' );
			}
			$candidate = $after_assignment;
		}

		$guard = wpagent_markdown_post_guard( $post_id, $lock );
		$final = wpagent_markdown_post_full_state( $post_id );
		if ( is_wp_error( $guard ) || is_wp_error( $final ) || ! hash_equals( wpagent_markdown_post_state_hash( $candidate ), wpagent_markdown_post_state_hash( $final ) ) ) {
			return wpagent_markdown_failure_after_write( is_wp_error( $guard ) ? $guard : new WP_Error( 'wpagent_post_write_unverified', __( 'Der endgültige Beitragszustand konnte nicht bestätigt werden.', 'wp-agent-companion' ), array( 'status' => 502 ) ), $post_id, $candidate, 'final_readback_or_lock_unverified' );
		}

		return array(
			'post_id'            => $post_id,
			'url'                => get_permalink( $post_id ),
			'status'             => (string) $final['post']['post_status'],
			'content_hash'       => hash( 'sha256', $content ),
			'content_state_hash' => wpagent_markdown_post_state_hash( $final ),
			'block_count'        => $report['block_count'],
			'freeform_count'     => $report['freeform_count'],
			'block_types'        => $report['block_types'],
			'updated'            => false,
		);
	} finally {
		if ( $post_id > 0 && '' !== $lock ) {
			wpagent_markdown_post_release_lock( $post_id, $lock );
		}
	}
}
