<?php
/** Shared validation, snapshots and locking for global SEO settings. */

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

define( 'WPAGENT_SEO_SETTINGS_LOCK_OPTION', 'wp_agent_seo_settings_lock' );
define( 'WPAGENT_SEO_SETTINGS_LOCK_TTL', 120 );

/** @return string[] */
function wpagent_seo_settings_fields(): array {
	return array(
		'title_separator',
		'homepage_title_template',
		'homepage_description_template',
		'author_archives_enabled',
		'author_archives_noindex',
		'author_archive_title_template',
		'author_archive_description_template',
		'date_archives_enabled',
		'date_archives_noindex',
		'date_archive_title_template',
		'date_archive_description_template',
		'search_noindex',
		'search_title_template',
		'search_description_template',
		'identity_type',
		'identity_name',
		'identity_logo_url',
		'website_name',
		'alternate_website_name',
	);
}

/** @return array<string,mixed> */
function wpagent_seo_settings_value_schema( string $field, bool $output = false ): array {
	$booleans = array( 'author_archives_enabled', 'author_archives_noindex', 'date_archives_enabled', 'date_archives_noindex', 'search_noindex' );
	if ( in_array( $field, $booleans, true ) ) {
		return array( 'type' => $output ? array( 'boolean', 'null' ) : 'boolean' );
	}
	if ( 'identity_type' === $field ) {
		$schema = array( 'type' => 'string', 'enum' => array( 'person', 'organization' ) );
		if ( $output ) {
			$schema['type'] = array( 'string', 'null' );
			$schema['enum'] = array( 'person', 'organization', null );
		}
		return $schema;
	}
	if ( 'title_separator' === $field ) {
		$schema = array( 'type' => 'string', 'enum' => array_keys( wpagent_seo_separator_entities() ) );
		if ( $output ) {
			$schema['type'] = array( 'string', 'null' );
			$schema['enum'][] = null;
		}
		return $schema;
	}
	$templates = array( 'homepage_title_template', 'homepage_description_template', 'author_archive_title_template', 'author_archive_description_template', 'date_archive_title_template', 'date_archive_description_template', 'search_title_template', 'search_description_template' );
	$max       = in_array( $field, $templates, true ) ? 4096 : ( 'identity_logo_url' === $field ? 2048 : 300 );
	return array( 'type' => $output ? array( 'string', 'null' ) : 'string', 'maxLength' => $max );
}

function wpagent_seo_settings_text_byte_limit( string $field ): int {
	$templates = array( 'homepage_title_template', 'homepage_description_template', 'author_archive_title_template', 'author_archive_description_template', 'date_archive_title_template', 'date_archive_description_template', 'search_title_template', 'search_description_template' );
	if ( in_array( $field, $templates, true ) ) {
		return 12000;
	}
	return 'identity_logo_url' === $field ? 6144 : 900;
}

/** @return array<string,mixed> */
function wpagent_seo_settings_schema( bool $output = false ): array {
	$properties = array();
	foreach ( wpagent_seo_settings_fields() as $field ) {
		$properties[ $field ] = wpagent_seo_settings_value_schema( $field, $output );
	}
	return array( 'type' => 'object', 'properties' => $properties, 'additionalProperties' => false );
}

/** @return array<string,mixed> */
function wpagent_seo_settings_written_schema(): array {
	$properties = array();
	foreach ( wpagent_seo_settings_fields() as $field ) {
		$properties[ $field ] = array(
			'type'                 => 'object',
			'properties'           => array(
				'previous'  => wpagent_seo_settings_value_schema( $field, true ),
				'current'   => wpagent_seo_settings_value_schema( $field, true ),
				'persisted' => array( 'type' => 'boolean' ),
			),
			'required'             => array( 'previous', 'current', 'persisted' ),
			'additionalProperties' => false,
		);
	}
	return array( 'type' => 'object', 'properties' => $properties, 'additionalProperties' => false );
}

/** @return string[] */
function wpagent_seo_settings_template_tokens(): array {
	return array(
		'{{site_name}}',
		'{{tagline}}',
		'{{separator}}',
		'{{page}}',
		'{{author_name}}',
		'{{author_bio}}',
		'{{date}}',
		'{{search_query}}',
	);
}

