<?php
/**
 * Ability: wp-agent/set-global-styles
 *
 * Setzt das Design-System der Site (Farb-Palette, Schriftgroessen, Abstands-Skala,
 * Schriftfamilien und ausgewaehlte Standard-Styles) site-weit, OHNE Theme-Dateien
 * anzufassen. Der Mechanismus ist der WordPress-eigene User-Global-Styles-Datensatz:
 * das aktive Theme hat genau einen wp_global_styles-Post (CPT), den WordPress als
 * User-Override ueber die theme.json-Defaults des Themes legt (Herkunft "custom" in
 * der zusammengefuehrten theme.json). Wir schreiben ausschliesslich in diesen Post,
 * die Theme-Dateien und die vom Theme gelieferten Slugs (z. B. accent-1..6) bleiben
 * unangetastet. Neue Marken-Slugs kommen additiv als "custom"-Herkunft hinzu, sodass
 * der Pattern-Builder (upsert-pattern.php) sie ueber wp_get_global_settings sieht und
 * automatisch verwenden kann.
 *
 * SICHERHEIT:
 *   - Strikte Schema-Validierung: es wird NUR die bekannte Design-Token-Struktur
 *     akzeptiert. Slugs nutzen Kleinbuchstaben, Ziffern und Bindestriche ohne
 *     fuehrenden/abschliessenden Bindestrich; Farben nur Hex oder eine eng
 *     validierte rgb()/hsl()-Form; Groessen nur validierte CSS-Laengen/clamp(); Font-
 *     Familien nur ein sauberer Font-Stack ohne url()/@font-face. KEIN beliebiges JSON
 *     wird durchgereicht, KEIN freies css-Feld (keine CSS-Injection).
 *   - Gating ueber edit_theme_options (design-privilegiert, hoeher als der edit_posts
 *     des Standard-Editor-Bots, siehe wpagent_perm_edit_theme_options in create-menu.php).
 *   - Re-Read-Verifikation: nach dem Schreiben wird der Post frisch aus der DB gelesen
 *     und geprueft, dass jeder geschriebene Token unveraendert zurueckkommt. Bei einer
 *     Abweichung schlaegt die Ability mit WP_Error fehl.
 *   - Idempotent: bei replace=false werden Tokens per Slug upgesertet, ein erneuter
 *     Aufruf mit denselben Werten fuehrt zum selben Zustand.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_set_global_styles(): array {
	$slug_schema  = array( 'type' => 'string', 'description' => __( 'Slug: nur Kleinbuchstaben, Ziffern und Bindestriche (z. B. brand-primary oder 40).', 'wp-agent-companion' ) );
	$color_item   = array(
		'type'       => 'object',
		'properties' => array(
			'slug'  => $slug_schema,
			'color' => array( 'type' => 'string', 'description' => __( 'Farbwert als Hex (#rgb, #rrggbb, #rrggbbaa) oder eng validiertes rgb()/rgba()/hsl()/hsla().', 'wp-agent-companion' ) ),
			'name'  => array( 'type' => 'string', 'description' => __( 'Optionaler Anzeigename.', 'wp-agent-companion' ) ),
		),
		'required'   => array( 'slug', 'color' ),
		'additionalProperties' => false,
	);
	$size_item    = array(
		'type'       => 'object',
		'properties' => array(
			'slug' => $slug_schema,
			'size' => array( 'type' => 'string', 'description' => __( 'CSS-Länge (z. B. 1.25rem, 18px) oder clamp()/min()/max()/calc().', 'wp-agent-companion' ) ),
			'name' => array( 'type' => 'string', 'description' => __( 'Optionaler Anzeigename.', 'wp-agent-companion' ) ),
		),
		'required'   => array( 'slug', 'size' ),
		'additionalProperties' => false,
	);
	$family_item  = array(
		'type'       => 'object',
		'properties' => array(
			'slug'       => $slug_schema,
			'fontFamily' => array( 'type' => 'string', 'description' => __( 'Font-Stack, z. B. "Geist", system-ui, sans-serif. Kein url()/@font-face, keine Remote-Ladung.', 'wp-agent-companion' ) ),
			'name'       => array( 'type' => 'string', 'description' => __( 'Optionaler Anzeigename.', 'wp-agent-companion' ) ),
		),
		'required'   => array( 'slug', 'fontFamily' ),
		'additionalProperties' => false,
	);

	return array(
		'label'               => __( 'Design-System (Global Styles) setzen', 'wp-agent-companion' ),
		'description'         => __( 'Setzt Farb-Palette, Schriftgrößen, Abstands-Skala, Schriftfamilien und ausgewählte Standard-Styles als site-weiten User-Override (wp_global_styles), ohne Theme-Dateien zu verändern. Strikt schema-validiert; benötigt edit_theme_options.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'expected_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$', 'description' => __( 'Exakter state_hash aus get-global-styles.', 'wp-agent-companion' ) ),
				'colors'       => array( 'type' => 'array', 'maxItems' => 60, 'items' => $color_item, 'description' => __( 'Custom-Farb-Palette (additiv zur Theme-Palette).', 'wp-agent-companion' ) ),
				'fontSizes'    => array( 'type' => 'array', 'maxItems' => 60, 'items' => $size_item, 'description' => __( 'Schriftgrößen-Skala.', 'wp-agent-companion' ) ),
				'spacingSizes' => array( 'type' => 'array', 'maxItems' => 60, 'items' => $size_item, 'description' => __( 'Abstands-Skala.', 'wp-agent-companion' ) ),
				'fontFamilies' => array( 'type' => 'array', 'maxItems' => 60, 'items' => $family_item, 'description' => __( 'Schriftfamilien.', 'wp-agent-companion' ) ),
				'styles'       => array(
					'type'        => 'object',
					'description' => __( 'Enger Allowlist-Ausschnitt der Standard-Styles: color.background, color.text, spacing.blockGap, elements.button.color.background/text, elements.link.color.text.', 'wp-agent-companion' ),
				),
				'replace'      => array(
					'type'        => 'boolean',
					'description' => __( 'true: die übergebenen Token-Kategorien ersetzen die bestehende Custom-Liste vollständig. false (Default): Upsert per Slug.', 'wp-agent-companion' ),
				),
			),
			'required'             => array( 'expected_hash' ),
			'additionalProperties' => false,
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'post_id'       => array( 'type' => 'integer' ),
				'theme'         => array( 'type' => 'string' ),
				'version'       => array( 'type' => 'integer' ),
				'replace'       => array( 'type' => 'boolean' ),
				'applied'       => array( 'type' => 'object' ),
				'palette_slugs' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'state_hash'    => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'verified'      => array( 'type' => 'boolean' ),
			),
			'required'             => array( 'post_id', 'theme', 'version', 'replace', 'applied', 'palette_slugs', 'state_hash', 'verified' ),
			'additionalProperties' => false,
		),
		'execute_callback'    => 'wpagent_exec_set_global_styles',
		'permission_callback' => 'wpagent_perm_edit_theme_options',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => true,
			),
		),
	);
}


/* ---------------------------------------------------------------------------
 * Ausfuehrung
 * ------------------------------------------------------------------------- */

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_set_global_styles( $input ) {
	$input = is_array( $input ) ? $input : array();
	$hash  = wpagent_gs_expected_hash( $input['expected_hash'] ?? '' );
	if ( is_wp_error( $hash ) ) {
		return $hash;
	}
	$lock = wpagent_gs_acquire_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		return wpagent_exec_set_global_styles_locked( $input, $hash );
	} finally {
		wpagent_gs_release_lock( $lock );
	}
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_set_global_styles_locked( array $input, string $expected_hash ) {
	$state = wpagent_gs_state();
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	if ( ! hash_equals( (string) $state['state_hash'], $expected_hash ) ) {
		return new WP_Error( 'wpagent_gs_conflict', __( 'Die Global Styles wurden seit dem Lesen verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $state['state_hash'] ) );
	}
	if ( (string) $state['theme'] !== (string) get_stylesheet() ) {
		return new WP_Error( 'wpagent_gs_theme_changed', __( 'Das aktive Theme wurde während der Vorbereitung gewechselt. Lies die Global Styles erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}

	$replace = ! empty( $input['replace'] );

	// 1. Alle uebergebenen Kategorien validieren, bevor irgendetwas geschrieben wird.
	$validated = array();
	if ( isset( $input['colors'] ) ) {
		$res = wpagent_gs_validate_preset_list( $input['colors'], 'color', 'wpagent_gs_validate_color', 'colors' );
		if ( is_wp_error( $res ) ) {
			return $res;
		}
		$validated['colors'] = $res;
	}
	if ( isset( $input['fontSizes'] ) ) {
		$res = wpagent_gs_validate_preset_list( $input['fontSizes'], 'size', 'wpagent_gs_validate_size', 'fontSizes' );
		if ( is_wp_error( $res ) ) {
			return $res;
		}
		$validated['fontSizes'] = $res;
	}
	if ( isset( $input['spacingSizes'] ) ) {
		$res = wpagent_gs_validate_preset_list( $input['spacingSizes'], 'size', 'wpagent_gs_validate_size', 'spacingSizes' );
		if ( is_wp_error( $res ) ) {
			return $res;
		}
		$validated['spacingSizes'] = $res;
	}
	if ( isset( $input['fontFamilies'] ) ) {
		$res = wpagent_gs_validate_preset_list( $input['fontFamilies'], 'fontFamily', 'wpagent_gs_validate_font_family', 'fontFamilies' );
		if ( is_wp_error( $res ) ) {
			return $res;
		}
		$validated['fontFamilies'] = $res;
	}
	$validated_styles = array();
	if ( isset( $input['styles'] ) ) {
		$res = wpagent_gs_validate_styles( $input['styles'] );
		if ( is_wp_error( $res ) ) {
			return $res;
		}
		$validated_styles = $res;
	}

	if ( array() === $validated && array() === $validated_styles ) {
		return new WP_Error( 'wpagent_gs_empty', __( 'Keine gültigen Design-Token übergeben (colors/fontSizes/spacingSizes/fontFamilies/styles).', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}

	// 2. User-Global-Styles-Post des aktiven Themes ermitteln (wird bei Bedarf von
	//    WordPress angelegt) und aktuellen Inhalt dekodieren.
	$post_id = (int) $state['post_id'];
	$data    = $state['snapshot'];
	$version                             = isset( $data['version'] ) && is_int( $data['version'] ) ? $data['version'] : 3;
	$data['version']                     = $version;
	$data['isGlobalStylesUserThemeJSON'] = true;
	if ( ! isset( $data['settings'] ) || ! is_array( $data['settings'] ) ) {
		$data['settings'] = array();
	}
	if ( ! isset( $data['styles'] ) || ! is_array( $data['styles'] ) ) {
		$data['styles'] = array();
	}

	// 3. Validierte Tokens einmergen.
	if ( isset( $validated['colors'] ) ) {
		$existing                             = ( $data['settings']['color']['palette'] ?? array() );
		$data['settings']['color']['palette'] = wpagent_gs_merge_preset_node( $existing, $validated['colors'], $replace );
	}
	if ( isset( $validated['fontSizes'] ) ) {
		$existing                                    = ( $data['settings']['typography']['fontSizes'] ?? array() );
		$data['settings']['typography']['fontSizes'] = wpagent_gs_merge_preset_node( $existing, $validated['fontSizes'], $replace );
	}
	if ( isset( $validated['fontFamilies'] ) ) {
		$existing                                       = ( $data['settings']['typography']['fontFamilies'] ?? array() );
		$data['settings']['typography']['fontFamilies'] = wpagent_gs_merge_preset_node( $existing, $validated['fontFamilies'], $replace );
	}
	if ( isset( $validated['spacingSizes'] ) ) {
		$existing                                    = ( $data['settings']['spacing']['spacingSizes'] ?? array() );
		$data['settings']['spacing']['spacingSizes'] = wpagent_gs_merge_preset_node( $existing, $validated['spacingSizes'], $replace );
	}
	if ( array() !== $validated_styles ) {
		$base           = ( isset( $data['styles'] ) && is_array( $data['styles'] ) ) ? $data['styles'] : array();
		$data['styles'] = wpagent_gs_deep_merge( $base, $validated_styles );
	}

	if ( array() !== $validated_styles ) {
		$color_slugs   = array_merge(
			wpagent_gs_base_preset_slugs( array( 'color', 'palette' ) ),
			wpagent_gs_collect_preset_slugs( $data['settings']['color']['palette'] ?? array() )
		);
		$spacing_slugs = array_merge(
			wpagent_gs_base_preset_slugs( array( 'spacing', 'spacingSizes' ) ),
			wpagent_gs_collect_preset_slugs( $data['settings']['spacing']['spacingSizes'] ?? array() )
		);
		$ref_check     = wpagent_gs_validate_style_references(
			$validated_styles,
			array_values( array_unique( $color_slugs ) ),
			array_values( array_unique( $spacing_slugs ) )
		);
		if ( is_wp_error( $ref_check ) ) {
			return $ref_check;
		}
	}

	// 4. Schreiben. wp_json_encode ohne Slash-Escapes, damit der gespeicherte Inhalt
	//    exakt dem Re-Read entspricht.
	$json = wpagent_gs_encode_user_config( $data );
	if ( is_wp_error( $json ) ) {
		return $json;
	}
	if ( strlen( $json ) > WPAGENT_GLOBAL_STYLES_MAX_BYTES ) {
		return new WP_Error( 'wpagent_gs_result_too_large', __( 'Der resultierende Global-Styles-Datensatz wäre größer als 512 KiB und wurde nicht gespeichert.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( (string) $state['theme'] !== (string) get_stylesheet() ) {
		return new WP_Error( 'wpagent_gs_theme_changed', __( 'Das aktive Theme wurde während der Vorbereitung gewechselt. Lies die Global Styles erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$updated = wp_update_post(
		array(
			'ID'           => $post_id,
			'post_content' => wp_slash( $json ),
		),
		true
	);
	if ( is_wp_error( $updated ) ) {
		return $updated;
	}

	// 5. Theme-JSON-Caches leeren, damit nachfolgende Requests (z. B. der Pattern-
	//    Builder) die neuen Tokens sehen.
	wpagent_gs_clean_caches( $post_id );

	// 6. Re-Read direkt aus der DB (Post-Cache invalidieren) und jeden geschriebenen
	//    Token gegenpruefen. So faellt jedes stille Verschlucken beim Speichern auf.
	clean_post_cache( $post_id );
	$reread = get_post( $post_id );
	$rdata  = $reread ? json_decode( (string) $reread->post_content, true ) : null;
	if ( ! is_array( $rdata ) ) {
		return wpagent_gs_fail_after_rollback( $state, new WP_Error( 'wpagent_gs_verify', __( 'Re-Read der Global-Styles fehlgeschlagen (Inhalt nicht dekodierbar).', 'wp-agent-companion' ), array( 'status' => 500 ) ) );
	}
	if ( $data !== $rdata ) {
		return wpagent_gs_fail_after_rollback( $state, new WP_Error( 'wpagent_gs_verify', __( 'Die Global Styles wurden nicht exakt wie geplant gespeichert.', 'wp-agent-companion' ), array( 'status' => 500 ) ) );
	}

	$verify = wpagent_gs_verify_written( $rdata, $validated, $validated_styles );
	if ( is_wp_error( $verify ) ) {
		return wpagent_gs_fail_after_rollback( $state, $verify );
	}
	$fresh_state = wpagent_gs_state();
	if ( is_wp_error( $fresh_state ) ) {
		return $fresh_state;
	}
	if ( (string) $fresh_state['theme'] !== (string) $state['theme'] ) {
		return new WP_Error( 'wpagent_gs_theme_changed', __( 'Das aktive Theme wurde während des Schreibens gewechselt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( ! wpagent_gs_final_state_matches( $fresh_state, $state, $data ) ) {
		// The first read-back already matched our plan. A later mismatch is an
		// external concurrent write and must never be overwritten by rollback.
		return new WP_Error( 'wpagent_gs_state_changed', __( 'Der aktive Global-Styles-Zustand wurde während des Schreibens erneut verändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}

	// 7. Report: welche Tokens angewandt wurden und die aktuell wirksame Palette
	//    (Theme + Custom), gelesen ueber wp_get_global_settings.
	$applied = array();
	foreach ( array( 'colors', 'fontSizes', 'spacingSizes', 'fontFamilies' ) as $cat ) {
		if ( isset( $validated[ $cat ] ) ) {
			$applied[ $cat ] = count( $validated[ $cat ] );
		}
	}
	if ( array() !== $validated_styles ) {
		$applied['styles'] = array_keys( $validated_styles );
	}

	$palette_slugs = array();
	$palette       = wp_get_global_settings( array( 'color', 'palette' ) );
	array_walk_recursive(
		$palette,
		function ( $v, $k ) use ( &$palette_slugs ) {
			if ( 'slug' === $k && is_string( $v ) && '' !== $v ) {
				$palette_slugs[] = $v;
			}
		}
	);

	return array(
		'post_id'       => $post_id,
		'theme'         => (string) get_stylesheet(),
		'version'       => $version,
		'replace'       => $replace,
		'applied'       => $applied,
		'palette_slugs' => array_values( array_unique( $palette_slugs ) ),
		'state_hash'    => $fresh_state['state_hash'],
		'verified'      => true,
	);
}

/**
 * Prueft, dass jeder geschriebene Token im re-gelesenen Datensatz mit exakt dem
 * geschriebenen Wert vorhanden ist. Liefert bei einer Abweichung WP_Error.
 *
 * @param array $rdata            Re-gelesener Global-Styles-Datensatz.
 * @param array $validated        Validierte Preset-Kategorien.
 * @param array $validated_styles Validierter styles-Teilbaum.
 * @return true|WP_Error
 */
function wpagent_gs_verify_written( array $rdata, array $validated, array $validated_styles ) {
	$paths = array(
		'colors'       => array( 'settings', 'color', 'palette' ),
		'fontSizes'    => array( 'settings', 'typography', 'fontSizes' ),
		'fontFamilies' => array( 'settings', 'typography', 'fontFamilies' ),
		'spacingSizes' => array( 'settings', 'spacing', 'spacingSizes' ),
	);
	$value_keys = array(
		'colors'       => 'color',
		'fontSizes'    => 'size',
		'fontFamilies' => 'fontFamily',
		'spacingSizes' => 'size',
	);

	foreach ( $paths as $cat => $path ) {
		if ( ! isset( $validated[ $cat ] ) ) {
			continue;
		}
		$node = $rdata;
		foreach ( $path as $seg ) {
			if ( ! is_array( $node ) || ! isset( $node[ $seg ] ) ) {
				/* translators: %s: global styles category name. */
				return new WP_Error( 'wpagent_gs_verify', sprintf( __( 'Re-Read: %s fehlt im gespeicherten Datensatz.', 'wp-agent-companion' ), $cat ), array( 'status' => 500 ) );
			}
			$node = $node[ $seg ];
		}
		$stored = wpagent_gs_preset_entries_by_slug( $node );
		$vk = $value_keys[ $cat ];
		foreach ( $validated[ $cat ] as $want ) {
			$slug = $want['slug'];
			if ( ! isset( $stored[ $slug ] ) ) {
				/* translators: 1: missing preset slug, 2: global styles category name. */
				return new WP_Error( 'wpagent_gs_verify', sprintf( __( 'Re-Read: Slug "%1$s" (%2$s) fehlt.', 'wp-agent-companion' ), $slug, $cat ), array( 'status' => 500 ) );
			}
			if ( (string) ( $stored[ $slug ][ $vk ] ?? '' ) !== (string) $want[ $vk ] ) {
				/* translators: 1: preset slug, 2: global styles category name. */
				return new WP_Error( 'wpagent_gs_verify', sprintf( __( 'Re-Read: Wert von "%1$s" (%2$s) weicht ab.', 'wp-agent-companion' ), $slug, $cat ), array( 'status' => 500 ) );
			}
		}
	}

	if ( array() !== $validated_styles ) {
		$stored_styles = ( isset( $rdata['styles'] ) && is_array( $rdata['styles'] ) ) ? $rdata['styles'] : array();
		if ( ! wpagent_gs_styles_contained( $validated_styles, $stored_styles ) ) {
			return new WP_Error( 'wpagent_gs_verify', __( 'Re-Read: styles wurden nicht wie geschrieben gespeichert.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
	}

	return true;
}

/**
 * Prueft rekursiv, dass jeder Blatt-Wert aus $want in $have mit gleichem Wert liegt.
 *
 * @param array $want
 * @param array $have
 * @return bool
 */
function wpagent_gs_styles_contained( array $want, array $have ): bool {
	foreach ( $want as $key => $value ) {
		if ( ! array_key_exists( $key, $have ) ) {
			return false;
		}
		if ( is_array( $value ) ) {
			if ( ! is_array( $have[ $key ] ) || ! wpagent_gs_styles_contained( $value, $have[ $key ] ) ) {
				return false;
			}
		} elseif ( (string) $have[ $key ] !== (string) $value ) {
			return false;
		}
	}
	return true;
}
