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

namespace RewindBrevoFormBridge\Adapters;

defined( 'ABSPATH' ) || exit;

/**
 * Integration with WPForms (Lite and Pro share this submission pipeline).
 *
 * Every signature below was read from the INSTALLED WPForms Lite 2.0.0.5
 * source, not documentation:
 *
 * - Submission hook: includes/class-process.php:1024
 *   `do_action( 'wpforms_process_complete', $fields, $entry, $form_data, $entry_id );`
 *   It fires only after validation, spam and anti-spam checks have passed,
 *   so there is no spam flag to honour here.
 * - Field payload shape: includes/fields/class-base.php:3833 writes
 *   `[ 'name', 'value', 'id', 'type' ]` per field, keyed by field id.
 * - The Name field adds its own parts: includes/fields/class-name.php:625
 *   `[ 'first', 'middle', 'last' ]` alongside the joined 'value'.
 * - Forms are the `wpforms` post type (includes/class-form.php:110) and the
 *   builder config is JSON in post_content, decoded with wpforms_decode()
 *   (includes/functions/forms.php:77).
 *
 * Forms are read through get_posts() rather than WPForms' own form object,
 * because that accessor was renamed between 1.8 and 2.0 (`wpforms()->form`
 * became `wpforms()->obj( 'form' )`) while the post type did not change.
 *
 * WPForms Lite does not store entries, so $entry_id arrives as 0 on Lite and
 * the delivery log simply carries no entry reference for those rows.
 *
 * @since 1.0.0
 */
class WpForms extends AbstractAdapter {

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

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

