<?php
/**
 * Formidable data access boundary.
 *
 * @package EntryFoundry
 */

namespace EntryFoundry\Formidable;

use RuntimeException;

/**
 * Converts Formidable records into neutral EntryFoundry data shapes.
 */
final class FormidableData {

	/**
	 * Field metadata cached for the current request.
	 *
	 * @var array<int, array<int, array<string, mixed>>>
	 */
	private $fields_cache = array();

	/**
	 * Formidable field records cached for the current request.
	 *
	 * @var array<int, array<int, object>>
	 */
	private $field_records_cache = array();

	/**
	 * Repeater definitions cached for the current request.
	 *
	 * @var array<int, array<int, array{id:int, child_form_id:int, columns:array<int, array{id:int, key:string, label:string, type:string, render_as:string}>}>>
	 */
	private $repeaters_cache = array();

	/**
	 * Formidable Lite field type identifiers.
	 *
	 * @var array<string, bool>|null
	 */
	private $lite_field_types;

	/**
	 * Formidable Pro field type identifiers.
	 *
	 * @var array<string, bool>|null
	 */
	private $pro_field_types;

	/**
	 * Get all published, non-template forms.
	 *
	 * @return array<int, array{id:int, label:string}>
	 */
	public function get_sources(): array {
		$this->assert_formidable_class( 'FrmForm' );

		$forms   = \FrmForm::getAll(
			array(
				'is_template' => 0,
				'status'      => 'published',
			),
			'name'
		);
		$sources = array();

		foreach ( is_array( $forms ) ? $forms : array() as $form ) {
			if ( ! is_object( $form ) || ! isset( $form->id, $form->name ) ) {
				continue;
			}

			$sources[] = array(
				'id'    => (int) $form->id,
				'label' => (string) $form->name,
			);
		}

		return $sources;
	}

	/**
	 * Get fields for a form.
	 *
	 * @param int $source_id Formidable form ID.
	 * @return array<int, array<string, mixed>>
	 */
	public function get_fields( int $source_id ): array {
		if ( $source_id < 1 ) {
			return array();
		}

		if ( isset( $this->fields_cache[ $source_id ] ) ) {
			return $this->fields_cache[ $source_id ];
		}

		$fields       = $this->get_formidable_fields( $source_id );
		$results      = array();
		$repeater_map = array();
		$likert_rows  = array();

		foreach ( $fields as $field ) {
			$likert_id = $this->get_likert_parent_id( $field );
			if ( $likert_id > 0 ) {
				$likert_rows[ $likert_id ][] = array(
					'id'    => (int) $field->id,
					'label' => isset( $field->name ) ? (string) $field->name : '',
				);
			}
		}

		foreach ( $fields as $field ) {
			if ( ! $this->is_repeater_field( $field ) ) {
				continue;
			}

			$child_form_id                  = $this->get_repeater_child_form_id( $field );
			$repeater_map[ $child_form_id ] = array(
				'id'            => (int) $field->id,
				'child_form_id' => $child_form_id,
				'columns'       => array(),
			);
		}

		foreach ( $fields as $field ) {
			if ( ! is_object( $field ) || ! isset( $field->id, $field->type ) ) {
				continue;
			}
			if ( $this->get_likert_parent_id( $field ) > 0 ) {
				continue;
			}

			$is_repeater = $this->is_repeater_field( $field );
			$type        = $is_repeater ? 'repeater' : (string) $field->type;
			$descriptor  = array(
				'id'              => (int) $field->id,
				'key'             => isset( $field->field_key ) ? (string) $field->field_key : '',
				'label'           => isset( $field->name ) ? (string) $field->name : '',
				'type'            => $type,
				'is_pro'          => $is_repeater || $this->is_pro_field( $type ),
				'render_as'       => $is_repeater ? 'table' : $this->get_render_type( $type ),
				'display_options' => $this->get_display_options( $field, $type ),
			);

			if ( isset( $likert_rows[ (int) $field->id ] ) ) {
				$descriptor['likert_rows'] = $likert_rows[ (int) $field->id ];
			}

			if ( $is_repeater ) {
				$descriptor['child_form_id'] = $this->get_repeater_child_form_id( $field );
			} elseif ( isset( $repeater_map[ (int) ( $field->form_id ?? 0 ) ] ) ) {
				$parent_id                        = $repeater_map[ (int) $field->form_id ]['id'];
				$descriptor['parent_repeater_id'] = $parent_id;
				$descriptor['is_pro']             = true;
				$repeater_map[ (int) $field->form_id ]['columns'][] = array(
					'id'        => (int) $field->id,
					'key'       => $descriptor['key'],
					'label'     => $descriptor['label'],
					'type'      => $type,
					'render_as' => $descriptor['render_as'],
				);
			}

			$results[] = $descriptor;
		}

		foreach ( $results as &$descriptor ) {
			if ( 'repeater' !== $descriptor['type'] ) {
				continue;
			}
			$child_form_id         = (int) $descriptor['child_form_id'];
			$descriptor['columns'] = $repeater_map[ $child_form_id ]['columns'];
		}
		unset( $descriptor );

		$this->repeaters_cache[ $source_id ] = array_values( $repeater_map );
		$this->fields_cache[ $source_id ]    = $results;

		return $results;
	}

