<?php
/**
 * Forminator adapter.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Adapters;

defined( 'ABSPATH' ) || exit;

/**
 * Integration with Forminator (free + pro share this codebase).
 *
 * Every hook signature below was verified against the INSTALLED Forminator
 * 1.57.1 source, not documentation:
 *
 * - AJAX submission path:
 *   library/abstracts/abstract-class-front-action.php:688
 *   `do_action( 'forminator_' . static::$module_slug . '_after_save_entry', self::$module_id, $response );`
 * - Page-reload submission path:
 *   library/abstracts/abstract-class-front-action.php:339
 *   `do_action( 'forminator_' . static::$module_slug . '_after_handle_submit', self::$module_id, $response );`
 * - The module slug for forms:
 *   library/modules/custom-forms/front/front-action.php:24
 *   `protected static $module_slug = 'form';`
 *   so the concrete hook names are `forminator_form_after_save_entry` and
 *   `forminator_form_after_handle_submit`, firing with ($form_id, $response)
 *   and NO entry id.
 * - Entry recovery:
 *   library/model/class-form-entry-model.php:2304
 *   `public static function get_latest_entry_by_form_id( $form_id, $order = 'DESC' )`
 * - Spam flag: library/model/class-form-entry-model.php:66 `public $is_spam = false;`
 *
 * Both hooks are attached because hook firing differs between AJAX and
 * page-reload submission modes; a per-request entry-id guard prevents double
 * processing if both ever fire. Verified against source for both modes —
 * please exercise BOTH modes in manual testing (Forminator form settings →
 * Behaviour → submission method).
 *
 * @since 0.3.0
 */
class Forminator extends AbstractAdapter {

	/**
	 * Entry ids already processed in this request (double-fire guard).
	 *
	 * @var array<string, bool>
	 */
	private array $processed = array();

	/**
	 * Adapter slug.
	 *
	 * @since 0.3.0
	 * @return string
	 */
	public function slug(): string {
		return 'forminator';
	}

	/**
	 * Adapter label.
	 *
	 * @since 0.3.0
	 * @return string
	 */
	public function label(): string {
		return 'Forminator';
	}

	/**
	 * Active and at a version whose hooks we verified.
	 *
	 * @since 0.3.0
	 * @return bool
	 */
	public function is_available(): bool {
		return defined( 'FORMINATOR_VERSION' )
			&& class_exists( '\Forminator_API' )
			&& class_exists( '\Forminator_Form_Entry_Model' )
			&& version_compare( FORMINATOR_VERSION, '1.15', '>=' );
	}

	/**
	 * All published Forminator forms.
	 *
	 * @since 0.3.0
	 * @return array [ ['id' => string, 'title' => string], ... ]
	 */
	public function get_forms(): array {
		if ( ! $this->is_available() ) {
			return array();
		}

		$forms = array();

		// Forminator_API::get_forms() pages internally (class-api.php:58).
		for ( $page = 1; $page <= 5; $page++ ) {
			$models = \Forminator_API::get_forms( null, $page, 50, 'publish' );

			if ( ! is_array( $models ) || 0 === count( $models ) ) {
				break;
			}

			foreach ( $models as $model ) {
				$title = '';
				if ( isset( $model->settings['formName'] ) && '' !== $model->settings['formName'] ) {
					$title = (string) $model->settings['formName'];
				} elseif ( isset( $model->name ) ) {
					$title = (string) $model->name;
				}

				$forms[] = array(
					'id'    => (string) $model->id,
					'title' => '' !== $title ? $title : sprintf( 'Form #%d', (int) $model->id ),
				);
			}

			if ( count( $models ) < 50 ) {
				break;
			}
		}

		return $forms;
	}

	/**
	 * Mappable fields of one form, composite fields flattened to sub-keys
	 * (name-1.first-name etc.) so first and last name are independently
	 * mappable — a real gap in competing plugins.
	 *
	 * @since 0.3.0
	 * @param string $form_id Forminator form id.
	 * @return array [ ['key','label','type'], ... ]
	 */
	public function get_fields( string $form_id ): array {
		if ( ! $this->is_available() ) {
			return array();
		}

		$model = \Forminator_API::get_form( (int) $form_id );

		if ( is_wp_error( $model ) || ! is_object( $model ) || ! method_exists( $model, 'get_real_fields' ) ) {
			return array();
		}

		$fields = array();

		foreach ( $model->get_real_fields() as $field ) {
			// to_formatted_array() (class-form-field-model.php:136) returns
			// the builder settings: element_id, type, field_label, ...
			$settings = $field->to_formatted_array();

			$key   = isset( $settings['element_id'] ) ? (string) $settings['element_id'] : (string) $field->slug;
			$type  = isset( $settings['type'] ) ? (string) $settings['type'] : 'text';
			$label = isset( $settings['field_label'] ) && '' !== $settings['field_label'] ? (string) $settings['field_label'] : $key;

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

			$ours = $this->map_type( $type, $settings );

			if ( null === $ours ) {
				continue; // Non-data fields (captcha, section, html, ...).
			}

			$fields[] = array(
				'key'   => $key,
				'label' => $label,
				'type'  => $ours,
			);

			// Composite sub-keys, labels suffixed for the mapping dropdown.
			foreach ( $this->composite_children( $type ) as $child => $child_label ) {
				$fields[] = array(
					'key'   => $key . '.' . $child,
					'label' => $label . ' — ' . $child_label,
					'type'  => 'text',
				);
			}
		}

		return $fields;
	}

