<?php
/**
 * File Path: includes/class-mbs-esp-broadcast.php
 *
 * ESP New Release Broadcast Queue -- F-026-7
 *
 * On first publish of a book post, queues a broadcast record in wp_options.
 * Surfaces a "Send New Release Email" button in the book editor.
 * AJAX handler sends the campaign via the configured ESP adapter.
 *
 * IMPORTANT: This feature does NOT auto-send. The author must click the
 * "Send New Release Email" button. This prevents accidental sends on
 * accidental publish/unpublish cycles.
 *
 * Queue: wp_option mbs_esp_broadcast_queue -- JSON ring-buffer, max 20 entries.
 *
 * @package MyBookShowroom
 * @since   1.1.0
 */

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

// phpcs:disable WordPress.Security.NonceVerification -- nonce verified in parent class verify_save() / check_admin_referer() before $_POST access; PHPCS cannot trace cross-method nonce verification
// phpcs:disable WordPress.Security.ValidatedSanitizedInput -- input sanitized at point of use (sanitize_text_field, absint, wp_kses_post, etc.); false positives from PHPCS not tracing sanitization helpers called before this method
class MBS_ESP_Broadcast {

	// RF-011: QUEUE_MAX and QUEUE_KEY removed — storage delegated to MBS_ESP_Queue_DAO.

	/**
	 * Register WordPress hooks for the broadcast queue.
	 *
	 * Hooks self-register on instantiation, keeping hook registration in
	 * the class that owns the callbacks (SRP — R1-001).
	 *
	 * @since 1.2.0
	 */
	public function __construct() {
		add_action( 'transition_post_status',              array( $this, 'on_publish' ), 10, 3 );
		add_action( 'wp_ajax_mbs_esp_send_broadcast',      array( $this, 'ajax_send_broadcast' ) );
		add_action( 'wp_ajax_mbs_esp_preview_broadcast',   array( $this, 'ajax_preview_broadcast' ) );
		add_action( 'wp_ajax_mbs_esp_send_test_broadcast', array( $this, 'ajax_send_test_broadcast' ) );
		add_action( 'admin_notices',                       array( $this, 'render_queued_notice' ) );
	}

	/**
	 * transition_post_status callback.
	 * Creates a broadcast record when a book is first published.
	 *
	 * @param string  $new  New post status.
	 * @param string  $old  Previous post status.
	 * @param WP_Post $post Post object.
	 */
	public function on_publish( string $new, string $old, WP_Post $post ): void {
		// Only mbs_book post type.
		if ( MBS_POST_TYPE !== $post->post_type ) {
			return;
		}
		// Only first publish -- skip re-saves of already-published posts.
		if ( 'publish' !== $new || 'publish' === $old ) {
			return;
		}
		// PRO gate.
		if ( ! mbs_is_pro() ) {
			return;
		}
		// Feature toggle.
		if ( '1' !== mbs_get_setting( 'email', 'broadcast_enabled', '0' ) ) {
			return;
		}
		// Adapter must be configured.
		if ( null === MBS_ESP_Registry::get_adapter() ) {
			return;
		}

		// Double-publish guard: skip if already queued for this post.
		if ( get_post_meta( $post->ID, '_mbs_esp_broadcast_queued', true ) ) {
			return;
		}

		$record = array(
			'post_id'    => $post->ID,
			'book_title' => get_the_title( $post->ID ),
			'status'     => 'pending',
			'queued_at'  => time(),
			'campaign_id' => null,
			'sent_at'    => null,
			'platform'   => MBS_ESP_Registry::get_platform(),
		);

		self::push_record( $record );
		update_post_meta( $post->ID, '_mbs_esp_broadcast_queued', '1' );

		// Flag for admin notice on next page load.
		set_transient( 'mbs_esp_broadcast_notice_' . $post->ID, $record['book_title'], HOUR_IN_SECONDS );
	}

