<?php
/** Read-only snapshot and comparison abilities for supported content-model providers. */

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

/** @return array<string,mixed> */
function wpagent_content_model_snapshot_schema(): array {
	$field = array(
		'type' => 'object',
		'properties' => array(
			'name' => array( 'type' => 'string' ), 'label' => array( 'type' => 'string' ), 'type' => array( 'type' => 'string' ),
			'required' => array( 'type' => 'boolean' ),
			'options' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( 'label' => array( 'type' => 'string' ), 'value' => array( 'type' => 'string' ) ), 'required' => array( 'label', 'value' ), 'additionalProperties' => false ) ),
			'settings' => array( 'type' => 'object' ),
		),
		'required' => array( 'name', 'label', 'type', 'required', 'options', 'settings' ), 'additionalProperties' => false,
	);
	return array(
		'type' => 'object',
		'properties' => array(
			'schema_version' => array( 'type' => 'integer', 'const' => WPAGENT_CONTENT_MODEL_SNAPSHOT_VERSION ),
			'site_binding' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			'provider' => array( 'type' => 'string', 'enum' => wpagent_content_model_providers() ), 'provider_version' => array( 'type' => 'string' ),
			'capabilities' => array( 'type' => 'object', 'properties' => array( 'snapshot' => array( 'type' => 'boolean' ), 'compare' => array( 'type' => 'boolean' ), 'migration_source' => array( 'type' => 'boolean' ), 'migration_target' => array( 'type' => 'boolean' ) ), 'required' => array( 'snapshot', 'compare', 'migration_source', 'migration_target' ), 'additionalProperties' => false ),
			'identifier' => array( 'type' => 'string' ), 'exists' => array( 'type' => 'boolean' ), 'kind' => array( 'type' => 'string' ), 'label' => array( 'type' => 'string' ),
			'target' => array( 'type' => 'object', 'properties' => array( 'type' => array( 'type' => 'string' ), 'names' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ) ), 'required' => array( 'type', 'names' ), 'additionalProperties' => false ),
			'settings' => array( 'type' => 'object' ), 'fields' => array( 'type' => 'array', 'maxItems' => WPAGENT_CONTENT_MODEL_SNAPSHOT_MAX_FIELDS, 'items' => $field ),
			'provider_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ), 'warnings' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ), 'snapshot_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
		),
		'required' => array( 'schema_version', 'site_binding', 'provider', 'provider_version', 'capabilities', 'identifier', 'exists', 'kind', 'label', 'target', 'settings', 'fields', 'provider_hash', 'warnings', 'snapshot_hash' ),
		'additionalProperties' => false,
	);
}

