<?php
/**
 * Transactional public submission acceptance.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

use SUBIN\Leads\LeadFieldRepository;
use SUBIN\Leads\LeadRepository;
use SUBIN\Notifications\NotificationEnvelope;
use SUBIN\Notifications\NotificationJobRepository;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This service owns the acceptance transaction across plugin custom tables.

/**
 * Atomically stores a receipt, lead data, and notification outbox jobs.
 */
class SubmissionAcceptanceService {
	const EMAIL_TYPE_ADMIN_NOTIFICATION = 'admin_notification';
	const EMAIL_TYPE_VISITOR_AUTO_REPLY  = 'visitor_auto_reply';

	/**
	 * Receipt repository.
	 *
	 * @var SubmissionReceiptRepository
	 */
	private $receipts;

	/**
	 * Lead repository.
	 *
	 * @var LeadRepository
	 */
	private $leads;

	/**
	 * Lead field repository.
	 *
	 * @var LeadFieldRepository
	 */
	private $lead_fields;

	/**
	 * Notification job repository.
	 *
	 * @var NotificationJobRepository
	 */
	private $notification_jobs;

	/**
	 * Constructor.
	 *
	 * @param SubmissionReceiptRepository $receipts Receipt repository.
	 * @param LeadRepository              $leads Lead repository.
	 * @param LeadFieldRepository         $lead_fields Lead field repository.
	 * @param NotificationJobRepository   $notification_jobs Notification job repository.
	 */
	public function __construct(
		SubmissionReceiptRepository $receipts,
		LeadRepository $leads,
		LeadFieldRepository $lead_fields,
		NotificationJobRepository $notification_jobs
	) {
		$this->receipts          = $receipts;
		$this->leads             = $leads;
		$this->lead_fields       = $lead_fields;
		$this->notification_jobs = $notification_jobs;
	}

	/**
	 * Read a completed receipt before expensive public-request gates run.
	 *
	 * @param string              $provider Provider key.
	 * @param string              $form_id Form id.
	 * @param string              $submission_id Client submission id.
	 * @param array<string,mixed> $payload Effective validated payload.
	 * @return array<string,mixed>|null|\WP_Error
	 */
	public function find_completed( $provider, $form_id, $submission_id, array $payload ) {
		return $this->receipts->find_completed( $provider, $form_id, $submission_id, $payload );
	}

