<?php
/**
 * MW WP Form → Contact Form 7 converter (pure conversion logic).
 *
 * Reads a MW WP Form post (CPT `mw-wp-form`, settings in post meta `mw-wp-form`)
 * and produces CF7 form template + mail properties + a migration report.
 *
 * @package CF7MasterPro\Migration
 */

namespace CF7MasterPro\Migration;

defined( 'ABSPATH' ) || exit;

/**
 * Converts one MW WP Form definition into CF7 properties.
 */
class MwConverter {

	const MW_CPT       = 'mw-wp-form';
	const MW_META      = 'mw-wp-form';
	const MW_DATA_CPT  = 'mwf_';
	const MW_DATA_META = '_mw-wp-form_data';

	/**
	 * Well-known Japanese field names → CF7-friendly names.
	 *
	 * @var array
	 */
	private static $name_dictionary = array(
		'お名前'          => 'your-name',
		'氏名'           => 'your-name',
		'名前'           => 'your-name',
		'メールアドレス'      => 'your-email',
		'メール'          => 'your-email',
		'電話番号'         => 'your-tel',
		'電話'           => 'your-tel',
		'件名'           => 'your-subject',
		'題名'           => 'your-subject',
		'お問い合わせ内容'     => 'your-message',
		'お問合せ内容'       => 'your-message',
		'本文'           => 'your-message',
		'メッセージ'        => 'your-message',
		'会社名'          => 'company',
		'貴社名'          => 'company',
		'郵便番号'         => 'zip',
		'住所'           => 'address',
		'ふりがな'         => 'furigana',
		'フリガナ'         => 'furigana',
		'種別'           => 'inquiry-type',
		'お問い合わせ種別'     => 'inquiry-type',
		'お問合せ種別'       => 'inquiry-type',
		'部署'           => 'department',
		'部署名'          => 'department',
		'役職'           => 'position',
		'性別'           => 'gender',
		'年齢'           => 'age',
		'生年月日'         => 'birthday',
		'ご住所'          => 'address',
		'都道府県'         => 'prefecture',
		'ホームページ'       => 'website',
		'ウェブサイト'       => 'website',
		'ご希望'          => 'request',
		'ご要望'          => 'request',
		'備考'           => 'note',
		'連絡方法'         => 'contact-method',
		'希望連絡方法'       => 'contact-method',
	);

	/**
	 * Field name rename map built during conversion (original => cf7 name).
	 *
	 * @var array
	 */
	private $renames = array();

	/**
	 * Report notes (manual follow-up items).
	 *
	 * @var array
	 */
	private $notes = array();

	/**
	 * Validation rules indexed by original field name.
	 *
	 * @var array
	 */
	private $validation = array();

	/**
	 * Counter for anonymous field names.
	 *
	 * @var int
	 */
	private $field_seq = 0;

	/**
	 * Converted / total field counters.
	 *
	 * @var int
	 */
	private $converted = 0;

	/**
	 * Total form field shortcodes found.
	 *
	 * @var int
	 */
	private $total = 0;

	/**
	 * Whether a submit tag was produced.
	 *
	 * @var bool
	 */
	private $has_submit = false;

	/**
	 * File field names (for mail attachments).
	 *
	 * @var array
	 */
	private $file_fields = array();

	/**
	 * Convert a MW WP Form post into CF7 properties.
	 *
	 * @param int $mw_post_id MW WP Form post ID.
	 * @return array|\WP_Error {title, form, mail, mail_2, notes, renames, total, converted, settings}
	 */
	public function convert( $mw_post_id ) {
		$post = get_post( $mw_post_id );
		if ( ! $post || self::MW_CPT !== $post->post_type ) {
			return new \WP_Error( 'cf7mp_mw_not_found', __( 'MW WP Form not found.', 'frontierline-forms' ) );
		}

		$this->renames     = array();
		$this->notes       = array();
		$this->field_seq   = 0;
		$this->converted   = 0;
		$this->total       = 0;
		$this->has_submit  = false;
		$this->file_fields = array();

		$settings         = get_post_meta( $mw_post_id, self::MW_META, true );
		$settings         = is_array( $settings ) ? $settings : array();
		$this->validation = $this->index_validation( $settings );

		$form = $this->convert_form_body( (string) $post->post_content );

		if ( ! $this->has_submit ) {
			$form .= "\n\n[submit \"" . __( 'Send', 'frontierline-forms' ) . "\"]";
		}

		return array(
			'title'     => $post->post_title,
			'form'      => $form,
			'mail'      => $this->build_admin_mail( $settings ),
			'mail_2'    => $this->build_reply_mail( $settings ),
			'notes'     => $this->notes,
			'renames'   => array_filter(
				$this->renames,
				function ( $to, $from ) {
					return $to !== $from;
				},
				ARRAY_FILTER_USE_BOTH
			),
			'total'     => $this->total,
			'converted' => $this->converted,
			'settings'  => array(
				'complete_message' => (string) ( $settings['complete_message'] ?? '' ),
				'usedb'            => ! empty( $settings['usedb'] ),
			),
		);
	}