	/**
	 * Attach both submission hooks (AJAX and page-reload modes).
	 *
	 * @since 0.3.0
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'forminator_form_after_save_entry', array( $this, 'capture' ), 10, 2 );
		add_action( 'forminator_form_after_handle_submit', array( $this, 'capture' ), 10, 2 );
	}

	/**
	 * Hook callback: ($form_id, $response) — no entry id (see class docblock).
	 *
	 * @since 0.3.0
	 * @param mixed $form_id  Forminator form id.
	 * @param mixed $response Forminator's front-end response array.
	 * @return void
	 */
	public function capture( $form_id, $response ): void {
		if ( ! is_array( $response ) || empty( $response['success'] ) ) {
			return; // Failed validation or blocked submission.
		}

		$this->dispatch(
			array(
				'form_id'  => (string) $form_id,
				'response' => $response,
			)
		);
	}

	/**
	 * Recover the just-saved entry and normalise it.
	 *
	 * @since 0.3.0
	 * @param mixed $raw ['form_id' => string, 'response' => array].
	 * @return Submission|null
	 */
	public function normalize_submission( $raw ): ?Submission {
		if ( ! is_array( $raw ) || empty( $raw['form_id'] ) || ! class_exists( '\Forminator_Form_Entry_Model' ) ) {
			return null;
		}

		$form_id = (string) $raw['form_id'];

		// The hooks do not pass the entry id — recover the newest entry for
		// this form (class-form-entry-model.php:2304). Runs in the same
		// request that saved it, so "latest" is this submission.
		$entry = \Forminator_Form_Entry_Model::get_latest_entry_by_form_id( (int) $form_id );

		if ( ! is_object( $entry ) || empty( $entry->entry_id ) ) {
			return null;
		}

		$entry_ref = (string) $entry->entry_id;

		if ( isset( $this->processed[ $entry_ref ] ) ) {
			return null; // Both hooks fired for one request.
		}
		$this->processed[ $entry_ref ] = true;

		$fields = array();
		$meta   = is_array( $entry->meta_data ) ? $entry->meta_data : array();

		foreach ( $meta as $key => $data ) {
			if ( 0 === strpos( (string) $key, '_' ) ) {
				continue; // Internal meta (_forminator_choice_values, ...).
			}

			$value = is_array( $data ) && array_key_exists( 'value', $data ) ? $data['value'] : $data;

			if ( is_array( $value ) && $this->is_composite_value( $value ) ) {
				$this->flatten_composite( $fields, (string) $key, $value );
			} else {
				$fields[ (string) $key ] = $value;
			}
		}

		return new Submission(
			'forminator',
			$form_id,
			$entry_ref,
			$fields,
			$this->source_url(),
			$this->client_ip(),
			array(
				'meta'       => $meta,
				'_rbfb_spam' => ! empty( $entry->is_spam ),
			)
		);
	}

	/**
	 * Forminator field type -> our vocabulary. Null = not mappable data.
	 *
	 * @since 0.3.0
	 * @param string $type     Forminator type.
	 * @param array  $settings Field settings, for select value_type.
	 * @return string|null
	 */
	private function map_type( string $type, array $settings ): ?string {
		switch ( $type ) {
			case 'text':
			case 'hidden':
			case 'website':
			case 'custom':
			case 'time':
				return 'text';
			case 'email':
				return 'email';
			case 'number':
			case 'currency':
			case 'calculation':
			case 'slider':
			case 'rating':
				return 'number';
			case 'date':
				return 'date';
			case 'phone':
				return 'phone';
			case 'checkbox':
			case 'gdprcheckbox':
			case 'consent':
				return 'checkbox';
			case 'select':
				return ( isset( $settings['value_type'] ) && 'multiselect' === $settings['value_type'] ) ? 'multiselect' : 'select';
			case 'radio':
				return 'select';
			case 'multivalue':
				return 'multiselect';
			case 'textarea':
				return 'textarea';
			case 'name':
				return 'name';
			case 'address':
				return 'address';
			case 'upload':
				return 'file';
		}

		return null;
	}

	/**
	 * Composite sub-keys per Forminator type. Child keys are the exact meta
	 * keys Forminator stores (library/fields/name.php: prefix, first-name,
	 * middle-name, last-name; library/fields/address.php: street_address,
	 * address_line, address_city, address_state, address_zip,
	 * address_country).
	 *
	 * @since 0.3.0
	 * @param string $type Forminator type.
	 * @return array<string, string> child key => child label.
	 */
	private function composite_children( string $type ): array {
		if ( 'name' === $type ) {
			return array(
				'prefix'      => __( 'Prefix', 'rewind-form-bridge-for-brevo' ),
				'first-name'  => __( 'First name', 'rewind-form-bridge-for-brevo' ),
				'middle-name' => __( 'Middle name', 'rewind-form-bridge-for-brevo' ),
				'last-name'   => __( 'Last name', 'rewind-form-bridge-for-brevo' ),
			);
		}

		if ( 'address' === $type ) {
			return array(
				'street_address'  => __( 'Street address', 'rewind-form-bridge-for-brevo' ),
				'address_line'    => __( 'Address line 2', 'rewind-form-bridge-for-brevo' ),
				'address_city'    => __( 'City', 'rewind-form-bridge-for-brevo' ),
				'address_state'   => __( 'State', 'rewind-form-bridge-for-brevo' ),
				'address_zip'     => __( 'Postal code', 'rewind-form-bridge-for-brevo' ),
				'address_country' => __( 'Country', 'rewind-form-bridge-for-brevo' ),
			);
		}

		return array();
	}

	/**
	 * Whether an array value is a composite (string-keyed parts) rather
	 * than a checkbox list (numeric keys).
	 *
	 * @param array $value The stored meta value.
	 * @return bool
	 */
	private function is_composite_value( array $value ): bool {
		foreach ( array_keys( $value ) as $key ) {
			if ( is_string( $key ) ) {
				return true;
			}
		}

		return false;
	}
}