	/**
	 * Accept one public submission as a single durable operation.
	 *
	 * @param array<string,mixed> $command Validated acceptance command.
	 * @return array<string,mixed>|\WP_Error
	 */
	public function accept( array $command ) {
		global $wpdb;

		$command = $this->normalize_command( $command );
		if ( is_wp_error( $command ) ) {
			return $command;
		}

		$transaction_open = false !== $wpdb->query( 'START TRANSACTION' );
		if ( ! $transaction_open ) {
			return $this->storage_error(
				'subin_submission_acceptance_start_failed',
				__( 'The submission could not be stored.', 'submission-inbox' ),
				array(
					'status'    => 503,
					'retryable' => true,
				)
			);
		}

		try {
			$reservation = $this->receipts->reserve_in_transaction(
				$command['provider'],
				$command['form_id'],
				$command['submission_id'],
				$command['payload']
			);
			if ( is_wp_error( $reservation ) ) {
				$this->rollback( $transaction_open );
				return $reservation;
			}

			if ( 'completed' === ( $reservation['state'] ?? '' ) ) {
				if ( ! $this->rollback( $transaction_open ) ) {
					return $this->storage_error(
						'subin_submission_acceptance_replay_release_failed',
						__( 'The saved submission result could not be released.', 'submission-inbox' )
					);
				}

				return array(
					'state'                  => 'completed',
					'receipt_id'             => absint( $reservation['receipt_id'] ?? 0 ),
					'lead_id'                => absint( $reservation['lead_id'] ?? 0 ),
					'job_ids'                => array(),
					'notification_job_count' => absint( $reservation['notification_job_count'] ?? 0 ),
					'result'                  => is_array( $reservation['response'] ?? null ) ? $reservation['response'] : array(),
					'duplicate'               => true,
				);
			}

			$receipt_id   = absint( $reservation['receipt_id'] ?? 0 );
			$request_hash = (string) ( $reservation['request_hash'] ?? '' );
			if ( 'reserved' !== ( $reservation['state'] ?? '' ) || $receipt_id <= 0 || '' === $request_hash ) {
				$this->rollback( $transaction_open );
				return new \WP_Error(
					'subin_submission_acceptance_invalid_reservation',
					__( 'The submission receipt could not be reserved.', 'submission-inbox' )
				);
			}

			$lead_id = 0;
			if ( $command['store_lead'] ) {
				$lead_id = $this->leads->create( $command['lead_data'] );
				if ( $lead_id <= 0 ) {
					$this->rollback( $transaction_open );
					return $this->storage_error(
						'subin_submission_acceptance_lead_failed',
						__( 'The submission could not be stored.', 'submission-inbox' )
					);
				}

				$fields_result = $this->lead_fields->insert_fields( $lead_id, $command['lead_fields'] );
				if ( is_wp_error( $fields_result ) ) {
					$this->rollback( $transaction_open );
					return $fields_result;
				}
				if ( true !== $fields_result ) {
					$this->rollback( $transaction_open );
					return new \WP_Error(
						'subin_submission_acceptance_fields_failed',
						__( 'The submission fields could not be stored.', 'submission-inbox' )
					);
				}

				if ( ! $this->leads->update_email_status( $lead_id, $command['lead_email_status'] ) ) {
					$this->rollback( $transaction_open );
					return $this->storage_error(
						'subin_submission_acceptance_email_status_failed',
						__( 'The submission email state could not be stored.', 'submission-inbox' )
					);
				}

				if ( ! $this->leads->update_auto_reply_status( $lead_id, $command['lead_auto_reply_status'] ) ) {
					$this->rollback( $transaction_open );
					return $this->storage_error(
						'subin_submission_acceptance_auto_reply_status_failed',
						__( 'The visitor auto-reply state could not be stored.', 'submission-inbox' )
					);
				}
			}

			$job_ids = array();
			foreach ( $command['notifications'] as $notification ) {
				$notification_payload = $notification['payload'];
				if ( array_key_exists( 'envelope', $notification_payload ) ) {
					if ( ! is_array( $notification_payload['envelope'] ) ) {
						$this->rollback( $transaction_open );
						return $this->invalid_command();
					}
					$draft_context = is_array( $notification_payload['envelope']['context'] ?? null )
						? $notification_payload['envelope']['context']
						: array();
					if (
						$command['provider'] !== sanitize_key( (string) ( $draft_context['provider'] ?? '' ) )
						|| $command['form_id'] !== sanitize_text_field( (string) ( $draft_context['form_id'] ?? '' ) )
					) {
						$this->rollback( $transaction_open );
						return $this->invalid_command();
					}

					$final_envelope = NotificationEnvelope::finalize(
						$notification_payload['envelope'],
						$receipt_id,
						$lead_id,
						$notification['email_type']
					);
					if ( is_wp_error( $final_envelope ) ) {
						$this->rollback( $transaction_open );
						return $final_envelope;
					}

					$notification_payload = array( 'envelope' => $final_envelope );
				}

				$job_id = $this->notification_jobs->enqueue_for_receipt(
					$receipt_id,
					$lead_id,
					$notification['email_type'],
					$notification_payload
				);
				if ( is_wp_error( $job_id ) ) {
					$this->rollback( $transaction_open );
					return $job_id;
				}
				if ( absint( $job_id ) <= 0 ) {
					$this->rollback( $transaction_open );
					return new \WP_Error(
						'subin_submission_acceptance_notification_failed',
						__( 'A submission notification could not be stored.', 'submission-inbox' )
					);
				}

				$job_ids[] = absint( $job_id );
			}

			$completed = $this->receipts->complete(
				$receipt_id,
				$request_hash,
				$command['success_result'],
				$lead_id,
				count( $job_ids )
			);
			if ( is_wp_error( $completed ) ) {
				$this->rollback( $transaction_open );
				return $completed;
			}
			if ( true !== $completed ) {
				$this->rollback( $transaction_open );
				return new \WP_Error(
					'subin_submission_acceptance_receipt_failed',
					__( 'The submission receipt could not be completed.', 'submission-inbox' )
				);
			}

			if ( false === $wpdb->query( 'COMMIT' ) ) {
				$this->rollback( $transaction_open );
				return $this->storage_error(
					'subin_submission_acceptance_commit_unknown',
					__( 'The submission storage result could not be confirmed.', 'submission-inbox' ),
					array(
						'status'    => 503,
						'retryable' => true,
					)
				);
			}
			$transaction_open = false;
		} catch ( \Throwable $exception ) {
			$this->rollback( $transaction_open );
			return new \WP_Error(
				'subin_submission_acceptance_exception',
				__( 'The submission could not be stored completely.', 'submission-inbox' ),
				array( 'exception' => sanitize_text_field( get_class( $exception ) ) )
			);
		}

		if ( $lead_id > 0 ) {
			try {
				do_action( 'subin_lead_created', $lead_id );
			} catch ( \Throwable $exception ) {
				try {
					do_action( 'subin_submission_acceptance_hook_failed', $lead_id, $exception );
				} catch ( \Throwable $ignored ) {
					unset( $ignored );
				}
			}
		}

		return array(
			'state'                  => 'accepted',
			'receipt_id'             => $receipt_id,
			'lead_id'                => $lead_id,
			'job_ids'                => $job_ids,
			'notification_job_count' => count( $job_ids ),
			'result'                  => $command['success_result'],
			'duplicate'               => false,
		);
	}