	/**
	 * Active and at a version whose hooks we verified.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public function is_available(): bool {
		return defined( 'WPFORMS_VERSION' )
			&& function_exists( 'wpforms_decode' )
			&& version_compare( WPFORMS_VERSION, '1.5', '>=' );
	}

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

		$forms = array();

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

		return $forms;
	}

	/**
	 * Mappable fields of one form, composite fields flattened to sub-keys
	 * so first and last name can be mapped to different Brevo attributes.
	 *
	 * @since 1.0.0
	 * @param string $form_id WPForms form (post) id.
	 * @return array [ ['key','label','type'], ... ]
	 */
	public function get_fields( string $form_id ): array {
		$data = $this->form_data( $form_id );

		if ( null === $data || empty( $data['fields'] ) || ! is_array( $data['fields'] ) ) {
			return array();
		}

		$fields = array();

		foreach ( $data['fields'] as $id => $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$key  = (string) ( isset( $field['id'] ) ? $field['id'] : $id );
			$type = isset( $field['type'] ) ? (string) $field['type'] : 'text';
			$ours = $this->map_type( $type );

			if ( '' === $key || null === $ours ) {
				continue; // Layout, captcha, HTML and other non-data fields.
			}

			$label = isset( $field['label'] ) && '' !== $field['label'] ? (string) $field['label'] : $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 submission hook.
	 *
	 * Priority 20 puts us after WPForms' own marketing providers (registered
	 * at 5 in src/Providers/Providers.php:68), so a failure of ours can never
	 * hold up the integrations the site already relies on.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'wpforms_process_complete', array( $this, 'capture' ), 20, 4 );
	}

	/**
	 * Hook callback: ( $fields, $entry, $form_data, $entry_id ).
	 *
	 * @since 1.0.0
	 * @param mixed $fields    Processed fields keyed by field id.
	 * @param mixed $entry     Raw $_POST-derived entry array.
	 * @param mixed $form_data Decoded builder config.
	 * @param mixed $entry_id  Stored entry id, 0 on Lite.
	 * @return void
	 */
	public function capture( $fields, $entry, $form_data, $entry_id = 0 ): void {
		if ( ! is_array( $fields ) || ! is_array( $form_data ) || empty( $form_data['id'] ) ) {
			return;
		}

		$this->dispatch(
			array(
				'form_id'  => (string) $form_data['id'],
				'fields'   => $fields,
				'entry_id' => (int) $entry_id,
			)
		);
	}

	/**
	 * Normalise the processed field array.
	 *
	 * @since 1.0.0
	 * @param mixed $raw ['form_id' => string, 'fields' => array, 'entry_id' => int].
	 * @return Submission|null
	 */
	public function normalize_submission( $raw ): ?Submission {
		if ( ! is_array( $raw ) || empty( $raw['form_id'] ) || ! isset( $raw['fields'] ) || ! is_array( $raw['fields'] ) ) {
			return null;
		}

		$fields = array();

		foreach ( $raw['fields'] as $id => $field ) {
			$key = (string) $id;

			if ( ! is_array( $field ) ) {
				$fields[ $key ] = $field;
				continue;
			}

			$fields[ $key ] = array_key_exists( 'value', $field ) ? $field['value'] : '';

			$type  = isset( $field['type'] ) ? (string) $field['type'] : '';
			$parts = array();

			foreach ( array_keys( $this->composite_children( $type ) ) as $child ) {
				if ( array_key_exists( $child, $field ) ) {
					$parts[ $child ] = $field[ $child ];
				}
			}

			if ( count( $parts ) > 0 ) {
				// flatten_composite() also rewrites the parent key with the
				// joined parts; keep WPForms' own joined 'value' instead.
				$parent = $fields[ $key ];
				$this->flatten_composite( $fields, $key, $parts );
				$fields[ $key ] = $parent;
			}
		}

		$entry_id = isset( $raw['entry_id'] ) ? (int) $raw['entry_id'] : 0;

		return new Submission(
			'wpforms',
			(string) $raw['form_id'],
			$entry_id > 0 ? (string) $entry_id : '',
			$fields,
			$this->source_url(),
			$this->client_ip(),
			array( 'fields' => $raw['fields'] )
		);
	}

	/**
	 * Published forms as WP_Post objects.
	 *
	 * @since 1.0.0
	 * @return \WP_Post[]
	 */
	private function form_posts(): array {
		$posts = get_posts(
			array(
				'post_type'        => 'wpforms',
				'post_status'      => 'publish',
				'numberposts'      => 200,
				'orderby'          => 'title',
				'order'            => 'ASC',
				'suppress_filters' => false,
			)
		);

		return is_array( $posts ) ? $posts : array();
	}

	/**
	 * Decoded builder config for one form.
	 *
	 * @since 1.0.0
	 * @param string $form_id Form (post) id.
	 * @return array|null
	 */
	private function form_data( string $form_id ): ?array {
		if ( ! $this->is_available() ) {
			return null;
		}

		$post = get_post( (int) $form_id );

		if ( ! $post instanceof \WP_Post || 'wpforms' !== $post->post_type ) {
			return null;
		}

		$data = wpforms_decode( $post->post_content );

		return is_array( $data ) ? $data : null;
	}

	/**
	 * WPForms field type -> our vocabulary. Null = not mappable data.
	 *
	 * Pro-only types are listed too: the free plugin never emits them, but a
	 * site running Pro shares this adapter.
	 *
	 * @since 1.0.0
	 * @param string $type WPForms type.
	 * @return string|null
	 */
	private function map_type( string $type ): ?string {
		switch ( $type ) {
			case 'text':
			case 'url':
			case 'hidden':
			case 'password':
				return 'text';
			case 'email':
				return 'email';
			case 'number':
			case 'number-slider':
			case 'rating':
			case 'payment-single':
			case 'payment-total':
				return 'number';
			case 'date-time':
				return 'date';
			case 'phone':
				return 'phone';
			case 'checkbox':
			case 'gdpr-checkbox':
			case 'payment-checkbox':
				return 'checkbox';
			case 'select':
			case 'payment-select':
				return 'select';
			case 'radio':
			case 'payment-multiple':
				return 'select';
			case 'textarea':
			case 'richtext':
				return 'textarea';
			case 'name':
				return 'name';
			case 'address':
				return 'address';
			case 'file-upload':
				return 'file';
		}

		return null;
	}

	/**
	 * Composite sub-keys per WPForms type. Child keys are the exact keys
	 * WPForms writes into the processed field array (class-name.php:625 for
	 * the Name field; the Pro Address field uses the same convention).
	 *
	 * @since 1.0.0
	 * @param string $type WPForms 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(
				'address1' => __( 'Street address', 'rewind-form-bridge-for-brevo' ),
				'address2' => __( 'Address line 2', 'rewind-form-bridge-for-brevo' ),
				'city'     => __( 'City', 'rewind-form-bridge-for-brevo' ),
				'state'    => __( 'State', 'rewind-form-bridge-for-brevo' ),
				'postal'   => __( 'Postal code', 'rewind-form-bridge-for-brevo' ),
				'country'  => __( 'Country', 'rewind-form-bridge-for-brevo' ),
			);
		}

		return array();
	}
}
