<?php
/**
 * Contact Form 7 adapter.
 *
 * @package RewindBrevoFormBridge
 */

namespace RewindBrevoFormBridge\Adapters;

defined( 'ABSPATH' ) || exit;

/**
 * Integration with Contact Form 7.
 *
 * Every signature below was read from the INSTALLED Contact Form 7 6.1.7
 * source, not documentation:
 *
 * - Submission hook: includes/submission.php:127
 *   `do_action( 'wpcf7_mail_sent', $contact_form );`
 *   It is reached only after validation, acceptance and spam checks have all
 *   passed (submission.php:100-110), which is why this adapter has no spam
 *   handling of its own. It also fires when mail is skipped: mail() returns
 *   true early under the `wpcf7_skip_mail` filter (submission.php:733), so
 *   mail-less forms that only store entries are still captured.
 * - Submitted values: WPCF7_Submission::get_instance()->get_posted_data()
 *   (includes/submission.php:319), a flat name => value map.
 * - Forms: WPCF7_ContactForm::find() (includes/contact-form.php:77).
 * - Fields: WPCF7_ContactForm::scan_form_tags() (contact-form.php:923)
 *   returning WPCF7_FormTag objects whose `basetype` is the type without the
 *   required asterisk (includes/form-tag.php:10).
 *
 * Contact Form 7 stores no entries and has no composite fields, so there is
 * no entry id to log beyond the submission hash and nothing to flatten.
 *
 * @since 1.0.0
 */
class ContactForm7 extends AbstractAdapter {

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

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

	/**
	 * Active and at a version whose hooks we verified.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public function is_available(): bool {
		return defined( 'WPCF7_VERSION' )
			&& class_exists( '\WPCF7_ContactForm' )
			&& class_exists( '\WPCF7_Submission' )
			&& version_compare( WPCF7_VERSION, '5.0', '>=' );
	}

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

		$models = \WPCF7_ContactForm::find( array( 'posts_per_page' => 200 ) );

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

		$forms = array();

		foreach ( $models as $model ) {
			if ( ! is_object( $model ) || ! method_exists( $model, 'id' ) ) {
				continue;
			}

			$title = method_exists( $model, 'title' ) ? trim( (string) $model->title() ) : '';

			$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.
	 *
	 * Contact Form 7 form-tags carry no label, only a name — so the name is
	 * shown as the label, which is also what the site owner wrote in the tag.
	 *
	 * @since 1.0.0
	 * @param string $form_id Contact Form 7 form id.
	 * @return array [ ['key','label','type'], ... ]
	 */
	public function get_fields( string $form_id ): array {
		if ( ! $this->is_available() ) {
			return array();
		}

		$form = \WPCF7_ContactForm::get_instance( (int) $form_id );

		if ( ! is_object( $form ) || ! method_exists( $form, 'scan_form_tags' ) ) {
			return array();
		}

		$fields = array();
		$seen   = array();

		foreach ( $form->scan_form_tags() as $tag ) {
			if ( ! is_object( $tag ) || empty( $tag->name ) ) {
				continue;
			}

			$key = (string) $tag->name;

			if ( isset( $seen[ $key ] ) ) {
				continue;
			}

			$ours = $this->map_type( isset( $tag->basetype ) ? (string) $tag->basetype : '' );

			if ( null === $ours ) {
				continue; // Submit buttons, captcha, quiz and other non-data tags.
			}

			$seen[ $key ] = true;

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

		return $fields;
	}

	/**
	 * Attach to the successful-submission hook.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'wpcf7_mail_sent', array( $this, 'capture' ), 10, 1 );
	}

	/**
	 * Hook callback: ( $contact_form ).
	 *
	 * @since 1.0.0
	 * @param mixed $contact_form The WPCF7_ContactForm that was submitted.
	 * @return void
	 */
	public function capture( $contact_form ): void {
		if ( ! is_object( $contact_form ) || ! method_exists( $contact_form, 'id' ) ) {
			return;
		}

		$this->dispatch( array( 'form_id' => (string) $contact_form->id() ) );
	}

	/**
	 * Read the current submission's posted data.
	 *
	 * @since 1.0.0
	 * @param mixed $raw ['form_id' => string].
	 * @return Submission|null
	 */
	public function normalize_submission( $raw ): ?Submission {
		if ( ! is_array( $raw ) || empty( $raw['form_id'] ) || ! $this->is_available() ) {
			return null;
		}

		$submission = \WPCF7_Submission::get_instance();

		if ( ! is_object( $submission ) || ! method_exists( $submission, 'get_posted_data' ) ) {
			return null;
		}

		$posted = $submission->get_posted_data();

		if ( ! is_array( $posted ) ) {
			return null;
		}

		$fields = array();

		foreach ( $posted as $key => $value ) {
			if ( 0 === strpos( (string) $key, '_wpcf7' ) ) {
				continue; // Internal tokens: _wpcf7, _wpcf7_unit_tag, nonces.
			}

			$fields[ (string) $key ] = $value;
		}

		return new Submission(
			'cf7',
			(string) $raw['form_id'],
			$this->entry_ref( $submission ),
			$fields,
			$this->source_url(),
			$this->client_ip(),
			array( 'posted' => $fields )
		);
	}

	/**
	 * A stable reference for the delivery log.
	 *
	 * Contact Form 7 keeps no entries, so the closest thing to an entry id is
	 * the submission hash it computes for duplicate detection. Older releases
	 * lack it, in which case the log row simply carries no reference.
	 *
	 * @since 1.0.0
	 * @param object $submission The current WPCF7_Submission.
	 * @return string
	 */
	private function entry_ref( $submission ): string {
		if ( ! method_exists( $submission, 'get_posted_data_hash' ) ) {
			return '';
		}

		return (string) $submission->get_posted_data_hash();
	}

	/**
	 * Contact Form 7 basetype -> our vocabulary. Null = not mappable data.
	 *
	 * @since 1.0.0
	 * @param string $basetype Type without the required asterisk.
	 * @return string|null
	 */
	private function map_type( string $basetype ): ?string {
		switch ( $basetype ) {
			case 'text':
			case 'url':
			case 'hidden':
				return 'text';
			case 'email':
				return 'email';
			case 'number':
			case 'range':
				return 'number';
			case 'date':
				return 'date';
			case 'tel':
				return 'phone';
			case 'checkbox':
			case 'acceptance':
				return 'checkbox';
			case 'select':
			case 'radio':
				return 'select';
			case 'textarea':
				return 'textarea';
			case 'file':
				return 'file';
		}

		return null;
	}
}