/** @return array<string,string> */
function wpagent_seo_settings_token_map( string $plugin ): array {
	$maps = array(
		'rankmath' => array(
			'{{site_name}}'    => '%sitename%',
			'{{tagline}}'      => '%sitedesc%',
			'{{separator}}'    => '%sep%',
			'{{page}}'         => '%page%',
			'{{author_name}}'  => '%name%',
			'{{author_bio}}'   => '%user_description%',
			'{{date}}'         => '%date%',
			'{{search_query}}' => '%search_query%',
		),
		'yoast' => array(
			'{{site_name}}'    => '%%sitename%%',
			'{{tagline}}'      => '%%sitedesc%%',
			'{{separator}}'    => '%%sep%%',
			'{{page}}'         => '%%page%%',
			'{{author_name}}'  => '%%name%%',
			'{{author_bio}}'   => '%%user_description%%',
			'{{date}}'         => '%%date%%',
			'{{search_query}}' => '%%searchphrase%%',
		),
		'aioseo' => array(
			'{{site_name}}'    => '#site_title',
			'{{tagline}}'      => '#tagline',
			'{{separator}}'    => '#separator_sa',
			'{{page}}'         => '#page_number',
			'{{author_name}}'  => '#author_name',
			'{{author_bio}}'   => '#author_bio',
			'{{date}}'         => '#archive_date',
			'{{search_query}}' => '#search_term',
		),
		'seopress' => array(
			'{{site_name}}'    => '%%sitetitle%%',
			'{{tagline}}'      => '%%tagline%%',
			'{{separator}}'    => '%%sep%%',
			'{{page}}'         => '%%current_pagination%%',
			'{{author_name}}'  => '%%post_author%%',
			'{{author_bio}}'   => '%%author_bio%%',
			'{{date}}'         => '%%archive_date%%',
			'{{search_query}}' => '%%search_keywords%%',
		),
	);
	return $maps[ $plugin ] ?? array();
}

function wpagent_seo_settings_template_from_provider( string $plugin, string $value ): string {
	$map = wpagent_seo_settings_token_map( $plugin );
	return str_replace( array_values( $map ), array_keys( $map ), $value );
}

function wpagent_seo_settings_template_to_provider( string $plugin, string $value ): string {
	$map = wpagent_seo_settings_token_map( $plugin );
	return str_replace( array_keys( $map ), array_values( $map ), $value );
}

function wpagent_seo_settings_provider_token( string $plugin, string $value ): ?string {
	$patterns = array(
		'rankmath' => '/%(?:[a-z0-9_-]+\([^)]*\)|[^\s%]+)%/i',
		'yoast'    => '/%%[^%]+%%/',
		'seopress' => '/%%[^%]+%%/',
	);
	if ( 'aioseo' === $plugin ) {
		foreach ( wpagent_aioseo_registered_provider_tokens() as $token => $_registered ) {
			// Mirror AIOSEO's own boundary: a registered tag is replaced unless
			// followed by a letter, number or underscore. This also catches custom
			// tags with hyphenated arguments without classifying arbitrary hashtags.
			if ( preg_match( '/' . preg_quote( $token, '/' ) . '(?![A-Za-z0-9_])/i', $value, $match ) ) {
				return $match[0];
			}
		}
		return null;
	}
	if ( ! isset( $patterns[ $plugin ] ) || ! preg_match( $patterns[ $plugin ], $value, $match ) ) {
		return null;
	}
	return $match[0];
}

/** @return array<string,true> */
function wpagent_aioseo_registered_provider_tokens(): array {
	static $tokens = null;
	if ( is_array( $tokens ) ) {
		return $tokens;
	}
	$tokens = array_fill_keys( array_values( wpagent_seo_settings_token_map( 'aioseo' ) ), true );
	if ( ! function_exists( 'aioseo' ) || ! isset( aioseo()->tags ) || ! is_callable( array( aioseo()->tags, 'all' ) ) ) {
		return $tokens;
	}
	try {
		$catalog = aioseo()->tags->all();
	} catch ( Throwable ) {
		return $tokens;
	}
	$entries = is_array( $catalog ) && is_array( $catalog['tags'] ?? null ) ? $catalog['tags'] : array();
	foreach ( $entries as $entry ) {
		$id = is_array( $entry ) ? ( $entry['id'] ?? null ) : null;
		if ( is_string( $id ) && preg_match( '/^[A-Za-z_][A-Za-z0-9_-]*$/', $id ) ) {
			$tokens[ '#' . $id ] = true;
		}
	}
	return $tokens;
}

