<?php
/** Gemeinsame, provider-native Post-Schema-Pfade für Rank Math und Yoast. */

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

/** @return string[] */
function wpagent_rankmath_schema_types(): array {
	return class_exists( 'RankMath\\Helper' )
		? array_keys( \RankMath\Helper::choices_rich_snippet_types() )
		: array();
}

/** @return string[] */
function wpagent_rankmath_allowed_schema_names(): array {
	$map = array(
		'article'    => array( 'Article', 'BlogPosting', 'NewsArticle' ),
		'book'       => array( 'Book' ),
		'course'     => array( 'Course' ),
		'event'      => array( 'Event' ),
		'jobposting' => array( 'JobPosting' ),
		'music'      => array( 'MusicAlbum', 'MusicGroup', 'MusicRecording' ),
		'product'    => array( 'Product', 'WooCommerceProduct', 'EDDProduct' ),
		'recipe'     => array( 'Recipe' ),
		'restaurant' => array( 'Restaurant' ),
		'video'      => array( 'VideoObject' ),
		'person'     => array( 'Person' ),
		'service'    => array( 'Service' ),
		'software'   => array( 'SoftwareApplication' ),
	);
	$allowed = array();
	foreach ( wpagent_rankmath_schema_types() as $type ) {
		if ( isset( $map[ $type ] ) ) {
			$allowed = array_merge( $allowed, $map[ $type ] );
		}
	}
	return array_values( array_unique( $allowed ) );
}

/** @return string[] */
function wpagent_yoast_page_schema_types(): array {
	return class_exists( 'Yoast\\WP\\SEO\\Config\\Schema_Types' )
		? array_keys( \Yoast\WP\SEO\Config\Schema_Types::PAGE_TYPES )
		: array();
}

/** @return string[] */
function wpagent_yoast_article_schema_types(): array {
	return class_exists( 'Yoast\\WP\\SEO\\Config\\Schema_Types' )
		? array_keys( apply_filters( 'wpseo_schema_article_types', \Yoast\WP\SEO\Config\Schema_Types::ARTICLE_TYPES ) )
		: array();
}

/** @param mixed $value @return mixed */
function wpagent_schema_canonicalize( $value ) {
	if ( ! is_array( $value ) ) {
		return $value;
	}
	if ( ! array_is_list( $value ) ) {
		ksort( $value, SORT_STRING );
	}
	foreach ( $value as $key => $child ) {
		$value[ $key ] = wpagent_schema_canonicalize( $child );
	}
	return $value;
}

/**
 * @param array<string,mixed> $state
 * @param array<string,mixed> $provider_state
 */
