<?php
/**
 * Cart Recovery — Mailer (send pipeline)
 *
 * Sole consumer of `ACTION_CART_RECOVERY_SEND_MAIL1` /
 * `ACTION_CART_RECOVERY_SEND_MAIL2` fired by the Phase 2 scheduler.
 *
 * Pipeline for both stages:
 *   1. Settings + per-row guards (enabled? email? suppression?).
 *   2. Reconstruct snapshot + transcript digest.
 *   3. (mail2 only) Ensure coupon exists for the row — call
 *      `Coupon_Service::get_or_create_coupon_for_row()`.
 *   4. Run composer → (subject, preview_text, html, text, is_fallback).
 *   5. Interpolate `{{COUPON_CODE}}` into html/text.
 *   6. Wrap through template wrapper (pixel + tracked links + unsub).
 *   7. `wp_mail()` with proper From / Reply-To / Content-Type headers.
 *   8. On success → repository `mark_sent_mail1/2`, fires
 *      `niwa_ai_cart_recovery_mail_sent` action.
 *   9. On failure → log (WP_DEBUG) and leave status untouched so the
 *      next scheduler tick can retry.
 *
 * All external deps (composer, coupon service, digester) are nullable
 * constructor args — if any is missing the mailer still produces a
 * best-effort mail through the composer's fallback template (or skips
 * cleanly). No exception ever escapes the hook callback.
 *
 * @package Niwa_AI
 * @since   0.12.0
 */

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

class Niwa_AI_Cart_Recovery_Mailer {

	public function __construct(
		private readonly Niwa_AI_Cart_Recovery_Repository $repository,
		private readonly Niwa_AI_Cart_Recovery_Template_Wrapper $template,
		private readonly ?Niwa_AI_Cart_Recovery_Composer $composer = null,
		private readonly ?Niwa_AI_Cart_Recovery_Coupon_Service $coupon_service = null,
		private readonly ?Niwa_AI_Cart_Recovery_Transcript_Digester $digester = null,
		private readonly ?Niwa_AI_Lead_WhatsApp_Outreach_Service $whatsapp_outreach = null
	) {}

	public function register_hooks(): void {
		add_action( Niwa_AI_Constants::ACTION_CART_RECOVERY_SEND_MAIL1, array( $this, 'send_mail1' ), 10, 1 );
		add_action( Niwa_AI_Constants::ACTION_CART_RECOVERY_SEND_MAIL2, array( $this, 'send_mail2' ), 10, 1 );
	}

	/** @param array<string, mixed> $row */
	public function send_mail1( $row ): void {
		$this->send( $row, Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL1 );
	}

	/** @param array<string, mixed> $row */
	public function send_mail2( $row ): void {
		$this->send( $row, Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL2 );
	}

	/**
	 * Admin-facing preview entry point. Runs the full compose+wrap
	 * pipeline but does NOT send the mail or flip status — used by the
	 * Phase 6 admin UI preview tab.
	 *
	 * @param array<string, mixed> $row
	 * @return array{subject: string, html: string, text: string, is_fallback: bool, from_cache: bool}|null
	 */
	public function preview( $row, string $stage ): ?array {
		if ( ! is_array( $row ) || ! $this->validate_stage( $stage ) ) {
			return null;
		}
		$built = $this->build( $row, $stage, true );
		if ( null === $built ) {
			return null;
		}
		return array(
			'subject'     => $built['subject'],
			'html'        => $built['html'],
			'text'        => $built['text'],
			'is_fallback' => $built['is_fallback'],
			'from_cache'  => $built['from_cache'],
		);
	}