	/**
	 * Count saved inquiry posts (mwf_{id}) for a MW form.
	 *
	 * @param int $mw_post_id MW form post ID.
	 * @return int
	 */
	public static function count_saved_data( $mw_post_id ) {
		$counts = wp_count_posts( self::MW_DATA_CPT . (int) $mw_post_id );
		if ( ! $counts ) {
			return 0;
		}
		$total = 0;
		foreach ( (array) $counts as $num ) {
			$total += (int) $num;
		}
		return $total;
	}

	/**
	 * Index validation rules by target field.
	 *
	 * @param array $settings MW settings meta.
	 * @return array
	 */
	private function index_validation( array $settings ) {
		$indexed = array();
		foreach ( (array) ( $settings['validation'] ?? array() ) as $rule_set ) {
			if ( ! is_array( $rule_set ) || empty( $rule_set['target'] ) ) {
				continue;
			}
			$target = (string) $rule_set['target'];
			unset( $rule_set['target'] );
			$indexed[ $target ] = isset( $indexed[ $target ] )
				? array_merge( $indexed[ $target ], $rule_set )
				: $rule_set;
		}
		return $indexed;
	}

	/**
	 * Replace mwform_* shortcodes inside the form HTML with CF7 tags.
	 *
	 * @param string $content MW form post_content.
	 * @return string
	 */
	private function convert_form_body( $content ) {
		// Enclosing button shortcodes: [mwform_bsubmit]Label[/mwform_bsubmit] etc.
		$content = preg_replace_callback(
			'/\[(mwform_(?:bsubmit|bconfirm|bback|bbutton))((?:\s+[^\]]*)?)\](.*?)\[\/\1\]/us',
			array( $this, 'replace_enclosing_button' ),
			(string) $content
		);

		$pattern = '/\[(mwform_[a-zA-Z_]+)((?:\s+[^\]]*)?)\]/u';
		$result  = preg_replace_callback(
			$pattern,
			array( $this, 'replace_shortcode' ),
			$content
		);

		// Orphan closing tags left by malformed markup.
		$result = preg_replace( '/\[\/mwform_[a-zA-Z_]+\]/u', '', (string) $result );

		return trim( (string) $result );
	}

	/**
	 * Convert an enclosing MW button shortcode pair (label as inner content).
	 *
	 * @param array $m Regex match: [full, tag, atts-string, inner-content].
	 * @return string
	 */
	private function replace_enclosing_button( array $m ) {
		$tag = $m[1];

		if ( 'mwform_bsubmit' === $tag ) {
			$this->total++;
			$this->converted++;
			$this->has_submit = true;
			$atts             = shortcode_parse_atts( trim( $m[2] ) );
			$atts             = is_array( $atts ) ? $atts : array();
			$label            = trim( wp_strip_all_tags( $m[3] ) );
			if ( '' === $label ) {
				$label = (string) ( $atts['value'] ?? __( 'Send', 'frontierline-forms' ) );
			}
			return '[submit "' . $this->esc_tag_value( $label ) . '"]';
		}

		// bconfirm / bback / bbutton: confirmation flow is provided by the plugin.
		return '';
	}

