<?php
/**
 * Fluent Forms 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 Fluent Forms submissions as leads.
 */
class FluentFormsIntegration {
	/**
	 * 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 while Fluent Forms is processing.
	 *
	 * @var string
	 */
	private $submission_started_at = '';

	/**
	 * Correlation id created before Fluent Forms 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 Fluent Forms hooks only when Fluent Forms is available.
	 *
	 * @return void
	 */
	public function register() {
		if ( ! self::is_available() ) {
			return;
		}

		add_action( 'fluentform/before_insert_submission', array( $this, 'prepare_mail_context' ), 5, 3 );
		add_action( 'fluentform/submission_inserted', array( $this, 'handle_submission_inserted' ), 10, 3 );
	}

	/**
	 * Detect Fluent Forms availability.
	 *
	 * @return bool
	 */
	public static function is_available() {
		return defined( 'FLUENTFORM' ) || function_exists( 'wpFluentForm' ) || class_exists( 'FluentForm\App\Modules\Form\Form' );
	}

	/**
	 * Whether Fluent Forms 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_fluent_forms_integration'] );
	}

	/**
	 * Set source context before Fluent Forms may send notification emails.
	 *
	 * @return void
	 */
	public function prepare_mail_context( $form_data = array(), $insert_data = array(), $form = null ) {
		unset( $form_data, $insert_data, $form );

		$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( 'fluent_forms', true );
		$this->submission_correlation_id = $this->context->get_correlation_id();
	}

	/**
	 * Capture a successful Fluent Forms submission after Fluent Forms stores it.
	 *
	 * @param int|string          $submission_id Fluent Forms submission id.
	 * @param array<string,mixed> $form_data Submitted form data.
	 * @param mixed               $form Fluent Forms form object/array.
	 * @return void
	 */
	public function handle_submission_inserted( $submission_id, $form_data, $form ) {
		$settings       = $this->settings->all();
		$correlation_id = $this->submission_correlation_id;
		$this->submission_correlation_id = '';
		$submission_id = absint( $submission_id );

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

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

		$existing = $this->leads->find_by_provider_external_entry_id( 'fluent_forms', (string) $submission_id, '_fluent_forms_submission_id' );
		if ( $existing ) {
			$existing_lead_id = absint( $existing->id ?? 0 );
			if ( $existing_lead_id > 0 ) {
				$this->context->set_current_lead_id( $existing_lead_id );
				$this->context->set_current_source( 'fluent_forms' );
				$this->mail_events->attach_recent_unlinked_events(
					$existing_lead_id,
					'fluent_forms',
					$this->get_event_link_window(),
					array(),
					$correlation_id
				);
				$this->update_lead_email_status( $existing_lead_id );
				add_action( 'shutdown', array( $this->context, 'clear' ), PHP_INT_MAX );
			} else {
				$this->context->clear();
			}
			return;
		}

		$field_labels = $this->field_labels_from_form( $form );
		$saved_fields = $this->prepare_fields( $form_data, $field_labels, $submission_id, ! empty( $settings['mask_sensitive_fields'] ) );
		$detected     = $this->detect_lead_values( $saved_fields );
		$capture_result = $this->lead_capture->capture(
			array(
				'provider'        => 'fluent_forms',
				'form_id'         => $this->get_form_id( $form ),
				'form_name'       => $this->get_form_title( $form ),
				'source_url'      => $this->get_source_url( $form_data ),
				'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->context->set_current_lead_id( $lead_id );
		$this->context->set_current_source( 'fluent_forms' );

		$this->mail_events->attach_recent_unlinked_events(
			$lead_id,
			'fluent_forms',
			$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'       => 'fluent_forms',
					'form_id'        => $this->get_form_id( $form ),
					'form_name'      => $this->get_form_title( $form ),
					'customer_email' => $detected['customer_email'],
				),
				$saved_fields
			);
		}

		add_action( 'shutdown', array( $this->context, 'clear' ), PHP_INT_MAX );
	}