	/**
	 * Common send orchestrator. `$row` is an array from the repository.
	 *
	 * @param array<string, mixed> $row
	 */
	private function send( $row, string $stage ): void {
		if ( ! is_array( $row ) || ! $this->validate_stage( $stage ) || ! Niwa_AI_Cart_Recovery_Settings::is_stage_enabled( $stage ) ) {
			return;
		}
		$id = (int) ( $row['id'] ?? 0 );
		if ( $id <= 0 || ! $this->repository->acquire_send_lock( $id, $stage ) ) {
			return;
		}

		try {
			$fresh = $this->repository->find_by_id( $id );
			if ( ! is_array( $fresh ) ) {
				return;
			}
			$row             = $fresh;
			$expected_status = Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL1 === $stage
				? Niwa_AI_Cart_Recovery_Repository::STATUS_SCHEDULED
				: Niwa_AI_Cart_Recovery_Repository::STATUS_SENT_MAIL1;
			$schedule_key = Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL1 === $stage ? 'mail1_scheduled_at' : 'mail2_scheduled_at';
			if ( $expected_status !== (string) ( $row['status'] ?? '' ) || empty( $row[ $schedule_key ] ) ) {
				return;
			}
			$channels = $this->sendable_channels( $row );
			if ( empty( $channels ) ) {
				return;
			}

			$built = $this->build( $row, $stage, false );
			if ( null === $built ) {
				$this->record_send_failure( $id, $stage, $row, $channels, array( 'message_build_failed' ) );
				return;
			}

			$errors = array();

			foreach ( $channels as $channel ) {
				if ( $this->channel_was_accepted( $row, $stage, $channel ) ) {
					continue;
				}

				$result = Niwa_AI_Cart_Recovery_Settings::CHANNEL_EMAIL === $channel
					? $this->send_email_channel( $row, $built )
					: $this->send_whatsapp_channel( $row, $stage, $built );

				if ( empty( $result['accepted'] ) ) {
					$errors[] = (string) ( $result['error'] ?? $channel . '_send_failed' );
					continue;
				}

				if ( ! $this->repository->mark_delivery_channel_queued( $id, $stage, $channel ) ) {
					$errors[] = $channel . '_marker_write_failed';
					continue;
				}
			}

			if ( ! empty( $errors ) ) {
				$this->record_send_failure( $id, $stage, $row, $channels, $errors );
				return;
			}

			if ( Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL1 === $stage ) {
				$this->repository->mark_sent_mail1( $id );
			} else {
				$this->repository->mark_sent_mail2( $id );
			}

			do_action( 'niwa_ai_cart_recovery_reminder_sent', $stage, $row, $built, $channels );
			if ( in_array( Niwa_AI_Cart_Recovery_Settings::CHANNEL_EMAIL, $channels, true ) ) {
				do_action( 'niwa_ai_cart_recovery_mail_sent', $stage, $row, $built );
			}
		} catch ( \Throwable $e ) {
			$this->log( 'send.exception', $e->getMessage() );
		} finally {
			$this->repository->release_send_lock( $id, $stage );
		}
	}