	/**
	 * Convert one mwform shortcode match.
	 *
	 * @param array $m Regex match: [full, tag, atts-string].
	 * @return string
	 */
	private function replace_shortcode( array $m ) {
		$tag  = $m[1];
		$atts = shortcode_parse_atts( trim( $m[2] ) );
		$atts = is_array( $atts ) ? $atts : array();

		switch ( $tag ) {
			case 'mwform_text':
			case 'mwform_email':
			case 'mwform_tel':
			case 'mwform_url':
			case 'mwform_number':
			case 'mwform_range':
			case 'mwform_textarea':
			case 'mwform_datepicker':
			case 'mwform_monthpicker':
			case 'mwform_hidden':
			case 'mwform_zip':
				return $this->convert_input( $tag, $atts );

			case 'mwform_radio':
			case 'mwform_checkbox':
			case 'mwform_select':
				return $this->convert_choices( $tag, $atts );

			case 'mwform_file':
			case 'mwform_image':
				return $this->convert_file( $atts );

			case 'mwform_submitButton':
				$this->total++;
				$this->converted++;
				$this->has_submit = true;
				return '[submit "' . $this->esc_tag_value( $atts['submit_value'] ?? __( 'Send', 'frontierline-forms' ) ) . '"]';

			case 'mwform_submit':
			case 'mwform_bsubmit':
				$this->total++;
				$this->converted++;
				$this->has_submit = true;
				$label            = $atts['value'] ?? ( $atts['element_content'] ?? __( 'Send', 'frontierline-forms' ) );
				return '[submit "' . $this->esc_tag_value( $label ) . '"]';

			case 'mwform_confirmButton':
			case 'mwform_backButton':
			case 'mwform_bconfirm':
			case 'mwform_bback':
			case 'mwform_bbutton':
			case 'mwform_button':
				// Confirmation flow is provided by Frontierline Forms itself.
				return '';

			case 'mwform_error':
				// CF7 renders inline validation messages automatically.
				return '';

			case 'mwform_akismet_error':
				return '';

			case 'mwform_password':
				$this->total++;
				$this->note(
					sprintf(
						/* translators: %s: field name */
						__( 'Password field "%s" has no Contact Form 7 equivalent and was skipped.', 'frontierline-forms' ),
						$atts['name'] ?? ''
					)
				);
				return '';

			case 'mwform_custom_mail_tag':
				$this->total++;
				$this->note(
					sprintf(
						/* translators: %s: field name */
						__( 'Custom mail tag "%s" requires manual migration (mwform_custom_mail_tag filter).', 'frontierline-forms' ),
						$atts['name'] ?? ''
					)
				);
				return '';

			default:
				$this->total++;
				$this->note(
					sprintf(
						/* translators: %s: shortcode name */
						__( 'Unsupported shortcode [%s] was removed. Please review manually.', 'frontierline-forms' ),
						$tag
					)
				);
				return '';
		}
	}