	/**
	 * Get one entry's raw stored values, keyed by field ID.
	 *
	 * @param int $source_id Formidable form ID.
	 * @param int $entry_id  Formidable entry ID.
	 * @return array<int, mixed>
	 */
	public function get_entry_values( int $source_id, int $entry_id ): array {
		if ( $source_id < 1 || $entry_id < 1 ) {
			return array();
		}

		$this->assert_formidable_class( 'FrmEntry' );
		$this->assert_formidable_class( 'FrmEntriesHelper' );

		$entry = \FrmEntry::getOne( $entry_id, true );

		if (
			! is_object( $entry ) ||
			! isset( $entry->form_id ) ||
			$source_id !== (int) $entry->form_id ||
			! isset( $entry->metas ) ||
			! is_array( $entry->metas )
		) {
			return array();
		}

		$values = array();

		foreach ( $entry->metas as $field_id => $value ) {
			if ( is_numeric( $field_id ) ) {
				$values[ (int) $field_id ] = $value;
			}
		}

		$records = array();
		foreach ( $this->get_formidable_fields( $source_id ) as $field ) {
			$records[ (int) $field->id ] = $field;
		}

		$this->get_fields( $source_id );
		foreach ( $this->repeaters_cache[ $source_id ] as $repeater ) {
			$rows = \FrmEntry::getAll(
				array(
					'it.form_id'        => $repeater['child_form_id'],
					'it.parent_item_id' => $entry_id,
				),
				' ORDER BY it.id ASC',
				'',
				true,
				false
			);

			$values[ $repeater['id'] ] = $this->normalize_repeater_rows( $rows, $records );
		}

		return $values;
	}

	/**
	 * Get Formidable-formatted values for one entry and every supported mode.
	 *
	 * Raw values remain available separately for calculations and conditions.
	 *
	 * @param int $source_id Formidable form ID.
	 * @param int $entry_id  Formidable entry ID.
	 * @return array<int, array<string, string>>
	 */
	public function get_entry_display_values( int $source_id, int $entry_id ): array {
		if ( $source_id < 1 || $entry_id < 1 ) {
			return array();
		}

		$this->assert_formidable_class( 'FrmEntry' );
		$this->assert_formidable_class( 'FrmEntriesHelper' );
		$entry = \FrmEntry::getOne( $entry_id, true );
		if ( ! is_object( $entry ) || (int) ( $entry->form_id ?? 0 ) !== $source_id ) {
			return array();
		}

		$records = array();
		foreach ( $this->get_formidable_fields( $source_id ) as $field ) {
			$records[ (int) $field->id ] = $field;
		}

		$display_values = array();
		foreach ( $this->get_fields( $source_id ) as $descriptor ) {
			$field_id = (int) $descriptor['id'];
			if ( ! isset( $records[ $field_id ] ) || 'repeater' === $descriptor['type'] ) {
				continue;
			}
			$field = $records[ $field_id ];
			$raw   = isset( $entry->metas[ $field_id ] ) ? $entry->metas[ $field_id ] : '';
			$atts  = array(
				'entry_id' => $entry_id,
				'entry'    => $entry,
				'html'     => 0,
				'keepjs'   => 0,
				'sep'      => ', ',
			);

			foreach ( $descriptor['display_options'] as $option ) {
				$mode = (string) $option['value'];
				if ( 'raw' === $mode ) {
					$value = $raw;
				} else {
					$mode_atts = $atts;
					if ( 'display' !== $mode ) {
						$mode_atts['show'] = $mode;
					}
					$value = \FrmEntriesHelper::display_value( $raw, $field, $mode_atts );
				}
				$display_values[ $field_id ][ $mode ] = $this->normalize_display_value( $value );
			}
		}

		return $display_values;
	}

