<?php
/** Shared state, locking and rollback helpers for WordPress Global Styles. */

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

define( 'WPAGENT_GLOBAL_STYLES_LOCK_OPTION', 'wp_agent_global_styles_lock' );
define( 'WPAGENT_GLOBAL_STYLES_LOCK_TTL', 300 );
define( 'WPAGENT_GLOBAL_STYLES_MAX_BYTES', 524288 );
define( 'WPAGENT_GLOBAL_STYLES_MAX_PRESETS', 60 );
define( 'WPAGENT_GLOBAL_STYLES_MAX_PALETTE_SLUGS', 512 );
define( 'WPAGENT_GLOBAL_STYLES_DOCUMENT_FORMAT', 'wpagently-json-document-v1' );

/** @return array<string,mixed> */
function wpagent_gs_schema_object( array $properties, array $required = array() ): array {
	return array(
		'type'                 => 'object',
		'properties'           => $properties,
		'required'             => $required,
		'additionalProperties' => false,
		'maxProperties'        => count( $properties ),
	);
}

/** @return array<string,mixed> */
function wpagent_gs_hash_schema(): array {
	return array( 'type' => 'string', 'minLength' => 64, 'maxLength' => 64, 'pattern' => '^[a-f0-9]{64}$' );
}

/** @return array<string,mixed> */
function wpagent_gs_slug_schema(): array {
	return array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 40, 'pattern' => '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$' );
}

/**
 * The published palette is allowed to retain the one exact native Spectra One
 * base slug that WordPress exposes. Custom input continues to use the strict
 * lower-case schema above.
 *
 * @return array<string,mixed>
 */
function wpagent_gs_palette_slug_schema(): array {
	$schema            = wpagent_gs_slug_schema();
	$schema['pattern'] = '^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|currentColor)$';
	return $schema;
}

/** @return array<string,mixed> */
function wpagent_gs_token_name_schema(): array {
	return array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 100 );
}

/** @return array<string,mixed> */
function wpagent_gs_color_schema(): array {
	return array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 64 );
}

/** @return array<string,mixed> */
function wpagent_gs_size_schema(): array {
	return array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 64 );
}

/** @return array<string,mixed> */
function wpagent_gs_font_family_schema(): array {
	return array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 200 );
}

/** @return array<string,mixed> */
function wpagent_gs_preset_schema( string $value_key, array $value_schema ): array {
	$properties = array(
		'slug' => wpagent_gs_slug_schema(),
		'name' => wpagent_gs_token_name_schema(),
	);
	$properties[ $value_key ] = $value_schema;
	return wpagent_gs_schema_object(
		$properties,
		array( 'slug', $value_key )
	);
}

/** @return array<string,mixed> */
function wpagent_gs_style_color_schema(): array {
	return array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 64 );
}

/** @return array<string,mixed> */
function wpagent_gs_styles_schema(): array {
	$color = wpagent_gs_schema_object(
		array(
			'background' => wpagent_gs_style_color_schema(),
			'text'       => wpagent_gs_style_color_schema(),
		),
		array()
	);
	$color['minProperties'] = 1;
	$spacing = wpagent_gs_schema_object( array( 'blockGap' => wpagent_gs_size_schema() ), array( 'blockGap' ) );
	$button  = wpagent_gs_schema_object( array( 'color' => $color ), array( 'color' ) );
	$link_color = wpagent_gs_schema_object( array( 'text' => wpagent_gs_style_color_schema() ), array( 'text' ) );
	$link = wpagent_gs_schema_object( array( 'color' => $link_color ), array( 'color' ) );
	$elements = wpagent_gs_schema_object( array( 'button' => $button, 'link' => $link ), array() );
	$elements['minProperties'] = 1;
	$styles = wpagent_gs_schema_object( array( 'color' => $color, 'spacing' => $spacing, 'elements' => $elements ), array() );
	$styles['minProperties'] = 1;
	return $styles;
}

