<?php
/** Shared CAS and lock helpers for the bounded comment trash transition. */

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

/** @return WP_Comment|WP_Error */
function wpagent_comment_snapshot( int $comment_id ) {
	clean_comment_cache( $comment_id );
	$comment = get_comment( $comment_id );
	if ( ! $comment instanceof WP_Comment ) {
		return new WP_Error( 'wpagent_no_comment', __( 'Kommentar nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return $comment;
}

/**
 * Hash every persisted comment-table field. The trash transition can change
 * only comment_approved, but an independent wp-admin edit to any field makes
 * the read baseline stale and must never be silently overwritten.
 */
function wpagent_comment_state_hash( WP_Comment $comment ): string {
	$state = array(
		'comment_id'      => (int) $comment->comment_ID,
		'comment_post_id' => (int) $comment->comment_post_ID,
		'author'          => (string) $comment->comment_author,
		'author_email'    => (string) $comment->comment_author_email,
		'author_url'      => (string) $comment->comment_author_url,
		'author_ip'       => (string) $comment->comment_author_IP,
		'date'            => (string) $comment->comment_date,
		'date_gmt'        => (string) $comment->comment_date_gmt,
		'content'         => (string) $comment->comment_content,
		'karma'           => (int) $comment->comment_karma,
		'approved'        => (string) $comment->comment_approved,
		'agent'           => (string) $comment->comment_agent,
		'type'            => (string) $comment->comment_type,
		'parent'          => (int) $comment->comment_parent,
		'user_id'         => (int) $comment->user_id,
	);
	return hash( 'sha256', "wpagently-comment-state-v1\0" . serialize( $state ) );
}

/** @return string|WP_Error */
function wpagent_comment_expected_hash( array $input ) {
	$hash = is_string( $input['expected_state_hash'] ?? null ) ? strtolower( $input['expected_state_hash'] ) : '';
	if ( 1 === preg_match( '/^[a-f0-9]{64}$/D', $hash ) ) {
		return $hash;
	}
	return new WP_Error( 'wpagent_comment_expected_hash', __( 'expected_state_hash muss der vollständige state_hash aus list-comments sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @return string|WP_Error */
function wpagent_comment_acquire_lock( int $comment_id ) {
	$token = wpagent_core_mutator_acquire_lock( 'comment', (string) $comment_id );
	if ( is_wp_error( $token ) ) {
		return new WP_Error( 'wpagent_comment_locked', __( 'Dieser Kommentar wird gerade geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $token;
}

function wpagent_comment_refresh_lock( int $comment_id, string $token ): bool {
	return wpagent_core_mutator_refresh_lock( 'comment', (string) $comment_id, $token );
}

function wpagent_comment_lock_is_owned( int $comment_id, string $token ): bool {
	return wpagent_core_mutator_lock_is_owned( 'comment', (string) $comment_id, $token );
}

function wpagent_comment_release_lock( int $comment_id, string $token ): void {
	wpagent_core_mutator_release_lock( 'comment', (string) $comment_id, $token );
}

/**
 * A third-party callback may leave a multisite blog switch active. The write
 * then returns recovery_required, but the lock still belongs to the original
 * blog and must be released there rather than leaked or deleted elsewhere.
 */
function wpagent_comment_release_lock_for_blog( int $comment_id, int $blog_id, string $token ): void {
	if ( $blog_id === (int) get_current_blog_id() || ! is_multisite() ) {
		wpagent_comment_release_lock( $comment_id, $token );
		return;
	}
	switch_to_blog( $blog_id );
	try {
		wpagent_comment_release_lock( $comment_id, $token );
	} finally {
		restore_current_blog();
	}
}

/** @return WP_Error */
function wpagent_comment_conflict( string $current_hash ): WP_Error {
	return new WP_Error(
		'wpagent_comment_conflict',
		__( 'Der Kommentar wurde seit dem Lesen verändert. Lies ihn neu und wiederhole den Vorgang mit dem aktuellen state_hash.', 'wp-agent-companion' ),
		array( 'status' => 409, 'current_state_hash' => $current_hash )
	);
}

/** @return WP_Error */
function wpagent_comment_recovery_required( int $comment_id, string $message, string $current_hash = '' ): WP_Error {
	$data = array( 'status' => 500, 'comment_id' => $comment_id, 'recovery_required' => true, 'rollback_verified' => false );
	if ( '' !== $current_hash ) {
		$data['current_state_hash'] = $current_hash;
	}
	return new WP_Error( 'wpagent_comment_trash_recovery_required', $message, $data );
}

/**
 * Atomically transitions only comment_approved while every other persisted
 * comment-table field still has the exact value included in the list hash.
 *
 * Core's wp_set_comment_status() uses an ID-only update, which is unsafe for
 * a remote CAS API: a native editor can change comment content or ownership
 * between the PHP read and that update. This conditional write preserves that
 * native change by returning false instead. On success it reproduces Core's
 * cache, hook and comment-count sequence after the guarded SQL mutation.
 */
function wpagent_comment_update_status_if_current( WP_Comment $before, string $requested_status ) {
	global $wpdb;

	$statuses = array(
		'hold'    => '0',
		'approve' => '1',
		'spam'    => 'spam',
		'trash'   => 'trash',
	);
	if ( ! isset( $statuses[ $requested_status ] ) ) {
		return new WP_Error( 'wpagent_comment_bad_status', __( 'Der Kommentarstatus ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$target_status = $statuses[ $requested_status ];
	$before_status = (string) $before->comment_approved;
	$updated       = $wpdb->query(
		$wpdb->prepare(
			"UPDATE {$wpdb->comments}
			 SET comment_approved = %s
			 WHERE comment_ID = %d
			   AND comment_post_ID = %d
			   AND HEX(comment_author) = HEX(%s)
			   AND HEX(comment_author_email) = HEX(%s)
			   AND HEX(comment_author_url) = HEX(%s)
			   AND HEX(comment_author_IP) = HEX(%s)
			   AND HEX(comment_date) = HEX(%s)
			   AND HEX(comment_date_gmt) = HEX(%s)
			   AND HEX(comment_content) = HEX(%s)
			   AND comment_karma = %d
			   AND HEX(comment_approved) = HEX(%s)
			   AND HEX(comment_agent) = HEX(%s)
			   AND HEX(comment_type) = HEX(%s)
			   AND comment_parent = %d
			   AND user_id = %d",
			$target_status,
			(int) $before->comment_ID,
			(int) $before->comment_post_ID,
			(string) $before->comment_author,
			(string) $before->comment_author_email,
			(string) $before->comment_author_url,
			(string) $before->comment_author_IP,
			(string) $before->comment_date,
			(string) $before->comment_date_gmt,
			(string) $before->comment_content,
			(int) $before->comment_karma,
			$before_status,
			(string) $before->comment_agent,
			(string) $before->comment_type,
			(int) $before->comment_parent,
			(int) $before->user_id
		)
	);
	if ( 1 !== $updated ) {
		return false;
	}

	clean_comment_cache( (int) $before->comment_ID );
	$fresh = get_comment( (int) $before->comment_ID );
	if ( ! $fresh instanceof WP_Comment ) {
		return new WP_Error( 'wpagent_comment_readback_failed', __( 'Der Kommentar konnte nach dem Statuswechsel nicht zurückgelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	// Keep the Core lifecycle observable to extensions without ever performing
	// an unbound second database write. Preserve the public Core input spelling
	// (notably approve/hold) so existing hook listeners observe the same values
	// and Core's approval notification remains available.
	if ( 'approve' === $requested_status ) {
		add_action( 'wp_set_comment_status', 'wp_new_comment_notify_postauthor' );
	}
	do_action( 'wp_set_comment_status', $fresh->comment_ID, $requested_status );
	wp_transition_comment_status( $requested_status, $before_status, $fresh );
	wp_update_comment_count( $fresh->comment_post_ID );
	return $fresh;
}

/**
 * wp_trash_comment() recursively trashes child notes for a top-level note.
 * This remote surface is intentionally a single ordinary comment only.
 */
function wpagent_comment_is_safe_trash_target( WP_Comment $comment ): bool {
	return in_array( (string) $comment->comment_type, array( '', 'comment' ), true );
}
