<?php
/**
 * Gemeinsamer, konfliktgeschützter Term-SEO-Vertrag.
 *
 * @package WPAgentCompanion
 */

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

define( 'WPAGENT_TERM_SEO_LOCK_TTL', 120 );

/** @return array<string,string> */
function wpagent_term_seo_meta_map( string $plugin ): array {
	if ( 'rankmath' === $plugin ) {
		return array(
			'title'                   => 'rank_math_title',
			'description'             => 'rank_math_description',
			'focus_keyword'           => 'rank_math_focus_keyword',
			'canonical_url'           => 'rank_math_canonical_url',
			'open_graph_title'        => 'rank_math_facebook_title',
			'open_graph_description'  => 'rank_math_facebook_description',
			'twitter_title'           => 'rank_math_twitter_title',
			'twitter_description'     => 'rank_math_twitter_description',
		);
	}
	if ( 'seopress' === $plugin ) {
		return array(
			'title'                   => '_seopress_titles_title',
			'description'             => '_seopress_titles_desc',
			'focus_keyword'           => '_seopress_analysis_target_kw',
			'canonical_url'           => '_seopress_robots_canonical',
			'open_graph_title'        => '_seopress_social_fb_title',
			'open_graph_description'  => '_seopress_social_fb_desc',
			'twitter_title'           => '_seopress_social_twitter_title',
			'twitter_description'     => '_seopress_social_twitter_desc',
		);
	}
	return array();
}

/** @return array<string,string> */
function wpagent_term_seo_yoast_map(): array {
	return array(
		'title'                   => 'wpseo_title',
		'description'             => 'wpseo_desc',
		'focus_keyword'           => 'wpseo_focuskw',
		'canonical_url'           => 'wpseo_canonical',
		'open_graph_title'        => 'wpseo_opengraph-title',
		'open_graph_description'  => 'wpseo_opengraph-description',
		'twitter_title'           => 'wpseo_twitter-title',
		'twitter_description'     => 'wpseo_twitter-description',
	);
}

/** @return array<string,mixed> */
function wpagent_term_seo_output_properties(): array {
	$text = array( 'type' => 'string' );
	return array(
		'term_id'                  => array( 'type' => 'integer' ),
		'taxonomy'                 => $text,
		'plugin'                   => $text,
		'seo_title'                => $text,
		'seo_description'          => $text,
		'focus_keyword'            => $text,
		'canonical_url'            => $text,
		'robots_noindex'           => array( 'type' => 'boolean' ),
		'robots_nofollow'          => array( 'type' => 'boolean' ),
		'open_graph_title'         => $text,
		'open_graph_description'   => $text,
		'twitter_title'            => $text,
		'twitter_description'      => $text,
		'state_hash'               => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
	);
}

/**
 * @return array{term:WP_Term,taxonomy:WP_Taxonomy}|WP_Error
 */
