<?php
/**
 * Sending one logged message again.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Messages;

use TextTango\Audit\Recorder;
use TextTango\Sending\Sender;
use TextTango\Support\Clock;
use WP_Error;

defined( 'ABSPATH' ) || exit;

/**
 * Resending a message from the log (spec §11).
 *
 * ### Why a new row rather than reviving the old one
 *
 * The dedupe key is what stops a customer being messaged — and the merchant
 * billed — twice for one event. Reusing a row would mean reusing its key, and
 * the whole point of a resend is that it is a **second** send of the same text.
 * So a resend gets its own row with its own key, and the log then shows what
 * actually happened: two attempts, one of which a human asked for.
 *
 * ### What may be resent, and what may not
 *
 * Only messages that did not reach anybody. Resending something already
 * delivered sends a customer a duplicate of a message they read yesterday, and
 * there is no reading of "resend" that makes that the intent. Anything still in
 * flight is refused too — it may be seconds from arriving, and a resend would
 * race it.
 *
 * A resend does **not** bypass the opt-out list or the consent gate. Those are
 * applied by {@see Sender} on the way out, which is the point: the reason a
 * message was skipped may be exactly the reason it must not be sent again, and
 * a button on a log screen is not a good enough argument to override it.
 */
final class Resend {

	/**
	 * Statuses a message can be resent from.
	 *
	 * Everything here means "nobody received this". `sandbox` is included
	 * because a message logged while sandbox mode was on was deliberately never
	 * sent, and resending it once the mode is off is the obvious thing to want.
	 *
	 * @var string[]
	 */
	public const RESENDABLE = array(
		MessageLog::STATUS_FAILED,
		MessageLog::STATUS_SKIPPED,
		MessageLog::STATUS_TIMEOUT,
		MessageLog::STATUS_SANDBOX,
	);

	/**
	 * Send a logged message again.
	 *
	 * @param int $id Message log id.
	 * @return array<string, mixed>|WP_Error The new send's outcome.
	 */
	public static function again( int $id ) {
		$row = MessageLog::find( $id );

		if ( null === $row ) {
			return new WP_Error( 'texttango_unknown_message', __( 'That message is not in the log.', 'texttango' ) );
		}

		$refusal = self::refuse( (string) $row['status'], (string) ( $row['body'] ?? '' ), (string) ( $row['recipient'] ?? '' ) );

		if ( '' !== $refusal ) {
			return new WP_Error( 'texttango_cannot_resend', self::refusal_text( $refusal ) );
		}

		$result = Sender::queue(
			array(
				'to'         => (string) $row['recipient'],
				'message'    => (string) $row['body'],
				'event'      => (string) $row['event'],
				// Unique to this attempt, so it can never collide with the row it
				// came from or with another resend of the same row.
				'dedupe_key' => MessageLog::build_dedupe_key( 'resend', (string) $id, (string) Clock::now() ),
				'order_id'   => isset( $row['order_id'] ) ? (int) $row['order_id'] : null,
				'wp_user_id' => isset( $row['wp_user_id'] ) ? (int) $row['wp_user_id'] : null,
			)
		);

		if ( ! is_wp_error( $result ) ) {
			Recorder::manual_send( (string) $row['recipient'], 'resend' );
		}

		return $result;
	}

	/**
	 * Why a message cannot be sent again, if it cannot.
	 *
	 * Pure. Every reason is a different sentence to the merchant, so they read
	 * as one list rather than as scattered guards.
	 *
	 * @param string $status    The logged status.
	 * @param string $body      The logged message.
	 * @param string $recipient The logged number.
	 * @return string Empty when it may be resent, otherwise a machine-readable reason.
	 */
	public static function refuse( string $status, string $body, string $recipient ): string {
		if ( '' === trim( $recipient ) ) {
			return 'no_recipient';
		}

		/*
		 * A skipped message often has no body — nothing was rendered because the
		 * send was stopped before that point — and there is nothing to resend.
		 */
		if ( '' === trim( $body ) ) {
			return 'no_body';
		}

		if ( in_array( $status, array( MessageLog::STATUS_DELIVERED, MessageLog::STATUS_SENT ), true ) ) {
			return 'already_arrived';
		}

		if ( in_array( $status, array( MessageLog::STATUS_QUEUED, MessageLog::STATUS_SENDING ), true ) ) {
			return 'still_going';
		}

		if ( ! in_array( $status, self::RESENDABLE, true ) ) {
			return 'not_resendable';
		}

		return '';
	}

	/**
	 * A refusal in words.
	 *
	 * @param string $reason Machine-readable reason from {@see self::refuse()}.
	 */
	public static function refusal_text( string $reason ): string {
		$reasons = array(
			'no_recipient'    => __( 'That message has no number on it, so there is nowhere to send it.', 'texttango' ),
			'no_body'         => __( 'That message was stopped before anything was written, so there is nothing to send.', 'texttango' ),
			'already_arrived' => __( 'That message was already sent. Resending it would deliver the same text to the customer twice.', 'texttango' ),
			'still_going'     => __( 'That message has not finished sending yet. Wait for it to settle before trying again.', 'texttango' ),
			'not_resendable'  => __( 'That message cannot be sent again.', 'texttango' ),
		);

		return $reasons[ $reason ] ?? '';
	}

	/**
	 * Whether a logged row offers a resend button.
	 *
	 * @param array<string, mixed> $row Message log row.
	 */
	public static function is_offered( array $row ): bool {
		return '' === self::refuse(
			(string) ( $row['status'] ?? '' ),
			(string) ( $row['body'] ?? '' ),
			(string) ( $row['recipient'] ?? '' )
		);
	}
}