/** @return array<string,mixed> */
function wpagent_ability_snapshot_content_model(): array {
	return array(
		'label' => __( 'Datenmodell-Snapshot erstellen', 'wp-agent-companion' ),
		'description' => __( 'Liest ein begrenztes ACF-, Pods-, ACPT-Lite- oder Meta-Box-Datenmodell in einer deterministischen, an Website und Provider-Version gebundenen Form. Nicht verfügbare Provider bleiben explizit nicht migrierbar.', 'wp-agent-companion' ),
		'category' => 'wp-agent',
		'input_schema' => array( 'type' => 'object', 'properties' => array( 'provider' => array( 'type' => 'string', 'enum' => wpagent_content_model_providers() ), 'identifier' => array( 'type' => 'string', 'maxLength' => 121 ) ), 'required' => array( 'provider', 'identifier' ), 'additionalProperties' => false ),
		'output_schema' => wpagent_content_model_snapshot_schema(), 'execute_callback' => 'wpagent_exec_snapshot_content_model', 'permission_callback' => 'wpagent_perm_manage_options',
		'meta' => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ) ),
	);
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_snapshot_content_model( $input ) {
	$input = is_array( $input ) ? $input : array();
	return wpagent_content_model_snapshot( is_string( $input['provider'] ?? null ) ? $input['provider'] : '', $input['identifier'] ?? null );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_content_model_validate_snapshot( $snapshot ) {
	if ( ! is_array( $snapshot ) || array_is_list( $snapshot ) ) {
		return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Ein Datenmodell-Snapshot muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$expected = array( 'schema_version', 'site_binding', 'provider', 'provider_version', 'capabilities', 'identifier', 'exists', 'kind', 'label', 'target', 'settings', 'fields', 'provider_hash', 'warnings', 'snapshot_hash' );
	if ( array_diff( array_keys( $snapshot ), $expected ) || array_diff( $expected, array_keys( $snapshot ) ) || WPAGENT_CONTENT_MODEL_SNAPSHOT_VERSION !== (int) ( $snapshot['schema_version'] ?? 0 ) || ! in_array( $snapshot['provider'] ?? '', wpagent_content_model_providers(), true ) || ! is_string( $snapshot['site_binding'] ?? null ) || ! preg_match( '/^[a-f0-9]{64}$/D', $snapshot['site_binding'] ) || ! is_string( $snapshot['provider_version'] ?? null ) || strlen( $snapshot['provider_version'] ) > 100 || ! is_string( $snapshot['identifier'] ?? null ) || strlen( $snapshot['identifier'] ) > 121 || ! is_bool( $snapshot['exists'] ?? null ) || ! is_string( $snapshot['kind'] ?? null ) || strlen( $snapshot['kind'] ) > 50 || ! is_string( $snapshot['label'] ?? null ) || strlen( $snapshot['label'] ) > 200 || ! is_array( $snapshot['target'] ?? null ) || ! is_array( $snapshot['settings'] ?? null ) || ! is_array( $snapshot['fields'] ?? null ) || count( $snapshot['fields'] ) > WPAGENT_CONTENT_MODEL_SNAPSHOT_MAX_FIELDS || ! is_array( $snapshot['warnings'] ?? null ) || count( $snapshot['warnings'] ) > 20 || ! is_string( $snapshot['provider_hash'] ?? null ) || ! preg_match( '/^[a-f0-9]{64}$/D', $snapshot['provider_hash'] ) || ! is_string( $snapshot['snapshot_hash'] ?? null ) || ! preg_match( '/^[a-f0-9]{64}$/D', $snapshot['snapshot_hash'] ) ) {
		return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot ist unvollständig oder liegt außerhalb der sicheren Grenze.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$identifier = wpagent_content_model_identifier( $snapshot['provider'], $snapshot['identifier'] );
	if ( is_wp_error( $identifier ) || ! hash_equals( $identifier, $snapshot['identifier'] ) ) {
		return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält einen ungültigen Anbieterbezeichner.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$capabilities = $snapshot['capabilities'];
	if ( array_diff( array_keys( $capabilities ), array( 'snapshot', 'compare', 'migration_source', 'migration_target' ) ) || array_diff( array( 'snapshot', 'compare', 'migration_source', 'migration_target' ), array_keys( $capabilities ) ) ) {
		return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält keine vollständigen Anbieterfähigkeiten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $capabilities as $capability ) if ( ! is_bool( $capability ) ) return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält ungültige Anbieterfähigkeiten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	$target = $snapshot['target'];
	if ( array_diff( array_keys( $target ), array( 'type', 'names' ) ) || array_diff( array( 'type', 'names' ), array_keys( $target ) ) || ! is_string( $target['type'] ?? null ) || ! is_array( $target['names'] ?? null ) || count( $target['names'] ) > 20 ) return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält eine ungültige Zielbindung.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	$target_names = array();
	foreach ( $target['names'] as $name ) {
		if ( ! is_string( $name ) || '' === $name || strlen( $name ) > 64 || isset( $target_names[ $name ] ) ) return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält einen ungültigen Zielnamen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		$target_names[ $name ] = true;
	}
	$names = array();
	foreach ( $snapshot['fields'] as $field ) {
		if ( ! is_array( $field ) || array_diff( array_keys( $field ), array( 'name', 'label', 'type', 'required', 'options', 'settings' ) ) || array_diff( array( 'name', 'label', 'type', 'required', 'options', 'settings' ), array_keys( $field ) ) || ! is_string( $field['name'] ?? null ) || '' === $field['name'] || strlen( $field['name'] ) > 64 || isset( $names[ $field['name'] ] ) || ! is_string( $field['label'] ?? null ) || strlen( $field['label'] ) > 200 || ! is_string( $field['type'] ?? null ) || strlen( $field['type'] ) > 64 || ! is_bool( $field['required'] ?? null ) || ! is_array( $field['options'] ?? null ) || count( $field['options'] ) > 20 || ! is_array( $field['settings'] ?? null ) ) {
			return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält ein ungültiges Feld.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$names[ $field['name'] ] = true;
		foreach ( $field['options'] as $option ) if ( ! is_array( $option ) || array_diff( array_keys( $option ), array( 'label', 'value' ) ) || array_diff( array( 'label', 'value' ), array_keys( $option ) ) || ! is_string( $option['label'] ?? null ) || ! is_string( $option['value'] ?? null ) || strlen( $option['label'] ) > 100 || strlen( $option['value'] ) > 100 ) return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot enthält eine ungültige Auswahloption.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$nodes = 0;
	if ( ! wpagent_content_model_within_limits( $snapshot, 0, $nodes ) ) return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot ist zu tief oder zu groß.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	$encoded = wp_json_encode( $snapshot, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	if ( false === $encoded || strlen( $encoded ) > WPAGENT_CONTENT_MODEL_SNAPSHOT_MAX_BYTES ) return new WP_Error( 'wpagent_content_model_snapshot_invalid', __( 'Der Datenmodell-Snapshot überschreitet die sichere Größengrenze.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	$hash = $snapshot['snapshot_hash']; unset( $snapshot['snapshot_hash'] );
	if ( ! hash_equals( wpagent_content_model_hash( $snapshot ), $hash ) ) return new WP_Error( 'wpagent_content_model_snapshot_tampered', __( 'Der Datenmodell-Snapshot stimmt nicht mehr mit seinem Integritätshash überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	$snapshot['snapshot_hash'] = $hash;
	return $snapshot;
}

/** @return array<string,mixed> */
function wpagent_content_model_comparison_schema(): array {
	$change = array( 'type' => 'object', 'properties' => array( 'scope' => array( 'type' => 'string' ), 'name' => array( 'type' => 'string' ), 'before' => array( 'type' => WPAGENT_MIXED_JSON_TYPES ), 'after' => array( 'type' => WPAGENT_MIXED_JSON_TYPES ) ), 'required' => array( 'scope', 'name', 'before', 'after' ), 'additionalProperties' => false );
	$loss = array( 'type' => 'object', 'properties' => array( 'scope' => array( 'type' => 'string' ), 'name' => array( 'type' => 'string' ), 'reason' => array( 'type' => 'string' ), 'before' => array( 'type' => WPAGENT_MIXED_JSON_TYPES ), 'after' => array( 'type' => WPAGENT_MIXED_JSON_TYPES ) ), 'required' => array( 'scope', 'name', 'reason', 'before', 'after' ), 'additionalProperties' => false );
	return array( 'type' => 'object', 'properties' => array( 'left_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ), 'right_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ), 'additions' => array( 'type' => 'array', 'items' => $change ), 'removals' => array( 'type' => 'array', 'items' => $change ), 'compatible_changes' => array( 'type' => 'array', 'items' => $change ), 'losses' => array( 'type' => 'array', 'items' => $loss ), 'lossless' => array( 'type' => 'boolean' ) ), 'required' => array( 'left_hash', 'right_hash', 'additions', 'removals', 'compatible_changes', 'losses', 'lossless' ), 'additionalProperties' => false );
}

/** @return array<string,mixed> */
function wpagent_ability_compare_content_model_snapshots(): array {
	return array(
		'label' => __( 'Datenmodell-Snapshots vergleichen', 'wp-agent-companion' ),
		'description' => __( 'Vergleicht zwei integritätsgeprüfte Datenmodell-Snapshots mit exakten Feldzugängen, Entfernungen, kompatiblen Änderungen und Verlusthinweisen. Es schreibt nichts.', 'wp-agent-companion' ),
		'category' => 'wp-agent',
		'input_schema' => array( 'type' => 'object', 'properties' => array( 'left' => wpagent_content_model_snapshot_schema(), 'right' => wpagent_content_model_snapshot_schema() ), 'required' => array( 'left', 'right' ), 'additionalProperties' => false ),
		'output_schema' => wpagent_content_model_comparison_schema(), 'execute_callback' => 'wpagent_exec_compare_content_model_snapshots', 'permission_callback' => 'wpagent_perm_manage_options',
		'meta' => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ) ),
	);
}

/** @return array<string,mixed> */
function wpagent_content_model_compare( array $left, array $right ): array {
	$additions = array(); $removals = array(); $changes = array(); $losses = array();
	if ( ! hash_equals( $left['site_binding'], $right['site_binding'] ) ) $losses[] = array( 'scope' => 'snapshot', 'name' => 'site_binding', 'reason' => 'different_site', 'before' => $left['site_binding'], 'after' => $right['site_binding'] );
	if ( $left['provider'] !== $right['provider'] ) $changes[] = array( 'scope' => 'provider', 'name' => 'provider', 'before' => $left['provider'], 'after' => $right['provider'] );
	if ( $left['provider_version'] !== $right['provider_version'] ) $changes[] = array( 'scope' => 'provider', 'name' => 'version', 'before' => $left['provider_version'], 'after' => $right['provider_version'] );
	foreach ( array( 'kind', 'target', 'settings' ) as $key ) if ( wpagent_content_model_canonicalize( $left[ $key ] ) !== wpagent_content_model_canonicalize( $right[ $key ] ) ) $losses[] = array( 'scope' => 'model', 'name' => $key, 'reason' => 'incompatible_model_change', 'before' => $left[ $key ], 'after' => $right[ $key ] );
	if ( $left['label'] !== $right['label'] ) $changes[] = array( 'scope' => 'model', 'name' => 'label', 'before' => $left['label'], 'after' => $right['label'] );
	foreach ( array( 'left' => $left, 'right' => $right ) as $side => $snapshot ) if ( ! $snapshot['capabilities']['snapshot'] ) $losses[] = array( 'scope' => 'provider', 'name' => $side, 'reason' => 'provider_unavailable', 'before' => $snapshot['warnings'], 'after' => array() );
	foreach ( array( 'left' => $left, 'right' => $right ) as $side => $snapshot ) foreach ( $snapshot['warnings'] as $warning ) $losses[] = array( 'scope' => 'provider', 'name' => $side, 'reason' => (string) $warning, 'before' => array(), 'after' => array() );
	$left_fields = array_column( $left['fields'], null, 'name' ); $right_fields = array_column( $right['fields'], null, 'name' );
	foreach ( $left_fields as $name => $field ) {
		if ( ! isset( $right_fields[ $name ] ) ) { $removals[] = array( 'scope' => 'field', 'name' => $name, 'before' => $field, 'after' => null ); continue; }
		$other = $right_fields[ $name ];
		if ( $field['type'] !== $other['type'] || wpagent_content_model_canonicalize( $field['settings'] ) !== wpagent_content_model_canonicalize( $other['settings'] ) ) { $losses[] = array( 'scope' => 'field', 'name' => $name, 'reason' => 'incompatible_field_change', 'before' => $field, 'after' => $other ); continue; }
		if ( wpagent_content_model_canonicalize( $field ) !== wpagent_content_model_canonicalize( $other ) ) $changes[] = array( 'scope' => 'field', 'name' => $name, 'before' => $field, 'after' => $other );
	}
	foreach ( $right_fields as $name => $field ) if ( ! isset( $left_fields[ $name ] ) ) $additions[] = array( 'scope' => 'field', 'name' => $name, 'before' => null, 'after' => $field );
	$sort = static function ( array &$items ): void { usort( $items, static fn( array $a, array $b ): int => strcmp( $a['scope'] . "\0" . $a['name'] . "\0" . ( $a['reason'] ?? '' ), $b['scope'] . "\0" . $b['name'] . "\0" . ( $b['reason'] ?? '' ) ) ); };
	$sort( $additions ); $sort( $removals ); $sort( $changes ); $sort( $losses );
	return array( 'left_hash' => $left['snapshot_hash'], 'right_hash' => $right['snapshot_hash'], 'additions' => $additions, 'removals' => $removals, 'compatible_changes' => $changes, 'losses' => $losses, 'lossless' => array() === $additions && array() === $removals && array() === $losses );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_compare_content_model_snapshots( $input ) {
	$input = is_array( $input ) ? $input : array(); $left = wpagent_content_model_validate_snapshot( $input['left'] ?? null ); $right = wpagent_content_model_validate_snapshot( $input['right'] ?? null );
	return is_wp_error( $left ) ? $left : ( is_wp_error( $right ) ? $right : wpagent_content_model_compare( $left, $right ) );
}