	/**
	 * Render the broadcast panel in the book editor.
	 * Called from MBS_Metabox_Book_Header::render() when conditions are met.
	 *
	 * @param int $post_id
	 */
	public static function maybe_render_panel( int $post_id ): void {
		if ( ! mbs_is_pro() ) { return; }
		if ( '1' !== mbs_get_setting( 'email', 'broadcast_enabled', '0' ) ) { return; }
		if ( 'publish' !== get_post_status( $post_id ) ) { return; }

		$record    = self::get_pending_record( $post_id );
		$retryable = self::get_retryable_record( $post_id );
		if ( null === $record && null === $retryable ) { return; }

		$platform       = MBS_ESP_Registry::get_platform();
		$adapter_obj    = MBS_ESP_Registry::get_adapter();
		$is_campaigns   = ( null !== $adapter_obj && $adapter_obj->supports_campaigns() );
		$platform_label = self::get_platform_label( $platform );
		$list_id        = mbs_get_setting( 'email', 'broadcast_list_id', '' );
		if ( '' === $list_id ) { $list_id = mbs_get_setting( 'email', 'default_list_id', '' ); }

		self::render_broadcast_panel_html( $post_id, $record, $retryable, $is_campaigns, $platform_label );
	}

	/**
	 * Output the broadcast panel HTML.
	 *
	 * @param int        $post_id        Book post ID.
	 * @param array|null $record         Pending broadcast record, or null.
	 * @param array|null $retryable      Retryable broadcast record, or null.
	 * @param bool       $is_campaigns   Whether the adapter supports campaigns.
	 * @param string     $platform_label Human-readable platform name.
	 */
	private static function render_broadcast_panel_html(
		int $post_id,
		?array $record,
		?array $retryable,
		bool $is_campaigns,
		string $platform_label
	): void {
		$nonce         = wp_create_nonce( 'mbs_esp_send_broadcast_' . $post_id );
		$preview_nonce = wp_create_nonce( 'mbs_esp_preview_broadcast_' . $post_id );
		?>
		<div id="mbs-broadcast-panel" class="mbs-broadcast-panel" style="margin:16px 0;padding:14px 16px;background:#f0f4ff;border-left:4px solid #2C3E6B;border-radius:4px;">
			<p style="margin:0 0 8px;font-weight:600;"><?php esc_html_e( 'New release broadcast ready.', 'my-book-showroom' ); ?></p>
			<?php if ( $is_campaigns && null !== $record ) : ?>
			<button type="button" class="button button-primary mbs-send-broadcast"
				data-post-id="<?php echo esc_attr( (string) $post_id ); ?>"
				data-nonce="<?php echo esc_attr( $nonce ); ?>">
				<?php esc_html_e( 'Send New Release Email', 'my-book-showroom' ); ?>
			</button>
			<button type="button" class="button mbs-preview-broadcast" style="margin-left:6px;"
				data-post-id="<?php echo esc_attr( (string) $post_id ); ?>"
				data-nonce="<?php echo esc_attr( $preview_nonce ); ?>">
				<?php esc_html_e( 'Preview Email', 'my-book-showroom' ); ?>
			</button>
			<span style="margin-left:8px;font-size:12px;color:#5a6472;">
				<?php
				/* translators: 1: email platform name, 2: email platform name (repeated) */
				printf( esc_html__( 'via %s', 'my-book-showroom' ), esc_html( $platform_label ) );
				?>
			</span>
			<p class="description" style="margin:8px 0 0;">
				<?php esc_html_e( 'This action cannot be undone.', 'my-book-showroom' ); ?>
			</p>
			<?php elseif ( $is_campaigns && null !== $retryable ) : ?>
			<p class="description" style="color:#b32d2e;margin:0 0 6px;">
				<?php esc_html_e( 'Campaign was created but could not be sent. Click Retry to attempt sending again.', 'my-book-showroom' ); ?>
			</p>
			<?php
			$retry_nonce = wp_create_nonce( 'mbs_esp_send_broadcast_' . $post_id );
			?>
			<button type="button" class="button button-primary mbs-retry-broadcast"
				data-post-id="<?php echo esc_attr( (string) $post_id ); ?>"
				data-nonce="<?php echo esc_attr( $retry_nonce ); ?>">
				<?php esc_html_e( 'Retry Send', 'my-book-showroom' ); ?>
			</button>
			<?php else : ?>
			<p class="description">
				<?php
				/* translators: 1: email platform name, 2: email platform name (repeated for both occurrences) */
				printf( esc_html__( 'Your email platform (%1$s) does not support sending campaigns via API. Log in to %2$s to send your new release announcement manually.', 'my-book-showroom' ), esc_html( $platform_label ), esc_html( $platform_label ) );
				?>
			</p>
			<?php endif; ?>
			<p class="mbs-broadcast-result" aria-live="polite" style="margin:8px 0 0;font-size:13px;"></p>
		</div>
		<?php
	}


