<?php
/**
 * Contact Form 7 integration.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

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

/**
 * Captures Contact Form 7 submissions as leads.
 */
class ContactForm7Integration {
	/**
	 * 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;

	/**
	 * Pending lead payload for finalize hook.
	 *
	 * @var array<string,mixed>
	 */
	private $pending_lead = array();

	/**
	 * Pending fields for finalize hook.
	 *
	 * @var array<int,array>
	 */
	private $pending_fields = array();

	/**
	 * 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.
	 */
	public function __construct(
		\SUBIN_SettingsRepository $settings,
		LeadRepository $leads,
		LeadFieldRepository $fields,
		MailEventRepository $mail_events,
		FormSubmissionContext $context,
		?AutoReplyRuleSender $auto_replies = null
	) {
		$this->settings    = $settings;
		$this->leads       = $leads;
		$this->fields      = $fields;
		$this->mail_events = $mail_events;
		$this->context     = $context;
		$this->auto_replies = $auto_replies;
	}

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

		add_action( 'wpcf7_before_send_mail', array( $this, 'capture_submission' ), 5, 3 );
		add_action( 'wpcf7_submit', array( $this, 'finalize_submission' ), 20, 2 );
	}

	/**
	 * Detect Contact Form 7 availability.
	 *
	 * @return bool
	 */
	public static function is_available() {
		return class_exists( 'WPCF7_ContactForm' ) && class_exists( 'WPCF7_Submission' );
	}

	/**
	 * Whether Contact Form 7 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_cf7_integration'] );
	}

	/**
	 * Capture a CF7 submission before mail is sent.
	 *
	 * @param mixed $contact_form Contact form object.
	 * @return void
	 */
	public function capture_submission( $contact_form ) {
		$settings = $this->settings->all();

		if ( empty( $settings['store_leads'] ) || ! self::is_available() ) {
			return;
		}

		if ( ! $this->is_enabled( $settings ) ) {
			$this->context->suppress_mail_logging();
			return;
		}

		$submission = \WPCF7_Submission::get_instance();
		if ( ! $submission || ! method_exists( $submission, 'get_posted_data' ) ) {
			return;
		}

		if ( $this->submission_should_be_skipped( $submission ) ) {
			return;
		}

		$posted_data = $submission->get_posted_data();
		if ( ! is_array( $posted_data ) ) {
			return;
		}

		$saved_fields = $this->prepare_fields( $posted_data, ! empty( $settings['mask_sensitive_fields'] ) );
		$lead_data = array(
				'provider'        => 'contact_form_7',
				'form_id'         => $this->get_form_id( $contact_form ),
				'form_name'       => $this->get_form_title( $contact_form ),
				'source_url'      => $this->get_source_url( $submission ),
				'referrer'        => $this->get_referrer(),
				'customer_name'   => $this->detect_value( $saved_fields, array( 'full-name', 'your-name', 'first-name', 'name' ), 'name' ),
				'customer_email'  => $this->detect_email( $saved_fields ),
				'customer_phone'  => $this->detect_value( $saved_fields, array( 'phone', 'tel', 'mobile' ), 'phone' ),
				'message_preview' => $this->detect_message_preview( $saved_fields ),
				'submitted_at'    => current_time( 'mysql' ),
				'expires_at'      => $this->get_expires_at( $settings ),
			);
		$lead_id      = $this->leads->create( $lead_data );

		if ( $lead_id <= 0 ) {
			return;
		}

		$this->fields->insert_fields( $lead_id, $saved_fields );
		$lead_data['id']       = $lead_id;
		$this->pending_lead    = $lead_data;
		$this->pending_fields  = $saved_fields;
		$this->context->set_current_lead_id( $lead_id );
		$this->context->set_current_source( 'contact_form_7' );
	}

	/**
	 * Finalize lead status after CF7 processing.
	 *
	 * @return void
	 */
	public function finalize_submission() {
		$lead_id = $this->context->get_current_lead_id();

		if ( ! $lead_id ) {
			$this->context->clear();
			return;
		}

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

		if ( $this->auto_replies && ! empty( $this->pending_lead ) ) {
			$this->auto_replies->send_for_lead( $this->pending_lead, $this->pending_fields );
		}

		$this->pending_lead   = array();
		$this->pending_fields = array();
		$this->context->clear();
	}

	/**
	 * Prepare fields for storage.
	 *
	 * @param array<string,mixed> $posted_data Posted data.
	 * @param bool               $mask_sensitive_fields Whether to mask questionable fields.
	 * @return array<int,array<string,mixed>>
	 */
	private function prepare_fields( array $posted_data, $mask_sensitive_fields ) {
		$fields = array();

		foreach ( $posted_data as $key => $value ) {
			$key = sanitize_key( $key );

			if ( '' === $key || $this->should_skip_field( $key ) ) {
				continue;
			}

			$is_sensitive = $this->is_questionable_sensitive_field( $key );
			$value        = $this->normalize_field_value( $value );

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

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

			$fields[] = array(
				'field_key'    => $key,
				'field_label'  => $this->fields->label_from_key( $key ),
				'field_value'  => $value,
				'is_sensitive' => $is_sensitive ? 1 : 0,
			);
		}

		return $fields;
	}

	/**
	 * Determine whether a field must not be stored.
	 *
	 * @param string $key Field key.
	 * @return bool
	 */
	private function should_skip_field( $key ) {
		$blocked_keys = array(
			'_wpcf7',
			'_wpcf7_version',
			'_wpcf7_locale',
			'_wpcf7_unit_tag',
			'_wpcf7_container_post',
			'_wpnonce',
			'g-recaptcha-response',
			'recaptcha',
			'captcha',
			'h-captcha-response',
			'cf-turnstile-response',
			'password',
			'pass',
			'pwd',
			'card',
			'credit_card',
			'cc',
			'cvv',
			'token',
			'secret',
			'api_key',
			'access_key',
		);

		if ( in_array( $key, $blocked_keys, true ) ) {
			return true;
		}

		foreach ( array( 'password', 'pass', 'pwd', 'captcha', 'recaptcha', 'h-captcha', 'turnstile', 'token', 'nonce', 'card', 'credit_card', 'cc', 'cvv', 'secret', 'api_key', 'access_key' ) as $needle ) {
			if ( false !== strpos( $key, $needle ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Detect questionable sensitive fields that may be masked.
	 *
	 * @param string $key Field key.
	 * @return bool
	 */
	private function is_questionable_sensitive_field( $key ) {
		foreach ( array( 'secret', 'private', 'sensitive' ) as $needle ) {
			if ( false !== strpos( $key, $needle ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize posted 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,
				function ( $item ) use ( &$flat ) {
					if ( is_scalar( $item ) ) {
						$flat[] = sanitize_text_field( wp_unslash( (string) $item ) );
					}
				}
			);
			$value = implode( ', ', $flat );
		} else {
			$value = sanitize_textarea_field( wp_unslash( (string) $value ) );
		}

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

	/**
	 * Skip spam/invalid submissions when CF7 exposes a status.
	 *
	 * @param mixed $submission CF7 submission.
	 * @return bool
	 */
	private function submission_should_be_skipped( $submission ) {
		if ( is_object( $submission ) && method_exists( $submission, 'is_spam' ) && $submission->is_spam() ) {
			return true;
		}

		if ( is_object( $submission ) && method_exists( $submission, 'get_status' ) ) {
			$status = sanitize_key( (string) $submission->get_status() );
			if ( in_array( $status, array( 'spam', 'validation_failed', 'acceptance_missing', 'aborted' ), true ) ) {
				return true;
			}
		}

		return false;
	}

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

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

		return '';
	}

	/**
	 * Detect value by key heuristics.
	 *
	 * @param array<int,array<string,mixed>> $fields Saved fields.
	 * @param array<int,string>             $preferred_keys Preferred keys.
	 * @param string                        $fallback_contains Fallback key fragment.
	 * @return string
	 */
	private function detect_value( array $fields, array $preferred_keys, $fallback_contains ) {
		foreach ( $preferred_keys as $preferred_key ) {
			foreach ( $fields as $field ) {
				if ( $preferred_key === (string) $field['field_key'] ) {
					return (string) $field['field_value'];
				}
			}
		}

		foreach ( $fields as $field ) {
			if ( false !== strpos( (string) $field['field_key'], $fallback_contains ) ) {
				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 ( array( 'message', 'comments', 'enquiry', 'inquiry', 'details' ) as $needle ) {
			foreach ( $fields as $field ) {
				if ( false !== strpos( (string) $field['field_key'], $needle ) ) {
					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 '';
	}

	/**
	 * Get form id.
	 *
	 * @param mixed $contact_form Contact form.
	 * @return string
	 */
	private function get_form_id( $contact_form ) {
		if ( is_object( $contact_form ) && method_exists( $contact_form, 'id' ) ) {
			return (string) absint( $contact_form->id() );
		}

		return '';
	}

	/**
	 * Get form title.
	 *
	 * @param mixed $contact_form Contact form.
	 * @return string
	 */
	private function get_form_title( $contact_form ) {
		if ( is_object( $contact_form ) && method_exists( $contact_form, 'title' ) ) {
			return sanitize_text_field( $contact_form->title() );
		}

		return __( 'Contact Form 7 form', 'submission-inbox' );
	}

	/**
	 * Get source URL.
	 *
	 * @param mixed $submission CF7 submission.
	 * @return string
	 */
	private function get_source_url( $submission ) {
		if ( is_object( $submission ) && method_exists( $submission, 'get_meta' ) ) {
			$url = $submission->get_meta( 'url' );
			if ( $url ) {
				return esc_url_raw( $url );
			}
		}

		return '';
	}

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

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

	/**
	 * 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 ) );
	}
}