function wpagent_term_seo_target( int $term_id, string $taxonomy ) {
	if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
		/* translators: %s: taxonomy name. */
		return new WP_Error( 'wpagent_bad_taxonomy', sprintf( __( 'Unbekannte Taxonomie "%s".', 'wp-agent-companion' ), $taxonomy ), array( 'status' => 400 ) );
	}
	$tax_object = get_taxonomy( $taxonomy );
	if ( ! $tax_object instanceof WP_Taxonomy || ! is_taxonomy_viewable( $tax_object ) ) {
		return new WP_Error( 'wpagent_term_seo_taxonomy_hidden', __( 'SEO-Daten werden nur für öffentlich sichtbare Taxonomien verwaltet.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$term = get_term( $term_id, $taxonomy );
	if ( ! $term instanceof WP_Term ) {
		return new WP_Error( 'wpagent_no_term', __( 'Term nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$edit_cap = isset( $tax_object->cap->edit_terms ) && '' !== (string) $tax_object->cap->edit_terms
		? (string) $tax_object->cap->edit_terms
		: 'do_not_allow';
	if ( ! current_user_can( $edit_cap ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung, SEO-Daten dieser Taxonomie zu bearbeiten.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	return array( 'term' => $term, 'taxonomy' => $tax_object );
}

/** @return true|WP_Error */
function wpagent_term_seo_provider_supported( string $plugin ) {
	if ( 'none' === $plugin ) {
		return new WP_Error( 'wpagent_seo_plugin_missing', __( 'Kein unterstütztes SEO-Plugin aktiv.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( 'aioseo' === $plugin ) {
		return new WP_Error( 'wpagent_aioseo_term_api_missing', __( 'Die aktive AIOSEO-Ausgabe stellt keine native, verifizierbare Term-SEO-Schnittstelle bereit. WPAgently schreibt keine wirkungslosen Ersatzdaten.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( ! in_array( $plugin, array( 'rankmath', 'yoast', 'seopress' ), true ) ) {
		return new WP_Error( 'wpagent_seo_plugin_missing', __( 'Kein unterstütztes SEO-Plugin aktiv.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return true;
}

/**
 * Liest Yoasts Option direkt aus der aktuellen Site-Tabelle. Yoasts
 * `option_...`-Filter formt selbst skalare Altwerte zu Arrays um. Nur der
 * unveränderte Datenbankwert lässt einen fehlenden Datensatz und einen
 * beschädigten Rohzustand sicher unterscheiden.
 *
 * @return mixed
 */
function wpagent_get_raw_yoast_taxonomy_meta() {
	global $wpdb;
	$value = $wpdb->get_var(
		$wpdb->prepare(
			"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
			'wpseo_taxonomy_meta'
		)
	);
	return null === $value ? null : maybe_unserialize( $value );
}

/**
 * Liest ausschließlich den exakten Providerzustand des Zielterms.
 *
 * @return array<string,mixed>|WP_Error
 */
function wpagent_capture_term_seo_state( int $term_id, string $taxonomy, string $plugin ) {
	if ( 'yoast' === $plugin ) {
		$stored = wpagent_get_raw_yoast_taxonomy_meta();
		if ( null !== $stored && ! is_array( $stored ) ) {
			return new WP_Error( 'wpagent_term_seo_provider_state_invalid', __( 'Yoast enthält für diesen Term keinen gültigen Metadatensatz. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$all = is_array( $stored ) ? $stored : array();
		if ( array_key_exists( $taxonomy, $all ) && ! is_array( $all[ $taxonomy ] ) ) {
			return new WP_Error( 'wpagent_term_seo_provider_state_invalid', __( 'Yoast enthält für diesen Term keinen gültigen Metadatensatz. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$terms  = isset( $all[ $taxonomy ] ) ? $all[ $taxonomy ] : array();
		$exists = array_key_exists( $term_id, $terms ) || array_key_exists( (string) $term_id, $terms );
		$value  = $terms[ $term_id ] ?? $terms[ (string) $term_id ] ?? null;
		if ( $exists && ! is_array( $value ) ) {
			return new WP_Error( 'wpagent_term_seo_provider_state_invalid', __( 'Yoast enthält für diesen Term keinen gültigen Metadatensatz. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		if ( is_array( $value ) ) {
			if ( ! class_exists( 'WPSEO_Taxonomy_Meta' ) ) {
				return new WP_Error( 'wpagent_yoast_term_api_missing', __( 'Yoast stellt seine native Taxonomie-Meta-Schnittstelle nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			$allowed_keys = array_keys( WPSEO_Taxonomy_Meta::$defaults_per_term );
			foreach ( $value as $key => $field_value ) {
				if ( ! is_string( $key ) || ! in_array( $key, $allowed_keys, true ) || ( ! is_scalar( $field_value ) && null !== $field_value ) ) {
					return new WP_Error(
						'wpagent_term_seo_provider_state_invalid',
						__( 'Yoast enthält für diesen Term keinen gültigen Metadatensatz. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ),
						array( 'status' => 502, 'meta_key' => is_scalar( $key ) ? (string) $key : '' )
					);
				}
			}
		}
		return array( 'kind' => 'yoast', 'exists' => $exists, 'value' => $value );
	}

	$keys = array_values( wpagent_term_seo_meta_map( $plugin ) );
	$keys[] = 'rankmath' === $plugin ? 'rank_math_robots' : '_seopress_robots_index';
	if ( 'seopress' === $plugin ) {
		$keys[] = '_seopress_robots_follow';
	}
	$values = array();
	foreach ( array_values( array_unique( $keys ) ) as $key ) {
		$rows = get_term_meta( $term_id, $key, false );
		if ( count( $rows ) > 1 ) {
			return new WP_Error( 'wpagent_term_seo_provider_state_invalid', __( 'Das SEO-Plugin enthält für diesen Term doppelte Einzelwerte. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ), array( 'status' => 502, 'meta_key' => $key ) );
		}
		if ( isset( $rows[0] ) ) {
			if ( 'rank_math_robots' === $key ) {
				if ( ! is_array( $rows[0] ) || array_filter( $rows[0], static fn( $directive ) => ! is_string( $directive ) ) ) {
					return new WP_Error( 'wpagent_term_seo_provider_state_invalid', __( 'Rank Math enthält für diesen Term ungültige Robots-Daten. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ), array( 'status' => 502, 'meta_key' => $key ) );
				}
			} elseif ( ! is_scalar( $rows[0] ) && null !== $rows[0] ) {
				return new WP_Error( 'wpagent_term_seo_provider_state_invalid', __( 'Das SEO-Plugin enthält für diesen Term einen ungültigen Einzelwert. Der Schreibpfad bleibt geschlossen.', 'wp-agent-companion' ), array( 'status' => 502, 'meta_key' => $key ) );
			}
		}
		$values[ $key ] = $rows;
	}
	return array( 'kind' => 'termmeta', 'values' => $values );
}

/** @param array<string,mixed> $raw */
function wpagent_term_seo_state_hash( int $term_id, string $taxonomy, string $plugin, array $raw ): string {
	return hash(
		'sha256',
		(string) wp_json_encode(
			array(
				'blog_id'  => get_current_blog_id(),
				'term_id'  => $term_id,
				'taxonomy' => $taxonomy,
				'plugin'   => $plugin,
				'raw'      => $raw,
			),
			JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
		)
	);
}

function wpagent_term_seo_lock_option( int $term_id, string $taxonomy, string $plugin = '' ): string {
	return 'yoast' === $plugin
		? 'wp_agent_term_seo_lock_yoast'
		: 'wp_agent_term_seo_lock_' . sanitize_key( $taxonomy ) . '_' . $term_id;
}

/** @return string|WP_Error */
function wpagent_term_seo_acquire_lock( int $term_id, string $taxonomy, string $plugin = '' ) {
	$option = wpagent_term_seo_lock_option( $term_id, $taxonomy, $plugin );
	$token  = wpagent_companion_acquire_option_lock( $option, WPAGENT_TERM_SEO_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_term_seo_locked', __( 'Die SEO-Daten dieses Terms werden gerade geändert. Bitte erneut versuchen.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_term_seo_release_lock( int $term_id, string $taxonomy, string $token, string $plugin = '' ): void {
	wpagent_companion_release_option_lock( wpagent_term_seo_lock_option( $term_id, $taxonomy, $plugin ), $token );
}

/**
 * @param array<string,mixed> $raw
 */
function wpagent_restore_term_seo_state( int $term_id, string $taxonomy, string $plugin, array $raw ): bool {
	if ( 'yoast' === ( $raw['kind'] ?? '' ) ) {
		$stored = wpagent_get_raw_yoast_taxonomy_meta();
		if ( null !== $stored && ! is_array( $stored ) ) {
			return false;
		}
		$all = is_array( $stored ) ? $stored : array();
		if ( array_key_exists( $taxonomy, $all ) && ! is_array( $all[ $taxonomy ] ) ) {
			return false;
		}
		if ( ! isset( $all[ $taxonomy ] ) || ! is_array( $all[ $taxonomy ] ) ) {
			$all[ $taxonomy ] = array();
		}
		if ( ! empty( $raw['exists'] ) ) {
			$all[ $taxonomy ][ $term_id ] = $raw['value'];
		} else {
			unset( $all[ $taxonomy ][ $term_id ], $all[ $taxonomy ][ (string) $term_id ] );
			if ( array() === $all[ $taxonomy ] ) {
				unset( $all[ $taxonomy ] );
			}
		}
		// Yoast hat den Zielzustand bereits beim ursprünglichen Schreiben
		// validiert. Der Sentinel verhindert, dass update_option() beim Rollback
		// auch unveränderte Daten anderer Terms erneut normalisiert oder verwirft.
		$all['wpseo_already_validated'] = true;
		update_option( 'wpseo_taxonomy_meta', $all, false );
		wp_cache_delete( 'wpseo_taxonomy_meta', 'options' );
		return wpagent_capture_term_seo_state( $term_id, $taxonomy, $plugin ) === $raw;
	}

	foreach ( (array) ( $raw['values'] ?? array() ) as $key => $rows ) {
		delete_term_meta( $term_id, (string) $key );
		foreach ( (array) $rows as $value ) {
			add_term_meta( $term_id, (string) $key, $value );
		}
	}
	return wpagent_capture_term_seo_state( $term_id, $taxonomy, $plugin ) === $raw;
}

/**
 * Stellt Providerdaten und, bei Yoast, den davon abgeleiteten Indexable wieder her.
 *
 * @param array<string,mixed> $raw
 */
function wpagent_rollback_term_seo_state( int $term_id, string $taxonomy, string $plugin, array $raw ): bool {
	$restored = wpagent_restore_term_seo_state( $term_id, $taxonomy, $plugin, $raw );
	if ( ! $restored ) {
		return false;
	}
	return 'yoast' !== $plugin || false !== wpagent_refresh_yoast_term_indexable( $term_id );
}

/**
 * Baut Yoasts Term-Indexable neu und vergleicht die gespeicherten SEO-Felder
 * mit dem gebauten Objekt. Auf lokalen und Staging-Systemen speichert Yoast
 * Indexables absichtlich nicht. In diesem Fall bedeutet null „nicht
 * anwendbar“, nicht „verifiziert“.
 *
 * @return bool|null
 */
function wpagent_refresh_yoast_term_indexable( int $term_id ) {
	if ( ! function_exists( 'YoastSEO' ) ) {
		return false;
	}
	try {
		$classes          = YoastSEO()->classes;
		$repository       = $classes->get( 'Yoast\\WP\\SEO\\Repositories\\Indexable_Repository' );
		$builder          = $classes->get( 'Yoast\\WP\\SEO\\Builders\\Indexable_Builder' );
		$indexable_helper = $classes->get( 'Yoast\\WP\\SEO\\Helpers\\Indexable_Helper' );
		$existing         = $repository->find_by_id_and_type( $term_id, 'term', false );
		$indexable        = $builder->build_for_id_and_type( $term_id, 'term', $existing );
		if ( ! is_object( $indexable ) ) {
			return false;
		}
		if ( ! $indexable_helper->should_index_indexable( $indexable ) ) {
			return null;
		}

		$persisted = $repository->find_by_id_and_type( $term_id, 'term', false );
		if ( ! is_object( $persisted ) ) {
			return false;
		}
		$fields = array(
			'title',
			'description',
			'primary_focus_keyword',
			'canonical',
			'is_robots_noindex',
			'open_graph_title',
			'open_graph_description',
			'twitter_title',
			'twitter_description',
		);
		foreach ( $fields as $field ) {
			if ( $persisted->{$field} !== $indexable->{$field} ) {
				return false;
			}
		}
		return true;
	} catch ( Throwable ) {
		return false;
	}
}

/**
 * @return array<string,mixed>|WP_Error
 */
function wpagent_read_term_seo_meta( int $term_id, string $taxonomy, string $plugin ) {
	$raw = wpagent_capture_term_seo_state( $term_id, $taxonomy, $plugin );
	if ( is_wp_error( $raw ) ) {
		return $raw;
	}
	$snapshot = array(
		'term_id' => $term_id,
		'taxonomy' => $taxonomy,
		'plugin' => $plugin,
	);
	if ( 'yoast' === $plugin ) {
		if ( ! class_exists( 'WPSEO_Taxonomy_Meta' ) ) {
			return new WP_Error( 'wpagent_yoast_term_api_missing', __( 'Yoast stellt seine native Taxonomie-Meta-Schnittstelle nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$provider = WPSEO_Taxonomy_Meta::get_term_meta( $term_id, $taxonomy );
		$provider = is_array( $provider ) ? $provider : array();
		foreach ( wpagent_term_seo_yoast_map() as $field => $key ) {
			$snapshot[ 'title' === $field ? 'seo_title' : ( 'description' === $field ? 'seo_description' : $field ) ] = (string) ( $provider[ $key ] ?? '' );
		}
		$snapshot['robots_noindex']  = 'noindex' === (string) ( $provider['wpseo_noindex'] ?? 'default' );
		$snapshot['robots_nofollow'] = false;
	} else {
		$rankmath_term = 'rankmath' === $plugin ? get_term( $term_id, $taxonomy ) : null;
		foreach ( wpagent_term_seo_meta_map( $plugin ) as $field => $key ) {
			$rows = (array) ( $raw['values'][ $key ] ?? array() );
			$value = isset( $rows[0] ) ? (string) $rows[0] : '';
			if ( $rankmath_term instanceof WP_Term && class_exists( 'RankMath\\Helper' ) ) {
				$value = (string) RankMath\Helper::get_term_meta( substr( $key, strlen( 'rank_math_' ) ), $rankmath_term, $taxonomy );
			}
			$snapshot[ 'title' === $field ? 'seo_title' : ( 'description' === $field ? 'seo_description' : $field ) ] = $value;
		}
		if ( 'rankmath' === $plugin ) {
			$robots = $rankmath_term instanceof WP_Term && class_exists( 'RankMath\\Helper' )
				? (array) RankMath\Helper::get_term_meta( 'robots', $rankmath_term, $taxonomy )
				: (array) ( $raw['values']['rank_math_robots'][0] ?? array() );
			$snapshot['robots_noindex']  = in_array( 'noindex', $robots, true );
			$snapshot['robots_nofollow'] = in_array( 'nofollow', $robots, true );
		} else {
			$snapshot['robots_noindex']  = 'yes' === (string) ( $raw['values']['_seopress_robots_index'][0] ?? '' );
			$snapshot['robots_nofollow'] = 'yes' === (string) ( $raw['values']['_seopress_robots_follow'][0] ?? '' );
		}
	}
	$snapshot['state_hash'] = wpagent_term_seo_state_hash( $term_id, $taxonomy, $plugin, $raw );
	return $snapshot;
}
