<?php
/**
 * WPForms integration.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

use SUBIN\Leads\LeadCaptureService;
use SUBIN\Leads\LeadFieldRepository;
use SUBIN\Leads\LeadRepository;
use SUBIN\MailLog\MailEventRepository;
use SUBIN\AutoReplies\AutoReplyRuleSender;

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

/**
 * Captures successful WPForms submissions as leads.
 */
class WPFormsIntegration {
	/**
	 * Settings repository.
	 *
	 * @var \SUBIN_SettingsRepository
	 */
	private $settings;

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

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

	/**
	 * Mail event repository.
	 *
	 * @var MailEventRepository
	 */
	private $mail_events;

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

	/**
	 * Auto-reply rule sender.
	 *
	 * @var AutoReplyRuleSender|null
	 */
	private $auto_replies;

	/**
	 * Transactional lead capture.
	 *
	 * @var LeadCaptureService
	 */
	private $lead_capture;

	/**
	 * Timestamp captured before WPForms sends notifications.
	 *
	 * @var string
	 */
	private $submission_started_at = '';

	/**
	 * Correlation id created before WPForms sends notifications.
	 *
	 * @var string
	 */
	private $submission_correlation_id = '';

	/**
	 * Constructor.
	 *
	 * @param \SUBIN_SettingsRepository $settings Settings repository.
	 * @param LeadRepository          $leads Lead repository.
	 * @param LeadFieldRepository     $fields Lead field repository.
	 * @param MailEventRepository     $mail_events Mail event repository.
	 * @param FormSubmissionContext   $context Submission context.
	 * @param AutoReplyRuleSender|null $auto_replies Auto-reply sender.
	 * @param LeadCaptureService|null $lead_capture Transactional lead capture.
	 */
	public function __construct(
		\SUBIN_SettingsRepository $settings,
		LeadRepository $leads,
		LeadFieldRepository $fields,
		MailEventRepository $mail_events,
		FormSubmissionContext $context,
		?AutoReplyRuleSender $auto_replies = null,
		?LeadCaptureService $lead_capture = null
	) {
		$this->settings    = $settings;
		$this->leads       = $leads;
		$this->fields      = $fields;
		$this->mail_events = $mail_events;
		$this->context     = $context;
		$this->auto_replies = $auto_replies;
		$this->lead_capture = $lead_capture ? $lead_capture : new LeadCaptureService( $leads, $fields );
	}

	/**
	 * Register WPForms hooks only when WPForms is available.
	 *
	 * @return void
	 */
	public function register() {
		if ( ! self::is_available() ) {
			return;
		}

		add_action( 'wpforms_process', array( $this, 'prepare_mail_context' ), 5, 3 );
		add_action( 'wpforms_process_complete', array( $this, 'capture_submission' ), 20, 4 );
	}

	/**
	 * Detect WPForms availability.
	 *
	 * @return bool
	 */
	public static function is_available() {
		return function_exists( 'wpforms' ) || class_exists( 'WPForms' );
	}

	/**
	 * Whether WPForms compatibility is enabled.
	 *
	 * @param array<string,mixed>|null $settings Optional settings.
	 * @return bool
	 */
	private function is_enabled( $settings = null ) {
		$settings = is_array( $settings ) ? $settings : $this->settings->all();

		return ! empty( $settings['enable_wpforms_integration'] );
	}

	/**
	 * Set source context before WPForms sends notification emails.
	 *
	 * @return void
	 */
	public function prepare_mail_context( $fields = array(), $entry = array(), $form_data = array() ) {
		unset( $fields, $entry, $form_data );

		$settings = $this->settings->all();
		if ( empty( $settings['store_leads'] ) || ! self::is_available() ) {
			return;
		}

		if ( ! $this->is_enabled( $settings ) ) {
			$this->context->suppress_mail_logging();
			add_action( 'shutdown', array( $this->context, 'clear' ), PHP_INT_MAX );
			return;
		}

		$this->submission_started_at = current_time( 'mysql' );
		$this->context->set_current_source( 'wpforms', true );
		$this->submission_correlation_id = $this->context->get_correlation_id();
	}

