<?php
/**
 * Ability: wp-agent/set-post-terms
 *
 * Weist einem Beitrag Terme einer Taxonomie zu (z. B. Kategorien oder
 * Schlagwörter). Einträge in terms[] dürfen Term-IDs (integer) oder Namen
 * (string) sein. wp_set_object_terms() unterscheidet intern exakt nach
 * is_int(): ein integer-Eintrag MUSS ein bereits existierender Term sein und
 * wird bei einer unbekannten ID NICHT automatisch angelegt (der unverifizierte
 * Schreibversuch wird zurückgerollt); ein string-Eintrag wird bei Bedarf erst
 * nach einer expliziten manage_terms-Prüfung als neuer Term angelegt.
 *
 * WICHTIGER FALLSTRICK: append=false (Default) ERSETZT alle bisher
 * zugeordneten Terme dieser Taxonomie am Beitrag, es ist KEIN additives
 * Zusammenführen. Wer bestehende Terme behalten will, muss append=true
 * setzen.
 *
 * Die Capability-Prüfung kombiniert zwei Ebenen: current_user_can( 'edit_post',
 * $post_id ) (darf der Aufrufer diesen konkreten Beitrag bearbeiten) UND die
 * assign_terms-Capability der konkreten Taxonomie aus get_taxonomy()->cap
 * (darf der Aufrufer überhaupt Terme dieser Taxonomie zuordnen). Wenn ein Name
 * noch nicht existiert, kommt die manage_terms-Capability als dritte Ebene
 * hinzu.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_set_post_terms(): array {
	return array(
		'label'               => __( 'Beitrags-Terme setzen', 'wp-agent-companion' ),
		'description'         => __( 'Weist einem Beitrag Terme einer Taxonomie zu (IDs oder Namen; unbekannte Namen werden nur mit der manage_terms-Berechtigung neu angelegt, unbekannte IDs nicht). append=false (Default) ersetzt alle bisherigen Terme dieser Taxonomie am Beitrag, append=true fügt hinzu. Verifiziert per Re-Read über wp_get_object_terms() und stellt bei Fehlern die vorherige Zuordnung wieder her.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'post_id'  => array( 'type' => 'integer' ),
				'taxonomy' => array(
					'type'        => 'string',
					'description' => __( 'Muss eine registrierte Taxonomie sein, die dem Post-Type des Beitrags zugeordnet ist.', 'wp-agent-companion' ),
				),
				'terms'    => array(
					'type'        => 'array',
					'description' => __( 'Term-IDs (integer) oder Term-Namen (string), gemischt erlaubt. Leeres Array entfernt bei append=false alle Terme dieser Taxonomie vom Beitrag.', 'wp-agent-companion' ),
					'items'       => array( 'type' => array( 'integer', 'string' ) ),
				),
				'append'   => array(
					'type'        => 'boolean',
					'default'     => false,
					'description' => __( 'false (Default) ersetzt alle bisherigen Terme dieser Taxonomie am Beitrag. true fügt hinzu, ohne bestehende zu entfernen.', 'wp-agent-companion' ),
				),
			),
			'required'   => array( 'post_id', 'taxonomy', 'terms' ),
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'post_id'  => array( 'type' => 'integer' ),
				'taxonomy' => array( 'type' => 'string' ),
				'append'   => array( 'type' => 'boolean' ),
				'terms'    => array(
					'type'  => 'array',
					'items' => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array( 'type' => 'integer' ),
							'name' => array( 'type' => 'string' ),
							'slug' => array( 'type' => 'string' ),
						),
					),
				),
				'written'  => array(
					'type'  => 'array',
					'items' => array(
						'type'       => 'object',
						'properties' => array(
							'input'   => array( 'type' => 'string' ),
							'matched' => array( 'type' => 'boolean' ),
						),
					),
				),
				'verified' => array( 'type' => 'boolean' ),
			),
		),
		'execute_callback'    => 'wpagent_exec_set_post_terms',
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				// Ersetzt (bei append=false) bestehende Term-Zuordnungen, genau wie
				// wp-agent/update-post bestehende Feldwerte überschreibt: eine
				// Überschreib-Operation, kein unumkehrbarer Datenverlust wie bei
				// wp-agent/delete-term.
				'destructive' => false,
				'idempotent'  => true,
			),
		),
	);
}

/**
 * @param int[] $previous_ids
 * @param int[] $created_term_ids
 */
