<?php
/**
 * Formidable Forms adapter.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Adapters;

defined( 'ABSPATH' ) || exit;

/**
 * Integration with Formidable Forms (Lite and Pro share these models).
 *
 * Every signature below was read from the INSTALLED Formidable 6.34 source,
 * not documentation:
 *
 * - Submission hook: classes/models/FrmEntry.php:1128
 *   `do_action( 'frm_after_create_entry', $entry_id, $new_values['form_id'], compact( 'is_child' ) );`
 *   The third argument carries `is_child`, true for repeater/embedded sub
 *   entries — those are skipped so a repeater row never creates a contact.
 * - Entry values: FrmEntry::getOne( $id, true ) (classes/models/FrmEntry.php:478)
 *   populates `$entry->metas`, keyed by field id (FrmEntry.php:567).
 * - Forms: FrmForm::get_published_forms() (classes/models/FrmForm.php:969).
 * - Fields: FrmField::get_all_for_form() (classes/models/FrmField.php:1029).
 * - Composite child keys: FrmFieldName.php:50 (first, middle, last) and
 *   FrmFieldAddress.php:43 (line1, line2, city, state, zip, country).
 *
 * The hook runs at priority 30 so Formidable has finished writing entry meta
 * — getOne() below reads those rows back.
 *
 * @since 1.0.0
 */
class Formidable extends AbstractAdapter {

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

