<?php
/**
 * WordPress mail logger.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\MailLog;

use SUBIN\Forms\FormSubmissionContext;
use SUBIN\Leads\LeadRepository;
use SUBIN\Mail\LeadEmailStatusService;

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

/**
 * Logs wp_mail events and links them to current lead context.
 */
class MailLogger {
	/**
	 * Event repository.
	 *
	 * @var MailEventRepository
	 */
	private $events;

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

	/**
	 * Form submission context.
	 *
	 * @var FormSubmissionContext
	 */
	private $context;

	/**
	 * Settings repository.
	 *
	 * @var \SUBIN_SettingsRepository
	 */
	private $settings;

	/**
	 * Lead email status service.
	 *
	 * @var LeadEmailStatusService
	 */
	private $lead_email_status;

	/**
	 * Pending event ids for the current request.
	 *
	 * @var array<int,int>
	 */
	private $pending_event_ids = array();

	/**
	 * Constructor.
	 *
	 * @param MailEventRepository   $events Event repository.
	 * @param LeadRepository        $leads Lead repository.
	 * @param FormSubmissionContext   $context Submission context.
	 * @param \SUBIN_SettingsRepository $settings Settings repository.
	 */
	public function __construct(
		MailEventRepository $events,
		LeadRepository $leads,
		FormSubmissionContext $context,
		\SUBIN_SettingsRepository $settings
	) {
		$this->events   = $events;
		$this->leads    = $leads;
		$this->context  = $context;
		$this->settings = $settings;
		$this->lead_email_status = new LeadEmailStatusService( $leads, $events );
	}

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register() {
		// One wp_mail() call creates one pending event; success/failure hooks only update that queued event.
		add_filter( 'wp_mail', array( $this, 'log_pending' ) );
		add_action( 'wp_mail_succeeded', array( $this, 'mark_succeeded' ) );
		add_action( 'wp_mail_failed', array( $this, 'mark_failed' ) );
	}

	/**
	 * Create a pending event before WordPress sends mail.
	 *
	 * @param array<string,mixed> $mail_data Mail data.
	 * @return array<string,mixed>
	 */
	public function log_pending( $mail_data ) {
		if ( $this->context->should_suppress_mail_logging() ) {
			return $mail_data;
		}

		$event_id = $this->events->create_pending(
			array(
				'lead_id'        => $this->context->get_current_lead_id(),
				'source'         => $this->context->get_current_source() ? $this->context->get_current_source() : 'wp_mail',
				'to_email'       => $this->normalize_recipients( $mail_data['to'] ?? '' ),
				'from_email'     => $this->resolve_logged_from_email( $mail_data['headers'] ?? array() ),
				'subject'        => sanitize_text_field( $mail_data['subject'] ?? '' ),
				'message_hash'   => hash( 'sha256', (string) ( $mail_data['message'] ?? '' ) ),
			)
		);

		if ( $event_id > 0 ) {
			$this->pending_event_ids[] = $event_id;
		}

		return $mail_data;
	}

	/**
	 * Mark latest pending event as sent.
	 *
	 * @param array<string,mixed> $mail_data Mail data.
	 * @return void
	 */
	public function mark_succeeded( $mail_data ) {
		unset( $mail_data );

		$event = $this->shift_pending_event();
		if ( ! $event ) {
			return;
		}

		$this->events->mark_sent( $event->id );

		if ( ! empty( $event->lead_id ) ) {
			$this->update_lead_after_event( absint( $event->lead_id ) );
		}
	}

	/**
	 * Mark latest pending event as failed.
	 *
	 * @param \WP_Error $wp_error Mail error.
	 * @return void
	 */
	public function mark_failed( $wp_error ) {
		$event = $this->shift_pending_event();
		if ( ! $event ) {
			return;
		}

		$error_message = is_wp_error( $wp_error ) ? $this->sanitize_error_message( $wp_error->get_error_message() ) : '';

		$this->events->mark_failed( $event->id, $error_message );

		if ( ! empty( $event->lead_id ) ) {
			$this->leads->update_email_status( absint( $event->lead_id ), LeadRepository::EMAIL_STATUS_FAILED );
		}
	}

	/**
	 * Update lead email status based on related mail events.
	 *
	 * @param int $lead_id Lead id.
	 * @return void
	 */
	public function update_lead_after_event( $lead_id ) {
		$this->lead_email_status->recalculate( $lead_id );
	}

	/**
	 * Shift latest pending event.
	 *
	 * @return object|null
	 */
	private function shift_pending_event() {
		$event_id = array_shift( $this->pending_event_ids );

		if ( ! $event_id ) {
			return null;
		}

		return $this->events->get( $event_id );
	}

	/**
	 * Normalize recipients to a display string.
	 *
	 * @param mixed $recipients Raw recipients.
	 * @return string
	 */
	private function normalize_recipients( $recipients ) {
		if ( is_array( $recipients ) ) {
			$recipients = implode( ', ', array_map( 'sanitize_text_field', $recipients ) );
		}

		return sanitize_text_field( (string) $recipients );
	}

	/**
	 * Extract From email from wp_mail headers.
	 *
	 * @param mixed $headers Headers.
	 * @return string
	 */
	private function extract_from_email( $headers ) {
		if ( is_string( $headers ) ) {
			$headers = explode( "\n", $headers );
		}

		if ( ! is_array( $headers ) ) {
			return '';
		}

		foreach ( $headers as $header ) {
			$header = trim( (string) $header );
			if ( 0 !== stripos( $header, 'from:' ) ) {
				continue;
			}

			if ( preg_match( '/<([^>]+)>/', $header, $matches ) ) {
				return sanitize_email( $matches[1] );
			}

			return sanitize_email( trim( substr( $header, 5 ) ) );
		}

		return '';
	}

	/**
	 * Resolve the From email shown in the log.
	 *
	 * @param mixed $headers Mail headers.
	 * @return string
	 */
	private function resolve_logged_from_email( $headers ) {
		$from = $this->extract_from_email( $headers );
		if ( '' !== $from && is_email( $from ) ) {
			return $from;
		}

		$settings_from = sanitize_email( (string) ( $this->settings->all()['from_email'] ?? '' ) );
		if ( '' !== $settings_from && is_email( $settings_from ) ) {
			return $settings_from;
		}

		$admin_email = sanitize_email( (string) get_option( 'admin_email' ) );
		return is_email( $admin_email ) ? $admin_email : '';
	}

	/**
	 * Sanitize a mail error before saving it to the log.
	 *
	 * @param string $message Error message.
	 * @return string
	 */
	private function sanitize_error_message( $message ) {
		$settings = $this->settings->all();
		$message  = wp_strip_all_tags( (string) $message );

		foreach ( array( 'smtp_password', 'smtp_username' ) as $key ) {
			if ( ! empty( $settings[ $key ] ) ) {
				$message = str_replace( (string) $settings[ $key ], '[redacted]', $message );
			}
		}

		return sanitize_text_field( $message );
	}
}
