<?php
/**
 * Durable auto-reply scheduler for captured third-party form leads.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\AutoReplies;

use SUBIN\Forms\FormSubmissionContext;
use SUBIN\Forms\FormSubmissionHandler;
use SUBIN\Leads\LeadRepository;
use SUBIN\MailLog\MailEventRepository;
use SUBIN\Notifications\NotificationJobRepository;

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

if ( ! class_exists( AutoReplyPreheader::class ) ) {
	require_once __DIR__ . '/AutoReplyPreheader.php';
}

/**
 * Selects, snapshots, and idempotently queues one external-form auto-reply.
 */
class AutoReplyRuleSender {
	const PREHEADER_RENDER_BUDGET_BYTES = 2000;

	/** @var AutoReplyRuleRepository */
	private $rules;

	/** @var AutoReplyTemplateRepository */
	private $templates;

	/** @var MailEventRepository */
	private $events;

	/** @var FormSubmissionContext */
	private $context;

	/** @var NotificationJobRepository */
	private $jobs;

	/** @var AutoReplyMergeTagRenderer */
	private $renderer;

	/** @var LeadRepository */
	private $leads;

	/**
	 * Constructor.
	 *
	 * The first four arguments preserve the public constructor used by existing
	 * integrations and tests.
	 *
	 * @param AutoReplyRuleRepository|null     $rules Rules.
	 * @param AutoReplyTemplateRepository|null $templates Templates.
	 * @param MailEventRepository|null         $events Events.
	 * @param FormSubmissionContext|null       $context Context.
	 * @param NotificationJobRepository|null   $jobs Durable jobs.
	 * @param AutoReplyMergeTagRenderer|null   $renderer Merge renderer.
	 * @param LeadRepository|null               $leads Lead projections.
	 */
	public function __construct(
		?AutoReplyRuleRepository $rules = null,
		?AutoReplyTemplateRepository $templates = null,
		?MailEventRepository $events = null,
		?FormSubmissionContext $context = null,
		?NotificationJobRepository $jobs = null,
		?AutoReplyMergeTagRenderer $renderer = null,
		?LeadRepository $leads = null
	) {
		$this->rules     = $rules ?: new AutoReplyRuleRepository();
		$this->templates = $templates ?: new AutoReplyTemplateRepository();
		$this->events    = $events ?: new MailEventRepository();
		$this->context   = $context ?: new FormSubmissionContext();
		$this->jobs      = $jobs ?: new NotificationJobRepository();
		$this->renderer  = $renderer ?: new AutoReplyMergeTagRenderer();
		$this->leads     = $leads ?: new LeadRepository();
	}

	/**
	 * Find the first matching effective rule for a lead.
	 *
	 * @param array<string,mixed>|object $lead Lead row.
	 * @param array<int,array>           $lead_fields Accepted fields.
	 * @return array<string,mixed>|null
	 */
	public function rule_for_lead( $lead, array $lead_fields = array() ) {
		$provider = is_array( $lead ) ? ( $lead['provider'] ?? '' ) : ( $lead->provider ?? '' );
		$form_id  = is_array( $lead ) ? ( $lead['form_id'] ?? '' ) : ( $lead->form_id ?? '' );

		return $this->rules->find_matching_rule( (string) $provider, (string) $form_id, $lead_fields );
	}