	/**
	 * Build everything needed to actually dispatch. Returns null on any
	 * hard guard failure (row missing fields, snapshot corrupt, etc.).
	 *
	 * @param array<string, mixed> $row
	 * @return array{subject: string, html: string, text: string, is_fallback: bool, from_cache: bool}|null
	 */
	private function build( array $row, string $stage, bool $is_preview ): ?array {
		$snapshot_json = (string) ( $row['cart_snapshot_json'] ?? '' );
		$snapshot      = '' !== $snapshot_json ? Niwa_AI_Cart_Recovery_Cart_Snapshot::from_json( $snapshot_json ) : null;
		if ( null === $snapshot || $snapshot->is_empty() ) {
			return null;
		}

		// (mail2) coupon ensure.
		$coupon_code = '';
		if ( Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL2 === $stage && Niwa_AI_Cart_Recovery_Settings::is_coupon_enabled() && ! $is_preview ) {
			if ( ! $this->coupon_service ) {
				return null;
			}
			$coupon = $this->coupon_service->get_or_create_coupon_for_row( $row );
			if ( is_array( $coupon ) && isset( $coupon['code'] ) ) {
				$coupon_code = (string) $coupon['code'];
			}
			if ( '' === $coupon_code ) {
				return null;
			}
		}

		// Preview with coupon placeholder: in preview mode we still want the
		// template to show a representative {{COUPON_CODE}} string — we
		// interpolate a recognizable placeholder so admins see where the
		// code will land in the real mail.
		if ( $is_preview && Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL2 === $stage && Niwa_AI_Cart_Recovery_Settings::is_coupon_enabled() ) {
			$coupon_code = 'PREVIEW-CODE-XXXX';
		}

		$transcript = $this->digester
			? $this->digester->digest( (string) ( $row['session_id'] ?? '' ) )
			: '';

		$ctx = new Niwa_AI_Cart_Recovery_Composition_Context(
			$stage,
			(string) ( $row['locale'] ?? ( function_exists( 'determine_locale' ) ? determine_locale() : '' ) ),
			(string) ( $row['customer_name'] ?? '' ),
			$snapshot,
			$transcript,
			$this->resolve_brand_name(),
			'' !== $coupon_code,
			$this->resolve_coupon_ttl_hours()
		);

		$composer = $this->composer ?? new Niwa_AI_Cart_Recovery_Composer();
		$result   = $composer->compose( $ctx );

		$subject = $result->subject;
		$html = $result->html;
		$text = $result->text;

		if ( '' !== $coupon_code ) {
			$subject = str_replace( '{{COUPON_CODE}}', $coupon_code, $subject );
			$html    = str_replace( '{{COUPON_CODE}}', $coupon_code, $html );
			$text    = str_replace( '{{COUPON_CODE}}', $coupon_code, $text );
		} else {
			// No coupon → strip any stray placeholder left in fallback templates.
			$subject = str_replace( '{{COUPON_CODE}}', '', $subject );
			$html    = str_replace( '{{COUPON_CODE}}', '', $html );
			$text    = str_replace( '{{COUPON_CODE}}', '', $text );
		}

		$token       = (string) ( $row['unsubscribe_token'] ?? '' );
		$late_map    = array(
			'{cart_recovery_url}' => $this->template->build_cart_recovery_url( $token ),
			'{coupon_discount}'   => $this->format_coupon_discount_label( $snapshot ),
		);
		$subject     = strtr( $subject, $late_map );
		$html        = strtr( $html, $late_map );
		$text        = strtr( $text, $late_map );
		$html        = $this->ensure_actionable_html( $html, $ctx, $token );
		$text        = $this->ensure_actionable_text( $text, $ctx, $token );
		$wrapped_html = $this->template->wrap_html( $html, $token, $ctx->locale );
		$wrapped_text = $this->template->wrap_text( $text, $token, $ctx->locale );

		return array(
			'subject'     => $subject,
			'html'        => $wrapped_html,
			'text'        => $wrapped_text,
			'is_fallback' => $result->is_fallback,
			'from_cache'  => $result->from_cache,
		);
	}

	/** @return string[] */
	private function sendable_channels( array $row ): array {
		$channels = Niwa_AI_Cart_Recovery_Settings::delivery_channels_for_row( $row );
		return array_values(
			array_filter(
				$channels,
				function ( string $channel ) use ( $row ): bool {
					if ( Niwa_AI_Cart_Recovery_Settings::CHANNEL_EMAIL === $channel ) {
						return ! $this->repository->is_suppressed( (string) ( $row['email'] ?? '' ) );
					}
					if ( Niwa_AI_Cart_Recovery_Settings::CHANNEL_WHATSAPP === $channel ) {
						return ! $this->repository->is_phone_suppressed( (string) ( $row['phone'] ?? '' ) );
					}
					return false;
				}
			)
		);
	}

	/** @param array<string, mixed> $row @param array<string, mixed> $built */
	private function send_email_channel( array $row, array $built ): array {
		$sent = $this->dispatch_mail(
			(string) ( $row['email'] ?? '' ),
			(string) $built['subject'],
			(string) $built['html'],
			$this->build_headers()
		);
		return array(
			'accepted' => $sent,
			'error'    => $sent ? '' : 'wp_mail returned false',
		);
	}