	/**
	 * AJAX handler: send broadcast.
	 * Nonce: mbs_esp_send_broadcast_{post_id}
	 */
	public function ajax_send_broadcast(): void {
		$post_id = absint( $_POST['post_id'] ?? 0 );

		// 1. Nonce.
		if ( ! check_ajax_referer( 'mbs_esp_send_broadcast_' . $post_id, 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'my-book-showroom' ) ), 403 );
			return;
		}
		// 2. PRO gate.
		if ( ! mbs_is_pro() ) {
			wp_send_json_error( array( 'message' => __( 'PRO license required.', 'my-book-showroom' ) ), 403 );
			return;
		}
		// 3. Capability.
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'my-book-showroom' ) ), 403 );
			return;
		}
		// 4. Pending record — fall through to retryable if no pending record exists.
		$record = self::get_pending_record( $post_id );
		if ( null === $record ) {
			$record = $this->get_retryable_record( $post_id );
		}
		if ( null === $record ) {
			wp_send_json_error( array( 'message' => __( 'No pending broadcast found for this book.', 'my-book-showroom' ) ) );
			return;
		}
		// 5. Adapter.
		$adapter = MBS_ESP_Registry::get_adapter();
		if ( null === $adapter ) {
			wp_send_json_error( array( 'message' => __( 'Email platform not configured.', 'my-book-showroom' ) ) );
			return;
		}

		$this->execute_campaign_send( $post_id, $record, $adapter );
	}

	/**
	 * Build and send the ESP campaign for a broadcast record.
	 *
	 * Sends JSON success/error responses directly.
	 *
	 * @param int    $post_id Book post ID.
	 * @param array  $record  Pending broadcast record from the queue.
	 * @param object $adapter ESP adapter instance.
	 */
	private function execute_campaign_send( int $post_id, array $record, object $adapter ): void {
		// Concurrency lock — prevent double-send from rapid clicks.
		$lock_key = 'mbs_esp_broadcast_sending_' . $post_id;
		if ( get_transient( $lock_key ) ) {
			wp_send_json_error( array( 'message' => __( 'Broadcast in progress. Please wait.', 'my-book-showroom' ) ) );
			return;
		}
		set_transient( $lock_key, 1, 60 );

		// 6. Subject.
		/* translators: %s: book title */
		$subject = sprintf( __( 'New Release: %s', 'my-book-showroom' ), $record['book_title'] );

		// 7. Body.
		$body_html = self::build_email_body( $post_id, $record );

		// 8. List ID.
		$list_id = mbs_get_setting( 'email', 'broadcast_list_id', '' );
		if ( '' === $list_id ) { $list_id = mbs_get_setting( 'email', 'default_list_id', '' ); }
		if ( '' === $list_id ) {
			wp_send_json_error( array( 'message' => __( 'No broadcast list configured.', 'my-book-showroom' ) ) );
			return;
		}

		// 9. Create campaign — skip if retrying with an existing campaign_id.
		if ( ! empty( $record['campaign_id'] ) ) {
			$campaign_id = $record['campaign_id'];
		} else {
			$campaign_id = $adapter->create_campaign( $subject, $body_html, $list_id );
			if ( null === $campaign_id ) {
				delete_transient( $lock_key );
				wp_send_json_error( array( 'message' => __( 'Failed to create campaign. Please check your ESP settings.', 'my-book-showroom' ) ) );
				return;
			}
		}

		// 10. Send campaign.
		$sent = $adapter->send_campaign( $campaign_id );
		if ( ! $sent ) {
			self::update_record( $post_id, array( 'campaign_id' => $campaign_id, 'status' => 'send_failed' ) );
			delete_transient( $lock_key );
			wp_send_json_error( array( 'message' => __( 'Campaign created but could not be sent. Retry or send manually from your ESP dashboard.', 'my-book-showroom' ) ) );
			return;
		}

		// 11. Mark sent.
		self::update_record( $post_id, array( 'status' => 'sent', 'sent_at' => time(), 'campaign_id' => $campaign_id ) );
		delete_transient( $lock_key );
		wp_send_json_success( array( 'message' => __( 'Broadcast sent successfully.', 'my-book-showroom' ) ) );
	}


	/**
	 * AJAX handler: return preview HTML for the broadcast email.
	 * Nonce: mbs_esp_preview_broadcast_{post_id}
	 */
	public function ajax_preview_broadcast(): void {
		$post_id = absint( $_POST['post_id'] ?? 0 );
		if ( ! check_ajax_referer( 'mbs_esp_preview_broadcast_' . $post_id, 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'my-book-showroom' ) ), 403 );
			return;
		}
		if ( ! mbs_is_pro() ) {
			wp_send_json_error( array( 'message' => __( 'PRO license required.', 'my-book-showroom' ) ), 403 );
			return;
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'my-book-showroom' ) ), 403 );
			return;
		}
		$record = self::get_pending_record( $post_id );
		if ( null === $record ) {
			$record = self::get_retryable_record( $post_id );
		}
		if ( null === $record ) {
			wp_send_json_error( array( 'message' => __( 'No broadcast record found for this book.', 'my-book-showroom' ) ) );
			return;
		}
		$html = self::build_email_body( $post_id, $record );
		wp_send_json_success( array( 'html' => $html ) );
	}

	/**
	 * AJAX handler: send a test email to the admin email address via wp_mail().
	 * Nonce: mbs_esp_preview_broadcast_{post_id}  (reuses preview nonce).
	 */
	public function ajax_send_test_broadcast(): void {
		$post_id = absint( $_POST['post_id'] ?? 0 );
		if ( ! check_ajax_referer( 'mbs_esp_preview_broadcast_' . $post_id, 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'my-book-showroom' ) ), 403 );
			return;
		}
		if ( ! mbs_is_pro() ) {
			wp_send_json_error( array( 'message' => __( 'PRO license required.', 'my-book-showroom' ) ), 403 );
			return;
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'my-book-showroom' ) ), 403 );
			return;
		}
		$record = self::get_pending_record( $post_id );
		if ( null === $record ) {
			$record = self::get_retryable_record( $post_id );
		}
		if ( null === $record ) {
			wp_send_json_error( array( 'message' => __( 'No broadcast record found for this book.', 'my-book-showroom' ) ) );
			return;
		}
		$to      = get_option( 'admin_email' );
		/* translators: %s: book title */
		$subject = sprintf( __( '[TEST] New Release: %s', 'my-book-showroom' ), $record['book_title'] );
		$body    = self::build_email_body( $post_id, $record );
		$headers = array( 'Content-Type: text/html; charset=UTF-8' );
		$sent    = wp_mail( $to, $subject, $body, $headers );
		if ( $sent ) {
			wp_send_json_success( array( 'message' => sprintf(
				/* translators: %s: admin email address */
				__( 'Test email sent to %s.', 'my-book-showroom' ),
				esc_html( $to )
			) ) );
		} else {
			wp_send_json_error( array( 'message' => __( 'wp_mail() failed. Check your site\'s email configuration.', 'my-book-showroom' ) ) );
		}
	}

	/**
	 * admin_notices callback: show a notice when a broadcast is queued.
	 */
	public function render_queued_notice(): void {
		$screen = get_current_screen();
		if ( ! $screen || 'post' !== $screen->base || MBS_POST_TYPE !== $screen->post_type ) {
			return;
		}
		$post_id = (int) ( $_GET['post'] ?? 0 );
		if ( $post_id <= 0 ) {
			return;
		}
		$title = get_transient( 'mbs_esp_broadcast_notice_' . $post_id );
		if ( ! $title || ! is_string( $title ) ) {
			return;
		}
		delete_transient( 'mbs_esp_broadcast_notice_' . $post_id );
		?>
		<div class="notice notice-success is-dismissible">
			<p>
				<?php
				/* translators: 1: book title, 2: anchor open, 3: anchor close */
				printf(
					esc_html__( 'Broadcast queued for "%1$s". %2$sReview and send%3$s when ready.', 'my-book-showroom' ), // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment -- translator comment on preceding line
					esc_html( $title ),
					'<a href="#mbs-broadcast-panel">',
					'</a>'
				);
				?>
			</p>
		</div>
		<?php
	}

	// ── Private helpers ─────────────────────────────────────────────────────────

	/** Push a record into the DB queue (RF-011: delegates to MBS_ESP_Queue_DAO). */
	private static function push_record( array $record ): void {
		MBS_ESP_Queue_DAO::push( $record );
	}

	/** Return all queue records from the DB (RF-011: delegates to MBS_ESP_Queue_DAO). */
	private static function get_queue(): array {
		return MBS_ESP_Queue_DAO::get_all();
	}

	/** Return the pending record for a post_id, or null if none. */
	private static function get_pending_record( int $post_id ): ?array {
		foreach ( self::get_queue() as $record ) {
			if ( isset( $record['post_id'] ) && (int) $record['post_id'] === $post_id
				&& 'pending' === ( $record['status'] ?? '' ) ) {
				return $record;
			}
		}
		return null;
	}

	/** Return the send_failed+campaign_id record for a post_id, or null. */
	private static function get_retryable_record( int $post_id ): ?array {
		foreach ( self::get_queue() as $record ) {
			if ( isset( $record['post_id'] ) && (int) $record['post_id'] === $post_id
				&& 'send_failed' === ( $record['status'] ?? '' )
				&& ! empty( $record['campaign_id'] ) ) {
				return $record;
			}
		}
		return null;
	}

	/** Update fields of the matching post_id record in the DB queue (RF-011). */
	private static function update_record( int $post_id, array $updates ): void {
		MBS_ESP_Queue_DAO::update_by_post_id( $post_id, $updates );
	}

	/**
	 * Build the HTML body for a broadcast email from the template.
	 *
	 * @param int   $post_id  Book post ID.
	 * @param array $record   Broadcast queue record.
	 * @return string         HTML email body.
	 */
	private static function build_email_body( int $post_id, array $record ): string {
		$book_title   = $record['book_title'] ?? get_the_title( $post_id );
		$teaser       = wp_kses(
			get_post_meta( $post_id, 'mbs_teaser', true ) ?: get_the_excerpt( $post_id ),
			array(
				'p'      => array(),
				'br'     => array(),
				'em'     => array(),
				'strong' => array(),
			)
		);
		$cover_url    = '';
		$thumbnail_id = (int) get_post_thumbnail_id( $post_id ); // R5-exempt: cover URL for ESP email HTML body
		if ( $thumbnail_id > 0 ) {
			$cover_url = (string) wp_get_attachment_image_url( $thumbnail_id, 'medium' ); // R5-exempt: cover URL for ESP email HTML body
		}
		$author_terms = get_the_terms( $post_id, MBS_TAX_AUTHOR );
		$author_names = '';
		if ( ! empty( $author_terms ) && ! is_wp_error( $author_terms ) ) {
			$author_names = implode( ', ', wp_list_pluck( $author_terms, 'name' ) );
		}
		$buy_links = MBS_Buy_Link_Renderer::get_buy_links( $post_id );

		ob_start();
		mbs_get_template_part( 'email/new-release-broadcast', array(
			'post_id'      => $post_id,
			'book_title'   => $book_title,
			'author_names' => $author_names,
			'teaser'       => $teaser,
			'cover_url'    => $cover_url,
			'buy_links'    => $buy_links,
		) );
		return (string) ob_get_clean();
	}

	/** Map platform slug to display label. */
	private static function get_platform_label( string $platform ): string {
		$labels = array(
			'mailchimp'       => 'Mailchimp',
			'mailerlite'      => 'MailerLite',
			'convertkit'      => 'ConvertKit (Kit)',
			'brevo'           => 'Brevo',
			'sendy'           => 'Sendy',
			'activecampaign'  => 'ActiveCampaign',
			'getresponse'     => 'GetResponse',
			'campaignmonitor' => 'Campaign Monitor',
			'moosend'         => 'Moosend',
			'flodesk'         => 'Flodesk',
			'authoremail'     => 'Author.Email',
		);
		return $labels[ $platform ] ?? $platform;
	}
}