	/**
	 * Queue an immutable visitor auto-reply once for a saved lead.
	 *
	 * @param array<string,mixed> $lead Lead.
	 * @param array<int,array>    $lead_fields Lead fields.
	 * @return string Email status.
	 */
	public function send_for_lead( array $lead, array $lead_fields ) {
		$lead_id = absint( $lead['id'] ?? 0 );
		if ( $lead_id <= 0 ) {
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}
		if ( $this->has_sent_auto_reply_event( $lead_id ) ) {
			$this->leads->update_auto_reply_status( $lead_id, LeadRepository::AUTO_REPLY_STATUS_SENT );
			return LeadRepository::EMAIL_STATUS_SENT;
		}

		$snapshot = $this->prepare_snapshot( $lead, $lead_fields );
		if ( null === $snapshot ) {
			$this->leads->update_auto_reply_status( $lead_id, LeadRepository::AUTO_REPLY_STATUS_NOT_CONFIGURED );
			return LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED;
		}
		if ( is_wp_error( $snapshot ) ) {
			do_action( 'subin_auto_reply_snapshot_failed', $lead_id, $snapshot );
			$this->leads->update_auto_reply_status( $lead_id, LeadRepository::AUTO_REPLY_STATUS_FAILED );
			return LeadRepository::EMAIL_STATUS_FAILED;
		}

		$payload = AutoReplyDeliverySnapshot::payload( $snapshot );
		if ( is_wp_error( $payload ) ) {
			do_action( 'subin_auto_reply_snapshot_failed', $lead_id, $payload );
			$this->leads->update_auto_reply_status( $lead_id, LeadRepository::AUTO_REPLY_STATUS_FAILED );
			return LeadRepository::EMAIL_STATUS_FAILED;
		}

		$job_id = $this->jobs->enqueue_at(
			'lead_' . $lead_id . '_visitor_auto_reply',
			$lead_id,
			AutoReplyDeliverySnapshot::EMAIL_TYPE,
			$payload,
			$snapshot['scheduled_at'],
			AutoReplyDeliverySnapshot::JOB_PAYLOAD_VERSION
		);
		if ( is_wp_error( $job_id ) || absint( $job_id ) <= 0 ) {
			$error = is_wp_error( $job_id )
				? $job_id
				: new \WP_Error( 'subin_auto_reply_job_failed', __( 'The auto-reply could not be queued.', 'submission-inbox' ) );
			do_action( 'subin_notification_job_failed', 0, $error );
			$this->leads->update_auto_reply_status( $lead_id, LeadRepository::AUTO_REPLY_STATUS_FAILED );
			return LeadRepository::EMAIL_STATUS_FAILED;
		}

		FormSubmissionHandler::schedule_notification_worker();
		FormSubmissionHandler::schedule_notification_at( $snapshot['scheduled_at'] );
		$this->leads->update_auto_reply_status( $lead_id, LeadRepository::AUTO_REPLY_STATUS_SCHEDULED );

		return LeadRepository::EMAIL_STATUS_UNKNOWN;
	}