/** @return array<string,mixed> */
function wpagent_gs_applied_schema(): array {
	return wpagent_gs_schema_object(
		array(
			'colors'       => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => WPAGENT_GLOBAL_STYLES_MAX_PRESETS ),
			'fontSizes'    => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => WPAGENT_GLOBAL_STYLES_MAX_PRESETS ),
			'spacingSizes' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => WPAGENT_GLOBAL_STYLES_MAX_PRESETS ),
			'fontFamilies' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => WPAGENT_GLOBAL_STYLES_MAX_PRESETS ),
			'styles'       => array( 'type' => 'array', 'maxItems' => 3, 'items' => array( 'type' => 'string', 'enum' => array( 'color', 'spacing', 'elements' ), 'maxLength' => 8 ) ),
		)
	);
}

/** @return array<string,mixed> */
function wpagent_gs_json_document_schema(): array {
	return wpagent_gs_schema_object(
		array(
			'format' => array( 'type' => 'string', 'enum' => array( WPAGENT_GLOBAL_STYLES_DOCUMENT_FORMAT ), 'maxLength' => 64 ),
			'json'   => array( 'type' => 'string', 'minLength' => 2, 'maxLength' => WPAGENT_GLOBAL_STYLES_MAX_BYTES ),
			'sha256' => wpagent_gs_hash_schema(),
			'bytes'  => array( 'type' => 'integer', 'minimum' => 2, 'maximum' => WPAGENT_GLOBAL_STYLES_MAX_BYTES ),
		),
		array( 'format', 'json', 'sha256', 'bytes' )
	);
}