	/**
	 * Adapter label.
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public function label(): string {
		return 'Formidable Forms';
	}

	/**
	 * Active and at a version whose models we verified.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public function is_available(): bool {
		return class_exists( '\FrmAppHelper' )
			&& class_exists( '\FrmForm' )
			&& class_exists( '\FrmField' )
			&& class_exists( '\FrmEntry' )
			&& version_compare( (string) \FrmAppHelper::plugin_version(), '5.0', '>=' );
	}

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

		$models = \FrmForm::get_published_forms();

		if ( ! is_array( $models ) ) {
			return array();
		}

		$forms = array();

		foreach ( $models as $model ) {
			if ( ! is_object( $model ) || empty( $model->id ) ) {
				continue;
			}

			$title = isset( $model->name ) ? trim( (string) $model->name ) : '';

			$forms[] = array(
				'id'    => (string) $model->id,
				/* translators: %d: form id, used when a form has no title. */
				'title' => '' !== $title ? $title : sprintf( __( 'Form #%d', 'rewind-form-bridge-for-brevo' ), (int) $model->id ),
			);
		}

		return $forms;
	}

	/**
	 * Mappable fields of one form, composite fields flattened to sub-keys.
	 *
	 * @since 1.0.0
	 * @param string $form_id Formidable form id.
	 * @return array [ ['key','label','type'], ... ]
	 */
	public function get_fields( string $form_id ): array {
		if ( ! $this->is_available() ) {
			return array();
		}

		$models = \FrmField::get_all_for_form( (int) $form_id );

		if ( ! is_array( $models ) ) {
			return array();
		}

		$fields = array();

		foreach ( $models as $model ) {
			if ( ! is_object( $model ) || empty( $model->id ) ) {
				continue;
			}

			$type = isset( $model->type ) ? (string) $model->type : 'text';
			$ours = $this->map_type( $type );

			if ( null === $ours ) {
				continue; // Dividers, HTML blocks, captcha, summaries.
			}

			$key   = (string) $model->id;
			$label = isset( $model->name ) && '' !== trim( (string) $model->name ) ? (string) $model->name : $key;

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

			foreach ( $this->composite_children( $type ) as $child => $child_label ) {
				$fields[] = array(
					'key'   => $key . '.' . $child,
					'label' => $label . ' — ' . $child_label,
					'type'  => 'text',
				);
			}
		}

		return $fields;
	}

	/**
	 * Attach to the entry-created hook.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'frm_after_create_entry', array( $this, 'capture' ), 30, 3 );
	}

	/**
	 * Hook callback: ( $entry_id, $form_id, $args ).
	 *
	 * @since 1.0.0
	 * @param mixed $entry_id Newly created entry id.
	 * @param mixed $form_id  Form id.
	 * @param mixed $args     ['is_child' => bool] on 4.0+, absent on older.
	 * @return void
	 */
	public function capture( $entry_id, $form_id, $args = array() ): void {
		if ( is_array( $args ) && ! empty( $args['is_child'] ) ) {
			return; // A repeater or embedded-form row, not a submission.
		}

		if ( (int) $entry_id < 1 || (int) $form_id < 1 ) {
			return;
		}

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

	/**
	 * Read the saved entry back and normalise its meta.
	 *
	 * @since 1.0.0
	 * @param mixed $raw ['entry_id' => int, 'form_id' => string].
	 * @return Submission|null
	 */
	public function normalize_submission( $raw ): ?Submission {
		if ( ! is_array( $raw ) || empty( $raw['entry_id'] ) || ! $this->is_available() ) {
			return null;
		}

		$entry = \FrmEntry::getOne( (int) $raw['entry_id'], true );

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

		$metas  = isset( $entry->metas ) && is_array( $entry->metas ) ? $entry->metas : array();
		$fields = array();

		foreach ( $metas as $field_id => $value ) {
			$key = (string) $field_id;

			if ( is_array( $value ) && $this->is_composite_value( $value ) ) {
				$this->flatten_composite( $fields, $key, $value );
				continue;
			}

			$fields[ $key ] = $value;
		}

		return new Submission(
			'formidable',
			(string) $raw['form_id'],
			(string) $entry->id,
			$fields,
			$this->source_url(),
			$this->client_ip(),
			array(
				'metas'      => $metas,
				// Formidable's anti-spam blocks a submission before the entry
				// is created, so anything reaching this hook is not spam. A
				// saved draft is not a completed submission, though.
				'_rbfb_spam' => ! empty( $entry->is_draft ),
			)
		);
	}

	/**
	 * Formidable field type -> our vocabulary. Null = not mappable data.
	 *
	 * Pro-only types are listed too: Lite never emits them, but a site
	 * running Pro shares this adapter.
	 *
	 * @since 1.0.0
	 * @param string $type Formidable type.
	 * @return string|null
	 */
	private function map_type( string $type ): ?string {
		switch ( $type ) {
			case 'text':
			case 'url':
			case 'hidden':
			case 'password':
			case 'time':
			case 'tag':
			case 'user_id':
				return 'text';
			case 'email':
				return 'email';
			case 'number':
			case 'star':
			case 'range':
			case 'scale':
			case 'quantity':
			case 'total':
				return 'number';
			case 'date':
				return 'date';
			case 'phone':
				return 'phone';
			case 'checkbox':
			case 'gdpr':
			case 'toggle':
				return 'checkbox';
			case 'select':
			case 'radio':
			case 'product':
			case 'lookup':
				return 'select';
			case 'textarea':
			case 'rte':
				return 'textarea';
			case 'name':
				return 'name';
			case 'address':
				return 'address';
			case 'file':
				return 'file';
		}

		return null;
	}

	/**
	 * Composite sub-keys per Formidable type.
	 *
	 * @since 1.0.0
	 * @param string $type Formidable type.
	 * @return array<string, string> child key => child label.
	 */
	private function composite_children( string $type ): array {
		if ( 'name' === $type ) {
			return array(
				'first'  => __( 'First name', 'rewind-form-bridge-for-brevo' ),
				'middle' => __( 'Middle name', 'rewind-form-bridge-for-brevo' ),
				'last'   => __( 'Last name', 'rewind-form-bridge-for-brevo' ),
			);
		}

		if ( 'address' === $type ) {
			return array(
				'line1'   => __( 'Street address', 'rewind-form-bridge-for-brevo' ),
				'line2'   => __( 'Address line 2', 'rewind-form-bridge-for-brevo' ),
				'city'    => __( 'City', 'rewind-form-bridge-for-brevo' ),
				'state'   => __( 'State', 'rewind-form-bridge-for-brevo' ),
				'zip'     => __( 'Postal code', 'rewind-form-bridge-for-brevo' ),
				'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).
	 *
	 * @since 1.0.0
	 * @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;
	}
}
