<?php
/**
 * Form submission context.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

/**
 * Holds the current lead id during a form submission request.
 */
class FormSubmissionContext {
	/**
	 * Current lead id.
	 *
	 * @var int|null
	 */
	private $current_lead_id = null;

	/**
	 * Current source.
	 *
	 * @var string
	 */
	private $current_source = '';

	/**
	 * Request-local correlation id for mail events.
	 *
	 * @var string
	 */
	private $correlation_id = '';

	/**
	 * Request-local metadata for the current delivery attempt.
	 *
	 * @var array<string,mixed>
	 */
	private $delivery_metadata = array();

	/** @var string */
	private $current_from_email = '';

	/** @var string */
	private $current_from_name = '';

	/**
	 * Whether mail logging should be suppressed for the current request context.
	 *
	 * @var bool
	 */
	private $suppress_mail_logging = false;

	/**
	 * Set current lead id.
	 *
	 * @param int|null $lead_id Lead id.
	 * @return void
	 */
	public function set_current_lead_id( $lead_id ) {
		$lead_id = absint( $lead_id );
		$this->current_lead_id = $lead_id > 0 ? $lead_id : null;
	}

	/**
	 * Set current source.
	 *
	 * @param string $source            Source.
	 * @param bool   $renew_correlation Whether to start a new mail correlation.
	 * @return void
	 */
	public function set_current_source( $source, $renew_correlation = false ) {
		$source = sanitize_key( $source );
		$allowed = array_merge(
			( new \SUBIN\Integrations\ProviderRegistry() )->active_keys(),
			array( 'submission_inbox_auto_reply' )
		);
		$source = in_array( $source, $allowed, true ) ? $source : '';

		if ( '' === $source ) {
			$this->current_source = '';
			$this->correlation_id = '';
			$this->delivery_metadata = array();
			$this->current_from_email = '';
			$this->current_from_name  = '';
			return;
		}

		if ( $renew_correlation || $source !== $this->current_source || '' === $this->correlation_id ) {
			$this->correlation_id = str_replace( '-', '', wp_generate_uuid4() );
			$this->delivery_metadata = array();
			$this->current_from_email = '';
			$this->current_from_name  = '';
		}

		$this->current_source = $source;
	}

	/**
	 * Get current lead id.
	 *
	 * @return int|null
	 */
	public function get_current_lead_id() {
		return $this->current_lead_id;
	}

	/**
	 * Get current source.
	 *
	 * @return string
	 */
	public function get_current_source() {
		return $this->current_source;
	}

	/**
	 * Get the request-local mail correlation id.
	 *
	 * @return string
	 */
	public function get_correlation_id() {
		return $this->correlation_id;
	}

