<?php
/**
 * WP-CLI integration for Mail Doctor.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

use MailDoctor\Helpers\Settings;
use WP_CLI;
use WP_CLI_Command;
use WP_Error;
use function WP_CLI\Utils\format_items;

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

/**
 * Expose plugin utilities to the WP-CLI toolchain.
 */
class CLI extends WP_CLI_Command {

	/**
	 * Register command handlers with WP-CLI.
	 *
	 * @return void
	 */
	public static function register(): void {
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			WP_CLI::add_command( 'maildoctor', __CLASS__ );
		}
	}

	/**
	 * Send a test email using the configured SMTP transport.
	 *
	 * ## OPTIONS
	 *
	 * [--to=<email>]
	 * : Recipient email address. Defaults to the site administrator email.
	 *
	 * ## EXAMPLES
	 *
	 *     wp maildoctor test-send --to=you@example.com
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Associative arguments.
	 * @return void
	 */
	public function test_send( array $args, array $assoc_args ): void {
		$recipient = isset( $assoc_args['to'] ) ? sanitize_email( $assoc_args['to'] ) : get_option( 'admin_email' );

		if ( ! $recipient || ! is_email( $recipient ) ) {
			WP_CLI::error( __( 'A valid email address is required for the test send.', 'mail-doctor' ) );
			return;
		}

		Mailer::force_next_override();
		Mailer::set_event_context( 'mail_doctor_cli_test' );

		$subject = __( 'Mail Doctor Test Email', 'mail-doctor' );
		$message = sprintf(
			/* translators: %s: site name. */
			__( "Hello from %s!\n\nThis message was sent from the Mail Doctor WP-CLI integration to validate SMTP routing.\n\nIf you are reading this, the transport path is healthy.", 'mail-doctor' ),
			wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES )
		);
		$headers = array( 'Content-Type: text/plain; charset=UTF-8' );

		$start   = microtime( true );
		$success = wp_mail( $recipient, $subject, $message, $headers );
		$latency = (int) round( ( microtime( true ) - $start ) * 1000 );

		Mailer::clear_event_context();

		if ( ! $success ) {
			WP_CLI::error( __( 'The test email could not be dispatched. Review SMTP configuration.', 'mail-doctor' ) );
			return;
		}

		$log = Logger::latest_by_event( 'mail_doctor_cli_test' );

		if ( $log ) {
			WP_CLI::log(
				sprintf(
					/* translators: 1: log id, 2: log status */
					__( 'Log #%1$d captured with status: %2$s', 'mail-doctor' ),
					(int) $log['id'],
					$log['status'] ?? 'unknown'
				)
			);
		}

		WP_CLI::success(
			sprintf(
				/* translators: 1: email, 2: latency */
				__( 'Test email sent to %1$s in %2$dms.', 'mail-doctor' ),
				$recipient,
				$latency
			)
		);
	}

	/**
	 * End-to-end pipeline verification harness.
	 *
	 * ## OPTIONS
	 *
	 * <subcommand>
	 * : Currently only supports "verify".
	 *
	 * [--email=<id>]
	 * : WooCommerce email identifier to render. Defaults to customer_processing_order.
	 *
	 * [--order=<id>]
	 * : Use a specific order for template tokens. Defaults to the most recent order.
	 *
	 * [--to=<email>]
	 * : Recipient address for the probe. Defaults to the site administrator.
	 *
	 * [--transport=<slug>]
	 * : Limit verification to a single transport (smtp, mailgun_api, sendgrid_api, ses_api, postmark_api, sparkpost_api, generic_http).
	 *
	 * ## EXAMPLES
	 *
	 *     wp maildoctor pipeline verify --email=customer_processing_order --to=ops@example.com
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Associative arguments.
	 * @return void
	 */
	public function pipeline( array $args, array $assoc_args ): void {
		if ( empty( $args ) ) {
			WP_CLI::error( __( 'A pipeline subcommand is required (e.g. verify).', 'mail-doctor' ) );
			return;
		}

		$subcommand = array_shift( $args );

		switch ( $subcommand ) {
			case 'verify':
				$this->pipeline_verify( $assoc_args );
				break;
			default:
				WP_CLI::error(
					sprintf(
						/* translators: %s: subcommand */
						__( 'Unknown pipeline subcommand: %s', 'mail-doctor' ),
						$subcommand
					)
				);
		}
	}

	/**
	 * Execute the pipeline verification flow.
	 *
	 * @param array<string, string> $assoc_args Command options.
	 * @return void
	 */
	protected function pipeline_verify( array $assoc_args ): void {
		if ( ! function_exists( 'WC' ) ) {
			WP_CLI::error( __( 'WooCommerce is required to verify email templates.', 'mail-doctor' ) );
			return;
		}

		$email_id  = isset( $assoc_args['email'] ) ? sanitize_key( $assoc_args['email'] ) : 'customer_processing_order';
		$order_id  = isset( $assoc_args['order'] ) ? (int) $assoc_args['order'] : null;
		$recipient = isset( $assoc_args['to'] ) ? sanitize_email( $assoc_args['to'] ) : sanitize_email( get_option( 'admin_email' ) );

		if ( ! $recipient || ! is_email( $recipient ) ) {
			WP_CLI::error( __( 'Provide a valid recipient email using --to.', 'mail-doctor' ) );
			return;
		}

		$transport_filter = isset( $assoc_args['transport'] ) ? sanitize_key( $assoc_args['transport'] ) : null;

		$current_settings = Settings::all();
		$transports       = $this->configured_transports( $current_settings, $transport_filter );

		if ( empty( $transports ) ) {
			WP_CLI::error( __( 'No configured transports were detected for verification.', 'mail-doctor' ) );
			return;
		}

		$stored_option = get_option( Settings::OPTION_KEY, array() );
		if ( ! is_array( $stored_option ) ) {
			$stored_option = array();
		}
		$original_option    = $stored_option;
		$original_transport = $stored_option['transport'] ?? ( $current_settings['transport'] ?? 'smtp' );

		$lock_key   = 'mail_doctor_cli_lock';
		$lock_token = wp_rand();
		if ( get_transient( $lock_key ) ) {
			WP_CLI::error( __( 'Another Mail Doctor pipeline verification is already running. Try again shortly.', 'mail-doctor' ) );
			return;
		}
		set_transient( $lock_key, $lock_token, 300 );

		Mailer::trace_maildbg(
			'cli.pipeline_start',
			array(
				'transports' => array_keys( $transports ),
				'email_id'   => $email_id,
				'recipient'  => $recipient,
			)
		);

		$rows = array();

		try {
			foreach ( $transports as $transport => $label ) {
				$stored_option['transport'] = $transport;
				$this->persist_settings( $stored_option );

				WP_CLI::log(
					sprintf(
						/* translators: 1: transport slug, 2: label */
						__( 'Testing %1$s (%2$s)...', 'mail-doctor' ),
						$transport,
						$label
					)
				);

				Mailer::force_next_override();
				Mailer::set_event_context( 'cli_pipeline_' . $transport );
				Mailer::set_email_id( $email_id );

				$preview = Woo::get_preview( $email_id, $order_id );
				if ( $preview instanceof WP_Error ) {
					$rows[] = array(
						'transport'   => $transport,
						'result'      => 'FAIL',
						'correlation' => '',
						'log_id'      => '',
						'note'        => $preview->get_error_message(),
					);
					Mailer::trace_maildbg(
						'cli.pipeline_preview_error',
						array(
							'transport' => $transport,
							'error'     => $preview->get_error_message(),
						)
					);
					Mailer::cleanup_context();
					Mailer::clear_event_context();
					continue;
				}

				$resolved_order = isset( $preview['order_id'] ) ? (int) $preview['order_id'] : 0;
				if ( $resolved_order > 0 ) {
					Mailer::set_order_context( $resolved_order );
				}

				/* translators: %s: transport label */
				$subject = $preview['subject'] ?? sprintf( __( '[Pipeline] %s', 'mail-doctor' ), $label );
				$html    = $preview['html'] ?? '';
				if ( '' === trim( (string) $html ) ) {
					$html = sprintf(
						/* translators: %s: transport label */
						__( 'Mail Doctor pipeline diagnostic for %s.', 'mail-doctor' ),
						$label
					);
				}

				$headers = array( 'Content-Type: text/html; charset=UTF-8' );

				Mailer::trace_maildbg(
					'cli.pipeline_send',
					array(
						'transport' => $transport,
						'latency'   => null,
					)
				);

				$start      = microtime( true );
				$send_value = wp_mail( $recipient, $subject, $html, $headers );
				$latency    = (int) round( ( microtime( true ) - $start ) * 1000 );

				$result = 'PASS';
				/* translators: %d: milliseconds */
				$note    = sprintf( __( '%dms', 'mail-doctor' ), $latency );
				$log_id  = '';
				$corr_id = '';

				if ( $send_value instanceof WP_Error ) {
					$result = 'FAIL';
					$note   = $send_value->get_error_message();
				} elseif ( ! $send_value ) {
					$result = 'FAIL';
					$note   = __( 'wp_mail returned false.', 'mail-doctor' );
				}

				$log = Logger::latest_by_event( 'cli_pipeline_' . $transport );

				if ( $log ) {
					$log_id  = (int) $log['id'];
					$payload = Logger::get_payload( $log_id );
					if ( isset( $payload['meta']['correlation_id'] ) ) {
						$corr_id = (string) $payload['meta']['correlation_id'];
					}
				}

				$rows[] = array(
					'transport'   => $transport,
					'result'      => $result,
					'correlation' => $corr_id,
					'log_id'      => $log_id,
					'note'        => $note,
				);

				Mailer::trace_maildbg(
					'cli.pipeline_result',
					array(
						'transport'   => $transport,
						'result'      => $result,
						'latency'     => $latency,
						'log_id'      => $log_id,
						'correlation' => $corr_id,
					)
				);

				Mailer::cleanup_context();
				Mailer::clear_event_context();
			}
		} finally {
			Mailer::clear_event_context();
			Mailer::cleanup_context();
			$original_option['transport'] = $original_transport;
			$this->persist_settings( $original_option );
			if ( get_transient( $lock_key ) === $lock_token ) {
				delete_transient( $lock_key );
			}
		}

		format_items( 'table', $rows, array( 'transport', 'result', 'correlation', 'log_id', 'note' ) );
	}

	/**
	 * Determine configured transports keyed by slug.
	 *
	 * @param array<string, mixed> $settings Current settings.
	 * @param string|null          $only     Optional specific transport.
	 * @return array<string, string>
	 */
	protected function configured_transports( array $settings, ?string $only = null ): array {
		$map = array();

		$include = static function ( string $slug ) use ( $only ): bool {
			return null === $only || $only === $slug;
		};

		if ( $include( 'smtp' ) && ! empty( $settings['smtp_host'] ) ) {
			$map['smtp'] = __( 'SMTP', 'mail-doctor' );
		}

		if ( $include( 'mailgun_api' ) && ! empty( $settings['mailgun_api_key'] ) && ! empty( $settings['mailgun_api_domain'] ) ) {
			$map['mailgun_api'] = __( 'Mailgun API', 'mail-doctor' );
		}

		if ( $include( 'sendgrid_api' ) && ! empty( $settings['sendgrid_api_key'] ) ) {
			$map['sendgrid_api'] = __( 'SendGrid API', 'mail-doctor' );
		}

		if ( $include( 'postmark_api' ) && ! empty( $settings['postmark_api_token'] ) ) {
			$map['postmark_api'] = __( 'Postmark API', 'mail-doctor' );
		}

		if ( $include( 'sparkpost_api' ) && ! empty( $settings['sparkpost_api_key'] ) ) {
			$map['sparkpost_api'] = __( 'SparkPost API', 'mail-doctor' );
		}

		if ( $include( 'ses_api' ) && ! empty( $settings['ses_api_key'] ) && ! empty( $settings['ses_api_secret'] ) ) {
			$map['ses_api'] = __( 'Amazon SES API', 'mail-doctor' );
		}

		if ( $include( 'generic_http' ) && ! empty( $settings['generic_api_endpoint'] ) ) {
			$map['generic_http'] = __( 'Generic HTTP API', 'mail-doctor' );
		}

		if ( null !== $only && empty( $map ) ) {
			WP_CLI::warning(
				sprintf(
					/* translators: %s transport slug */
					__( 'Requested transport "%s" is not configured.', 'mail-doctor' ),
					$only
				)
			);
		}

		return $map;
	}

	/**
	 * Persist sanitized plugin settings for verification flows.
	 *
	 * @param array<string,mixed> $settings Settings payload.
	 * @return void
	 */
	protected function persist_settings( array $settings ): void {
		update_option( Settings::OPTION_KEY, $settings );
	}

	/**
	 * Inspect logs through WP-CLI.
	 *
	 * ## OPTIONS
	 *
	 * <subcommand>
	 * : The logs subcommand to execute. Currently supports "tail".
	 *
	 * [--status=<status>]
	 * : Filter by status (queued, sent, failed, skipped). Defaults to failed.
	 *
	 * [--count=<number>]
	 * : Number of records to display. Defaults to 10.
	 *
	 * ## EXAMPLES
	 *
	 *     wp maildoctor logs tail --status=failed --count=5
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Associative arguments.
	 * @return void
	 */
	public function logs( array $args, array $assoc_args ): void {
		if ( empty( $args ) ) {
			WP_CLI::error( __( 'A logs subcommand is required (e.g. tail).', 'mail-doctor' ) );
			return;
		}

		$subcommand = array_shift( $args );

		switch ( $subcommand ) {
			case 'tail':
				$this->logs_tail( $assoc_args );
				break;
			default:
				WP_CLI::error(
					sprintf(
						/* translators: %s: subcommand */
						__( 'Unknown logs subcommand: %s', 'mail-doctor' ),
						$subcommand
					)
				);
		}
	}

	/**
	 * Trigger retries from the queue.
	 *
	 * ## OPTIONS
	 *
	 * [--all-due]
	 * : Process queued messages that are due for retry.
	 *
	 * [--id=<log_id>]
	 * : Manually retry a specific log entry.
	 *
	 * ## EXAMPLES
	 *
	 *     wp maildoctor retry --all-due
	 *     wp maildoctor retry --id=123
	 *
	 * @param array<int, string>    $args       Positional arguments.
	 * @param array<string, string> $assoc_args Associative arguments.
	 * @return void
	 */
	public function retry( array $args, array $assoc_args ): void {
		$all_due = isset( $assoc_args['all-due'] ) ? (bool) $assoc_args['all-due'] : false;
		$log_id  = isset( $assoc_args['id'] ) ? (int) $assoc_args['id'] : 0;

		if ( $all_due ) {
			$processed = Retry::run();

			if ( $processed > 0 ) {
				WP_CLI::success(
					sprintf(
						/* translators: %d: processed count */
						__( 'Retry worker processed %d log(s).', 'mail-doctor' ),
						$processed
					)
				);
			} else {
				WP_CLI::log( __( 'No eligible retries were found in the queue.', 'mail-doctor' ) );
			}

			return;
		}

		if ( $log_id > 0 ) {
			$result = Retry::manual_retry( $log_id );

			if ( $result instanceof WP_Error ) {
				WP_CLI::error( $result->get_error_message() );
				return;
			}

			WP_CLI::success(
				sprintf(
					/* translators: %d: log id */
					__( 'Manual retry dispatched for log #%d.', 'mail-doctor' ),
					$log_id
				)
			);

			return;
		}

		WP_CLI::error( __( 'Specify either --all-due or --id=<log_id> when running retries.', 'mail-doctor' ) );
	}

	/**
	 * Render a tail of recent log entries.
	 *
	 * @param array<string, string> $assoc_args Command arguments.
	 * @return void
	 */
	protected function logs_tail( array $assoc_args ): void {
		$status = isset( $assoc_args['status'] ) ? sanitize_key( $assoc_args['status'] ) : 'failed';
		$count  = isset( $assoc_args['count'] ) ? (int) $assoc_args['count'] : 10;

		if ( $count <= 0 ) {
			$count = 10;
		}

		$allowed_statuses = array( 'queued', 'sent', 'failed', 'skipped' );

		if ( ! in_array( $status, $allowed_statuses, true ) ) {
			WP_CLI::error(
				sprintf(
					/* translators: %s: status */
					__( 'Invalid status supplied: %s', 'mail-doctor' ),
					$status
				)
			);
			return;
		}

		$results = Logger::query(
			array(
				'status'   => $status,
				'per_page' => $count,
				'paged'    => 1,
			)
		);

		if ( empty( $results['items'] ) ) {
			WP_CLI::log( __( 'No log entries matched the requested filters.', 'mail-doctor' ) );
			return;
		}

		$rows = array();

		foreach ( $results['items'] as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$rows[] = array(
				'id'      => (int) $row['id'],
				'status'  => $row['status'],
				'event'   => $row['event'],
				'created' => $row['created_at'],
				'subject' => $this->truncate_field( (string) $row['subject'] ),
				'error'   => $row['error'] ? $this->truncate_field( (string) $row['error'] ) : '',
			);
		}

		if ( empty( $rows ) ) {
			WP_CLI::log( __( 'No log entries matched the requested filters.', 'mail-doctor' ) );
			return;
		}

		\WP_CLI\Utils\format_items( 'table', $rows, array( 'id', 'status', 'event', 'created', 'subject', 'error' ) );
	}

	/**
	 * Safely truncate CLI output fields.
	 *
	 * @param string $value  Raw value.
	 * @param int    $length Maximum length.
	 * @return string
	 */
	protected function truncate_field( string $value, int $length = 48 ): string {
		if ( function_exists( 'mb_strimwidth' ) ) {
			return mb_strimwidth( $value, 0, $length, '…', 'UTF-8' );
		}

		if ( strlen( $value ) <= $length ) {
			return $value;
		}

		return substr( $value, 0, max( 0, $length - 3 ) ) . '...';
	}
}