	/** @param array<string, mixed> $row @param array<string, mixed> $built */
	private function send_whatsapp_channel( array $row, string $stage, array $built ): array {
		if ( ! $this->whatsapp_outreach ) {
			return array( 'accepted' => false, 'error' => 'whatsapp_outreach_unavailable' );
		}

		$token        = (string) ( $row['unsubscribe_token'] ?? '' );
		$recovery_url = $this->template->build_cart_recovery_url( $token );
		$message      = (string) $built['text'];
		if ( '' !== $recovery_url ) {
			$message = str_replace(
				$recovery_url,
				$this->template->build_click_wrap_url( $token, $recovery_url ),
				$message
			);
		}

		$cycle_key = substr( hash( 'sha256', (string) ( $row['unsubscribe_token'] ?? '' ) ), 0, 16 );
		$result    = $this->whatsapp_outreach->send_outreach(
			array(
				'contact_id'  => (int) ( $row['contact_id'] ?? 0 ),
				'phone'       => (string) ( $row['phone'] ?? '' ),
				'name'        => (string) ( $row['customer_name'] ?? '' ),
				'email'       => (string) ( $row['email'] ?? '' ),
				'message'     => $message,
				'source'      => 'cart_recovery_whatsapp',
				'workflow_key'=> 'cart_recovery_' . (int) ( $row['id'] ?? 0 ) . '_' . $stage . '_' . $cycle_key,
			)
		);
		$accepted = ! empty( $result['success'] ) && ! empty( $result['queued'] );

		return array(
			'accepted' => $accepted,
			'error'    => $accepted ? '' : (string) ( $result['error'] ?? 'whatsapp_queue_failed' ),
		);
	}

	private function channel_was_accepted( array $row, string $stage, string $channel ): bool {
		$key = Niwa_AI_Cart_Recovery_Settings::CHANNEL_EMAIL === $channel
			? $stage . '_email_sent_at'
			: $stage . '_whatsapp_queued_at';
		return '' !== $key && ! empty( $row[ $key ] );
	}

	/** @param string[] $channels @param string[] $errors */
	private function record_send_failure( int $id, string $stage, array $row, array $channels, array $errors ): void {
		$error = implode( '; ', array_filter( $errors ) );
		$this->log( 'send', sprintf( 'Row %d: %s', $id, $error ) );
		$attempts = $this->repository->increment_send_attempts( $id, $error );
		do_action( 'niwa_ai_cart_recovery_reminder_failed', $stage, $row, $channels, $attempts, $error );
		$max      = (int) apply_filters(
			'niwa_ai_cart_recovery_max_send_attempts',
			Niwa_AI_Constants::CART_RECOVERY_MAX_SEND_ATTEMPTS
		);
		if ( $attempts < $max ) {
			return;
		}

		$this->repository->mark_send_failed( $id, 'exceeded max send attempts' );
		if ( in_array( Niwa_AI_Cart_Recovery_Settings::CHANNEL_EMAIL, $channels, true ) ) {
			do_action( 'niwa_ai_cart_recovery_mail_failed', $stage, $row, $attempts );
		}
	}

	private function ensure_actionable_html( string $html, Niwa_AI_Cart_Recovery_Composition_Context $ctx, string $token ): string {
		$recovery_url = $this->template->build_cart_recovery_url( $token );
		$map          = Niwa_AI_Cart_Recovery_Placeholder_Renderer::build_map( $ctx );
		$cards_html   = (string) ( $map['{cart_items_html}'] ?? '' );

		if ( '' !== $cards_html && ! str_contains( $html, 'data-niwa-cart-items="1"' ) ) {
			$html = rtrim( $html ) . "\n" . $cards_html;
		}

		if ( '' !== $recovery_url && ! str_contains( $html, $recovery_url ) ) {
			$html = rtrim( $html ) . "\n" . '<p><a href="' . esc_url( $recovery_url ) . '"><strong>' . esc_html( $this->recovery_cta_label( $ctx->locale ) ) . '</strong></a></p>';
		}

		return $html;
	}

	private function ensure_actionable_text( string $text, Niwa_AI_Cart_Recovery_Composition_Context $ctx, string $token ): string {
		$recovery_url = $this->template->build_cart_recovery_url( $token );
		$map          = Niwa_AI_Cart_Recovery_Placeholder_Renderer::build_map( $ctx );
		$items        = trim( (string) ( $map['{cart_items}'] ?? '' ) );

		if ( '' !== $items && ! $this->text_mentions_cart_items( $text, $ctx ) ) {
			$text = rtrim( $text ) . "\n\n" . $items;
		}

		if ( '' !== $recovery_url && ! str_contains( $text, $recovery_url ) ) {
			$text = rtrim( $text ) . "\n\n" . $this->recovery_cta_label( $ctx->locale ) . ":\n" . $recovery_url;
		}

		return $text;
	}

