<?php
/** Semantic read-back verification helpers for Beaver Builder writes. */

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

/**
 * Builds an ID-independent digest for a Beaver Builder subtree.
 *
 * Node IDs and the root position intentionally differ after duplication. Child
 * order, node types, settings and local/global metadata must remain identical.
 *
 * @param array<string,object> $data
 * @return string|WP_Error
 */
function wpagent_beaver_subtree_signature( array $data, string $root_id ) {
	$root_id = sanitize_key( $root_id );
	if ( '' === $root_id || ! isset( $data[ $root_id ] ) || ! is_object( $data[ $root_id ] ) ) {
		return new WP_Error( 'wpagent_beaver_node_not_found', __( 'Beaver-Builder-Knoten nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$children = array();
	foreach ( $data as $node_id => $node ) {
		if ( is_object( $node ) ) {
			$children[ sanitize_key( (string) ( $node->parent ?? '' ) ) ][] = sanitize_key( (string) $node_id );
		}
	}
	foreach ( $children as &$child_ids ) {
		usort(
			$child_ids,
			static function ( string $left, string $right ) use ( $data ): int {
				$position = (int) ( $data[ $left ]->position ?? 0 ) <=> (int) ( $data[ $right ]->position ?? 0 );
				return 0 !== $position ? $position : strcmp( $left, $right );
			}
		);
	}
	unset( $child_ids );

	$visited = array();
	$build   = static function ( string $node_id ) use ( &$build, &$visited, $children, $data ) {
		if ( isset( $visited[ $node_id ] ) || ! isset( $data[ $node_id ] ) || ! is_object( $data[ $node_id ] ) ) {
			return new WP_Error( 'wpagent_beaver_verify_failed', __( 'Die Beaver-Builder-Änderung entspricht nicht dem angeforderten Zielzustand.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		$visited[ $node_id ] = true;
		$node                = $data[ $node_id ];
		$signature           = array(
			'type'        => sanitize_key( (string) ( $node->type ?? '' ) ),
			'settings'    => wpagent_beaver_canonicalize( $node->settings ?? new stdClass() ),
			'global'      => ! empty( $node->global ),
			'template_id' => sanitize_key( (string) ( $node->template_id ?? '' ) ),
			'children'    => array(),
		);
		foreach ( $children[ $node_id ] ?? array() as $child_id ) {
			$child = $build( $child_id );
			if ( is_wp_error( $child ) ) {
				return $child;
			}
			$signature['children'][] = $child;
		}
		return $signature;
	};
	$signature = $build( $root_id );
	if ( is_wp_error( $signature ) ) {
		return $signature;
	}
	$payload = wp_json_encode( $signature, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	return hash( 'sha256', is_string( $payload ) ? $payload : serialize( $signature ) );
}

/** Returns whether the actual provider value contains the requested value. */
function wpagent_beaver_value_contains( $actual, $expected, int $depth = 0 ): bool {
	if ( $depth > WPAGENT_BEAVER_MAX_VALUE_DEPTH ) {
		return wpagent_beaver_canonicalize( $actual ) === wpagent_beaver_canonicalize( $expected );
	}
	$actual   = is_object( $actual ) ? get_object_vars( $actual ) : $actual;
	$expected = is_object( $expected ) ? get_object_vars( $expected ) : $expected;
	if ( ! is_array( $expected ) ) {
		return wpagent_beaver_canonicalize( $actual ) === wpagent_beaver_canonicalize( $expected );
	}
	if ( ! is_array( $actual ) || array_is_list( $expected ) !== array_is_list( $actual ) ) {
		return false;
	}
	if ( array_is_list( $expected ) && count( $expected ) !== count( $actual ) ) {
		return false;
	}
	foreach ( $expected as $key => $value ) {
		if ( ! array_key_exists( $key, $actual ) || ! wpagent_beaver_value_contains( $actual[ $key ], $value, $depth + 1 ) ) {
			return false;
		}
	}
	return true;
}

/**
 * Confirms that a copied subtree preserves its source semantics while allowing
 * Beaver Builder to materialize additional default setting keys.
 *
 * @param array<string,object> $data
 * @return true|WP_Error
 */
function wpagent_beaver_subtree_matches( array $data, string $source_id, string $target_id ) {
	$source_id = sanitize_key( $source_id );
	$target_id = sanitize_key( $target_id );
	if ( '' === $source_id || '' === $target_id || $source_id === $target_id || ! isset( $data[ $source_id ], $data[ $target_id ] ) ) {
		return new WP_Error( 'wpagent_beaver_verify_failed', __( 'Die Beaver-Builder-Änderung entspricht nicht dem angeforderten Zielzustand.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$children = array();
	foreach ( $data as $node_id => $node ) {
		if ( is_object( $node ) ) {
			$children[ sanitize_key( (string) ( $node->parent ?? '' ) ) ][] = sanitize_key( (string) $node_id );
		}
	}
	foreach ( $children as &$child_ids ) {
		usort(
			$child_ids,
			static function ( string $left, string $right ) use ( $data ): int {
				$position = (int) ( $data[ $left ]->position ?? 0 ) <=> (int) ( $data[ $right ]->position ?? 0 );
				return 0 !== $position ? $position : strcmp( $left, $right );
			}
		);
	}
	unset( $child_ids );
	$visited = array();
	$match   = static function ( string $left_id, string $right_id ) use ( &$match, &$visited, $children, $data ): bool {
		$key = $left_id . ':' . $right_id;
		if ( isset( $visited[ $key ] ) || ! isset( $data[ $left_id ], $data[ $right_id ] ) || ! is_object( $data[ $left_id ] ) || ! is_object( $data[ $right_id ] ) ) {
			return false;
		}
		$visited[ $key ] = true;
		$left            = $data[ $left_id ];
		$right           = $data[ $right_id ];
		$left_global     = ! empty( $left->global );
		$right_global    = ! empty( $right->global );
		if ( sanitize_key( (string) ( $left->type ?? '' ) ) !== sanitize_key( (string) ( $right->type ?? '' ) )
			|| $left_global !== $right_global
			|| sanitize_key( (string) ( $left->template_id ?? '' ) ) !== sanitize_key( (string) ( $right->template_id ?? '' ) )
			|| ! wpagent_beaver_value_contains( $right->settings ?? new stdClass(), $left->settings ?? new stdClass() )
		) {
			return false;
		}
		$left_children  = $children[ $left_id ] ?? array();
		$right_children = $children[ $right_id ] ?? array();
		if ( count( $left_children ) !== count( $right_children ) ) {
			return false;
		}
		foreach ( $left_children as $index => $left_child_id ) {
			if ( ! $match( $left_child_id, $right_children[ $index ] ) ) {
				return false;
			}
		}
		return true;
	};
	return $match( $source_id, $target_id )
		? true
		: new WP_Error( 'wpagent_beaver_verify_failed', __( 'Die Beaver-Builder-Änderung entspricht nicht dem angeforderten Zielzustand.', 'wp-agent-companion' ), array( 'status' => 500 ) );
}

/**
 * Verifies a private, server-generated mutation contract against provider data.
 *
 * @param array<string,mixed>  $contract
 * @param array<string,object> $data
 * @return true|WP_Error
 */
function wpagent_beaver_verify_mutation_contract( array $contract, array $data, $settings ) {
	$failure = static fn( string $verification_path = 'contract' ): WP_Error => new WP_Error(
		str_starts_with( $verification_path, 'nodes.' ) ? 'wpagent_beaver_verify_nodes_failed' : 'wpagent_beaver_verify_contract_failed',
		__( 'Die Beaver-Builder-Änderung entspricht nicht dem angeforderten Zielzustand.', 'wp-agent-companion' ),
		array( 'status' => 500, 'verification_path' => $verification_path )
	);
	if ( array_diff( array_keys( $contract ), array( 'nodes', 'absent_ids', 'state_hash', 'subtree_signatures', 'subtree_matches', 'safe_subtree_roots' ) ) ) {
		return $failure();
	}
	if ( isset( $contract['state_hash'] ) ) {
		$expected_hash = is_string( $contract['state_hash'] ) ? $contract['state_hash'] : '';
		if ( ! preg_match( '/^[a-f0-9]{64}$/', $expected_hash ) || ! hash_equals( $expected_hash, wpagent_beaver_state_hash( $data, $settings ) ) ) {
			return $failure();
		}
	}
	if ( isset( $contract['nodes'] ) ) {
		if ( ! is_array( $contract['nodes'] ) || count( $contract['nodes'] ) > WPAGENT_BEAVER_MAX_NODES ) {
			return $failure();
		}
		foreach ( $contract['nodes'] as $node_id => $expected ) {
			if ( ! is_string( $node_id ) || sanitize_key( $node_id ) !== $node_id || ! is_array( $expected ) || ! isset( $data[ $node_id ] ) || ! is_object( $data[ $node_id ] ) ) {
				return $failure();
			}
			if ( array_diff( array_keys( $expected ), array( 'type', 'parent', 'position', 'settings' ) ) ) {
				return $failure();
			}
			$node   = $data[ $node_id ];
			$actual = array(
				'type'     => sanitize_key( (string) ( $node->type ?? '' ) ),
				'parent'   => sanitize_key( (string) ( $node->parent ?? '' ) ),
				'position' => max( 0, (int) ( $node->position ?? 0 ) ),
				'settings' => $node->settings ?? new stdClass(),
			);
			foreach ( $expected as $field => $value ) {
				$matches = 'settings' === $field
					? wpagent_beaver_value_contains( $actual[ $field ], $value )
					: wpagent_beaver_canonicalize( $actual[ $field ] ) === wpagent_beaver_canonicalize( $value );
				if ( ! $matches ) {
					return $failure( 'nodes.' . $field );
				}
			}
		}
	}
	if ( isset( $contract['absent_ids'] ) ) {
		if ( ! is_array( $contract['absent_ids'] ) || count( $contract['absent_ids'] ) > WPAGENT_BEAVER_MAX_NODES ) {
			return $failure();
		}
		foreach ( $contract['absent_ids'] as $node_id ) {
			if ( ! is_string( $node_id ) || sanitize_key( $node_id ) !== $node_id || isset( $data[ $node_id ] ) ) {
				return $failure();
			}
		}
	}
	if ( isset( $contract['subtree_signatures'] ) ) {
		if ( ! is_array( $contract['subtree_signatures'] ) || count( $contract['subtree_signatures'] ) > WPAGENT_BEAVER_MAX_NODES ) {
			return $failure();
		}
		foreach ( $contract['subtree_signatures'] as $node_id => $expected_signature ) {
			$actual_signature = is_string( $node_id ) ? wpagent_beaver_subtree_signature( $data, $node_id ) : $failure();
			if ( is_wp_error( $actual_signature ) || ! is_string( $expected_signature ) || ! hash_equals( $expected_signature, $actual_signature ) ) {
				return $failure();
			}
		}
	}
	if ( isset( $contract['subtree_matches'] ) ) {
		if ( ! is_array( $contract['subtree_matches'] ) || count( $contract['subtree_matches'] ) > WPAGENT_BEAVER_MAX_NODES ) {
			return $failure();
		}
		foreach ( $contract['subtree_matches'] as $pair ) {
			if ( ! is_array( $pair ) || array( 'source_id', 'target_id' ) !== array_keys( $pair )
				|| ! is_string( $pair['source_id'] ) || ! is_string( $pair['target_id'] )
				|| sanitize_key( $pair['source_id'] ) !== $pair['source_id'] || sanitize_key( $pair['target_id'] ) !== $pair['target_id']
				|| is_wp_error( wpagent_beaver_subtree_matches( $data, $pair['source_id'], $pair['target_id'] ) )
			) {
				return $failure();
			}
		}
	}
	if ( isset( $contract['safe_subtree_roots'] ) ) {
		if ( ! is_array( $contract['safe_subtree_roots'] ) || count( $contract['safe_subtree_roots'] ) > WPAGENT_BEAVER_MAX_NODES ) {
			return $failure();
		}
		$root_ids = array();
		foreach ( $contract['safe_subtree_roots'] as $root_id ) {
			if ( ! is_string( $root_id ) || sanitize_key( $root_id ) !== $root_id || '' === $root_id ) {
				return $failure();
			}
			$root_ids[] = $root_id;
		}
		if ( array() === $root_ids || is_wp_error( wpagent_beaver_validate_safe_subtrees( $data, $root_ids ) ) ) {
			return $failure();
		}
	}
	return true;
}