	/**
	 * Set a stable correlation id for a durable delivery attempt.
	 *
	 * @param string $correlation_id Correlation id owned by the queued job.
	 * @return void
	 */
	public function set_correlation_id( $correlation_id ) {
		$correlation_id = preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $correlation_id );
		$this->correlation_id = '' !== $this->current_source
			? substr( (string) $correlation_id, 0, 64 )
			: '';
	}

	/**
	 * Set metadata for the current delivery attempt.
	 *
	 * Only the documented delivery fields are retained. Invalid values are
	 * omitted instead of being reinterpreted, so request data cannot introduce
	 * arbitrary columns or turn a negative identifier into a valid one.
	 *
	 * @param array<string,mixed> $metadata Delivery metadata.
	 * @return void
	 */
	public function set_delivery_metadata( array $metadata ) {
		$this->delivery_metadata = self::sanitize_delivery_metadata( $metadata );
	}

	/**
	 * Get metadata for the current delivery attempt.
	 *
	 * @return array<string,mixed>
	 */
	public function get_delivery_metadata() {
		return $this->delivery_metadata;
	}

	/**
	 * Set a scoped policy From mailbox for the current wp_mail call.
	 *
	 * @param mixed $email Sender email.
	 * @param mixed $name Sender name.
	 * @return void
	 */
	public function set_current_from( $email, $name = '' ) {
		$email = is_scalar( $email ) ? trim( (string) $email ) : '';
		$name  = is_scalar( $name ) ? (string) $name : '';
		if ( false !== strpos( $email, "\r" ) || false !== strpos( $email, "\n" ) || false !== strpos( $name, "\r" ) || false !== strpos( $name, "\n" ) ) {
			$this->current_from_email = '';
			$this->current_from_name  = '';
			return;
		}

		$email = sanitize_email( $email );
		$this->current_from_email = is_email( $email ) ? $email : '';
		$this->current_from_name  = '' !== $this->current_from_email ? sanitize_text_field( $name ) : '';
	}

	/** @return string */
	public function get_current_from_email() {
		return $this->current_from_email;
	}

	/** @return string */
	public function get_current_from_name() {
		return $this->current_from_name;
	}

	/**
	 * Sanitize the mail-event metadata contract.
	 *
	 * @param array<string,mixed> $metadata Raw metadata.
	 * @return array<string,mixed>
	 */
	public static function sanitize_delivery_metadata( array $metadata ) {
		$clean = array();

		foreach ( array( 'job_id', 'rule_id', 'template_id', 'rule_version', 'parent_event_id', 'retry_of_event_id' ) as $key ) {
			$value = self::positive_integer( $metadata[ $key ] ?? null );
			if ( $value > 0 ) {
				$clean[ $key ] = $value;
			}
		}

		$event_kind = sanitize_key( (string) ( $metadata['event_kind'] ?? '' ) );
		if ( in_array( $event_kind, array( 'generic', 'admin_notification', 'visitor_confirmation', 'auto_reply_test', 'health_verification' ), true ) ) {
			$clean['event_kind'] = $event_kind;
		}

		$provider = substr( sanitize_key( (string) ( $metadata['provider'] ?? '' ) ), 0, 64 );
		if ( '' !== $provider ) {
			$clean['provider'] = $provider;
		}

		$form_id = self::truncate( sanitize_text_field( (string) ( $metadata['form_id'] ?? '' ) ), 190 );
		if ( '' !== $form_id ) {
			$clean['form_id'] = $form_id;
		}

		foreach ( array( 'form_name' => 255, 'rule_name' => 190, 'template_name' => 255 ) as $key => $length ) {
			$value = self::truncate( sanitize_text_field( (string) ( $metadata[ $key ] ?? '' ) ), $length );
			if ( '' !== $value ) {
				$clean[ $key ] = $value;
			}
		}

		$delivery_mode = sanitize_key( (string) ( $metadata['delivery_mode'] ?? '' ) );
		if ( in_array( $delivery_mode, array( 'immediate', 'delay' ), true ) ) {
			$clean['delivery_mode'] = $delivery_mode;
		}

		$scheduled_at = sanitize_text_field( (string) ( $metadata['scheduled_at'] ?? '' ) );
		if ( self::is_mysql_datetime( $scheduled_at ) ) {
			$clean['scheduled_at'] = $scheduled_at;
		}

		$reason_code = substr( sanitize_key( (string) ( $metadata['reason_code'] ?? '' ) ), 0, 100 );
		if ( '' !== $reason_code ) {
			$clean['reason_code'] = $reason_code;
		}

		return $clean;
	}

	/**
	 * Accept an integer or digit-only string greater than zero.
	 *
	 * @param mixed $value Raw value.
	 * @return int
	 */
	private static function positive_integer( $value ) {
		if ( ! is_int( $value ) && ! is_string( $value ) ) {
			return 0;
		}

		$value = trim( (string) $value );
		if ( '' === $value || ! preg_match( '/^[0-9]+$/', $value ) ) {
			return 0;
		}

		$value = absint( $value );

		return $value > 0 ? $value : 0;
	}

	/**
	 * Validate a MySQL DATETIME without changing its timezone or value.
	 *
	 * @param string $value Datetime candidate.
	 * @return bool
	 */
	private static function is_mysql_datetime( $value ) {
		if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/', (string) $value, $parts ) ) {
			return false;
		}

		return checkdate( (int) $parts[2], (int) $parts[3], (int) $parts[1] )
			&& (int) $parts[4] <= 23
			&& (int) $parts[5] <= 59
			&& (int) $parts[6] <= 59;
	}

	/**
	 * Truncate a sanitized string by characters.
	 *
	 * @param string $value Value.
	 * @param int    $length Maximum characters.
	 * @return string
	 */
	private static function truncate( $value, $length ) {
		return function_exists( 'mb_substr' )
			? mb_substr( (string) $value, 0, $length )
			: substr( (string) $value, 0, $length );
	}

	/**
	 * Suppress mail logging for the current form submission context.
	 *
	 * @return void
	 */
	public function suppress_mail_logging() {
		$this->suppress_mail_logging = true;
	}

	/**
	 * Whether mail logging is suppressed for the current context.
	 *
	 * @return bool
	 */
	public function should_suppress_mail_logging() {
		return $this->suppress_mail_logging;
	}

	/**
	 * Clear current lead context.
	 *
	 * @return void
	 */
	public function clear() {
		$this->current_lead_id       = null;
		$this->current_source        = '';
		$this->correlation_id        = '';
		$this->delivery_metadata     = array();
		$this->current_from_email    = '';
		$this->current_from_name     = '';
		$this->suppress_mail_logging = false;
	}
}
