<?php
/**
 * Request-scoped mail-evidence collector. An adapter marks a submission in
 * flight; every wp_mail() during the request is counted as notification
 * evidence (pure observation — the filter returns its input untouched);
 * the submission event is finalized on shutdown.
 *
 * @package Form_Flight_Recorder
 */

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

/**
 * Collector.
 */
class Ffrd_Collector {

	/**
	 * Container.
	 *
	 * @var Ffrd_Plugin
	 */
	private $plugin;

	/**
	 * The submission currently in flight (null when none).
	 *
	 * @var array|null
	 */
	private $active = null;

	/**
	 * Constructor.
	 *
	 * @param Ffrd_Plugin $plugin Container.
	 */
	public function __construct( $plugin ) {
		$this->plugin = $plugin;

		add_filter( 'wp_mail', array( $this, 'observe_mail' ), 9999 );
		add_action( 'wp_mail_failed', array( $this, 'observe_failure' ) );
		add_action( 'shutdown', array( $this, 'finalize' ), 5 );
	}

	/**
	 * Mark a submission in flight. Called by adapters as early as possible in
	 * the submitting request so every notification the form plugin generates
	 * afterwards is observed.
	 *
	 * @param string $source    Adapter slug.
	 * @param string $form_id   Form id.
	 * @param string $title     Form title.
	 * @param bool   $expects   Whether ≥1 notification is expected.
	 * @param bool   $confirmed Is this already a confirmed-real submission?
	 *                          (false = tentative; dropped unless confirm()ed —
	 *                          e.g. WPForms validation may still abort).
	 */
	public function begin( $source, $form_id, $title, $expects, $confirmed = true ) {
		if ( null !== $this->active ) {
			$this->finalize();
		}
		$this->active = array(
			'source'    => sanitize_key( $source ),
			'form_id'   => (string) $form_id,
			'title'     => sanitize_text_field( (string) $title ),
			'expects'   => (bool) $expects,
			'confirmed' => (bool) $confirmed,
			'ts'        => time(),
			'mail'      => array(),
			'failures'  => array(),
			'meta'      => array(),
		);
	}

	/**
	 * Confirm the in-flight submission is real (validation passed / entry saved).
	 *
	 * @param array $meta Extra metadata to merge.
	 */
	public function confirm( $meta = array() ) {
		if ( null === $this->active ) {
			return;
		}
		$this->active['confirmed'] = true;
		if ( $meta ) {
			$this->active['meta'] = array_merge( $this->active['meta'], $meta );
		}
	}

	/**
	 * Attach metadata (e.g. the form plugin's own sent/failed status).
	 *
	 * @param string $key   Key.
	 * @param mixed  $value Value.
	 */
	public function annotate( $key, $value ) {
		if ( null === $this->active ) {
			return;
		}
		$this->active['meta'][ sanitize_key( $key ) ] = $value;
	}

	/**
	 * Is a submission currently in flight?
	 *
	 * @return bool
	 */
	public function in_flight() {
		return null !== $this->active;
	}

	/**
	 * wp_mail observer — pure passthrough, records evidence only.
	 *
	 * @param array $args wp_mail args.
	 * @return array Unchanged.
	 */
	public function observe_mail( $args ) {
		if ( null !== $this->active && is_array( $args ) ) {
			$to = isset( $args['to'] ) ? $args['to'] : '';
			if ( is_array( $to ) ) {
				$to = implode( ',', $to );
			}
			$this->active['mail'][] = array(
				'to'      => self::mask_email_list( (string) $to ),
				'subject' => substr( isset( $args['subject'] ) ? (string) $args['subject'] : '', 0, 40 ),
			);
		}
		return $args;
	}

	/**
	 * wp_mail_failed observer.
	 *
	 * @param WP_Error $error Error.
	 */
	public function observe_failure( $error ) {
		if ( null === $this->active || ! is_wp_error( $error ) ) {
			return;
		}
		$this->active['failures'][] = substr( $error->get_error_message(), 0, 120 );
	}

	/**
	 * Finalize the in-flight submission into a submission event (and a
	 * notification_failed event when the transport or the form plugin said so).
	 */
	public function finalize() {
		if ( null === $this->active ) {
			return;
		}
		$sub          = $this->active;
		$this->active = null;

		if ( empty( $sub['confirmed'] ) ) {
			return; // Validation aborted — never a real submission.
		}

		$events = $this->plugin->get( 'events' );
		$ref    = Ffrd_Registry::key( $sub['source'], $sub['form_id'] );

		$plugin_status  = isset( $sub['meta']['plugin_status'] ) ? $sub['meta']['plugin_status'] : '';
		$sent_confirmed = ( 'mail_sent' === $plugin_status );
		$plugin_failed  = ( 'mail_failed' === $plugin_status );

		$event_id = $events->insert(
			'submission',
			$ref,
			'info',
			array(
				'plugin'         => $sub['source'],
				'form_id'        => $sub['form_id'],
				'title'          => $sub['title'],
				'expects'        => $sub['expects'],
				'attempts'       => count( $sub['mail'] ),
				'mail'           => array_slice( $sub['mail'], 0, 5 ),
				'failures'       => $sub['failures'],
				'sent_confirmed' => $sent_confirmed,
				'meta'           => $sub['meta'],
			)
		);

		$this->plugin->get( 'registry' )->touch( $sub['source'], $sub['form_id'], $sub['title'] );

		if ( $sub['failures'] || $plugin_failed ) {
			$events->insert(
				'notification_failed',
				$ref,
				'critical',
				array(
					'title'      => $sub['title'],
					'submission' => $event_id,
					'errors'     => $sub['failures'] ? $sub['failures'] : array( 'the form plugin reported its notification email failed' ),
				)
			);
		}
	}

	/**
	 * Mask every address in a comma-separated list (a***@domain).
	 *
	 * @param string $list Address list.
	 * @return string
	 */
	public static function mask_email_list( $list ) {
		$out = array();
		foreach ( explode( ',', $list ) as $addr ) {
			$addr = trim( $addr );
			if ( ! is_email( $addr ) ) {
				continue;
			}
			list( $user, $domain ) = explode( '@', $addr, 2 );
			$out[]                 = substr( $user, 0, 1 ) . '***@' . $domain;
		}
		return implode( ', ', $out );
	}
}