	/**
	 * Prepare submitted Fluent Forms fields for storage.
	 *
	 * @param array<string,mixed>  $form_data Submitted data.
	 * @param array<string,string> $field_labels Field labels keyed by field name.
	 * @param int                  $submission_id Submission id.
	 * @param bool                 $mask_sensitive_fields Whether to mask questionable fields.
	 * @return array<int,array<string,mixed>>
	 */
	private function prepare_fields( array $form_data, array $field_labels, $submission_id, $mask_sensitive_fields ) {
		$fields = array();

		foreach ( $form_data as $key => $value ) {
			$field_key = sanitize_key( $key );
			$label     = sanitize_text_field( $field_labels[ $field_key ] ?? $this->fields->label_from_key( $field_key ) );
			$type      = $this->field_type_from_key_label_value( $field_key, $label, $value );

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

			$is_sensitive = $this->is_questionable_sensitive_field( $field_key, $label, $type );
			$value        = $this->normalize_field_value( $value, $type );

			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,
			);
		}

		$fields[] = array(
			'field_key'    => '_fluent_forms_submission_id',
			'field_label'  => __( 'Fluent Forms submission ID', 'submission-inbox' ),
			'field_value'  => (string) absint( $submission_id ),
			'is_sensitive' => 1,
			'field_type'   => 'metadata',
		);

