<?php
/**
 * WP-CLI commands.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Cli;

use TextTango\Api\PhoneNumber;
use TextTango\Audit\Recorder;
use TextTango\Contacts\ContactReconciler;
use TextTango\Contacts\ContactSync;
use TextTango\Messages\MessageLog;
use TextTango\Sending\Sender;
use TextTango\Support\Sandbox;
use TextTango\Support\Settings;
use TextTango\Webhook\Reconciler;
use TextTango\Webhook\Registration;
use TextTango\Webhook\WebhookState;
use WP_CLI;

defined( 'ABSPATH' ) || exit;

/**
 * Manage TextTango from the command line (spec §18).
 *
 * For agencies and CI, where the settings screen is not reachable: checking a
 * deploy left the connection intact, sending from a script, and forcing the
 * background sweeps rather than waiting for cron.
 *
 * Everything routes through the same code the admin screens use, so a message
 * sent from here is subject to the same consent gate, opt-out list, sandbox mode
 * and delivery log. There is no back door, and no output ever prints a token or
 * a signing secret.
 */
final class Commands {

	/**
	 * Show the connection, sandbox state and queue at a glance.
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Render as a table, or as JSON for scripting.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     wp texttango status
	 *     wp texttango status --format=json
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Flags.
	 */
	public function status( $args, $assoc_args ): void {
		unset( $args );

		$connected = texttango_is_connected();
		$balance   = $connected ? texttango_get_balance() : null;

		$rows = array(
			$this->row( 'Connected', $connected ? 'yes' : 'no' ),
			$this->row( 'API base URL', (string) Settings::get( 'api_base_url', '' ) ),
			$this->row(
				'Balance',
				is_array( $balance ) ? $balance['formatted'] : ( $balance instanceof \WP_Error ? 'error: ' . $balance->get_error_message() : 'unknown' )
			),
			$this->row( 'Sender ID', (string) Settings::get( 'sender_id', '' ) ),
			$this->row( 'Sandbox', Sandbox::is_active() ? 'on — ' . Sandbox::describe() : 'off' ),
			$this->row( 'Environment', wp_get_environment_type() ),
			// Presence only. A command that printed a credential would put it into
			// shell history, CI logs and anyone's terminal scrollback.
			$this->row( 'API token', Settings::secret_exists( Settings::OPTION_API_KEY ) ? 'stored' : 'not set' ),
			$this->row( 'Webhook secret', Settings::secret_exists( Settings::OPTION_WEBHOOK_SECRET ) ? 'stored' : 'not set' ),
			$this->row(
				'Receipts proven',
				WebhookState::is_verified()
					? 'yes, ' . human_time_diff( WebhookState::last_arrival() ) . ' ago'
					: 'never'
			),
			$this->row( 'Messages logged', (string) MessageLog::count() ),
		);

		WP_CLI\Utils\format_items(
			$assoc_args['format'] ?? 'table',
			$rows,
			array( 'setting', 'value' )
		);

		if ( ! $connected ) {
			WP_CLI::warning( 'No usable API token, so nothing will send.' );
		}
	}

	/**
	 * Check the stored credentials against the API.
	 *
	 * Costs nothing and sends nothing — it reads the wallet, which is enough to
	 * prove the token is accepted.
	 *
	 * ## EXAMPLES
	 *
	 *     wp texttango test
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Flags.
	 */
	public function test( $args, $assoc_args ): void {
		unset( $args, $assoc_args );

		$balance = texttango_get_balance();

		if ( is_wp_error( $balance ) ) {
			WP_CLI::error( $balance->get_error_message() );
		}

		WP_CLI::success( sprintf( 'Connected. Balance %s.', $balance['formatted'] ) );
	}