	/**
	 * Validate and normalize all inputs before opening a transaction.
	 *
	 * @param array<string,mixed> $command Raw command.
	 * @return array<string,mixed>|\WP_Error
	 */
	private function normalize_command( array $command ) {
		if (
			! isset( $command['provider'], $command['form_id'] )
			|| ! is_scalar( $command['provider'] )
			|| ! is_scalar( $command['form_id'] )
			|| ! array_key_exists( 'payload', $command )
			|| ! is_array( $command['payload'] )
			|| ! array_key_exists( 'success_result', $command )
			|| ! is_array( $command['success_result'] )
		) {
			return $this->invalid_command();
		}

		$provider = substr( sanitize_key( (string) $command['provider'] ), 0, 50 );
		$form_id  = substr( sanitize_text_field( (string) $command['form_id'] ), 0, 100 );
		if ( '' === $provider || '' === $form_id ) {
			return $this->invalid_command();
		}

		$submission_id = $command['submission_id'] ?? '';
		if ( ! is_scalar( $submission_id ) && null !== $submission_id ) {
			return $this->invalid_command();
		}

		$store_lead  = ! empty( $command['store_lead'] );
		$lead_data   = $command['lead_data'] ?? array();
		$lead_fields = $command['lead_fields'] ?? array();
		if ( ! is_array( $lead_data ) || ! is_array( $lead_fields ) ) {
			return $this->invalid_command();
		}

		if ( $store_lead ) {
			foreach ( $lead_fields as $field ) {
				if ( ! is_array( $field ) || '' === sanitize_text_field( (string) ( $field['field_key'] ?? '' ) ) ) {
					return $this->invalid_command();
				}
			}

			$lead_data['provider'] = $provider;
			$lead_data['form_id']  = $form_id;
		}

		$email_status = sanitize_key(
			(string) ( $command['lead_email_status'] ?? LeadRepository::EMAIL_STATUS_UNKNOWN )
		);
		$allowed_statuses = array(
			LeadRepository::EMAIL_STATUS_SENT,
			LeadRepository::EMAIL_STATUS_FAILED,
			LeadRepository::EMAIL_STATUS_NO_EMAIL_GENERATED,
			LeadRepository::EMAIL_STATUS_UNKNOWN,
		);
		if ( ! in_array( $email_status, $allowed_statuses, true ) ) {
			return $this->invalid_command();
		}

		$auto_reply_status = sanitize_key(
			(string) ( $command['lead_auto_reply_status'] ?? LeadRepository::AUTO_REPLY_STATUS_NOT_CONFIGURED )
		);
		$allowed_auto_reply_statuses = array(
			LeadRepository::AUTO_REPLY_STATUS_NOT_CONFIGURED,
			LeadRepository::AUTO_REPLY_STATUS_SCHEDULED,
			LeadRepository::AUTO_REPLY_STATUS_SENT,
			LeadRepository::AUTO_REPLY_STATUS_FAILED,
			LeadRepository::AUTO_REPLY_STATUS_NO_EMAIL_GENERATED,
			LeadRepository::AUTO_REPLY_STATUS_UNKNOWN,
		);
		if ( ! in_array( $auto_reply_status, $allowed_auto_reply_statuses, true ) ) {
			return $this->invalid_command();
		}

		$notifications = $this->normalize_notifications( $command['notifications'] ?? array() );
		if ( is_wp_error( $notifications ) ) {
			return $notifications;
		}
		if ( ! $store_lead && empty( $notifications ) ) {
			return $this->invalid_command();
		}
		return array(
			'provider'          => $provider,
			'form_id'           => $form_id,
			'submission_id'     => trim( (string) $submission_id ),
			'payload'           => $command['payload'],
			'store_lead'        => $store_lead,
			'lead_data'         => $store_lead ? $lead_data : array(),
			'lead_fields'       => $store_lead ? array_values( $lead_fields ) : array(),
			'lead_email_status' => $email_status,
			'lead_auto_reply_status' => $auto_reply_status,
			'notifications'     => $notifications,
			'success_result'    => $command['success_result'],
		);
	}