		return $fields;
	}

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

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

		if ( in_array( sanitize_key( $key ), array( 'source_url', 'page_url', 'referer', 'referrer', '_wp_http_referer' ), true ) ) {
			return true;
		}

		if ( is_array( $value ) ) {
			foreach ( array( 'tmp_name', 'error', 'size' ) as $file_key ) {
				if ( array_key_exists( $file_key, $value ) ) {
					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 Fluent Forms field value.
	 *
	 * @param mixed  $value Field value.
	 * @param string $type Field type.
	 * @return string
	 */
	private function normalize_field_value( $value, $type = '' ) {
		if ( is_object( $value ) ) {
			return '';
		}

		if ( is_array( $value ) ) {
			$flat = array();
			array_walk_recursive(
				$value,
				static function ( $item ) use ( &$flat ) {
					if ( is_scalar( $item ) ) {
						$item = sanitize_text_field( wp_unslash( (string) $item ) );
						if ( '' !== $item ) {
							$flat[] = $item;
						}
					}
				}
			);
			$value = implode( ', ', array_filter( $flat, 'strlen' ) );
		} elseif ( in_array( sanitize_key( $type ), array( 'textarea', 'message' ), true ) ) {
			$value = sanitize_textarea_field( wp_unslash( (string) $value ) );
		} else {
			$value = sanitize_text_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', 'tel' ), 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' ) || false !== strpos( $haystack, 'enquiry' ) || false !== strpos( $haystack, 'inquiry' ) ) {
				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 );
	}

	/**
	 * Infer a field type from key/label/value.
	 *
	 * @param string $key Field key.
	 * @param string $label Field label.
	 * @param mixed  $value Field value.
	 * @return string
	 */
	private function field_type_from_key_label_value( $key, $label, $value ) {
		$text = strtolower( $key . ' ' . $label );

		if ( false !== strpos( $text, 'email' ) || false !== strpos( $text, 'e-mail' ) ) {
			return 'email';
		}
		if ( false !== strpos( $text, 'phone' ) || false !== strpos( $text, 'mobile' ) || false !== strpos( $text, 'tel' ) ) {
			return 'phone';
		}
		if ( false !== strpos( $text, 'message' ) || false !== strpos( $text, 'comment' ) || false !== strpos( $text, 'details' ) ) {
			return 'textarea';
		}
		if ( false !== strpos( $text, 'url' ) || false !== strpos( $text, 'website' ) ) {
			return 'url';
		}
		if ( false !== strpos( $text, 'date' ) || false !== strpos( $text, 'time' ) ) {
			return 'date';
		}
		if ( false !== strpos( $text, 'name' ) ) {
			return 'name';
		}
		if ( is_array( $value ) ) {
			return 'checkbox';
		}
		if ( is_numeric( $value ) ) {
			return 'number';
		}

		return 'text';
	}

	/**
	 * Build field labels from Fluent Forms form metadata when available.
	 *
	 * @param mixed $form Fluent Forms form object/array.
	 * @return array<string,string>
	 */
	private function field_labels_from_form( $form ) {
		$labels = array();
		$fields = $this->form_fields_from_form( $form );

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

			$key = sanitize_key( $field['attributes']['name'] ?? $field['name'] ?? $field['element'] ?? '' );
			$label = sanitize_text_field( $field['settings']['label'] ?? $field['label'] ?? $field['admin_label'] ?? '' );

			if ( '' !== $key && '' !== $label ) {
				$labels[ $key ] = $label;
			}
		}

		return $labels;
	}

	/**
	 * Extract nested form fields from common Fluent Forms structures.
	 *
	 * @param mixed $form Fluent Forms form object/array.
	 * @return array<int,mixed>
	 */
	private function form_fields_from_form( $form ) {
		$candidates = array();

		if ( is_array( $form ) ) {
			$candidates[] = $form['fields'] ?? null;
			$candidates[] = $form['form_fields'] ?? null;
			$candidates[] = $form['fields']['fields'] ?? null;
		} elseif ( is_object( $form ) ) {
			$candidates[] = $form->fields ?? null;
			$candidates[] = $form->form_fields ?? null;
		}

		$fields = array();
		foreach ( $candidates as $candidate ) {
			if ( is_string( $candidate ) ) {
				$decoded = json_decode( $candidate, true );
				$candidate = is_array( $decoded ) ? $decoded : array();
			}
			if ( is_array( $candidate ) ) {
				foreach ( array( $candidate['fields'] ?? null, $candidate['form_fields'] ?? null, $candidate['fields']['fields'] ?? null ) as $nested_candidate ) {
					if ( is_array( $nested_candidate ) ) {
						$this->flatten_form_fields( $nested_candidate, $fields );
					}
				}
				$this->flatten_form_fields( $candidate, $fields );
			}
		}

		return $fields;
	}

	/**
	 * Flatten nested Fluent Forms field metadata.
	 *
	 * @param array<int|string,mixed> $items Items.
	 * @param array<int,mixed>        $fields Output fields.
	 * @return void
	 */
	private function flatten_form_fields( array $items, array &$fields ) {
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			if ( isset( $item['attributes']['name'] ) || isset( $item['name'] ) ) {
				$fields[] = $item;
			}

			foreach ( array( 'fields', 'columns', 'children' ) as $child_key ) {
				if ( isset( $item[ $child_key ] ) && is_array( $item[ $child_key ] ) ) {
					$this->flatten_form_fields( $item[ $child_key ], $fields );
				}
			}
		}
	}

	/**
	 * Get form id.
	 *
	 * @param mixed $form Fluent Forms form object/array.
	 * @return string
	 */
	private function get_form_id( $form ) {
		if ( is_array( $form ) ) {
			return ! empty( $form['id'] ) ? (string) absint( $form['id'] ) : '';
		}

		if ( is_object( $form ) && isset( $form->id ) ) {
			return (string) absint( $form->id );
		}

		return '';
	}

	/**
	 * Get form title.
	 *
	 * @param mixed $form Fluent Forms form object/array.
	 * @return string
	 */
	private function get_form_title( $form ) {
		$title = '';
		if ( is_array( $form ) ) {
			$title = $form['title'] ?? $form['name'] ?? '';
		} elseif ( is_object( $form ) ) {
			$title = $form->title ?? $form->name ?? '';
		}

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

	/**
	 * Get source URL.
	 *
	 * @param array<string,mixed> $form_data Submitted data.
	 * @return string
	 */
	private function get_source_url( array $form_data ) {
		foreach ( array( 'source_url', 'page_url', 'referer', 'referrer', '_wp_http_referer' ) as $key ) {
			if ( ! empty( $form_data[ $key ] ) && is_scalar( $form_data[ $key ] ) ) {
				return esc_url_raw( (string) $form_data[ $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 Fluent Forms 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 ) );
	}
}
