<?php
/**
 * Email delivery verification service.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Mail;

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

/**
 * Sends one safe WordPress mail test and stores a non-PII result.
 */
class EmailVerificationService {
	const OPTION_KEY                  = 'subin_email_health_last_verification';
	const RESULT_VERSION              = 2;
	const LEGACY_RESULT_VERSION       = 1;
	const RATE_LIMIT_SECONDS          = 60;
	const RATE_PREFIX                 = 'subin_email_verify_';
	const CONFIRMATION_WINDOW_SECONDS = 604800;

	/** @var \SUBIN_SettingsRepository */
	private $settings;

	/** @var EmailErrorClassifier */
	private $classifier;

	/** @var callable|null */
	private $mailer;

	/**
	 * Constructor.
	 *
	 * @param \SUBIN_SettingsRepository $settings Settings repository.
	 * @param EmailErrorClassifier|null  $classifier Error classifier.
	 * @param callable|null              $mailer Optional test mailer.
	 */
	public function __construct( \SUBIN_SettingsRepository $settings, ?EmailErrorClassifier $classifier = null, $mailer = null ) {
		$this->settings   = $settings;
		$this->classifier = $classifier ? $classifier : new EmailErrorClassifier();
		$this->mailer     = is_callable( $mailer ) ? $mailer : null;
	}