	/**
	 * Convert simple input shortcodes.
	 *
	 * @param string $tag  MW shortcode name.
	 * @param array  $atts Shortcode atts.
	 * @return string
	 */
	private function convert_input( $tag, array $atts ) {
		$this->total++;
		$name  = $this->map_name( $atts['name'] ?? '' );
		$rules = $this->rules_for( $atts['name'] ?? '' );

		$type_map = array(
			'mwform_text'        => 'text',
			'mwform_email'       => 'email',
			'mwform_tel'         => 'tel',
			'mwform_url'         => 'url',
			'mwform_number'      => 'number',
			'mwform_range'       => 'range',
			'mwform_textarea'    => 'textarea',
			'mwform_datepicker'  => 'date',
			'mwform_monthpicker' => 'text',
			'mwform_hidden'      => 'hidden',
			'mwform_zip'         => 'text',
		);
		$type     = $type_map[ $tag ];

		// Validation rules can upgrade a text field's type.
		if ( 'text' === $type ) {
			if ( isset( $rules['mail'] ) ) {
				$type = 'email';
			} elseif ( isset( $rules['tel'] ) ) {
				$type = 'tel';
			} elseif ( isset( $rules['url'] ) ) {
				$type = 'url';
			} elseif ( isset( $rules['numeric'] ) ) {
				$type = 'number';
			} elseif ( isset( $rules['date'] ) ) {
				$type = 'date';
			}
		}

		$required = ( isset( $rules['noempty'] ) || isset( $rules['required'] ) ) && 'hidden' !== $type;
		$options  = array();

		$maxlength = isset( $atts['maxlength'] ) ? (int) $atts['maxlength'] : 0;
		if ( isset( $rules['between']['max'] ) ) {
			$maxlength = (int) $rules['between']['max'];
		}
		$minlength = 0;
		if ( isset( $rules['between']['min'] ) ) {
			$minlength = (int) $rules['between']['min'];
		} elseif ( isset( $rules['minlength']['min'] ) ) {
			$minlength = (int) $rules['minlength']['min'];
		}
		if ( in_array( $type, array( 'text', 'email', 'tel', 'url', 'textarea' ), true ) ) {
			if ( $minlength > 0 ) {
				$options[] = 'minlength:' . $minlength;
			}
			if ( $maxlength > 0 ) {
				$options[] = 'maxlength:' . $maxlength;
			}
		}

		if ( ! empty( $atts['id'] ) ) {
			$options[] = 'id:' . sanitize_html_class( $atts['id'] );
		}
		if ( ! empty( $atts['class'] ) ) {
			foreach ( preg_split( '/\s+/', (string) $atts['class'] ) as $class ) {
				if ( '' !== $class ) {
					$options[] = 'class:' . sanitize_html_class( $class );
				}
			}
		}
		if ( ! empty( $atts['placeholder'] ) ) {
			$options[] = 'placeholder "' . $this->esc_tag_value( $atts['placeholder'] ) . '"';
		}

		$value = '';
		if ( isset( $atts['value'] ) && '' !== $atts['value'] ) {
			$value = ' "' . $this->esc_tag_value( $atts['value'] ) . '"';
		}

		// Unsupported rules (no Contact Form 7 equivalent) → manual note.
		foreach ( array( 'kana', 'hiragana', 'katakana', 'zip', 'eq', 'in', 'alpha', 'alphanumeric', 'month', 'akismet_check' ) as $unsupported ) {
			if ( isset( $rules[ $unsupported ] ) ) {
				$this->note(
					sprintf(
						/* translators: 1: rule name 2: field name */
						__( 'Validation rule "%1$s" on "%2$s" is not supported by Contact Form 7 and was skipped.', 'frontierline-forms' ),
						$unsupported,
						$atts['name'] ?? ''
					)
				);
			}
		}
		if ( 'mwform_zip' === $tag ) {
			$this->note(
				sprintf(
					/* translators: %s: field name */
					__( 'Zip field "%s" was converted to a single text field (MW WP Form used two boxes).', 'frontierline-forms' ),
					$atts['name'] ?? ''
				)
			);
		}

		$this->converted++;

		$tag_body = $type . ( $required ? '*' : '' ) . ' ' . $name;
		if ( $options ) {
			$tag_body .= ' ' . implode( ' ', $options );
		}
		return '[' . $tag_body . $value . ']';
	}