/** @return array<string,string> */
function wpagent_seo_separator_entities(): array {
	return array(
		'-' => '&#45;', '–' => '&ndash;', '—' => '&mdash;', ':' => ':', '·' => '&middot;',
		'•' => '&bull;', '*' => '*', '⋆' => '&#8902;', '|' => '|', '~' => '~',
		'«' => '&laquo;', '»' => '&raquo;', '<' => '&lt;', '>' => '&gt;',
	);
}

function wpagent_seo_separator_from_provider( string $value ): string {
	return html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
}

/** @return array<string,string|bool>|WP_Error */
function wpagent_validate_seo_settings_patch( $value, string $plugin ) {
	if ( ! is_array( $value ) || array() === $value ) {
		return new WP_Error( 'wpagent_seo_settings_empty', __( 'settings muss mindestens ein globales SEO-Feld enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$known   = wpagent_seo_settings_fields();
	$unknown = array_diff( array_keys( $value ), $known );
	if ( array() !== $unknown ) {
		/* translators: %s: comma-separated unknown SEO setting field names. */
		return new WP_Error( 'wpagent_seo_settings_unknown_field', sprintf( __( 'Unbekanntes globales SEO-Feld: %s', 'wp-agent-companion' ), implode( ', ', $unknown ) ), array( 'status' => 400 ) );
	}

	$booleans  = array( 'author_archives_enabled', 'author_archives_noindex', 'date_archives_enabled', 'date_archives_noindex', 'search_noindex' );
	$templates = array( 'homepage_title_template', 'homepage_description_template', 'author_archive_title_template', 'author_archive_description_template', 'date_archive_title_template', 'date_archive_description_template', 'search_title_template', 'search_description_template' );
	$clean     = array();
	foreach ( $value as $field => $raw ) {
		if ( in_array( $field, $booleans, true ) ) {
			if ( ! is_bool( $raw ) ) {
				/* translators: %s: input field name. */
				return new WP_Error( 'wpagent_seo_settings_invalid_boolean', sprintf( __( '%s muss ein boolescher Wert sein.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
			}
			$clean[ $field ] = $raw;
			continue;
		}
		if ( ! is_string( $raw ) || ! wp_is_valid_utf8( $raw ) ) {
			/* translators: %s: SEO setting field name. */
			return new WP_Error( 'wpagent_seo_settings_invalid_text', sprintf( __( '%s muss gültiger UTF-8-Text sein.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
		}
		if ( 'identity_type' === $field ) {
			if ( ! in_array( $raw, array( 'person', 'organization' ), true ) ) {
				return new WP_Error( 'wpagent_seo_settings_invalid_identity', __( 'identity_type muss person oder organization sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$clean[ $field ] = $raw;
			continue;
		}
		if ( 'title_separator' === $field ) {
			if ( ! array_key_exists( $raw, wpagent_seo_separator_entities() ) ) {
				return new WP_Error( 'wpagent_seo_settings_invalid_separator', __( 'title_separator ist kein unterstütztes Trennzeichen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$clean[ $field ] = $raw;
			continue;
		}
		if ( 'identity_logo_url' === $field ) {
			$logo_length = function_exists( 'mb_strlen' ) ? mb_strlen( $raw, 'UTF-8' ) : strlen( $raw );
			if ( $logo_length > 2048 || strlen( $raw ) > wpagent_seo_settings_text_byte_limit( $field ) ) {
				return new WP_Error( 'wpagent_seo_settings_unsafe_text', __( 'identity_logo_url überschreitet die erlaubte Feldgrenze.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			if ( '' === $raw ) {
				$clean[ $field ] = '';
				continue;
			}
			$parts = wp_parse_url( $raw );
			if ( ! is_array( $parts ) || ! isset( $parts['scheme'], $parts['host'] ) || ! in_array( strtolower( (string) $parts['scheme'] ), array( 'http', 'https' ), true ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || esc_url_raw( $raw, array( 'http', 'https' ) ) !== $raw ) {
				return new WP_Error( 'wpagent_seo_settings_invalid_logo', __( 'identity_logo_url muss leer oder eine unveränderte absolute HTTP- beziehungsweise HTTPS-URL sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$clean[ $field ] = $raw;
			continue;
		}

		$limit      = in_array( $field, $templates, true ) ? 4096 : ( 'identity_logo_url' === $field ? 2048 : 300 );
		$byte_limit = wpagent_seo_settings_text_byte_limit( $field );
		$length     = function_exists( 'mb_strlen' ) ? mb_strlen( $raw, 'UTF-8' ) : strlen( $raw );
		if ( $length > $limit || strlen( $raw ) > $byte_limit || preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $raw ) || false !== strpos( $raw, "\r" ) || false !== strpos( $raw, "\n" ) || wp_strip_all_tags( $raw ) !== $raw || trim( $raw ) !== $raw ) {
			/* translators: %s: SEO setting field name. */
			return new WP_Error( 'wpagent_seo_settings_unsafe_text', sprintf( __( '%s enthält HTML, Steuerzeichen, äußere Leerzeichen oder zu viele Zeichen.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
		}
		if ( in_array( $field, $templates, true ) ) {
			$provider_token = wpagent_seo_settings_provider_token( $plugin, $raw );
			if ( null !== $provider_token ) {
				/* translators: 1: SEO setting field name, 2: provider-specific template token. */
				return new WP_Error( 'wpagent_seo_settings_provider_token', sprintf( __( '%1$s enthält den provider-spezifischen Platzhalter %2$s. Verwende ausschließlich die WPAgently-Platzhalter aus template_tokens.', 'wp-agent-companion' ), $field, $provider_token ), array( 'status' => 400 ) );
			}
			preg_match_all( '/\{\{[^{}]+\}\}/', $raw, $matches );
			$invalid = array_diff( array_unique( $matches[0] ), wpagent_seo_settings_template_tokens() );
			if ( array() !== $invalid ) {
				/* translators: %s: comma-separated unknown template tokens. */
				return new WP_Error( 'wpagent_seo_settings_unknown_token', sprintf( __( 'Unbekannter Template-Platzhalter: %s', 'wp-agent-companion' ), implode( ', ', $invalid ) ), array( 'status' => 400 ) );
			}
			$without_tokens = str_replace( wpagent_seo_settings_template_tokens(), '', $raw );
			if ( false !== strpos( $without_tokens, '{{' ) || false !== strpos( $without_tokens, '}}' ) ) {
				/* translators: %s: SEO setting field name. */
				return new WP_Error( 'wpagent_seo_settings_malformed_token', sprintf( __( '%s enthält einen unvollständigen oder ungültigen Template-Platzhalter.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
			}
		}
		$clean[ $field ] = $raw;
	}
	return $clean;
}

/** @return string[] */
function wpagent_seo_settings_option_names( string $plugin ): array {
	$names = array(
		'rankmath' => array( 'rank-math-options-titles' ),
		'yoast'    => array( 'wpseo_titles' ),
		'aioseo'   => array( 'aioseo_options', 'aioseo_options_lite', 'aioseo_options_localized' ),
		'seopress' => array( 'seopress_titles_option_name', 'seopress_social_option_name' ),
	);
	return $names[ $plugin ] ?? array();
}

/** @return array<string,array{exists:bool,value:mixed}> */
function wpagent_seo_settings_snapshot( string $plugin ): array {
	$snapshot = array();
	foreach ( wpagent_seo_settings_option_names( $plugin ) as $name ) {
		$sentinel          = new stdClass();
		$value             = get_option( $name, $sentinel );
		$snapshot[ $name ] = array( 'exists' => ! $value instanceof stdClass, 'value' => $value instanceof stdClass ? null : $value );
	}
	return $snapshot;
}

/** @param array<string,array{exists:bool,value:mixed}> $snapshot */
function wpagent_seo_settings_snapshot_hash( string $plugin, array $snapshot ): string {
	return hash( 'sha256', get_current_blog_id() . "\0" . $plugin . "\0" . maybe_serialize( $snapshot ) );
}

function wpagent_aioseo_refresh_options_from_database(): void {
	$provider        = aioseo();
	$options_service = is_object( $provider ) && isset( $provider->options ) ? $provider->options : null;
	$options_cache   = is_object( $provider ) && isset( $provider->core, $provider->core->optionsCache ) ? $provider->core->optionsCache : null;
	if (
		! is_object( $options_service ) ||
		! property_exists( $options_service, 'shouldSave' ) ||
		! is_object( $options_cache ) ||
		! is_callable( array( $options_cache, 'resetDb' ) ) ||
		! is_callable( array( $options_service, 'noConflict' ) )
	) {
		throw new RuntimeException( 'AIOSEO options cache cannot be refreshed safely.' );
	}

	// save(true) leaves AIOSEO's shutdown save armed. Disarm it before reloading
	// so a verified write or restored snapshot cannot be rewritten at shutdown.
	$options_service->shouldSave = false;
	$options_cache->resetDb();
	$isolated = $options_service->noConflict( true );
	if ( ! is_object( $isolated ) || $isolated === $options_service ) {
		throw new RuntimeException( 'AIOSEO options proxy cannot be refreshed safely.' );
	}

	// The Lite init path loaded by noConflict(true) merges aioseo_options_lite
	// into the shared options cache. Keep the original service's localized
	// values aligned as well in case another callback writes later in this request.
	if ( property_exists( $isolated, 'localized' ) && property_exists( $options_service, 'localized' ) ) {
		$options_service->localized = $isolated->localized;
	}
}

function wpagent_seo_settings_refresh_provider( string $plugin ): void {
	if ( 'rankmath' === $plugin && function_exists( 'rank_math' ) && isset( rank_math()->settings ) ) {
		rank_math()->settings->reset();
	} elseif ( 'yoast' === $plugin && class_exists( 'WPSEO_Options' ) ) {
		WPSEO_Options::clear_cache();
	} elseif ( 'aioseo' === $plugin && function_exists( 'aioseo' ) ) {
		wpagent_aioseo_refresh_options_from_database();
	}
}

/** @param array<string,array{exists:bool,value:mixed}> $snapshot */
function wpagent_restore_seo_settings_snapshot( string $plugin, array $snapshot, array $candidate ): bool {
	if ( wpagent_seo_settings_snapshot( $plugin ) !== $candidate ) {
		return false;
	}
	foreach ( $snapshot as $name => $state ) {
		if ( $state['exists'] ) {
			update_option( $name, $state['value'] );
		} else {
			delete_option( $name );
		}
		wp_cache_delete( $name, 'options' );
	}
	try {
		wpagent_seo_settings_refresh_provider( $plugin );
	} catch ( Throwable ) {
		return false;
	}
	return wpagent_seo_settings_snapshot( $plugin ) === $snapshot;
}

/** @return string|WP_Error */
function wpagent_seo_settings_acquire_lock() {
	$token = wpagent_companion_acquire_option_lock( WPAGENT_SEO_SETTINGS_LOCK_OPTION, WPAGENT_SEO_SETTINGS_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_seo_settings_locked', __( 'Die globalen SEO-Einstellungen werden gerade geändert. Bitte erneut versuchen.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_seo_settings_release_lock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_SEO_SETTINGS_LOCK_OPTION, $token );
}

function wpagent_seo_settings_lock_is_owned( string $token ): bool {
	return wpagent_companion_option_lock_is_owned( WPAGENT_SEO_SETTINGS_LOCK_OPTION, $token );
}

function wpagent_seo_settings_refresh_lock( string $token ): bool {
	return wpagent_companion_refresh_option_lock( WPAGENT_SEO_SETTINGS_LOCK_OPTION, $token, WPAGENT_SEO_SETTINGS_LOCK_TTL );
}

function wpagent_seo_settings_lock_checkpoint( string $token ): bool {
	return wpagent_seo_settings_lock_is_owned( $token ) && wpagent_seo_settings_refresh_lock( $token );
}