	/**
	 * Send a verification email.
	 *
	 * The recipient is validated and passed to wp_mail, but never persisted in
	 * the verification option. Detailed message history remains in Email Log.
	 *
	 * @param mixed $recipient Raw recipient.
	 * @param int   $actor_id Current user id.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function send( $recipient, $actor_id ) {
		$raw_recipient = is_scalar( $recipient ) ? trim( (string) $recipient ) : '';
		$recipient     = sanitize_email( $raw_recipient );
		$actor_id      = absint( $actor_id );

		if ( '' === $recipient || ! is_email( $raw_recipient ) || $recipient !== $raw_recipient ) {
			return new \WP_Error( 'subin_verification_invalid_recipient', __( 'Enter a valid recipient email address.', 'submission-inbox' ) );
		}

		if ( $actor_id < 1 ) {
			return new \WP_Error( 'subin_verification_actor_required', __( 'Sign in before sending a verification email.', 'submission-inbox' ) );
		}

		$rate_key = self::RATE_PREFIX . $actor_id;
		if ( get_transient( $rate_key ) ) {
			return new \WP_Error( 'subin_verification_rate_limited', __( 'A verification email was just requested. Wait one minute and try again.', 'submission-inbox' ) );
		}
		set_transient( $rate_key, 1, self::RATE_LIMIT_SECONDS );

		$mail_error  = null;
		$failed_hook = static function ( $error ) use ( &$mail_error ) {
			if ( is_wp_error( $error ) ) {
				$mail_error = $error;
			}
		};
		add_action( 'wp_mail_failed', $failed_hook, 99 );
		\SUBIN\MailLog\MailEventTypeContext::push( \SUBIN\MailLog\MailEventTypeContext::KIND_HEALTH_VERIFICATION );

		try {
			$args = array(
				$recipient,
				__( 'InboxMend Email Health verification', 'submission-inbox' ),
				__( 'InboxMend asked WordPress to send this verification email. Receiving it confirms the full path to this mailbox; WordPress acceptance alone does not.', 'submission-inbox' ),
			);
			$sent = $this->mailer ? (bool) call_user_func_array( $this->mailer, $args ) : wp_mail( $args[0], $args[1], $args[2] );
		} catch ( \Throwable $throwable ) {
			$sent       = false;
			$mail_error = new \WP_Error( 'subin_verification_exception', $throwable->getMessage() );
		} finally {
			\SUBIN\MailLog\MailEventTypeContext::pop();
			remove_action( 'wp_mail_failed', $failed_hook, 99 );
		}

		if ( $sent ) {
			$result = $this->store_result(
				array(
					'status'           => 'accepted',
					'confirmation'     => 'awaiting',
					'confirmed_at_gmt' => '',
					'issue_code'       => '',
					'issue_title'      => '',
					'technical_error'  => '',
					'transport'        => $this->transport_name(),
				)
			);

			$result['message'] = __( 'WordPress accepted the verification email. Confirm that it arrived in the recipient inbox or spam folder.', 'submission-inbox' );
			return $result;
		}

		$raw_error = is_wp_error( $mail_error ) ? (string) $mail_error->get_error_message() : __( 'wp_mail returned false without a transport error.', 'submission-inbox' );
		$issue     = $this->classifier->classify( $this->redact_configured_secrets( $raw_error ) );
		$result    = $this->store_result(
			array(
				'status'           => 'failed',
				'confirmation'     => 'not_applicable',
				'confirmed_at_gmt' => '',
				'issue_code'       => $issue['code'],
				'issue_title'      => $issue['title'],
				'technical_error'  => $issue['technical_error'],
				'transport'        => $this->transport_name(),
			)
		);
		$message = sprintf(
			/* translators: %s: classified email issue title. */
			__( 'Verification failed: %s. Review Email Health for the next action.', 'submission-inbox' ),
			$issue['title']
		);

		return new \WP_Error( 'subin_verification_failed', $message, array( 'result' => $result ) );
	}

	/**
	 * Get the latest validated result.
	 *
	 * @return array<string,mixed>|null
	 */
	public function last_result() {
		$stored = get_option( self::OPTION_KEY, null );
		if ( ! is_array( $stored ) ) {
			return null;
		}

		$stored_version = absint( $stored['version'] ?? 0 );
		if ( ! in_array( $stored_version, array( self::LEGACY_RESULT_VERSION, self::RESULT_VERSION ), true ) ) {
			return null;
		}

		$status = sanitize_key( $stored['status'] ?? '' );
		if ( ! in_array( $status, array( 'accepted', 'failed' ), true ) ) {
			return null;
		}

		$confirmation = sanitize_key( $stored['confirmation'] ?? '' );
		if ( self::LEGACY_RESULT_VERSION === $stored_version ) {
			$confirmation = 'accepted' === $status ? 'awaiting' : 'not_applicable';
		} elseif ( 'failed' === $status ) {
			$confirmation = 'not_applicable';
		} elseif ( ! in_array( $confirmation, array( 'awaiting', 'received', 'not_received' ), true ) ) {
			$confirmation = 'awaiting';
		}

		$confirmed_at_gmt = sanitize_text_field( $stored['confirmed_at_gmt'] ?? '' );
		if ( ! in_array( $confirmation, array( 'received', 'not_received' ), true ) ) {
			$confirmed_at_gmt = '';
		}

		return array(
			'version'          => self::RESULT_VERSION,
			'status'           => $status,
			'confirmation'     => $confirmation,
			'tested_at_gmt'    => sanitize_text_field( $stored['tested_at_gmt'] ?? '' ),
			'confirmed_at_gmt' => $confirmed_at_gmt,
			'issue_code'       => substr( sanitize_key( $stored['issue_code'] ?? '' ), 0, 100 ),
			'issue_title'      => sanitize_text_field( $stored['issue_title'] ?? '' ),
			'technical_error'  => $this->classifier->sanitize_technical_error( (string) ( $stored['technical_error'] ?? '' ) ),
			'transport'        => in_array( $stored['transport'] ?? '', array( 'smtp', 'wordpress_default' ), true ) ? $stored['transport'] : 'wordpress_default',
		);
	}

	/**
	 * Record whether the most recently accepted verification reached the mailbox.
	 *
	 * Only a strict boolean is accepted so request-boundary normalization cannot
	 * silently turn an ambiguous value into a delivery claim.
	 *
	 * @param mixed $received Whether the message arrived.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function confirm_arrival( $received ) {
		if ( ! is_bool( $received ) ) {
			return new \WP_Error( 'subin_verification_confirmation_invalid', __( 'Choose whether the verification email arrived.', 'submission-inbox' ) );
		}

		$result = $this->last_result();
		if ( ! is_array( $result ) || 'accepted' !== ( $result['status'] ?? '' ) ) {
			return new \WP_Error( 'subin_verification_confirmation_unavailable', __( 'Send a successful verification email before confirming delivery.', 'submission-inbox' ) );
		}

		$tested_at = $this->gmt_timestamp( $result['tested_at_gmt'] ?? '' );
		$now       = time();
		if ( ! $tested_at || $tested_at > $now || $tested_at < $now - self::CONFIRMATION_WINDOW_SECONDS ) {
			return new \WP_Error( 'subin_verification_confirmation_expired', __( 'This verification is too old to confirm. Send a new verification email.', 'submission-inbox' ) );
		}

		$result['version']          = self::RESULT_VERSION;
		$result['confirmation']     = $received ? 'received' : 'not_received';
		$result['confirmed_at_gmt'] = gmdate( 'Y-m-d H:i:s' );
		update_option( self::OPTION_KEY, $result, false );
		do_action( 'subin_email_health_verification_updated', $result );

		return $result;
	}

	/**
	 * Remove configured credentials from a transport error before classification.
	 *
	 * @param string $message Raw message.
	 * @return string
	 */
	public function redact_configured_secrets( $message ) {
		$message  = (string) $message;
		$settings = $this->settings->all();
		foreach ( array( 'smtp_password', 'smtp_username' ) as $key ) {
			$value = (string) ( $settings[ $key ] ?? '' );
			if ( '' !== $value ) {
				$message = str_ireplace( $value, '[redacted]', $message );
			}
		}

		return $this->classifier->sanitize_technical_error( $message );
	}

	/** Store a non-PII result. */
	private function store_result( array $result ) {
		$status       = in_array( $result['status'] ?? '', array( 'accepted', 'failed' ), true ) ? $result['status'] : 'failed';
		$confirmation = sanitize_key( $result['confirmation'] ?? '' );
		if ( 'failed' === $status ) {
			$confirmation = 'not_applicable';
		} elseif ( ! in_array( $confirmation, array( 'awaiting', 'received', 'not_received' ), true ) ) {
			$confirmation = 'awaiting';
		}

		$stored = array(
			'version'          => self::RESULT_VERSION,
			'status'           => $status,
			'confirmation'     => $confirmation,
			'tested_at_gmt'    => gmdate( 'Y-m-d H:i:s' ),
			'confirmed_at_gmt' => in_array( $confirmation, array( 'received', 'not_received' ), true ) ? sanitize_text_field( $result['confirmed_at_gmt'] ?? '' ) : '',
			'issue_code'       => substr( sanitize_key( $result['issue_code'] ?? '' ), 0, 100 ),
			'issue_title'      => sanitize_text_field( $result['issue_title'] ?? '' ),
			'technical_error'  => $this->classifier->sanitize_technical_error( (string) ( $result['technical_error'] ?? '' ) ),
			'transport'        => in_array( $result['transport'] ?? '', array( 'smtp', 'wordpress_default' ), true ) ? $result['transport'] : 'wordpress_default',
		);
		update_option( self::OPTION_KEY, $stored, false );

		return $stored;
	}

	/** Parse the plugin's fixed UTC date format without using the site timezone. */
	private function gmt_timestamp( $value ) {
		$value = sanitize_text_field( (string) $value );
		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value ) ) {
			return 0;
		}

		$timestamp = strtotime( $value . ' UTC' );
		return false === $timestamp ? 0 : $timestamp;
	}

	/** Return the configured transport label key without exposing its settings. */
	private function transport_name() {
		$settings = $this->settings->all();
		return ! empty( $settings['enable_smtp'] ) && '' !== trim( (string) ( $settings['smtp_host'] ?? '' ) ) ? 'smtp' : 'wordpress_default';
	}
}