	/**
	 * Convert radio / checkbox / select.
	 *
	 * @param string $tag  MW shortcode name.
	 * @param array  $atts Shortcode atts.
	 * @return string
	 */
	private function convert_choices( $tag, array $atts ) {
		$this->total++;
		$name  = $this->map_name( $atts['name'] ?? '' );
		$rules = $this->rules_for( $atts['name'] ?? '' );

		$type_map = array(
			'mwform_radio'    => 'radio',
			'mwform_checkbox' => 'checkbox',
			'mwform_select'   => 'select',
		);
		$type     = $type_map[ $tag ];
		$required = isset( $rules['noempty'] ) || isset( $rules['required'] ) || isset( $rules['nofalse'] );

		if ( isset( $rules['nofalse'] ) ) {
			$this->note(
				sprintf(
					/* translators: %s: field name */
					__( 'The "nofalse" consent rule on "%s" was converted to a required checkbox. Consider Contact Form 7\'s [acceptance] tag for consent checkboxes.', 'frontierline-forms' ),
					$atts['name'] ?? ''
				)
			);
		}

		// Radio tags in CF7 are implicitly required and reject "*".
		$star = $required && 'radio' !== $type ? '*' : '';

		if ( 'radio' === $type && ! $required ) {
			$this->note(
				sprintf(
					/* translators: %s: field name */
					__( 'Radio "%s" was optional in MW WP Form, but Contact Form 7 radio buttons always require a selection.', 'frontierline-forms' ),
					$atts['name'] ?? ''
				)
			);
		}

		$choices    = array();
		$has_keyval = false;
		foreach ( explode( ',', (string) ( $atts['children'] ?? '' ) ) as $child ) {
			$child = trim( $child );
			if ( '' === $child ) {
				continue;
			}
			list( $key, $label ) = $this->split_mw_child( $child );
			if ( $key !== $label ) {
				$has_keyval = true;
			}
			if ( '' !== $label ) {
				$choices[] = '"' . $this->esc_tag_value( $label ) . '"';
			}
		}
		if ( $has_keyval ) {
			$this->note(
				sprintf(
					/* translators: %s: field name */
					__( 'Choices of "%s" used key:value pairs. Display labels were kept as submitted values.', 'frontierline-forms' ),
					$atts['name'] ?? ''
				)
			);
		}

		$options = array();
		if ( ! empty( $atts['id'] ) ) {
			$options[] = 'id:' . sanitize_html_class( $atts['id'] );
		}
		if ( ! empty( $atts['class'] ) ) {
			foreach ( preg_split( '/\s+/', (string) $atts['class'] ) as $class ) {
				if ( '' !== $class ) {
					$options[] = 'class:' . sanitize_html_class( $class );
				}
			}
		}

		$this->converted++;

		$tag_body = $type . $star . ' ' . $name;
		if ( $options ) {
			$tag_body .= ' ' . implode( ' ', $options );
		}
		if ( $choices ) {
			$tag_body .= ' ' . implode( ' ', $choices );
		}
		return '[' . $tag_body . ']';
	}

	/**
	 * Split a MW WP Form choice token into [key, label], mirroring MW's
	 * MW_WP_Form_Abstract_Form_Field::get_children(): "::" is a literal colon,
	 * a single ":" separates key:label.
	 *
	 * @param string $child Raw child token.
	 * @return array [key, label]
	 */
	private function split_mw_child( $child ) {
		$placeholder = '@-[_-_]-@';
		if ( preg_match( '/(^:[^:])|([^:]:[^:])/', $child ) ) {
			$parts = explode( ':', str_replace( '::', $placeholder, $child ), 2 );
		} else {
			$parts = array( str_replace( '::', $placeholder, $child ) );
		}
		$parts = array_map(
			function ( $p ) use ( $placeholder ) {
				return trim( str_replace( $placeholder, ':', $p ) );
			},
			$parts
		);
		if ( 1 === count( $parts ) ) {
			return array( $parts[0], $parts[0] );
		}
		return array( $parts[0], $parts[1] );
	}

	/**
	 * Convert file / image upload fields.
	 *
	 * @param array $atts Shortcode atts.
	 * @return string
	 */
	private function convert_file( array $atts ) {
		$this->total++;
		$name  = $this->map_name( $atts['name'] ?? '' );
		$rules = $this->rules_for( $atts['name'] ?? '' );

		$required = isset( $rules['noempty'] ) || isset( $rules['required'] );
		$options  = array();

		if ( isset( $rules['filesize']['bytes'] ) && (int) $rules['filesize']['bytes'] > 0 ) {
			$options[] = 'limit:' . (int) $rules['filesize']['bytes'];
		}
		if ( ! empty( $rules['filetype']['types'] ) ) {
			$types     = str_replace( ',', '|', (string) $rules['filetype']['types'] );
			$options[] = 'filetypes:' . preg_replace( '/[^A-Za-z0-9|]/', '', $types );
		}

		// Image-dimension rules have no Contact Form 7 equivalent → manual note.
		foreach ( array( 'maximagesize', 'minimagesize', 'maxfilesize', 'minfilesize' ) as $unsupported ) {
			if ( isset( $rules[ $unsupported ] ) ) {
				$this->note(
					sprintf(
						/* translators: 1: rule name 2: field name */
						__( 'Validation rule "%1$s" on "%2$s" is not supported by Contact Form 7 and was skipped.', 'frontierline-forms' ),
						$unsupported,
						$atts['name'] ?? ''
					)
				);
			}
		}

		$this->file_fields[] = $name;
		$this->converted++;
		$this->note(
			sprintf(
				/* translators: %s: field name */
				__( 'Uploaded file "%s" is attached to the notification mail. Review the Mail tab attachments.', 'frontierline-forms' ),
				$name
			)
		);

		$tag_body = 'file' . ( $required ? '*' : '' ) . ' ' . $name;
		if ( $options ) {
			$tag_body .= ' ' . implode( ' ', $options );
		}
		return '[' . $tag_body . ']';
	}