/** @return mixed */
function wpagent_gs_canonicalize_json_value( $value ) {
	if ( is_object( $value ) ) {
		$properties = get_object_vars( $value );
		ksort( $properties, SORT_STRING );
		$out = new stdClass();
		foreach ( $properties as $key => $child ) {
			$out->{ (string) $key } = wpagent_gs_canonicalize_json_value( $child );
		}
		return $out;
	}
	if ( is_array( $value ) ) {
		if ( array_is_list( $value ) ) {
			return array_map( 'wpagent_gs_canonicalize_json_value', $value );
		}
		ksort( $value, SORT_STRING );
		$out = new stdClass();
		foreach ( $value as $key => $child ) {
			$out->{ (string) $key } = wpagent_gs_canonicalize_json_value( $child );
		}
		return $out;
	}
	return $value;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_gs_json_document( $value ) {
	if ( ! is_object( $value ) && ( ! is_array( $value ) || array_is_list( $value ) ) ) {
		return new WP_Error( 'wpagent_gs_document_invalid', __( 'Das Global-Styles-Dokument muss ein JSON-Objekt sein.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$canonical = wpagent_gs_canonicalize_json_value( $value );
	$json      = wp_json_encode( $canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	if ( false === $json || strlen( $json ) > WPAGENT_GLOBAL_STYLES_MAX_BYTES ) {
		return new WP_Error( 'wpagent_gs_document_too_large', __( 'Das Global-Styles-Dokument ist größer als 512 KiB oder nicht sicher kodierbar.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	return array(
		'format' => WPAGENT_GLOBAL_STYLES_DOCUMENT_FORMAT,
		'json'   => $json,
		'sha256' => hash( 'sha256', $json ),
		'bytes'  => strlen( $json ),
	);
}

function wpagent_gs_missing_post_error(): WP_Error {
	return new WP_Error( 'wpagent_gs_no_post', __( 'Für das aktive Theme existiert noch kein User-Global-Styles-Datensatz. Öffne und speichere Global Styles einmal im WordPress-Editor, bevor du sie mit dem Agenten liest oder änderst.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_gs_state() {
	if ( ! class_exists( 'WP_Theme_JSON_Resolver' ) ) {
		return new WP_Error( 'wpagent_gs_unsupported', __( 'WP_Theme_JSON_Resolver ist nicht verfügbar (Block-Theme/Global-Styles benötigt).', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$existing = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), false );
	$post_id  = is_array( $existing ) ? (int) ( $existing['ID'] ?? 0 ) : 0;
	if ( $post_id <= 0 ) {
		return wpagent_gs_missing_post_error();
	}
	$row = wpagent_gs_post_row_snapshot( $post_id );
	if ( is_wp_error( $row ) ) {
		return $row;
	}
	$raw      = $row['raw'];
	if ( strlen( $raw ) > WPAGENT_GLOBAL_STYLES_MAX_BYTES ) {
		return new WP_Error( 'wpagent_gs_state_too_large', __( 'Der gespeicherte Global-Styles-Datensatz ist größer als 512 KiB und wird deshalb nicht über den Agenten verarbeitet.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$snapshot_export = json_decode( $raw );
	$snapshot        = json_decode( $raw, true );
	if ( ! is_object( $snapshot_export ) || ! is_array( $snapshot ) ) {
		return new WP_Error( 'wpagent_gs_invalid_state', __( 'Der gespeicherte Global-Styles-Datensatz ist kein gültiges JSON-Objekt.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$encoded = wp_json_encode( $snapshot_export, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	if ( false === $encoded ) {
		return new WP_Error( 'wpagent_gs_invalid_state', __( 'Der gespeicherte Global-Styles-Datensatz konnte nicht sicher gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	if ( strlen( $encoded ) > WPAGENT_GLOBAL_STYLES_MAX_BYTES ) {
		return new WP_Error( 'wpagent_gs_state_too_large', __( 'Der gespeicherte Global-Styles-Datensatz ist größer als 512 KiB und wird deshalb nicht über den Agenten verarbeitet.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$theme = (string) get_stylesheet();
	return array(
		'post_id'         => $row['post_id'],
		'theme'           => $theme,
		'raw'             => $raw,
		'post_modified'   => $row['post_modified'],
		'post_modified_gmt' => $row['post_modified_gmt'],
		'snapshot'        => $snapshot,
		'snapshot_export' => $snapshot_export,
		'snapshot_bytes'  => strlen( $encoded ),
		'state_hash'      => wpagent_gs_state_hash( $theme, $row['post_id'], $raw, $row['post_modified'], $row['post_modified_gmt'] ),
	);
}

/** @return array{post_id:int,raw:string,post_modified:string,post_modified_gmt:string}|WP_Error */
function wpagent_gs_post_row_snapshot( int $post_id ) {
	clean_post_cache( $post_id );
	$post = get_post( $post_id );
	if ( ! $post instanceof WP_Post || 'wp_global_styles' !== $post->post_type ) {
		return wpagent_gs_missing_post_error();
	}
	return array(
		'post_id'           => (int) $post->ID,
		'raw'               => (string) $post->post_content,
		'post_modified'     => (string) $post->post_modified,
		'post_modified_gmt' => (string) $post->post_modified_gmt,
	);
}

function wpagent_gs_state_hash( string $theme, int $post_id, string $raw, string $post_modified, string $post_modified_gmt ): string {
	return hash( 'sha256', "wpagently-global-styles-v2\0" . $theme . "\0" . $post_id . "\0" . $raw . "\0" . $post_modified . "\0" . $post_modified_gmt );
}

/** Confirms that the active dataset, plan and (when provided) exact written row still match. */
function wpagent_gs_final_state_matches( array $fresh, array $initial, array $planned, ?array $candidate = null ): bool {
	$candidate_matches = null === $candidate
		|| (
			isset( $candidate['post_id'], $candidate['raw'], $candidate['post_modified'], $candidate['post_modified_gmt'] )
			&& (int) ( $fresh['post_id'] ?? 0 ) === (int) $candidate['post_id']
			&& (string) ( $fresh['raw'] ?? '' ) === (string) $candidate['raw']
			&& (string) ( $fresh['post_modified'] ?? '' ) === (string) $candidate['post_modified']
			&& (string) ( $fresh['post_modified_gmt'] ?? '' ) === (string) $candidate['post_modified_gmt']
		);
	return $candidate_matches
		&& (int) ( $fresh['post_id'] ?? 0 ) === (int) ( $initial['post_id'] ?? 0 )
		&& isset( $fresh['snapshot'] )
		&& is_array( $fresh['snapshot'] )
		&& $fresh['snapshot'] === $planned;
}

/**
 * Encodes a user Global Styles document while keeping its object-shaped roots.
 * PHP otherwise serializes an empty associative array as JSON `[]`, although
 * WordPress theme.json requires `settings` and `styles` to remain objects.
 *
 * @return string|WP_Error
 */
function wpagent_gs_encode_user_config( array $data ) {
	$encoded_data             = $data;
	$encoded_data['settings'] = (object) ( isset( $data['settings'] ) && is_array( $data['settings'] ) ? $data['settings'] : array() );
	$encoded_data['styles']   = (object) ( isset( $data['styles'] ) && is_array( $data['styles'] ) ? $data['styles'] : array() );
	$json                     = wp_json_encode( $encoded_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	if ( false === $json ) {
		return new WP_Error( 'wpagent_gs_encode', __( 'Global-Styles-Daten konnten nicht kodiert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return $json;
}

/** @return string|WP_Error */
function wpagent_gs_expected_hash( $value ) {
	$hash = is_string( $value ) ? strtolower( $value ) : '';
	return preg_match( '/^[a-f0-9]{64}$/', $hash )
		? $hash
		: new WP_Error( 'wpagent_gs_expected_hash', __( 'expected_hash muss ein vollständiger SHA-256-Hash sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @return string|WP_Error */
function wpagent_gs_acquire_lock() {
	$token = wpagent_companion_acquire_option_lock( WPAGENT_GLOBAL_STYLES_LOCK_OPTION, WPAGENT_GLOBAL_STYLES_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_gs_busy', __( 'Das globale Design wird gerade geändert. Versuche es gleich erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_gs_release_lock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_GLOBAL_STYLES_LOCK_OPTION, $token );
}

function wpagent_gs_refresh_lock( string $token ): bool {
	return wpagent_companion_refresh_option_lock( WPAGENT_GLOBAL_STYLES_LOCK_OPTION, $token, WPAGENT_GLOBAL_STYLES_LOCK_TTL );
}

function wpagent_gs_lock_is_owned( string $token ): bool {
	return wpagent_companion_option_lock_is_owned( WPAGENT_GLOBAL_STYLES_LOCK_OPTION, $token );
}

function wpagent_gs_clean_caches( int $post_id ): void {
	clean_post_cache( $post_id );
	WP_Theme_JSON_Resolver::clean_cached_data();
	if ( function_exists( 'wp_clean_theme_json_cache' ) ) {
		wp_clean_theme_json_cache();
	}
}

/** @param array<string,mixed> $before @return array{post_id:int,raw:string,post_modified:string,post_modified_gmt:string}|WP_Error */
function wpagent_gs_write_if_current( array $before, string $raw ) {
	if (
		! isset( $before['post_id'], $before['raw'], $before['post_modified'], $before['post_modified_gmt'] )
		|| ! is_int( $before['post_id'] )
		|| ! is_string( $before['raw'] )
		|| ! is_string( $before['post_modified'] )
		|| ! is_string( $before['post_modified_gmt'] )
	) {
		return new WP_Error( 'wpagent_gs_snapshot_invalid', __( 'Der Global-Styles-Snapshot ist für eine bedingte Änderung unvollständig.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	if ( $raw === $before['raw'] ) {
		return $before;
	}
	$modified     = current_time( 'mysql' );
	$modified_gmt = current_time( 'mysql', true );
	global $wpdb;
	$updated = $wpdb->update(
		$wpdb->posts,
		array(
			'post_content'      => $raw,
			'post_modified'     => $modified,
			'post_modified_gmt' => $modified_gmt,
		),
		array(
			'ID'                => $before['post_id'],
			'post_type'         => 'wp_global_styles',
			'post_content'      => (string) $before['raw'],
			'post_modified'     => (string) $before['post_modified'],
			'post_modified_gmt' => (string) $before['post_modified_gmt'],
		),
		array( '%s', '%s', '%s' ),
		array( '%d', '%s', '%s', '%s', '%s' )
	);
	if ( 1 !== $updated ) {
		$fresh = wpagent_gs_post_row_snapshot( $before['post_id'] );
		if ( is_wp_error( $fresh ) ) {
			return new WP_Error( 'wpagent_gs_write_unverified', __( 'Die bedingte Global-Styles-Änderung konnte nicht sicher zurückgelesen werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
		}
		return new WP_Error( 'wpagent_gs_conflict', __( 'Die Global Styles wurden vor dem Schreiben durch einen anderen Editor verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_raw_hash' => hash( 'sha256', $fresh['raw'] ) ) );
	}
	wpagent_gs_clean_caches( $before['post_id'] );
	$fresh = wpagent_gs_post_row_snapshot( $before['post_id'] );
	if ( is_wp_error( $fresh ) || $raw !== ( $fresh['raw'] ?? null ) ) {
		return new WP_Error( 'wpagent_gs_write_unverified', __( 'Die bedingte Global-Styles-Änderung konnte nicht exakt zurückgelesen werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => true ) );
	}
	return $fresh;
}

/** @param array<string,mixed> $snapshot @param mixed $candidate */
function wpagent_gs_restore_snapshot( array $snapshot, $candidate = null, string $lock = '' ): bool {
	if ( ! is_array( $candidate ) || '' === $lock || ! current_user_can( 'edit_theme_options' ) || ! wpagent_gs_refresh_lock( $lock ) || ! wpagent_gs_lock_is_owned( $lock ) ) {
		return false;
	}
	$restored = wpagent_gs_write_if_current( $candidate, (string) ( $snapshot['raw'] ?? '' ) );
	return ! is_wp_error( $restored ) && (string) ( $snapshot['raw'] ?? '' ) === $restored['raw'];
}

/** @param array<string,mixed> $snapshot @param array<string,mixed> $candidate */
function wpagent_gs_fail_after_rollback( array $snapshot, array $candidate, string $lock, WP_Error $failure ): WP_Error {
	if ( wpagent_gs_restore_snapshot( $snapshot, $candidate, $lock ) ) {
		$failure->add_data( array_merge( (array) $failure->get_error_data(), array( 'rolled_back' => true, 'rollback_verified' => true, 'recovery_required' => false ) ) );
		return $failure;
	}
	return new WP_Error(
		'wpagent_gs_recovery_required',
		__( 'Die Global-Styles-Änderung konnte nicht verifiziert werden. Ein fremder Zwischenstand wird nicht automatisch überschrieben. Prüfe den aktuellen Zustand manuell.', 'wp-agent-companion' ),
		array( 'status' => 500, 'original_error' => $failure->get_error_code(), 'recovery_required' => true, 'rolled_back' => false, 'rollback_verified' => false )
	);
}

function wpagent_ability_get_global_styles(): array {
	$snapshot = wpagent_gs_json_document_schema();
	return array(
		'label'               => __( 'Global Styles lesen und exportieren', 'wp-agent-companion' ),
		'description'         => __( 'Liest den vollständigen User-Global-Styles-Datensatz als exportierbaren Snapshot mit exaktem Konflikt-Hash.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array( 'type' => 'object', 'properties' => array(), 'additionalProperties' => false, 'maxProperties' => 0, 'default' => array() ),
		'output_schema'       => array(
			'type' => 'object', 'properties' => array(
				'post_id' => array( 'type' => 'integer', 'minimum' => 1 ), 'theme' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 200 ),
				'snapshot' => $snapshot, 'snapshot_bytes' => array( 'type' => 'integer', 'minimum' => 2, 'maximum' => WPAGENT_GLOBAL_STYLES_MAX_BYTES ),
				'state_hash' => wpagent_gs_hash_schema(),
			),
			'required' => array( 'post_id', 'theme', 'snapshot', 'snapshot_bytes', 'state_hash' ), 'additionalProperties' => false, 'maxProperties' => 5,
		),
		'execute_callback'    => 'wpagent_exec_get_global_styles',
		'permission_callback' => 'wpagent_perm_edit_theme_options',
		'meta'                => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ) ),
	);
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_get_global_styles() {
	$state = wpagent_gs_state();
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	$snapshot = wpagent_gs_json_document( $state['snapshot_export'] );
	if ( is_wp_error( $snapshot ) ) {
		return $snapshot;
	}
	return array(
		'post_id'        => (int) $state['post_id'],
		'theme'          => (string) $state['theme'],
		'snapshot'       => $snapshot,
		'snapshot_bytes' => (int) $snapshot['bytes'],
		'state_hash'     => (string) $state['state_hash'],
	);
}
