<?php
/**
 * Logger mutation helpers.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use MailDoctor\Helpers\Settings;
use WP_Error;

trait Logger_Mutators_Trait {


	/**
	 * Record a new email attempt.
	 *
	 * @param array<string, mixed> $mail_args wp_mail arguments.
	 * @param array<string, mixed> $context Additional context (event/order_id/payload).
	 * @param string               $status  Initial status.
	 * @return int Log identifier.
	 */
	public static function log_attempt( array $mail_args, array $context = array(), string $status = 'queued' ): int {
		global $wpdb;

		$table = self::table_name();

		$event    = $context['event'] ?? null;
		$order_id = isset( $context['order_id'] ) ? (int) $context['order_id'] : null;

		$idempotency_key = null;
		if ( ! empty( $context['idempotency_key'] ) ) {
			$idempotency_key = strtolower( preg_replace( '/[^a-f0-9]/', '', (string) $context['idempotency_key'] ) );
			if ( empty( $idempotency_key ) ) {
				$idempotency_key = null;
			}
		}

		$idempotency_bucket = isset( $context['idempotency_bucket'] ) ? (int) $context['idempotency_bucket'] : null;

		$privacy_mode     = Settings::privacy_mode_enabled();
		$hash_recipients  = Settings::hash_recipients_enabled();
		$recipient_list   = self::normalize_recipient_array( $mail_args['to'] ?? '' );
		$recipient_hashes = array_map( array( Settings::class, 'hash_email' ), $recipient_list );

		$stored_recipients = self::stringify_recipients( $recipient_list );

		if ( $hash_recipients && ! empty( $recipient_list ) ) {
			$stored_recipients = implode( ', ', $recipient_hashes );
		}

		$payload = array(
			'mail_args' => self::prepare_payload_args( $mail_args ),
			'retry'     => array(
				'attempts'         => $context['attempt'] ?? 0,
				'origin_log_id'    => $context['origin_log_id'] ?? null,
				'manual_trigger'   => $context['manual'] ?? false,
				'next_attempt_gmt' => null,
				'history'          => array(),
				'disabled'         => $privacy_mode,
			),
			'meta'      => array(
				'email_class'        => isset( $context['email_class'] ) ? sanitize_text_field( (string) $context['email_class'] ) : null,
				'idempotency_key'    => $idempotency_key,
				'idempotency_bucket' => $idempotency_bucket,
				'smtp_profile'       => isset( $context['smtp_profile'] ) ? sanitize_key( (string) $context['smtp_profile'] ) : null,
				'smtp_profile_label' => isset( $context['smtp_profile_label'] ) ? sanitize_text_field( (string) $context['smtp_profile_label'] ) : null,
				'smtp_host'          => isset( $context['smtp_host'] ) ? sanitize_text_field( (string) $context['smtp_host'] ) : null,
				'transport'          => isset( $context['transport'] ) ? sanitize_key( (string) $context['transport'] ) : null,
				'transport_label'    => isset( $context['transport_label'] ) ? sanitize_text_field( (string) $context['transport_label'] ) : null,
				'correlation_id'     => isset( $context['correlation_id'] ) ? sanitize_text_field( (string) $context['correlation_id'] ) : null,
				'recipients_hash'    => $recipient_hashes,
				'recipients_plain'   => $privacy_mode ? array() : $recipient_list,
				'privacy_mode'       => $privacy_mode,
			),
		);

		$headers_normalized = self::normalize_headers( $mail_args['headers'] ?? array() );

		$payload['meta']['envelope'] = array(
			'from'        => self::extract_header_value( $headers_normalized, 'From' ) ?: Settings::get( 'from_email', '' ),
			'reply_to'    => self::extract_header_value( $headers_normalized, 'Reply-To' ),
			'return_path' => self::extract_header_value( $headers_normalized, 'Return-Path' ),
		);

		$payload['meta']['environment'] = array(
			'plugin_version' => defined( 'MAIL_DOCTOR_VERSION' ) ? MAIL_DOCTOR_VERSION : '',
			'wp_version'     => get_bloginfo( 'version' ),
			'php_version'    => PHP_VERSION,
			'async_mode'     => Settings::async_mode(),
		);

		if ( ! $privacy_mode ) {
			$body                       = isset( $mail_args['message'] ) ? (string) $mail_args['message'] : '';
			$payload['meta']['content'] = array(
				'hash'    => self::hash_body_preview( $body ),
				'bytes'   => strlen( $body ),
				'preview' => self::trim_body_preview( $body ),
			);
		}

		if ( isset( $context['suppressed'] ) && is_array( $context['suppressed'] ) ) {
			$payload['meta']['suppressed'] = array_values(
				array_filter(
					array_map( 'sanitize_email', $context['suppressed'] )
				)
			);
		}

		$status = self::normalize_status( $status );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$inserted = $wpdb->insert(
			$table,
			array(
				'created_at'         => current_time( 'mysql', true ),
				'event'              => $event ? sanitize_text_field( $event ) : null,
				'order_id'           => $order_id ?: null,
				'to_email'           => $stored_recipients,
				'subject'            => sanitize_text_field( $mail_args['subject'] ?? '' ),
				'status'             => $status,
				'provider_code'      => null,
				'error'              => null,
				'duration_ms'        => null,
				'headers'            => self::prepare_headers_for_storage( $mail_args['headers'] ?? array() ),
				'payload'            => wp_json_encode( $payload ),
				'idempotency_key'    => $idempotency_key,
				'idempotency_bucket' => $idempotency_bucket ?: null,
			),
			array(
				'%s',
				'%s',
				'%d',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%d',
				'%s',
				'%s',
				'%s',
				'%d',
			)
		);

		if ( false === $inserted ) {
			return 0;
		}

		$log_id = (int) $wpdb->insert_id;

		self::$active[ $log_id ] = array(
			'started_at' => microtime( true ),
		);

		/**
		 * Fires immediately after an email attempt has been logged.
		 *
		 * @param array<string, mixed> $payload Logged payload.
		 * @param int                  $log_id  Log identifier.
		 */
		do_action( 'mail_doctor_before_send', $payload, $log_id );

		return $log_id;
	}

	/**
	 * Update log when mail succeeds.
	 *
	 * @param array<string, mixed> $mail_data Mail data from wp_mail_succeeded.
	 * @return void
	 */
	public static function capture_success( array $mail_data ): void {
		$log_id = self::extract_log_id_from_headers( $mail_data['headers'] ?? array() );

		if ( ! $log_id ) {
			return;
		}

		$duration = self::calculate_duration( $log_id );

		self::mark_sent(
			$log_id,
			array(
				'duration_ms'   => $duration,
				'provider_code' => self::extract_smtp_code( $mail_data ),
			)
		);
	}

	/**
	 * Update log when mail fails.
	 *
	 * @param WP_Error $error Error object.
	 * @return void
	 */
	public static function capture_failure( $error ): void {
		if ( ! ( $error instanceof WP_Error ) ) {
			return;
		}

		$data       = (array) $error->get_error_data();
		$maildoctor = isset( $data['maildoctor'] ) && is_array( $data['maildoctor'] ) ? $data['maildoctor'] : array();
		$mail_data  = array();

		if ( isset( $data['mail_doctor_raw'] ) && is_array( $data['mail_doctor_raw'] ) ) {
			$mail_data = $data['mail_doctor_raw'];
		}

		$log_id = isset( $maildoctor['log_id'] ) ? (int) $maildoctor['log_id'] : 0;

		if ( ! $log_id && isset( $mail_data['headers'] ) ) {
			$log_id = self::extract_log_id_from_headers( $mail_data['headers'] );
		}

		if ( ! $log_id && isset( $maildoctor['mail_args_masked']['headers']['sample'] ) ) {
			$log_id = self::extract_log_id_from_headers( $maildoctor['mail_args_masked']['headers']['sample'] );
		}

		if ( ! $log_id ) {
			return;
		}

		self::mark_failed( $log_id, $error );
	}

	/**
	 * Mark queued log as successfully sent.
	 *
	 * @param int                  $id   Log ID.
	 * @param array<string, mixed> $meta Optional metadata.
	 * @return void
	 */
	public static function mark_sent( int $id, array $meta = array() ): void {
		global $wpdb;

		$table = self::table_name();

		if ( ! isset( $meta['duration_ms'] ) ) {
			$duration = self::calculate_duration( $id );
			if ( null !== $duration ) {
				$meta['duration_ms'] = $duration;
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			$table,
			array(
				'status'        => 'sent',
				'error'         => null,
				'provider_code' => isset( $meta['provider_code'] ) ? $meta['provider_code'] : null,
				'duration_ms'   => isset( $meta['duration_ms'] ) ? (int) $meta['duration_ms'] : null,
			),
			array( 'id' => $id ),
			array( '%s', '%s', '%s', '%d' ),
			array( '%d' )
		);

		unset( self::$active[ $id ] );

		$payload = self::get_payload( $id );
		$origin  = isset( $payload['retry']['origin_log_id'] ) ? (int) $payload['retry']['origin_log_id'] : 0;

		if ( ! isset( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			$payload['retry']['history'] = array();
		}

		$payload['retry']['history'][] = array(
			'attempt'         => isset( $payload['retry']['attempts'] ) ? (int) $payload['retry']['attempts'] : 1,
			'status'          => 'sent',
			'occurred_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
			'provider_code'   => isset( $meta['provider_code'] ) ? $meta['provider_code'] : null,
			'duration_ms'     => isset( $meta['duration_ms'] ) ? (int) $meta['duration_ms'] : null,
		);

		self::update_payload( $id, $payload );

		$result = array(
			'status'  => 'sent',
			'meta'    => array(
				'duration_ms'   => isset( $meta['duration_ms'] ) ? (int) $meta['duration_ms'] : null,
				'provider_code' => isset( $meta['provider_code'] ) ? $meta['provider_code'] : null,
			),
			'payload' => $payload,
		);

		do_action( 'mail_doctor_after_send', $result, $id );

		if ( class_exists( __NAMESPACE__ . '\RateLimiter' ) ) {
			RateLimiter::record_success();
		}

		if ( $origin ) {
			self::resolve_origin_log( $origin, $id, $meta );
		}

		do_action( 'mail_doctor_log_sent', $id, $meta );

		if ( class_exists( __NAMESPACE__ . '\Exporter' ) ) {
			Exporter::maybe_export( $id );
		}
		self::flush_metrics_cache();
	}

	/**
	 * Mark log as failed and persist payload.
	 *
	 * @param int            $id    Log ID.
	 * @param WP_Error|mixed $error Error details.
	 * @return void
	 */
	public static function mark_failed( int $id, $error ): void {
		global $wpdb;

		$table    = self::table_name();
		$payload  = self::get_payload( $id );
		$duration = self::calculate_duration( $id );

		if ( ! isset( $payload['retry'] ) || ! is_array( $payload['retry'] ) ) {
			$payload['retry'] = array();
		}

		$payload['retry']['attempts'] = ( $payload['retry']['attempts'] ?? 0 ) + 1;

		$normalized = self::normalize_error_info( $error );

		if ( ! isset( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			$payload['retry']['history'] = array();
		}

		$payload['retry']['history'][] = array(
			'attempt'         => (int) $payload['retry']['attempts'],
			'status'          => 'failed',
			'occurred_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
			'error'           => $normalized['message'],
			'provider_code'   => $normalized['provider_code'],
			'duration_ms'     => null !== $duration ? (int) $duration : null,
		);

		$update = array(
			'status'        => 'failed',
			'error'         => $normalized['message'],
			'provider_code' => $normalized['provider_code'],
			'payload'       => wp_json_encode( $payload ),
		);

		$formats = array( '%s', '%s', '%s', '%s' );

		if ( null !== $duration ) {
			$update['duration_ms'] = (int) $duration;
			$formats[]             = '%d';
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			$table,
			$update,
			array( 'id' => $id ),
			$formats,
			array( '%d' )
		);

		unset( self::$active[ $id ] );

		$result = array(
			'status'  => 'failed',
			'meta'    => array(
				'provider_code' => $normalized['provider_code'],
				'error'         => $normalized['message'],
			),
			'payload' => $payload,
		);

		do_action( 'mail_doctor_after_send', $result, $id );

		if ( class_exists( __NAMESPACE__ . '\RateLimiter' ) ) {
			RateLimiter::record_pushback( $normalized['provider_code'], $normalized['message'] );
		}

		do_action( 'mail_doctor_log_failed', $id, $payload );

		if ( class_exists( __NAMESPACE__ . '\Exporter' ) ) {
			Exporter::maybe_export( $id );
		}
		self::flush_metrics_cache();
	}

	/**
	 * Remove messages from a specific queue state.
	 *
	 * @param string $status Message status to remove.
	 * @return int Rows deleted.
	 */
	public static function clear_queue( string $status = 'queued' ): int {
		global $wpdb;

		$status = sanitize_key( $status );

		if ( ! in_array( $status, array( 'queued', 'failed' ), true ) ) {
			return 0;
		}

		$table = self::table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Queue cleanup.
		return (int) $wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
				"DELETE FROM {$table} WHERE status = %s",
				$status
			)
		);
	}

	/**
	 * Permanently delete all log entries from active and archive tables.
	 *
	 * @return int Total rows deleted.
	 */
	public static function purge_logs(): int {
		global $wpdb;

		$tables = array_unique(
			array_filter(
				array(
					self::table_name(),
					self::archive_table_name(),
				)
			)
		);

		$total_deleted = 0;

		foreach ( $tables as $table ) {
			if ( ! self::table_exists( $table ) ) {
				continue;
			}

			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$result = $wpdb->query( "DELETE FROM {$table}" );

			if ( false !== $result ) {
				$total_deleted += (int) $result;
			}

			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$wpdb->query( "ALTER TABLE {$table} AUTO_INCREMENT = 1" );
		}

		self::$active = array();
		self::flush_metrics_cache();

		do_action( 'mail_doctor_logs_purged', $total_deleted );

		return $total_deleted;
	}

	/**
	 * Delete specific log entries.
	 *
	 * @param array<int> $log_ids Log identifiers.
	 * @return int Rows deleted.
	 */
	public static function delete_logs( array $log_ids ): int {
		global $wpdb;

		$ids = array();

		foreach ( $log_ids as $id ) {
			$id = absint( $id );
			if ( $id > 0 ) {
				$ids[] = $id;
			}
		}

		$ids = array_values( array_unique( $ids ) );

		if ( empty( $ids ) ) {
			return 0;
		}

		$table        = self::table_name();
		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$deleted = $wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Table and placeholders safely generated.
				"DELETE FROM {$table} WHERE id IN ({$placeholders})",
				...$ids
			)
		);

		if ( false === $deleted ) {
			$deleted = 0;
		}

		foreach ( $ids as $id ) {
			unset( self::$active[ $id ] );
		}

		if ( $deleted > 0 ) {
			self::flush_metrics_cache();
			do_action( 'mail_doctor_logs_deleted', $ids, $deleted );
		}

		return (int) $deleted;
	}

	/**
	 * Annotate recent logs with suppression feedback.
	 *
	 * @param string               $email Email address.
	 * @param string               $reason Reason label.
	 * @param array<string, mixed> $meta   Additional metadata.
	 * @return void
	 */
	public static function annotate_suppression( string $email, string $reason, array $meta = array() ): void {
		global $wpdb;

		$email = sanitize_email( $email );

		if ( '' === $email ) {
			return;
		}

		$table = self::table_name();
		$like  = '%' . $wpdb->esc_like( $email ) . '%';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from internal method.
				"SELECT id, payload FROM {$table} WHERE to_email LIKE %s ORDER BY id DESC LIMIT 5",
				$like
			),
			ARRAY_A
		);

		if ( empty( $rows ) ) {
			return;
		}

		foreach ( $rows as $row ) {
			$payload = json_decode( $row['payload'] ?? '', true );
			if ( ! is_array( $payload ) ) {
				$payload = array();
			}

			if ( ! isset( $payload['meta'] ) || ! is_array( $payload['meta'] ) ) {
				$payload['meta'] = array();
			}

			$notes   = isset( $payload['meta']['suppression_notes'] ) && is_array( $payload['meta']['suppression_notes'] ) ? $payload['meta']['suppression_notes'] : array();
			$notes[] = array(
				'email'       => $email,
				'reason'      => $reason,
				'context'     => $meta,
				'recorded_at' => gmdate( 'Y-m-d H:i:s' ),
			);

			if ( count( $notes ) > 10 ) {
				$notes = array_slice( $notes, -10 );
			}

			$payload['meta']['suppression_notes'] = $notes;

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->update(
				$table,
				array( 'payload' => wp_json_encode( $payload ) ),
				array( 'id' => (int) $row['id'] ),
				array( '%s' ),
				array( '%d' )
			);
		}
	}

	/**
	 * Annotate attachments for a logged attempt.
	 *
	 * @param int   $id    Log identifier.
	 * @param array $items Attachment metadata.
	 * @param int   $total Total bytes.
	 * @return void
	 */
	public static function annotate_attachments( int $id, array $items, int $total ): void {
		$payload = self::get_payload( $id );

		if ( ! isset( $payload['meta'] ) || ! is_array( $payload['meta'] ) ) {
			$payload['meta'] = array();
		}

		$payload['meta']['attachments_meta']        = array_values( $items );
		$payload['meta']['attachment_total_bytes']  = max( 0, (int) $total );
		$payload['meta']['attachments_recorded_at'] = gmdate( 'Y-m-d H:i:s' );

		self::update_payload( $id, $payload );
	}

	/**
	 * Annotate async metadata for log entry.
	 *
	 * @param int    $id   Log identifier.
	 * @param string $mode Async mode.
	 * @return void
	 */
	public static function annotate_async( int $id, string $mode ): void {
		$payload = self::get_payload( $id );

		if ( ! isset( $payload['meta'] ) || ! is_array( $payload['meta'] ) ) {
			$payload['meta'] = array();
		}

		$existing = isset( $payload['meta']['async'] ) && is_array( $payload['meta']['async'] ) ? $payload['meta']['async'] : array();

		$payload['meta']['async'] = array_merge(
			$existing,
			array(
				'mode'       => $mode,
				'updated_at' => gmdate( 'Y-m-d H:i:s' ),
			)
		);

		self::update_payload( $id, $payload );
	}

	/**
	 * Annotate fallback metadata for log entry.
	 *
	 * @param int   $id   Log identifier.
	 * @param array $data Fallback data.
	 * @return void
	 */
	public static function annotate_fallback( int $id, array $data ): void {
		$payload = self::get_payload( $id );

		if ( ! isset( $payload['meta'] ) || ! is_array( $payload['meta'] ) ) {
			$payload['meta'] = array();
		}

		if ( ! isset( $payload['meta']['fallback'] ) || ! is_array( $payload['meta']['fallback'] ) ) {
			$payload['meta']['fallback'] = array();
		}

		$payload['meta']['fallback'][] = array_merge(
			array(
				'recorded_at' => gmdate( 'Y-m-d H:i:s' ),
			),
			$data
		);

		if ( count( $payload['meta']['fallback'] ) > 5 ) {
			$payload['meta']['fallback'] = array_slice( $payload['meta']['fallback'], -5 );
		}

		self::update_payload( $id, $payload );
	}

	/**
	 * Annotate transport request/response metadata.
	 *
	 * @param int   $id   Log identifier.
	 * @param array $data Debug data to merge.
	 * @return void
	 */
	public static function annotate_transport_debug( int $id, array $data ): void {
		if ( empty( $data ) ) {
			return;
		}

		$payload = self::get_payload( $id );

		if ( ! isset( $payload['meta'] ) || ! is_array( $payload['meta'] ) ) {
			$payload['meta'] = array();
		}

		$existing = isset( $payload['meta']['transport_debug'] ) && is_array( $payload['meta']['transport_debug'] ) ? $payload['meta']['transport_debug'] : array();

		$payload['meta']['transport_debug'] = array_merge(
			$existing,
			array_filter(
				$data,
				static function ( $value ) {
					return null !== $value && '' !== $value && false !== $value;
				}
			),
			array(
				'recorded_at' => gmdate( 'Y-m-d H:i:s' ),
			)
		);

		self::update_payload( $id, $payload );
	}

	/**
	 * Mark log as skipped due to suppression.
	 *
	 * @param int   $id         Log identifier.
	 * @param array $suppressed Suppressed recipients.
	 * @return void
	 */
	public static function mark_skipped( int $id, array $suppressed ): void {
		global $wpdb;

		$table   = self::table_name();
		$payload = self::get_payload( $id );

		if ( ! isset( $payload['meta'] ) || ! is_array( $payload['meta'] ) ) {
			$payload['meta'] = array();
		}

		$payload['meta']['suppressed'] = array(
			'addresses'   => array_keys( $suppressed ),
			'reasons'     => $suppressed,
			'recorded_at' => gmdate( 'Y-m-d H:i:s' ),
		);

		if ( ! isset( $payload['retry'] ) || ! is_array( $payload['retry'] ) ) {
			$payload['retry'] = array();
		}

		if ( ! isset( $payload['retry']['history'] ) || ! is_array( $payload['retry']['history'] ) ) {
			$payload['retry']['history'] = array();
		}

		$payload['retry']['history'][] = array(
			'attempt'         => $payload['retry']['attempts'] ?? 0,
			'occurred_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
			'email_addresses' => array_keys( $suppressed ),
			'provider_code'   => 'suppressed',
			'error'           => __( 'Suppressed recipient skipped', 'mail-doctor' ),
			'status'          => 'skipped',
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			$table,
			array(
				'payload'       => wp_json_encode( $payload ),
				'provider_code' => 'suppressed',
				'error'         => __( 'Delivery skipped due to suppression list match.', 'mail-doctor' ),
				'status'        => 'skipped',
			),
			array( 'id' => $id ),
			array( '%s', '%s', '%s', '%s' ),
			array( '%d' )
		);

		unset( self::$active[ $id ] );

		$result = array(
			'status'  => 'skipped',
			'meta'    => array(
				'suppressed' => array_keys( $suppressed ),
			),
			'payload' => $payload,
		);

		do_action( 'mail_doctor_after_send', $result, $id );

		do_action( 'mail_doctor_log_skipped', $id, $payload );

		self::flush_metrics_cache();
	}

	/**
	 * Update stored payload with changes.
	 *
	 * @param int                  $id      Log identifier.
	 * @param array<string, mixed> $payload Payload data.
	 * @return void
	 */
	public static function update_payload( int $id, array $payload ): void {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			self::table_name(),
			array(
				'payload' => wp_json_encode( $payload ),
			),
			array( 'id' => $id ),
			array( '%s' ),
			array( '%d' )
		);
	}
}