	/**
	 * Prepare an immutable simulation/delivery snapshot without sending mail.
	 *
	 * @param array<string,mixed> $lead Lead.
	 * @param array<int,array>    $lead_fields Lead fields.
	 * @return array<string,mixed>|null|\WP_Error
	 */
	public function prepare_snapshot( array $lead, array $lead_fields ) {
		$lead_id = absint( $lead['id'] ?? 0 );
		$rule    = $lead_id > 0 ? $this->rule_for_lead( $lead, $lead_fields ) : null;
		if ( ! $rule ) {
			return null;
		}

		$template_id = absint( $rule['template_id'] ?? 0 );
		$template    = $this->delivery_template( $template_id );
		$subject_source = '' !== trim( (string) ( $rule['subject'] ?? '' ) )
			? (string) $rule['subject']
			: (string) ( $template['subject'] ?? '' );
		$body_source = '' !== trim( (string) ( $rule['body'] ?? '' ) )
			? (string) $rule['body']
			: AutoReplyTemplateRepository::effective_body( $template );
		$preheader_source = '' !== trim( (string) ( $rule['preheader'] ?? '' ) )
			? (string) $rule['preheader']
			: (string) ( $template['preheader'] ?? '' );
		$recipient = $this->visitor_email_from_rule( $rule, $lead_fields );
		$context   = array(
			'site_name'       => get_bloginfo( 'name' ),
			'form_name'       => sanitize_text_field( (string) ( $lead['form_name'] ?? '' ) ),
			'submission_date' => $this->display_submission_date( $lead['submitted_at'] ?? '' ),
			'lead_id'         => (string) $lead_id,
			'visitor_email'   => is_email( $recipient ) ? $recipient : sanitize_email( (string) ( $lead['customer_email'] ?? '' ) ),
		);
		$subject_result = $this->renderer->render_subject( $subject_source, $context, $lead_fields, AutoReplyDeliverySnapshot::MAX_SUBJECT_BYTES );
		$preheader_result = $this->renderer->render_subject( $preheader_source, $context, $lead_fields, self::PREHEADER_RENDER_BUDGET_BYTES );
		$body_result    = $this->renderer->render_html( $body_source, $context, $lead_fields, AutoReplyDeliverySnapshot::MAX_BODY_BYTES );
		$subject        = (string) ( $subject_result['content'] ?? '' );
		$subject        = '' !== $subject ? $subject : __( 'We received your request', 'submission-inbox' );
		$body           = (string) ( $body_result['content'] ?? '' );
		$preheader      = (string) ( $preheader_result['content'] ?? '' );
		$delay_seconds  = AutoReplyRuleRepository::DELIVERY_DELAY === ( $rule['delivery_mode'] ?? '' )
			? min( AutoReplyRuleRepository::MAX_DELAY_SECONDS, absint( $rule['delay_seconds'] ?? 0 ) )
			: 0;
		$scheduled_at = $this->scheduled_at( $lead['submitted_at'] ?? '', $delay_seconds );
		$from = is_email( $rule['from_email'] ?? '' )
			? array( 'email' => sanitize_email( $rule['from_email'] ), 'name' => sanitize_text_field( $rule['from_name'] ?? '' ) )
			: null;
		$reply_to = is_email( $rule['reply_to_email'] ?? '' )
			? array( 'email' => sanitize_email( $rule['reply_to_email'] ), 'name' => sanitize_text_field( $rule['reply_to_name'] ?? '' ) )
			: null;

		$state          = AutoReplyDeliverySnapshot::STATE_READY;
		$reason_code    = '';
		$reason_message = '';
		if ( '' === trim( $body_source ) || ( empty( $body_result['overflowed'] ) && ! AutoReplyPreheader::has_meaningful_content( $body ) ) ) {
			$state          = AutoReplyDeliverySnapshot::STATE_NO_EMAIL_GENERATED;
			$reason_code    = 'missing_content';
			$reason_message = __( 'Auto-reply content is missing.', 'submission-inbox' );
			$body           = '';
		} elseif ( ! is_email( $recipient ) ) {
			$state          = AutoReplyDeliverySnapshot::STATE_NO_EMAIL_GENERATED;
			$reason_code    = 'invalid_recipient';
			$reason_message = __( 'Auto-reply visitor email field did not contain a valid email address.', 'submission-inbox' );
			$recipient      = '';
			$body           = '';
		} elseif ( ! empty( $subject_result['overflowed'] ) ) {
			$state          = AutoReplyDeliverySnapshot::STATE_NO_EMAIL_GENERATED;
			$reason_code    = 'subject_too_long';
			$reason_message = __( 'Auto-reply subject is too long after merge tags are rendered.', 'submission-inbox' );
			$body           = '';
		} elseif ( ! empty( $body_result['overflowed'] ) ) {
			$state          = AutoReplyDeliverySnapshot::STATE_NO_EMAIL_GENERATED;
			$reason_code    = 'content_too_large';
			$reason_message = __( 'Auto-reply content is too large after merge tags are rendered.', 'submission-inbox' );
			$body           = '';
		} else {
			$body = AutoReplyPreheader::inject( $body, $preheader );
			if ( strlen( $body ) > AutoReplyDeliverySnapshot::MAX_BODY_BYTES ) {
				$state          = AutoReplyDeliverySnapshot::STATE_NO_EMAIL_GENERATED;
				$reason_code    = 'content_too_large';
				$reason_message = __( 'Auto-reply content is too large after preview text is added.', 'submission-inbox' );
				$body           = '';
			}
		}

		return AutoReplyDeliverySnapshot::create(
			array(
				'state'          => $state,
				'provider'       => sanitize_key( (string) ( $lead['provider'] ?? '' ) ),
				'form_id'        => sanitize_text_field( (string) ( $lead['form_id'] ?? '' ) ),
				'form_name'      => sanitize_text_field( (string) ( $lead['form_name'] ?? '' ) ),
				'lead_id'        => $lead_id,
					'rule_id'        => absint( $rule['id'] ?? 0 ),
					'rule_name'      => sanitize_text_field( (string) ( $rule['name'] ?? '' ) ),
					'rule_version'   => max( 1, absint( $rule['rule_version'] ?? 1 ) ),
					'template_id'    => $template_id,
					'template_name'  => sanitize_text_field( (string) ( $template['name'] ?? '' ) ),
				'delivery_mode'  => $delay_seconds > 0 ? AutoReplyDeliverySnapshot::DELIVERY_DELAY : AutoReplyDeliverySnapshot::DELIVERY_IMMEDIATE,
				'scheduled_at'   => $scheduled_at,
				'to'             => sanitize_email( $recipient ),
				'subject'        => sanitize_text_field( $subject ),
				'body_html'      => $body,
				'from'           => $from,
				'reply_to'       => $reply_to,
				'reason_code'    => $reason_code,
				'reason_message' => $reason_message,
			)
		);
	}