	/**
	 * Send one SMS.
	 *
	 * ## OPTIONS
	 *
	 * --to=<number>
	 * : Recipient. Local formats are accepted and normalized.
	 *
	 * --message=<message>
	 * : Body to send.
	 *
	 * [--event=<event>]
	 * : Event name recorded in the log.
	 * ---
	 * default: cli
	 * ---
	 *
	 * [--country=<code>]
	 * : ISO 3166-1 alpha-2 hint for normalizing the number.
	 *
	 * [--real]
	 * : Send for real even when sandbox mode is on. Spends credits.
	 *
	 * ## EXAMPLES
	 *
	 *     wp texttango send --to=+233244123456 --message="Deploy finished"
	 *     wp texttango send --to=0244123456 --country=GH --message="Hello" --real
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Flags.
	 */
	public function send( $args, $assoc_args ): void {
		unset( $args );

		$to      = (string) ( $assoc_args['to'] ?? '' );
		$message = (string) ( $assoc_args['message'] ?? '' );

		if ( '' === $to || '' === trim( $message ) ) {
			WP_CLI::error( 'Both --to and --message are required.' );
		}

		$real = isset( $assoc_args['real'] );

		if ( $real && Sandbox::is_active() ) {
			WP_CLI::confirm( 'Sandbox is on. Sending for real will spend credits. Continue?', $assoc_args );
		}

		/*
		 * Goes through Sender rather than texttango_send_sms(), because --real has
		 * to reach `bypass_sandbox`, which the public API deliberately does not
		 * expose. Somebody at a terminal has asked for it explicitly and been
		 * warned; a plugin calling the API has not.
		 */
		$result = Sender::send(
			array(
				'to'             => $to,
				'message'        => $message,
				'event'          => (string) ( $assoc_args['event'] ?? 'cli' ),
				'country'        => $assoc_args['country'] ?? null,
				'bypass_sandbox' => $real,
			)
		);

		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		Recorder::manual_send( $to, 'WP-CLI' );

		if ( MessageLog::STATUS_SANDBOX === ( $result['status'] ?? '' ) ) {
			WP_CLI::success(
				sprintf( 'Logged, not sent — sandbox mode is on. Row %d. Use --real to send anyway.', (int) $result['message_id'] )
			);

			return;
		}

		WP_CLI::success(
			sprintf(
				'Sent to %1$s. UUID %2$s, log row %3$d.',
				PhoneNumber::mask( (string) ( MessageLog::find( (int) $result['message_id'] )['recipient'] ?? $to ) ),
				(string) ( $result['uuid'] ?? '' ),
				(int) $result['message_id']
			)
		);
	}

	/**
	 * Reconcile the TextTango contact group with who has consented.
	 *
	 * The same nightly sweep, run now. Adds anyone who agreed but was missed and
	 * removes anyone who withdrew but was not taken off.
	 *
	 * ## EXAMPLES
	 *
	 *     wp texttango sync-contacts
	 *
	 * @subcommand sync-contacts
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Flags.
	 */
	public function sync_contacts( $args, $assoc_args ): void {
		unset( $args, $assoc_args );

		if ( ! ContactSync::is_enabled() ) {
			WP_CLI::error( 'Contact sync is off, or no contact group is chosen.' );
		}

		$report = ContactReconciler::run();

		WP_CLI::success(
			sprintf(
				'%d added, %d removed, %d could not be added.',
				(int) $report['pushed'],
				(int) $report['removed'],
				(int) $report['failed']
			)
		);

		if ( ! empty( $report['truncated'] ) ) {
			WP_CLI::warning( 'More were waiting than one run handles. Run again to continue.' );
		}
	}

	/**
	 * Chase delivery status for messages whose receipt never arrived.
	 *
	 * ## EXAMPLES
	 *
	 *     wp texttango resync-status
	 *
	 * @subcommand resync-status
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Flags.
	 */
	public function resync_status( $args, $assoc_args ): void {
		unset( $args, $assoc_args );

		$result = Reconciler::run();

		WP_CLI::success(
			sprintf(
				'%d checked, %d resolved.',
				(int) ( $result['checked'] ?? 0 ),
				(int) ( $result['updated'] ?? 0 )
			)
		);
	}

	/**
	 * Show how TextTango is configured to send delivery receipts here.
	 *
	 * ## OPTIONS
	 *
	 * [--register]
	 * : Point the token's SMS callback at this site.
	 *
	 * ## EXAMPLES
	 *
	 *     wp texttango webhook
	 *     wp texttango webhook --register
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Flags.
	 */
	public function webhook( $args, $assoc_args ): void {
		unset( $args );

		if ( isset( $assoc_args['register'] ) ) {
			$registered = Registration::register();

			if ( is_wp_error( $registered ) ) {
				WP_CLI::error( $registered->get_error_message() );
			}

			WP_CLI::success( 'Registered.' );
		}

		$status = Registration::status();

		if ( is_wp_error( $status ) ) {
			WP_CLI::error( $status->get_error_message() );
		}

		WP_CLI::log( 'Registered here : ' . ( $status['registered'] ? 'yes' : 'no' ) );
		WP_CLI::log( 'Signing secret  : ' . ( $status['has_secret'] ? 'set' : 'missing' ) );
		WP_CLI::log( 'Receipts proven : ' . ( $status['verified'] ? 'yes' : 'never' ) );

		if ( $status['test_goes_elsewhere'] ) {
			WP_CLI::warning( 'A campaign callback URL points elsewhere, so test events go there rather than here.' );
		}
	}

	/**
	 * One row of the status table.
	 *
	 * @param string $setting Label.
	 * @param string $value   Value.
	 * @return array{setting: string, value: string}
	 */
	private function row( string $setting, string $value ): array {
		return array(
			'setting' => $setting,
			'value'   => '' !== $value ? $value : '—',
		);
	}
}