	/**
	 * Capture a successful WPForms submission after processing completes.
	 *
	 * @param array<int|string,mixed> $fields WPForms fields.
	 * @param array<string,mixed>     $entry Entry data.
	 * @param array<string,mixed>     $form_data Form data.
	 * @param int|string              $entry_id Entry id.
	 * @return void
	 */
	public function capture_submission( $fields, $entry, $form_data, $entry_id ) {
		$settings       = $this->settings->all();
		$correlation_id = $this->submission_correlation_id;
		$this->submission_correlation_id = '';

		if ( empty( $settings['store_leads'] ) || ! self::is_available() || ! is_array( $fields ) ) {
			$this->context->clear();
			return;
		}

		if ( ! $this->is_enabled( $settings ) ) {
			add_action( 'shutdown', array( $this->context, 'clear' ), PHP_INT_MAX );
			return;
		}

		$saved_fields = $this->prepare_fields( $fields, $entry_id, ! empty( $settings['mask_sensitive_fields'] ) );
		$detected     = $this->detect_lead_values( $saved_fields );
		$form_id      = $this->get_form_id( $form_data );
		$capture_result = $this->lead_capture->capture(
			array(
				'provider'        => 'wpforms',
				'form_id'         => $form_id,
				'form_name'       => $this->get_form_title( $form_data ),
				'source_url'      => $this->get_source_url( $entry ),
				'referrer'        => $this->get_referrer(),
				'customer_name'   => $detected['customer_name'],
				'customer_email'  => $detected['customer_email'],
				'customer_phone'  => $detected['customer_phone'],
				'message_preview' => $detected['message_preview'],
				'submitted_at'    => current_time( 'mysql' ),
				'expires_at'      => $this->get_expires_at( $settings ),
			),
			$saved_fields
		);

		if ( is_wp_error( $capture_result ) ) {
			$this->context->clear();
			return;
		}

		$lead_id = absint( $capture_result );
		$this->mail_events->attach_recent_unlinked_events(
			$lead_id,
			'wpforms',
			$this->get_event_link_window(),
			array(),
			$correlation_id
		);

		if ( $this->mail_events->count_by_lead_id( $lead_id ) <= 0 ) {
			$this->leads->update_email_status( $lead_id, LeadRepository::EMAIL_STATUS_UNKNOWN );
		} else {
			$this->update_lead_email_status( $lead_id );
		}

		if ( $this->auto_replies ) {
			$this->auto_replies->send_for_lead(
				array(
					'id'             => $lead_id,
					'provider'       => 'wpforms',
					'form_id'        => $form_id,
					'form_name'      => $this->get_form_title( $form_data ),
					'customer_email' => $detected['customer_email'],
				),
				$saved_fields
			);
		}

		$this->context->clear();
	}

	/**
	 * Prepare fields for storage.
	 *
	 * @param array<int|string,mixed> $wpforms_fields WPForms fields.
	 * @param int|string              $entry_id Entry id.
	 * @param bool                    $mask_sensitive_fields Whether to mask questionable fields.
	 * @return array<int,array<string,mixed>>
	 */
	private function prepare_fields( array $wpforms_fields, $entry_id, $mask_sensitive_fields ) {
		$fields = array();

		foreach ( $wpforms_fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$field_key = $this->field_key( $field );
			$type      = sanitize_key( $field['type'] ?? '' );
			$label     = sanitize_text_field( $field['name'] ?? $field['label'] ?? $this->fields->label_from_key( $field_key ) );
			if ( '' !== $type && 0 === strpos( $field_key, 'wpforms_field_' ) ) {
				$field_key .= '_' . $type;
			}

			if ( '' === $field_key || $this->should_skip_field( $field_key, $type, $label ) ) {
				continue;
			}

			$is_sensitive = $this->is_questionable_sensitive_field( $field_key, $label, $type );
			$value        = $this->normalize_field_value( $field['value'] ?? '' );

			if ( '' === $value ) {
				continue;
			}

			if ( $is_sensitive && $mask_sensitive_fields ) {
				$value = '[masked]';
			}

			$fields[] = array(
				'field_key'    => $field_key,
				'field_label'  => $label,
				'field_value'  => $value,
				'is_sensitive' => $is_sensitive ? 1 : 0,
				'field_type'   => $type,
			);
		}

		$entry_id = absint( $entry_id );
		if ( $entry_id > 0 ) {
			$fields[] = array(
				'field_key'    => '_wpforms_entry_id',
				'field_label'  => __( 'WPForms entry ID', 'submission-inbox' ),
				'field_value'  => (string) $entry_id,
				'is_sensitive' => 1,
				'field_type'   => 'metadata',
			);
		}

		return $fields;
	}