	/**
	 * Build CF7 admin notification mail from MW settings.
	 *
	 * @param array $settings MW settings.
	 * @return array
	 */
	private function build_admin_mail( array $settings ) {
		$subject = (string) ( $settings['admin_mail_subject'] ?? '' );
		if ( '' === $subject ) {
			$subject = (string) ( $settings['mail_subject'] ?? '' );
		}
		$body = (string) ( $settings['admin_mail_content'] ?? '' );
		if ( '' === $body ) {
			$body = (string) ( $settings['mail_content'] ?? '' );
		}

		$from_email = (string) ( $settings['admin_mail_from'] ?? '' );
		if ( '' === $from_email ) {
			$from_email = (string) ( $settings['mail_from'] ?? '' );
		}
		$from_name = (string) ( $settings['admin_mail_sender'] ?? '' );
		if ( '' === $from_name ) {
			$from_name = (string) ( $settings['mail_sender'] ?? '' );
		}

		$headers = array();
		if ( ! empty( $settings['mail_cc'] ) ) {
			$headers[] = 'Cc: ' . $this->convert_mail_text( $settings['mail_cc'] );
		}
		if ( ! empty( $settings['mail_bcc'] ) ) {
			$headers[] = 'Bcc: ' . $this->convert_mail_text( $settings['mail_bcc'] );
		}
		if ( ! empty( $settings['admin_mail_reply_to'] ) ) {
			$headers[] = 'Reply-To: ' . $this->convert_mail_text( $settings['admin_mail_reply_to'] );
		}

		return array(
			'active'             => true,
			'subject'            => $this->convert_mail_text( $subject, '[_site_title]' ),
			'sender'             => $this->build_sender( $from_name, $from_email ),
			'recipient'          => '' !== (string) ( $settings['mail_to'] ?? '' )
				? $this->convert_mail_text( $settings['mail_to'] )
				: '[_site_admin_email]',
			'body'               => $this->convert_mail_text( $body, $this->default_body() ),
			'additional_headers' => implode( "\n", $headers ),
			'attachments'        => $this->file_fields ? '[' . implode( "]\n[", $this->file_fields ) . ']' : '',
			'use_html'           => false,
			'exclude_blank'      => false,
		);
	}

	/**
	 * Build CF7 auto-reply mail (mail_2) from MW settings.
	 *
	 * @param array $settings MW settings.
	 * @return array
	 */
	private function build_reply_mail( array $settings ) {
		$reply_field = (string) ( $settings['automatic_reply_email'] ?? '' );
		$active      = false;
		$recipient   = '[your-email]';

		if ( '' !== $reply_field ) {
			// Only use a field that actually exists in the converted form.
			// (Do not fabricate a phantom field-N via map_name.)
			if ( isset( $this->renames[ $reply_field ] ) ) {
				$active    = true;
				$recipient = '[' . $this->renames[ $reply_field ] . ']';
			} else {
				$this->note(
					sprintf(
						/* translators: %s: field name */
						__( 'The auto-reply destination field "%s" was not found in the form, so the auto-reply email was left disabled. Set the recipient manually in the Mail (2) tab.', 'frontierline-forms' ),
						$reply_field
					)
				);
			}
		}

		return array(
			'active'             => $active,
			'subject'            => $this->convert_mail_text( (string) ( $settings['mail_subject'] ?? '' ), '[_site_title]' ),
			'sender'             => $this->build_sender(
				(string) ( $settings['mail_sender'] ?? '' ),
				(string) ( $settings['mail_from'] ?? '' )
			),
			'recipient'          => $recipient,
			'body'               => $this->convert_mail_text( (string) ( $settings['mail_content'] ?? '' ), $this->default_body() ),
			'additional_headers' => ! empty( $settings['mail_reply_to'] )
				? 'Reply-To: ' . $this->convert_mail_text( $settings['mail_reply_to'] )
				: '',
			'attachments'        => '',
			'use_html'           => false,
			'exclude_blank'      => false,
		);
	}