	/**
	 * Convert Formidable child entries into neutral repeater rows.
	 *
	 * @param mixed              $entries Formidable child entries.
	 * @param array<int, object> $records Formidable field records keyed by field ID.
	 * @return array<int, array{entry_id:int, values:array<int, mixed>, display_values:array<int, string>}>
	 */
	private function normalize_repeater_rows( $entries, array $records ): array {
		$rows = array();

		foreach ( is_array( $entries ) ? $entries : array() as $entry ) {
			if ( ! is_object( $entry ) || ! isset( $entry->id, $entry->metas ) || ! is_array( $entry->metas ) ) {
				continue;
			}

			$values         = array();
			$display_values = array();
			foreach ( $entry->metas as $field_id => $value ) {
				if ( is_numeric( $field_id ) ) {
					$field_id            = (int) $field_id;
					$values[ $field_id ] = $value;
					if ( isset( $records[ $field_id ] ) ) {
						$display_values[ $field_id ] = $this->normalize_display_value(
							\FrmEntriesHelper::display_value(
								$value,
								$records[ $field_id ],
								array(
									'entry_id' => (int) $entry->id,
									'entry'    => $entry,
									'html'     => 0,
									'keepjs'   => 0,
									'sep'      => ', ',
								)
							)
						);
					}
				}
			}

			$rows[] = array(
				'entry_id'       => (int) $entry->id,
				'values'         => $values,
				'display_values' => $display_values,
			);
		}

		return $rows;
	}

	/**
	 * Get recent entries for the builder's preview-entry selector.
	 *
	 * @param int $source_id Formidable form ID.
	 * @return array<int, array{id:int, label:string}>
	 */
	public function get_entries( int $source_id ): array {
		if ( $source_id < 1 ) {
			return array();
		}

		$this->assert_formidable_class( 'FrmEntry' );

		$entries = \FrmEntry::getAll(
			array( 'it.form_id' => $source_id ),
			' ORDER BY it.created_at DESC, it.id DESC',
			100
		);
		$results = array();

		foreach ( is_array( $entries ) ? $entries : array() as $entry ) {
			if ( ! is_object( $entry ) || ! isset( $entry->id ) ) {
				continue;
			}

			$entry_id  = (int) $entry->id;
			$name      = isset( $entry->name ) ? trim( (string) $entry->name ) : '';
			$results[] = array(
				'id'    => $entry_id,
				'label' => '#' . $entry_id . ( '' !== $name ? ', ' . $name : '' ),
			);
		}

		return $results;
	}

	/**
	 * Get the most recently created entry for live preview.
	 *
	 * @param int $source_id Formidable form ID.
	 * @return array{entry_id:int, values:array<int, mixed>}|null
	 */
	public function get_preview_entry( int $source_id ): ?array {
		if ( $source_id < 1 ) {
			return null;
		}

		$this->assert_formidable_class( 'FrmEntry' );

		$entries = \FrmEntry::getAll(
			array( 'it.form_id' => $source_id ),
			' ORDER BY it.created_at DESC, it.id DESC',
			1
		);
		$entry   = is_array( $entries ) ? reset( $entries ) : false;

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

		$entry_id = (int) $entry->id;

		return array(
			'entry_id'       => $entry_id,
			'values'         => $this->get_entry_values( $source_id, $entry_id ),
			'display_values' => $this->get_entry_display_values( $source_id, $entry_id ),
		);
	}