	/**
	 * Build a stable field key.
	 *
	 * @param array<string,mixed> $field Field data.
	 * @return string
	 */
	private function field_key( array $field ) {
		$id = isset( $field['id'] ) ? absint( $field['id'] ) : 0;
		if ( $id > 0 ) {
			return 'wpforms_field_' . $id;
		}

		$key = sanitize_key( $field['key'] ?? $field['name'] ?? $field['label'] ?? '' );
		return '' !== $key ? $key : '';
	}

	/**
	 * Determine whether a field must not be stored.
	 *
	 * @param string $key Field key.
	 * @param string $type Field type.
	 * @param string $label Field label.
	 * @return bool
	 */
	private function should_skip_field( $key, $type, $label ) {
		$text = strtolower( $key . ' ' . $type . ' ' . $label );

		foreach ( array( 'captcha', 'recaptcha', 'hcaptcha', 'h-captcha', 'turnstile', 'honeypot', 'nonce', 'security', 'csrf', 'token', 'password', 'credit', 'card', 'payment', 'stripe', 'paypal', 'file-upload', 'file_upload', 'signature', 'html', 'divider', 'pagebreak' ) as $needle ) {
			if ( false !== strpos( $text, $needle ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Detect questionable sensitive fields that may be masked.
	 *
	 * @param string $key Field key.
	 * @param string $label Field label.
	 * @param string $type Field type.
	 * @return bool
	 */
	private function is_questionable_sensitive_field( $key, $label, $type ) {
		$text = strtolower( $key . ' ' . $label . ' ' . $type );

		foreach ( array( 'secret', 'private', 'sensitive' ) as $needle ) {
			if ( false !== strpos( $text, $needle ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize a WPForms field value.
	 *
	 * @param mixed $value Field value.
	 * @return string
	 */
	private function normalize_field_value( $value ) {
		if ( is_object( $value ) ) {
			return '';
		}

		if ( is_array( $value ) ) {
			$flat = array();
			array_walk_recursive(
				$value,
				static function ( $item ) use ( &$flat ) {
					if ( is_scalar( $item ) ) {
						$flat[] = sanitize_text_field( wp_unslash( (string) $item ) );
					}
				}
			);
			$value = implode( ', ', array_filter( $flat, 'strlen' ) );
		} else {
			$value = sanitize_textarea_field( wp_unslash( (string) $value ) );
		}

		return trim( wp_html_excerpt( (string) $value, 2000, '...' ) );
	}

	/**
	 * Detect lead summary values.
	 *
	 * @param array<int,array<string,mixed>> $fields Saved fields.
	 * @return array{customer_name:string,customer_email:string,customer_phone:string,message_preview:string}
	 */
	private function detect_lead_values( array $fields ) {
		return array(
			'customer_name'   => $this->detect_by_type_or_label( $fields, array( 'name' ), array( 'name', 'full name', 'first name' ) ),
			'customer_email'  => $this->detect_email( $fields ),
			'customer_phone'  => $this->detect_by_type_or_label( $fields, array( 'phone' ), array( 'phone', 'mobile', 'tel' ) ),
			'message_preview' => $this->detect_message_preview( $fields ),
		);
	}

	/**
	 * Detect first valid email.
	 *
	 * @param array<int,array<string,mixed>> $fields Saved fields.
	 * @return string
	 */
	private function detect_email( array $fields ) {
		$email = $this->detect_by_type_or_label( $fields, array( 'email' ), array( 'email', 'e-mail' ) );
		if ( is_email( $email ) ) {
			return $email;
		}

		foreach ( $fields as $field ) {
			$value = (string) ( $field['field_value'] ?? '' );
			if ( is_email( $value ) ) {
				return $value;
			}
		}

		return '';
	}

	/**
	 * Detect a value by field type or label fragments.
	 *
	 * @param array<int,array<string,mixed>> $fields Saved fields.
	 * @param array<int,string>             $types Preferred field types.
	 * @param array<int,string>             $label_needles Label fragments.
	 * @return string
	 */
	private function detect_by_type_or_label( array $fields, array $types, array $label_needles ) {
		foreach ( $fields as $field ) {
			if ( in_array( sanitize_key( $field['field_type'] ?? '' ), $types, true ) ) {
				return (string) $field['field_value'];
			}
		}

		foreach ( $fields as $field ) {
			$haystack = strtolower( (string) ( $field['field_label'] ?? '' ) . ' ' . (string) ( $field['field_key'] ?? '' ) );
			foreach ( $label_needles as $needle ) {
				if ( false !== strpos( $haystack, $needle ) ) {
					return (string) $field['field_value'];
				}
			}
		}

		return '';
	}

	/**
	 * Detect message preview.
	 *
	 * @param array<int,array<string,mixed>> $fields Saved fields.
	 * @return string
	 */
	private function detect_message_preview( array $fields ) {
		foreach ( $fields as $field ) {
			$type     = sanitize_key( $field['field_type'] ?? '' );
			$haystack = strtolower( (string) ( $field['field_label'] ?? '' ) . ' ' . (string) ( $field['field_key'] ?? '' ) );
			if ( 'textarea' === $type || false !== strpos( $haystack, 'message' ) || false !== strpos( $haystack, 'comment' ) || false !== strpos( $haystack, 'details' ) ) {
				return wp_trim_words( (string) $field['field_value'], 35, '...' );
			}
		}

		foreach ( $fields as $field ) {
			$value = (string) $field['field_value'];
			if ( strlen( $value ) > 40 ) {
				return wp_trim_words( $value, 35, '...' );
			}
		}

		return '';
	}

	/**
	 * Update lead email status from linked events.
	 *
	 * @param int $lead_id Lead id.
	 * @return void
	 */
	private function update_lead_email_status( $lead_id ) {
		$events   = $this->mail_events->get_by_lead_id( $lead_id );
		$has_sent = false;

		foreach ( $events as $event ) {
			$status = sanitize_key( $event->status ?? '' );
			if ( MailEventRepository::STATUS_FAILED === $status ) {
				$this->leads->update_email_status( $lead_id, LeadRepository::EMAIL_STATUS_FAILED );
				return;
			}
			if ( MailEventRepository::STATUS_SENT === $status ) {
				$has_sent = true;
			}
		}

		$this->leads->update_email_status( $lead_id, $has_sent ? LeadRepository::EMAIL_STATUS_SENT : LeadRepository::EMAIL_STATUS_UNKNOWN );
	}

	/**
	 * Get form id.
	 *
	 * @param array<string,mixed> $form_data Form data.
	 * @return string
	 */
	private function get_form_id( array $form_data ) {
		return ! empty( $form_data['id'] ) ? (string) absint( $form_data['id'] ) : '';
	}

	/**
	 * Get form title.
	 *
	 * @param array<string,mixed> $form_data Form data.
	 * @return string
	 */
	private function get_form_title( array $form_data ) {
		$title = $form_data['settings']['form_title'] ?? $form_data['title'] ?? '';
		$title = sanitize_text_field( $title );

		return '' !== $title ? $title : __( 'WPForms form', 'submission-inbox' );
	}

	/**
	 * Get source URL.
	 *
	 * @param array<string,mixed> $entry Entry data.
	 * @return string
	 */
	private function get_source_url( array $entry ) {
		foreach ( array( 'page_url', 'source_url', 'referer', 'referrer' ) as $key ) {
			if ( ! empty( $entry[ $key ] ) ) {
				return esc_url_raw( $entry[ $key ] );
			}
		}

		return $this->current_url();
	}

	/**
	 * Get referrer.
	 *
	 * @return string
	 */
	private function get_referrer() {
		$referrer = wp_get_referer();

		return $referrer ? esc_url_raw( $referrer ) : '';
	}

	/**
	 * Current URL fallback.
	 *
	 * @return string
	 */
	private function current_url() {
		$host = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) );
		$uri  = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );

		if ( '' === $host ) {
			return '';
		}

		$scheme = is_ssl() ? 'https://' : 'http://';
		return esc_url_raw( $scheme . $host . $uri );
	}

	/**
	 * Calculate expiration date.
	 *
	 * @param array<string,mixed> $settings Settings.
	 * @return string|null
	 */
	private function get_expires_at( array $settings ) {
		$retention_days = absint( $settings['retention_days'] ?? 0 );
		if ( $retention_days <= 0 ) {
			return null;
		}

		return gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) + ( DAY_IN_SECONDS * $retention_days ) );
	}

	/**
	 * Get the safest lower bound for linking WPForms mail events.
	 *
	 * @return string
	 */
	private function get_event_link_window() {
		if ( '' !== $this->submission_started_at ) {
			return $this->submission_started_at;
		}

		return gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - ( 5 * MINUTE_IN_SECONDS ) );
	}
}