	/**
	 * Normalize the fixed notification types accepted by the built-in form.
	 *
	 * @param mixed $notifications Raw notifications.
	 * @return array<int,array{email_type:string,payload:array<string,mixed>,available_at:string}>|\WP_Error
	 */
	private function normalize_notifications( $notifications ) {
		if ( ! is_array( $notifications ) ) {
			return $this->invalid_command();
		}

		$normalized = array();
		$seen       = array();
		$allowed    = array(
			self::EMAIL_TYPE_ADMIN_NOTIFICATION,
			self::EMAIL_TYPE_VISITOR_AUTO_REPLY,
		);

		foreach ( $notifications as $notification ) {
			if ( ! is_array( $notification ) ) {
				return $this->invalid_command();
			}

			$email_type = sanitize_key( (string) ( $notification['email_type'] ?? '' ) );
			$payload    = $notification['payload'] ?? array();
			if ( ! in_array( $email_type, $allowed, true ) || ! is_array( $payload ) || isset( $seen[ $email_type ] ) ) {
				return $this->invalid_command();
			}
			$available_at = is_scalar( $notification['available_at'] ?? '' )
				? trim( (string) ( $notification['available_at'] ?? '' ) )
				: '';
			if ( '' !== $available_at && ! $this->valid_mysql_datetime( $available_at ) ) {
				return $this->invalid_command();
			}

			$seen[ $email_type ] = true;
			$normalized[] = array(
				'email_type'  => $email_type,
				'payload'     => $payload,
				'available_at' => $available_at,
			);
		}

		return $normalized;
	}

	/**
	 * Validate a local MySQL-style datetime.
	 *
	 * @param string $value Datetime.
	 * @return bool
	 */
	private function valid_mysql_datetime( $value ) {
		if ( 1 !== preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', (string) $value ) ) {
			return false;
		}

		$date = \DateTime::createFromFormat( '!Y-m-d H:i:s', (string) $value );

		return $date instanceof \DateTime && $date->format( 'Y-m-d H:i:s' ) === $value;
	}

	/**
	 * Roll back an open transaction and clear the local ownership flag.
	 *
	 * @param bool $transaction_open Transaction ownership flag.
	 * @return bool
	 */
	private function rollback( &$transaction_open ) {
		if ( ! $transaction_open ) {
			return true;
		}

		global $wpdb;
		$rolled_back      = false !== $wpdb->query( 'ROLLBACK' );
		$transaction_open = false;

		return $rolled_back;
	}

	/**
	 * Build a safe command-validation error.
	 *
	 * @return \WP_Error
	 */
	private function invalid_command() {
		return new \WP_Error(
			'subin_invalid_submission_acceptance',
			__( 'The submission acceptance request is invalid.', 'submission-inbox' )
		);
	}

	/**
	 * Build a safe storage error with the current database diagnostic.
	 *
	 * @param string              $code Error code.
	 * @param string              $message Safe message.
	 * @param array<string,mixed> $data Additional public error data.
	 * @return \WP_Error
	 */
	private function storage_error( $code, $message, array $data = array() ) {
		global $wpdb;

		$data['db_error'] = sanitize_text_field( (string) $wpdb->last_error );

		return new \WP_Error( sanitize_key( $code ), sanitize_text_field( $message ), $data );
	}
}