	/**
	 * Default CF7 mail body.
	 *
	 * @return string
	 */
	private function default_body() {
		return "[your-name]\n[your-email]\n\n[your-message]";
	}

	/**
	 * Build a CF7 sender string.
	 *
	 * @param string $name  Sender name.
	 * @param string $email Sender email.
	 * @return string
	 */
	private function build_sender( $name, $email ) {
		if ( '' === $email ) {
			return '[_site_title] <[_site_admin_email]>';
		}
		$email = $this->convert_mail_text( $email );
		return '' !== $name ? $name . ' <' . $email . '>' : $email;
	}

	/**
	 * Convert MW mail placeholders {field} → CF7 [field], applying renames.
	 *
	 * @param string $text    Source text.
	 * @param string $default Fallback when empty.
	 * @return string
	 */
	private function convert_mail_text( $text, $default = '' ) {
		$text = (string) $text;
		if ( '' === trim( $text ) ) {
			return $default;
		}

		$converter = $this;
		$text      = preg_replace_callback(
			'/\{([^{}]+)\}/u',
			function ( $m ) use ( $converter ) {
				$key = trim( $m[1] );
				if ( 'tracking_number' === $key ) {
					$converter->note(
						__( 'The {tracking_number} mail tag has no Contact Form 7 equivalent and was removed from mails.', 'frontierline-forms' )
					);
					return '';
				}
				if ( ! isset( $converter->renames[ $key ] ) ) {
					// Mail tag references a name that is not a converted field.
					$converter->note(
						sprintf(
							/* translators: %s: mail tag name */
							__( 'The mail tag "{%s}" does not match any form field and may output nothing. Please review the Mail tab.', 'frontierline-forms' ),
							$key
						)
					);
					return '[' . $key . ']';
				}
				return '[' . $converter->renames[ $key ] . ']';
			},
			$text
		);
		return $text;
	}

	/**
	 * Map a MW field name to a CF7-safe name (memoized).
	 *
	 * @param string $name Original field name.
	 * @return string
	 */
	private function map_name( $name ) {
		$name = trim( (string) $name );
		if ( '' === $name ) {
			$this->field_seq++;
			return 'field-' . $this->field_seq;
		}
		if ( isset( $this->renames[ $name ] ) ) {
			return $this->renames[ $name ];
		}

		if ( preg_match( '/^[A-Za-z][A-Za-z0-9_-]*$/', $name ) ) {
			$this->renames[ $name ] = $name;
			return $name;
		}

		if ( isset( self::$name_dictionary[ $name ] ) ) {
			$mapped = self::$name_dictionary[ $name ];
		} else {
			$this->field_seq++;
			$mapped = 'field-' . $this->field_seq;
		}
		// Avoid collisions.
		while ( in_array( $mapped, $this->renames, true ) ) {
			$this->field_seq++;
			$mapped .= '-' . $this->field_seq;
		}
		$this->renames[ $name ] = $mapped;
		$this->note(
			sprintf(
				/* translators: 1: original name 2: new name */
				__( 'Field name "%1$s" is not usable in Contact Form 7 and was renamed to "%2$s".', 'frontierline-forms' ),
				$name,
				$mapped
			)
		);
		return $mapped;
	}

	/**
	 * Make a string safe inside a CF7 tag quoted value.
	 *
	 * @param string $value Raw value.
	 * @return string
	 */
	private function esc_tag_value( $value ) {
		return trim( str_replace( array( '"', '[', ']' ), '', (string) $value ) );
	}

	/**
	 * Validation rules for a field.
	 *
	 * @param string $name Original field name.
	 * @return array
	 */
	private function rules_for( $name ) {
		return $this->validation[ (string) $name ] ?? array();
	}

	/**
	 * Add a unique report note.
	 *
	 * @param string $note Note text.
	 */
	public function note( $note ) {
		if ( ! in_array( $note, $this->notes, true ) ) {
			$this->notes[] = $note;
		}
	}
}