	/**
	 * Kept for backwards-compatible callers that used this filter callback.
	 *
	 * @return string
	 */
	public function html_mail_content_type() {
		return 'text/html';
	}

	/**
	 * Resolve the selected visitor email field.
	 *
	 * @param array<string,mixed> $rule Rule.
	 * @param array<int,array>    $lead_fields Lead fields.
	 * @return string
	 */
	private function visitor_email_from_rule( array $rule, array $lead_fields ) {
		$selected = sanitize_key( (string) ( $rule['visitor_email_field'] ?? '' ) );
		if ( '' === $selected ) {
			return '';
		}

		foreach ( $lead_fields as $field ) {
			if ( $selected !== sanitize_key( $field['field_key'] ?? '' ) || 'email' !== sanitize_key( $field['field_type'] ?? 'email' ) ) {
				continue;
			}

			$email = sanitize_email( $field['field_value'] ?? $field['value'] ?? '' );
			return is_email( $email ) ? $email : '';
		}

		return '';
	}

	/**
	 * Return whether a previous synchronous/queued attempt already succeeded.
	 *
	 * @param int $lead_id Lead id.
	 * @return bool
	 */
	private function has_sent_auto_reply_event( $lead_id ) {
		foreach ( $this->events->get_by_lead_id( $lead_id ) as $event ) {
			if (
				AutoReplyDeliverySnapshot::SOURCE === sanitize_key( $event->source ?? '' )
				&& MailEventRepository::STATUS_SENT === sanitize_key( $event->status ?? '' )
			) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Format the accepted local datetime for merge content.
	 *
	 * @param mixed $submitted_at Accepted datetime.
	 * @return string
	 */
	private function display_submission_date( $submitted_at ) {
		$date = \DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', (string) $submitted_at, wp_timezone() );
		if ( ! $date instanceof \DateTimeImmutable ) {
			$date = new \DateTimeImmutable( 'now', wp_timezone() );
		}

		return wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $date->getTimestamp(), wp_timezone() );
	}

	/**
	 * Calculate deterministic queue eligibility from the accepted lead time.
	 *
	 * @param mixed $submitted_at Accepted local datetime.
	 * @param int   $delay_seconds Policy delay.
	 * @return string
	 */
	private function scheduled_at( $submitted_at, $delay_seconds ) {
		$date = \DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', (string) $submitted_at, wp_timezone() );
		if ( ! $date instanceof \DateTimeImmutable ) {
			$date = new \DateTimeImmutable( 'now', wp_timezone() );
		}
		$date = $date->modify( '+' . min( AutoReplyRuleRepository::MAX_DELAY_SECONDS, absint( $delay_seconds ) ) . ' seconds' );

		return $date->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Load only delivery-owned template fields with a bounded body projection.
	 *
	 * @param int $template_id Template id.
	 * @return array<string,mixed>|null
	 */
	private function delivery_template( $template_id ) {
		$template_id = absint( $template_id );
		if ( $template_id <= 0 ) {
			return null;
		}
		if ( method_exists( $this->templates, 'get_design_content' ) ) {
			return $this->templates->get_design_content( $template_id, AutoReplyDeliverySnapshot::MAX_BODY_BYTES );
		}

		return $this->templates->get( $template_id );
	}
}