	/**
	 * Get and cache Formidable field records.
	 *
	 * @param int $source_id Formidable form ID.
	 * @return array<int, object>
	 */
	private function get_formidable_fields( int $source_id ): array {
		if ( isset( $this->field_records_cache[ $source_id ] ) ) {
			return $this->field_records_cache[ $source_id ];
		}
		$this->assert_formidable_class( 'FrmField' );
		$fields = \FrmField::get_all_for_form( $source_id );
		$fields = is_array( $fields ) ? array_values( $fields ) : array();

		// Formidable stores repeater children in a generated child form. The
		// parent form query contains the divider, but not the columns that the
		// document builder needs for its neutral repeater descriptor.
		$known_ids = array();
		foreach ( $fields as $field ) {
			if ( is_object( $field ) && isset( $field->id ) ) {
				$known_ids[ (int) $field->id ] = true;
			}
		}
		foreach ( $fields as $field ) {
			if ( ! $this->is_repeater_field( $field ) ) {
				continue;
			}
			$child_form_id = $this->get_repeater_child_form_id( $field );
			$child_fields  = \FrmField::get_all_for_form( $child_form_id );
			foreach ( is_array( $child_fields ) ? $child_fields : array() as $child_field ) {
				$child_id = is_object( $child_field ) && isset( $child_field->id ) ? (int) $child_field->id : 0;
				if ( $child_id < 1 || isset( $known_ids[ $child_id ] ) ) {
					continue;
				}
				$known_ids[ $child_id ] = true;
				$fields[]               = $child_field;
			}
		}

		$this->field_records_cache[ $source_id ] = $fields;
		return $this->field_records_cache[ $source_id ];
	}

	/**
	 * Return the parent Likert field ID for an internal row field.
	 *
	 * @param object $field Formidable field record.
	 * @return int
	 */
	private function get_likert_parent_id( $field ): int {
		return is_object( $field ) && isset( $field->field_options['likert_id'] )
			? (int) $field->field_options['likert_id']
			: 0;
	}

	/**
	 * Describe field-specific display modes for the builder.
	 *
	 * @param object $field Formidable field record.
	 * @param string $type  Field type.
	 * @return array<int, array{value:string,label:string}>
	 */
	private function get_display_options( $field, string $type ): array {
		$options = array(
			array(
				'value' => 'display',
				'label' => 'Formatted / display value',
			),
		);
		if ( 'user_id' === $type ) {
			return array_merge(
				$options,
				array(
					array(
						'value' => 'id',
						'label' => 'User ID',
					),
					array(
						'value' => 'user_login',
						'label' => 'Username',
					),
					array(
						'value' => 'user_email',
						'label' => 'Email address',
					),
					array(
						'value' => 'display_name',
						'label' => 'Display name',
					),
					array(
						'value' => 'first_name',
						'label' => 'First name',
					),
					array(
						'value' => 'last_name',
						'label' => 'Last name',
					),
				)
			);
		}
		if ( 'data' === $type ) {
			return array(
				array(
					'value' => 'display',
					'label' => 'Linked field value / label',
				),
				array(
					'value' => 'id',
					'label' => 'Linked entry ID',
				),
				array(
					'value' => 'key',
					'label' => 'Linked entry key',
				),
			);
		}
		if ( 'name' === $type ) {
			return array_merge(
				$options,
				array(
					array(
						'value' => 'first',
						'label' => 'First name',
					),
					array(
						'value' => 'last',
						'label' => 'Last name',
					),
				)
			);
		}

		$format    = is_object( $field ) && isset( $field->field_options['format'] ) ? (string) $field->field_options['format'] : '';
		$raw_types = array( 'radio', 'checkbox', 'select', 'lookup', 'number', 'total', 'product', 'quantity', 'range', 'time', 'scale', 'nps', 'toggle' );
		if ( in_array( $type, $raw_types, true ) || 'currency' === $format ) {
			$options[] = array(
				'value' => 'raw',
				'label' => 'Stored value',
			);
		}
		return $options;
	}