function wpagent_post_schema_state_hash( array $state, array $provider_state = array() ): string {
	$material = array(
		'post_id'      => (int) $state['post_id'],
		'plugin'       => (string) $state['plugin'],
		'page_type'    => $state['page_type'],
		'article_type' => $state['article_type'],
		'schemas'      => $state['schemas'],
		'provider_state' => $provider_state,
	);
	return hash( 'sha256', (string) wp_json_encode( wpagent_schema_canonicalize( $material ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
}

/** @param array<string,mixed> $schema */
function wpagent_rankmath_schema_payload_hash( array $schema ): string {
	return hash( 'sha256', (string) wp_json_encode( wpagent_schema_canonicalize( $schema ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
}

function wpagent_rankmath_schema_idempotency_meta_key( string $key ): string {
	return '_wpagent_rankmath_schema_create_' . hash( 'sha256', $key );
}

/**
 * Bindet die von Schema-Mutationen berührten Rank-Math-Nebenfelder in den
 * Konflikthash ein. Dadurch kann ein Löschvorgang weder den Rich-Snippet-Status
 * noch eine Shortcode-Zuordnung überschreiben, die sich seit dem Lesen geändert
 * hat.
 *
 * @return array<string,array<int,mixed>>
 */
function wpagent_rankmath_schema_provider_state( int $post_id ): array {
	$state = array(
		'rank_math_rich_snippet' => get_post_meta( $post_id, 'rank_math_rich_snippet', false ),
	);
	foreach ( array_keys( get_post_meta( $post_id ) ) as $meta_key ) {
		if ( str_starts_with( (string) $meta_key, 'rank_math_shortcode_schema_' ) ) {
			$state[ (string) $meta_key ] = get_post_meta( $post_id, (string) $meta_key, false );
		}
	}
	ksort( $state, SORT_STRING );
	return $state;
}

/**
 * @param array<string,mixed> $raw
 * @return array<string,mixed>
 */
function wpagent_rankmath_schema_state_from_raw( int $post_id, array $raw ): array {
	$schemas = array();
	foreach ( $raw as $schema_id => $schema ) {
		$schemas[] = array( 'schema_id' => (string) $schema_id, 'schema' => $schema );
	}
	usort( $schemas, static fn( array $left, array $right ): int => strnatcmp( $left['schema_id'], $right['schema_id'] ) );
	$state = array(
		'post_id'                 => $post_id,
		'plugin'                  => 'rankmath',
		'custom_schema_supported' => true,
		'page_type'               => null,
		'article_type'            => null,
		'schemas'                 => $schemas,
		'available_types'         => wpagent_rankmath_schema_types(),
		'available_page_types'    => array(),
		'available_article_types' => array(),
	);
	$state['state_hash'] = wpagent_post_schema_state_hash( $state, wpagent_rankmath_schema_provider_state( $post_id ) );
	return $state;
}

/**
 * Stellt einen gelöschten Rank-Math-Schemadatensatz mit seiner ursprünglichen
 * Meta-ID sowie die betroffenen Nebenfelder wieder her. Die stabile Meta-ID ist
 * für Schema-Referenzen und wpagentlys Idempotenznachweis erforderlich.
 *
 * @param array<string,mixed>            $schema
 * @param array<string,array<int,mixed>> $side_state
 */
function wpagent_restore_deleted_rankmath_schema( int $post_id, int $meta_id, string $meta_key, array $schema, array $side_state ): bool {
	$record = get_metadata_by_mid( 'post', $meta_id );
	if ( false === $record ) {
		global $wpdb;
		$inserted = $wpdb->insert(
			$wpdb->postmeta,
			array(
				'meta_id'    => $meta_id,
				'post_id'    => $post_id,
				'meta_key'   => $meta_key,
				'meta_value' => maybe_serialize( $schema ),
			),
			array( '%d', '%d', '%s', '%s' )
		);
		if ( 1 !== $inserted ) {
			return false;
		}
		wp_cache_delete( $post_id, 'post_meta' );
	} else {
		$record_post_id = absint( $record->post_id ?? $record->object_id ?? 0 );
		$record_value   = maybe_unserialize( $record->meta_value ?? null );
		if ( $post_id !== $record_post_id || $meta_key !== (string) ( $record->meta_key ?? '' ) || $schema !== $record_value ) {
			return false;
		}
	}

	if ( ! wpagent_restore_seo_postmeta( $post_id, $side_state ) ) {
		return false;
	}
	$fresh = \RankMath\Schema\DB::get_schemas( $post_id, 'postmeta', true );
	return isset( $fresh[ 'schema-' . $meta_id ] ) && $fresh[ 'schema-' . $meta_id ] === $schema;
}

/**
 * Entfernt ausschließlich neue Rank-Math-Schemas, die dem erwarteten Payload
 * entsprechen. Unerwartete parallele Daten werden nie geraten oder gelöscht.
 *
 * @param array<string,mixed> $before
 * @param array<string,mixed> $expected
 */
function wpagent_rollback_new_rankmath_schemas( int $post_id, array $before, array $expected, string $shortcode ): bool {
	$fresh    = \RankMath\Schema\DB::get_schemas( $post_id, 'postmeta', true );
	$new_ids  = array_diff( array_keys( $fresh ), array_keys( $before ) );
	$verified = true;
	foreach ( $new_ids as $new_id ) {
		if ( ! isset( $fresh[ $new_id ] ) || $fresh[ $new_id ] !== $expected ) {
			$verified = false;
			continue;
		}
		$mid      = absint( str_replace( 'schema-', '', (string) $new_id ) );
		$verified = $mid && delete_metadata_by_mid( 'post', $mid ) && $verified;
	}
	if ( '' !== $shortcode ) {
		delete_post_meta( $post_id, 'rank_math_shortcode_schema_' . $shortcode );
		$verified = $verified && ! metadata_exists( 'post', $post_id, 'rank_math_shortcode_schema_' . $shortcode );
	}
	$fresh_after = \RankMath\Schema\DB::get_schemas( $post_id, 'postmeta', true );
	return $verified && $fresh_after === $before;
}

/**
 * Begrenzt beliebige JSON-LD-Daten vor dem nativen Provider-Sanitizer.
 *
 * @param mixed $value
 * @return true|WP_Error
 */
function wpagent_validate_schema_tree( $value, int $depth = 0, int &$nodes = 0 ) {
	++$nodes;
	if ( $depth > 12 || $nodes > 2000 ) {
		return new WP_Error( 'wpagent_schema_too_complex', __( 'Das Schema überschreitet die zulässige Tiefe oder Knotenzahl.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( is_string( $value ) ) {
		return strlen( $value ) <= 10000
			? true
			: new WP_Error( 'wpagent_schema_string_too_long', __( 'Ein Schema-Textwert überschreitet 10.000 Bytes.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( is_null( $value ) || is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
		return true;
	}
	if ( ! is_array( $value ) ) {
		return new WP_Error( 'wpagent_invalid_schema_value', __( 'Das Schema enthält einen nicht unterstützten Wert.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $value as $key => $child ) {
		if ( is_string( $key ) && ( strlen( $key ) > 128 || ! preg_match( '/^[A-Za-z@][A-Za-z0-9_:@.-]*$/', $key ) ) ) {
			return new WP_Error( 'wpagent_invalid_schema_key', __( 'Das Schema enthält einen ungültigen oder zu langen Schlüssel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$valid = wpagent_validate_schema_tree( $child, $depth + 1, $nodes );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	return true;
}

/** @param mixed $schema @return array<string,mixed>|WP_Error */
function wpagent_validate_rankmath_schema( $schema, bool $is_new ) {
	if ( ! is_array( $schema ) || array_is_list( $schema ) ) {
		return new WP_Error( 'wpagent_invalid_schema', __( 'Das Rank-Math-Schema muss ein JSON-Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$encoded = wp_json_encode( $schema );
	if ( false === $encoded || strlen( $encoded ) > 100000 ) {
		return new WP_Error( 'wpagent_schema_too_large', __( 'Das Schema ist ungültig oder größer als 100.000 Bytes.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$nodes = 0;
	$valid = wpagent_validate_schema_tree( $schema, 0, $nodes );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	$type = $schema['@type'] ?? null;
	if ( ! is_string( $type ) || ! preg_match( '/^[A-Za-z][A-Za-z0-9]{0,63}$/', $type ) ) {
		return new WP_Error( 'wpagent_invalid_schema_type', __( 'Das Schema benötigt genau einen alphanumerischen @type.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( $is_new && ! in_array( $type, wpagent_rankmath_allowed_schema_names(), true ) ) {
		return new WP_Error( 'wpagent_unsupported_schema_type', __( 'Dieser Schematyp ist in der aktiven Rank-Math-Installation nicht bestätigt verfügbar.', 'wp-agent-companion' ), array( 'status' => 400, 'schema_type' => $type ) );
	}
	if ( isset( $schema['metadata'] ) && ( ! is_array( $schema['metadata'] ) || array_is_list( $schema['metadata'] ) ) ) {
		return new WP_Error( 'wpagent_invalid_schema_metadata', __( 'Die Rank-Math-Schemametadaten müssen ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( isset( $schema['metadata']['shortcode'] ) && ! preg_match( '/^[A-Za-z0-9_-]{1,64}$/', (string) $schema['metadata']['shortcode'] ) ) {
		return new WP_Error( 'wpagent_invalid_schema_shortcode', __( 'Die interne Rank-Math-Shortcode-ID ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( isset( $schema['metadata']['isPrimary'] ) && ! in_array( $schema['metadata']['isPrimary'], array( true, false, 1, 0, '1', '0' ), true ) ) {
		return new WP_Error( 'wpagent_invalid_primary_schema_flag', __( 'Die Primärmarkierung des Schemas muss ein boolescher Wert sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $schema;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_read_post_schema( int $post_id, string $plugin ) {
	if ( 'rankmath' === $plugin ) {
		if ( ! class_exists( 'RankMath\\Helper' ) || ! \RankMath\Helper::has_cap( 'onpage_snippet' ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Rank Math erlaubt diesem Benutzer keinen Zugriff auf Schemadaten.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		if ( ! class_exists( 'RankMath\\Schema\\DB' ) ) {
			return new WP_Error( 'wpagent_schema_api_missing', __( 'Rank Math stellt seine Schema-Datenbank nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$raw = \RankMath\Schema\DB::get_schemas( $post_id, 'postmeta', true );
		return wpagent_rankmath_schema_state_from_raw( $post_id, $raw );
	}
	if ( 'yoast' === $plugin ) {
		if ( ! class_exists( 'WPSEO_Meta' ) ) {
			return new WP_Error( 'wpagent_schema_api_missing', __( 'Yoast stellt seine native Meta-API nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$state = array(
			'post_id'                    => $post_id,
			'plugin'                     => $plugin,
			'custom_schema_supported'    => false,
			'page_type'                  => (string) WPSEO_Meta::get_value( 'schema_page_type', $post_id ),
			'article_type'               => (string) WPSEO_Meta::get_value( 'schema_article_type', $post_id ),
			'schemas'                    => array(),
			'available_types'            => array(),
			'available_page_types'       => wpagent_yoast_page_schema_types(),
			'available_article_types'    => wpagent_yoast_article_schema_types(),
		);
		$state['state_hash'] = wpagent_post_schema_state_hash( $state );
		return $state;
	}
	return new WP_Error( 'wpagent_schema_provider_unsupported', __( 'Das aktive SEO-Plugin stellt keinen bestätigten Post-Schema-Pfad bereit.', 'wp-agent-companion' ), array( 'status' => 409, 'plugin' => $plugin ) );
}

/**
 * Baut Yoasts Indexable über den nativen Builder neu auf und prüft die beiden
 * Schemafelder im Speicher sowie, falls aktiviert, erneut aus der Datenbank.
 */
function wpagent_refresh_yoast_schema_indexable( int $post_id, ?string $page_type, ?string $article_type ): bool {
	if ( ! function_exists( 'YoastSEO' ) ) {
		return false;
	}
	try {
		$repository = YoastSEO()->classes->get( 'Yoast\\WP\\SEO\\Repositories\\Indexable_Repository' );
		$builder    = YoastSEO()->classes->get( 'Yoast\\WP\\SEO\\Builders\\Indexable_Builder' );
		$existing   = $repository->find_by_id_and_type( $post_id, 'post', false );
		$indexable  = $builder->build_for_id_and_type( $post_id, 'post', $existing );
		if ( ! is_object( $indexable ) ) {
			return false;
		}
		$matches = static function ( $candidate ) use ( $page_type, $article_type ): bool {
			return ( null === $page_type || (string) ( $candidate->schema_page_type ?? '' ) === $page_type )
				&& ( null === $article_type || (string) ( $candidate->schema_article_type ?? '' ) === $article_type );
		};
		if ( ! $matches( $indexable ) ) {
			return false;
		}
		$helper = YoastSEO()->helpers->indexable ?? null;
		if ( is_object( $helper ) && method_exists( $helper, 'should_index_indexables' ) && $helper->should_index_indexables() ) {
			$persisted = $repository->find_by_id_and_type( $post_id, 'post', false );
			return is_object( $persisted ) && $matches( $persisted );
		}
		return true;
	} catch ( Throwable ) {
		return false;
	}
}