function wpagent_set_post_terms_failure_after_rollback( WP_Error $failure, int $post_id, string $taxonomy, array $previous_ids, array $created_term_ids ): WP_Error {
	$restored    = wp_set_object_terms( $post_id, $previous_ids, $taxonomy, false );
	$rollback_ok = ! is_wp_error( $restored );
	if ( $rollback_ok ) {
		$fresh = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) );
		if ( is_wp_error( $fresh ) ) {
			$rollback_ok = false;
		} else {
			$actual = array_map( 'intval', $fresh );
			sort( $actual, SORT_NUMERIC );
			sort( $previous_ids, SORT_NUMERIC );
			$rollback_ok = $actual === $previous_ids;
		}
	}

	foreach ( $created_term_ids as $term_id ) {
		$term = get_term( $term_id, $taxonomy );
		if ( ! $term instanceof WP_Term ) {
			continue;
		}
		if ( (int) $term->count > 0 ) {
			$rollback_ok = false;
			continue;
		}
		$deleted = wp_delete_term( $term_id, $taxonomy );
		if ( is_wp_error( $deleted ) || term_exists( $term_id, $taxonomy ) ) {
			$rollback_ok = false;
		}
	}

	if ( $rollback_ok ) {
		$data = $failure->get_error_data();
		$data = is_array( $data ) ? $data : array();
		$data['rollback_verified'] = true;
		$failure->add_data( $data );
		return $failure;
	}
	return new WP_Error(
		'wpagent_terms_rollback_failed',
		__( 'Die fehlgeschlagene Term-Zuordnung konnte nicht sicher zurückgerollt werden. Prüfe Beitrag und Terme sofort manuell.', 'wp-agent-companion' ),
		array( 'status' => 500, 'rollback_verified' => false, 'original_error' => $failure->get_error_code() )
	);
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_set_post_terms( $input ) {
	$input    = is_array( $input ) ? $input : array();
	$post_id  = (int) ( $input['post_id'] ?? 0 );
	$taxonomy = (string) ( $input['taxonomy'] ?? '' );
	$post     = get_post( $post_id );

	if ( ! $post ) {
		return new WP_Error( 'wpagent_no_post', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung für diesen Beitrag.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}

	if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
		return new WP_Error( 'wpagent_bad_taxonomy', sprintf( __( 'Unbekannte Taxonomie "%s".', 'wp-agent-companion' ), $taxonomy ), array( 'status' => 400 ) );
	}
	if ( 'product_shipping_class' === $taxonomy ) {
		return new WP_Error(
			'wpagent_wc_shipping_class_requires_product_ability',
			__( 'WooCommerce-Versandklassen dürfen nur über create-product oder update-product mit shipping_class_id zugeordnet werden. Der generische Term-Pfad ist dafür gesperrt.', 'wp-agent-companion' ),
			array( 'status' => 409 )
		);
	}
	if ( ! is_object_in_taxonomy( $post->post_type, $taxonomy ) ) {
		return new WP_Error(
			'wpagent_taxonomy_not_supported',
			/* translators: 1: taxonomy name, 2: post type name. */
			sprintf( __( 'Taxonomie "%1$s" ist dem Post-Type "%2$s" nicht zugeordnet.', 'wp-agent-companion' ), $taxonomy, $post->post_type ),
			array( 'status' => 400 )
		);
	}

	// Feingranulare Capability der konkreten Taxonomie (get_taxonomy()->cap),
	// erst berechenbar, nachdem die Taxonomie oben als gültig bestätigt wurde.
	// Fail-closed analog zu wpagent_post_type_cap() in helpers.php.
	$tax_object = get_taxonomy( $taxonomy );
	$assign_cap = ( $tax_object && isset( $tax_object->cap->assign_terms ) && '' !== (string) $tax_object->cap->assign_terms )
		? (string) $tax_object->cap->assign_terms
		: 'do_not_allow';
	if ( ! current_user_can( $assign_cap ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, Terme dieser Taxonomie zuzuordnen.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}

	$raw_terms = $input['terms'] ?? null;
	if ( ! is_array( $raw_terms ) ) {
		return new WP_Error( 'wpagent_bad_terms', __( 'terms muss ein Array sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}

	// Der Typ jedes Eintrags entscheidet über die Bedeutung, exakt wie in
	// wp_set_object_terms() selbst: is_int() -> erwartete existierende Term-ID
	// (kein Auto-Anlegen), string -> Name (Auto-Anlegen bei Bedarf). Eine
	// eigene Umdeutung numerischer Strings zu IDs würde von diesem
	// Kernverhalten abweichen und wäre für einen Term, der zufällig rein
	// numerisch heißt (z. B. "2024"), sogar falsch.
	$normalized = array();
	foreach ( $raw_terms as $raw ) {
		if ( is_int( $raw ) ) {
			$normalized[] = $raw;
		} elseif ( is_string( $raw ) && '' !== trim( $raw ) ) {
			$normalized[] = $raw;
		} else {
			return new WP_Error(
				'wpagent_bad_term_entry',
				__( 'Jeder Eintrag in terms[] muss eine Term-ID (integer) oder ein nicht-leerer Name (string) sein.', 'wp-agent-companion' ),
				array( 'status' => 400 )
			);
		}
	}

	$append   = ! empty( $input['append'] );
	$previous = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) );
	if ( is_wp_error( $previous ) ) {
		return $previous;
	}
	$previous_ids = array_map( 'intval', $previous );

	// WordPress creates missing string terms inside wp_set_object_terms(). That
	// core path checks no manage_terms capability, so resolve and create them
	// explicitly before touching the post's current associations.
	$manage_cap = ( $tax_object && isset( $tax_object->cap->manage_terms ) && '' !== (string) $tax_object->cap->manage_terms )
		? (string) $tax_object->cap->manage_terms
		: 'do_not_allow';
	$created_term_ids = array();
	foreach ( $normalized as $requested ) {
		if ( ! is_string( $requested ) || term_exists( $requested, $taxonomy ) ) {
			continue;
		}
		if ( ! current_user_can( $manage_cap ) ) {
			return new WP_Error( 'wpagent_forbidden_manage_terms', __( 'Mindestens ein Term existiert noch nicht. Zum automatischen Anlegen ist die manage_terms-Berechtigung dieser Taxonomie erforderlich.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$created = wp_insert_term( $requested, $taxonomy );
		if ( is_wp_error( $created ) ) {
			$existing_id = ( 'term_exists' === $created->get_error_code() ) ? (int) $created->get_error_data() : 0;
			if ( $existing_id <= 0 ) {
				return wpagent_set_post_terms_failure_after_rollback( $created, $post_id, $taxonomy, $previous_ids, $created_term_ids );
			}
		} else {
			$created_term_ids[] = (int) $created['term_id'];
		}
	}

	$res = wp_set_object_terms( $post_id, $normalized, $taxonomy, $append );
	if ( is_wp_error( $res ) ) {
		return wpagent_set_post_terms_failure_after_rollback( $res, $post_id, $taxonomy, $previous_ids, $created_term_ids );
	}

	$final_terms = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'all' ) );
	if ( is_wp_error( $final_terms ) ) {
		return wpagent_set_post_terms_failure_after_rollback( $final_terms, $post_id, $taxonomy, $previous_ids, $created_term_ids );
	}

	$final_ids   = array();
	$final_names = array();
	$terms_out   = array();
	foreach ( $final_terms as $ft ) {
		$final_ids[]   = (int) $ft->term_id;
		$final_names[] = $ft->name;
		$terms_out[]   = array(
			'id'   => (int) $ft->term_id,
			'name' => $ft->name,
			'slug' => $ft->slug,
		);
	}

	// Re-Read-Verifikation: jeder angeforderte Eintrag muss im finalen
	// Term-Set des Beitrags auftauchen. Eine unbekannte Term-ID, die
	// wp_set_object_terms() intern still übersprungen hat, löst anschließend den
	// verifizierten Rollback auf die vorherige Zuordnung aus.
	$written  = array();
	$verified = true;
	foreach ( $normalized as $requested ) {
		if ( is_int( $requested ) ) {
			$ok = in_array( $requested, $final_ids, true );
		} else {
			$ok = in_array( (string) $requested, $final_names, true );
		}
		$written[] = array(
			'input'   => (string) $requested,
			'matched' => $ok,
		);
		if ( ! $ok ) {
			$verified = false;
		}
	}
	if ( ! $verified ) {
		return wpagent_set_post_terms_failure_after_rollback(
			new WP_Error( 'wpagent_terms_unverified', __( 'Mindestens ein angeforderter Term wurde nicht exakt zugeordnet.', 'wp-agent-companion' ), array( 'status' => 502 ) ),
			$post_id,
			$taxonomy,
			$previous_ids,
			$created_term_ids
		);
	}

	return array(
		'post_id'  => $post_id,
		'taxonomy' => $taxonomy,
		'append'   => $append,
		'terms'    => $terms_out,
		'written'  => $written,
		'verified' => $verified,
	);
}
