<?php
/**
 * Texting the link that lets somebody back into their account.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Otp;

use TextTango\Messages\MessageLog;
use TextTango\Sending\Sender;
use TextTango\WooCommerce\Events;
use TextTango\WooCommerce\Templates;

defined( 'ABSPATH' ) || exit;

/**
 * The password reset link, by SMS as well as by email (spec §6).
 *
 * ### Who this is for
 *
 * Somebody with an ordinary account and a verified phone who has lost access to
 * their inbox. They know their email address — they simply cannot read what is
 * sent to it — so they can start the reset and then finish it from their
 * handset.
 *
 * It is deliberately **not** for accounts this plugin created from a phone
 * number. Those have no email at all and a generated username nobody has seen,
 * so they cannot use the lost-password form in the first place; they also do
 * not need to, because they sign in with a code and can change their password
 * from their profile.
 *
 * ### One link, two channels
 *
 * The URL sent here is the one WordPress has just built for its own email, taken
 * from `retrieve_password_key`. Nothing new is minted and nothing extra is
 * granted — the same single-use key travels by a second route.
 *
 * ### Only a verified number, and this is the important part
 *
 * {@see \TextTango\Consent\Recipient::for_user()} falls back to `billing_phone`
 * when no verified number exists, and that fallback must never apply here. A
 * billing phone can be stale, mistyped at checkout, or a shop's own landline; a
 * reset link grants access to the account, so sending it to an unproven number
 * is handing somebody's account to whoever holds that handset.
 *
 * This reads {@see UserRepository::phone_for()} directly — the number the owner
 * proved they hold — and sends nothing at all when there is not one.
 */
final class PasswordReset {

	/**
	 * Event id.
	 */
	public const EVENT = 'password_reset';

	/**
	 * Register hooks.
	 */
	public function register(): void {
		add_action( 'retrieve_password_key', array( self::class, 'on_reset_requested' ), 10, 2 );
	}

	/**
	 * WordPress has just issued a reset key.
	 *
	 * @param string $user_login The account's login.
	 * @param string $key        The reset key.
	 */
	public static function on_reset_requested( $user_login, $key ): void {
		$user_login = (string) $user_login;
		$key        = (string) $key;

		if ( '' === $user_login || '' === $key || ! Events::is_enabled( self::EVENT ) ) {
			return;
		}

		$user = get_user_by( 'login', $user_login );

		if ( ! $user instanceof \WP_User ) {
			return;
		}

		// The verified number, never the billing one. See the class docblock:
		// this link is account access, and an unproven number is somebody else.
		$recipient = UserRepository::phone_for( (int) $user->ID );

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

		$link = self::link( $user_login, $key );
		$body = Templates::render(
			Events::template( self::EVENT ),
			Templates::reset_values( (int) $user->ID, $link )
		);

		/** This filter is documented in src/WooCommerce/Notifications.php */
		$body = (string) apply_filters( 'texttango_message_template', $body, self::EVENT, null );

		if ( '' === trim( $body ) ) {
			return;
		}

		/*
		 * Sent now rather than queued, and that is forced by where the link ends
		 * up. A queued message is delivered from what the log holds, so a redacted
		 * log body would be the thing that actually went out — `Sender::queue()`
		 * drops `log_body` for exactly that reason. Sending immediately is the
		 * only way to put the real link in the message and a masked one in the
		 * log, which matters because the log is readable by every shop manager
		 * and this link is account access.
		 *
		 * The synchronous cost is one HTTP call on a request that is already
		 * sending an email synchronously, so it is the same shape of latency the
		 * lost-password POST already carries.
		 */
		Sender::send(
			array(
				'to'         => $recipient,
				'message'    => $body,
				'event'      => self::EVENT,
				// Keyed on the key itself, so asking twice sends twice. Each
				// request invalidates the previous link, and somebody who asked
				// again because the first never arrived must get the one that now
				// works rather than be deduplicated into silence.
				'dedupe_key' => MessageLog::build_dedupe_key( self::EVENT, (string) $user->ID, $key ),
				'wp_user_id' => (int) $user->ID,
				'log_body'   => str_replace( $link, '••••••', $body ),
			)
		);
	}

	/**
	 * The reset URL WordPress itself would email.
	 *
	 * Built the same way core does in `wp-login.php`, so the two channels carry
	 * exactly the same link rather than two that merely look alike.
	 *
	 * @param string $user_login The account's login.
	 * @param string $key        The reset key.
	 */
	public static function link( string $user_login, string $key ): string {
		return network_site_url(
			'wp-login.php?action=rp&key=' . rawurlencode( $key ) . '&login=' . rawurlencode( $user_login ),
			'login'
		);
	}
}