	/**
	 * Convert a native display value into safe readable PDF text.
	 *
	 * @param mixed $value Native Formidable display value.
	 * @return string
	 */
	private function normalize_display_value( $value ): string {
		if ( is_array( $value ) ) {
			$parts = array();
			array_walk_recursive(
				$value,
				static function ( $part ) use ( &$parts ) {
					$parts[] = (string) $part;
				}
			);
			$value = implode( ', ', $parts );
		}
		$value = preg_replace( '/<(?:br\s*\/?|\/(?:div|p|li|tr))\s*>/i', "\n", (string) $value );
		$value = html_entity_decode( wp_strip_all_tags( (string) $value ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$value = preg_replace( "/[\t ]+\n/", "\n", $value );
		$value = preg_replace( "/\n{3,}/", "\n\n", $value );
		return trim( (string) $value );
	}

	/**
	 * Determine whether a type belongs exclusively to Formidable's Pro registry.
	 *
	 * A type present in both registries is treated as Lite so fields moved into
	 * the base Formidable plugin do not remain incorrectly locked.
	 *
	 * @param string $type Field type identifier.
	 * @return bool
	 */
	private function is_pro_field( string $type ): bool {
		if ( null === $this->lite_field_types || null === $this->pro_field_types ) {
			$lite_types = \FrmField::field_selection();
			$pro_types  = \FrmField::pro_field_selection();

			$this->lite_field_types = array_fill_keys( array_keys( is_array( $lite_types ) ? $lite_types : array() ), true );
			$this->pro_field_types  = array_fill_keys( array_keys( is_array( $pro_types ) ? $pro_types : array() ), true );
		}

		return isset( $this->pro_field_types[ $type ] ) && ! isset( $this->lite_field_types[ $type ] );
	}

	/**
	 * Map a Formidable field type to an EntryFoundry rendering primitive.
	 *
	 * @param string $type Field type identifier.
	 * @return string
	 */
	private function get_render_type( string $type ): string {
		$image_types = array( 'file', 'image', 'signature' );

		return in_array( $type, $image_types, true ) ? 'image' : 'text';
	}

	/**
	 * Identify Formidable's repeating-section divider without calling Pro code.
	 *
	 * @param mixed $field Formidable field record.
	 * @return bool
	 */
	private function is_repeater_field( $field ): bool {
		if ( ! is_object( $field ) || ! isset( $field->id, $field->type ) ) {
			return false;
		}
		$is_repeating = method_exists( '\\FrmField', 'is_repeating_field' )
			? \FrmField::is_repeating_field( $field )
			: 'divider' === (string) $field->type && ! empty( $field->field_options['repeat'] );

		return $is_repeating && $this->get_repeater_child_form_id( $field ) > 0;
	}

	/**
	 * Read a repeater's generated child form through Formidable's option API.
	 *
	 * @param object $field Formidable field record.
	 * @return int
	 */
	private function get_repeater_child_form_id( $field ): int {
		$value = method_exists( '\\FrmField', 'get_option' )
			? \FrmField::get_option( $field, 'form_select' )
			: ( $field->field_options['form_select'] ?? 0 );

		return is_numeric( $value ) ? max( 0, (int) $value ) : 0;
	}

	/**
	 * Fail with an actionable error when the required plugin is unavailable.
	 *
	 * @param string $class_name Formidable model class.
	 * @return void
	 * @throws RuntimeException When Formidable is unavailable.
	 */
	private function assert_formidable_class( string $class_name ): void {
		if ( ! class_exists( $class_name ) ) {
			throw new RuntimeException( 'Formidable Forms is not available.' );
		}
	}
}