	private function text_mentions_cart_items( string $text, Niwa_AI_Cart_Recovery_Composition_Context $ctx ): bool {
		foreach ( $ctx->snapshot->items as $item ) {
			if ( $item instanceof Niwa_AI_Cart_Recovery_Line_Item && '' !== $item->name && str_contains( $text, $item->name ) ) {
				return true;
			}
		}
		return false;
	}

	private function recovery_cta_label( string $locale ): string {
		$lang = strtolower( trim( str_replace( '-', '_', $locale ) ) );
		$lang = (string) strtok( $lang, '_' );

		switch ( $lang ) {
			case 'sr':
			case 'bs':
				return 'Vrati me u korpu';
			case 'hr':
				return 'Vrati me u košaricu';
			case 'de':
				return 'Zurück zu meinem Warenkorb';
			case 'es':
				return 'Volver a mi carrito';
			case 'fr':
				return 'Retourner à mon panier';
			default:
				return 'Return to my cart';
		}
	}

	private function validate_stage( string $stage ): bool {
		return in_array(
			$stage,
			array(
				Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL1,
				Niwa_AI_Cart_Recovery_Composition_Context::STAGE_MAIL2,
			),
			true
		);
	}

	/**
	 * @return array<int, string>
	 */
	private function build_headers(): array {
		$settings = Niwa_AI_Cart_Recovery_Settings::get_settings();

		$from_name = (string) ( $settings['from_name'] ?? '' );
		if ( '' === $from_name ) {
			$from_name = function_exists( 'get_bloginfo' ) ? (string) get_bloginfo( 'name' ) : 'Niva';
		}
		$from_email = (string) ( $settings['from_email'] ?? '' );
		if ( '' === $from_email ) {
			$from_email = function_exists( 'get_bloginfo' )
				? (string) get_bloginfo( 'admin_email' )
				: 'no-reply@example.com';
		}

		return array(
			'Content-Type: text/html; charset=UTF-8',
			sprintf( 'From: %s <%s>', $this->sanitize_header( $from_name ), $this->sanitize_header( $from_email ) ),
			sprintf( 'Reply-To: %s', $this->sanitize_header( $from_email ) ),
		);
	}

	private function sanitize_header( string $value ): string {
		// Strip line breaks to prevent header injection.
		return (string) preg_replace( "/[\r\n]+/", ' ', $value );
	}

	protected function dispatch_mail( string $to, string $subject, string $html, array $headers ): bool {
		if ( ! function_exists( 'wp_mail' ) ) {
			return false;
		}
		return (bool) wp_mail( $to, $subject, $html, $headers );
	}

	private function resolve_coupon_ttl_hours(): int {
		if ( ! class_exists( 'Niwa_AI_Cart_Recovery_Settings' ) ) {
			return (int) Niwa_AI_Constants::CART_RECOVERY_COUPON_TTL_HOURS;
		}
		$config = Niwa_AI_Cart_Recovery_Settings::get_coupon_config();
		return max( 1, (int) $config->ttl_hours );
	}

	private function format_coupon_discount_label( Niwa_AI_Cart_Recovery_Cart_Snapshot $snapshot ): string {
		$config = class_exists( 'Niwa_AI_Cart_Recovery_Settings' )
			? Niwa_AI_Cart_Recovery_Settings::get_coupon_config()
			: Niwa_AI_Cart_Recovery_Coupon_Config::defaults();

		$amount = (float) $config->amount;
		if ( Niwa_AI_Cart_Recovery_Coupon_Config::TYPE_PERCENT === $config->discount_type ) {
			return rtrim( rtrim( number_format( $amount, 2, '.', '' ), '0' ), '.' ) . '%';
		}

		$formatted = number_format( $amount, 2, '.', '' );
		return '' !== $snapshot->currency ? $formatted . ' ' . $snapshot->currency : $formatted;
	}

	private function resolve_brand_name(): string {
		$settings = Niwa_AI_Cart_Recovery_Settings::get_settings();
		$brand    = (string) ( $settings['store_brand_name'] ?? '' );
		if ( '' !== $brand ) {
			return $brand;
		}
		if ( function_exists( 'get_bloginfo' ) ) {
			return (string) get_bloginfo( 'name' );
		}
		return 'Shop';
	}

	private function log( string $label, string $message ): void {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( sprintf( '[niwa-ai][cart-recovery][mailer.%s] %s', $label, $message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		}
	}
}
