<?php
/**
 * Contact Form 7.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Forms;

defined( 'ABSPATH' ) || exit;

/**
 * Contact Form 7 submissions (spec §21).
 *
 * Hooked on `wpcf7_mail_sent` rather than `wpcf7_before_send_mail`, and the
 * difference matters: the earlier hook fires while the submission is still
 * being decided, including for one that then fails validation or is caught as
 * spam. Texting somebody about a submission the form itself rejected is worse
 * than not texting at all.
 */
final class ContactForm7 implements Provider {

	/**
	 * {@inheritDoc}
	 */
	public static function slug(): string {
		return 'cf7';
	}

	/**
	 * {@inheritDoc}
	 */
	public static function label(): string {
		return __( 'Contact Form 7', 'texttango' );
	}

	/**
	 * {@inheritDoc}
	 */
	public static function is_active(): bool {
		return class_exists( 'WPCF7_ContactForm' );
	}

	/**
	 * {@inheritDoc}
	 *
	 * @return array<string, string>
	 */
	public static function forms(): array {
		if ( ! self::is_active() ) {
			return array();
		}

		$forms = array();

		foreach ( \WPCF7_ContactForm::find( array( 'posts_per_page' => -1 ) ) as $form ) {
			$forms[ (string) $form->id() ] = (string) $form->title();
		}

		return $forms;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @param string $form_id Form id.
	 * @return string[]
	 */
	public static function fields( string $form_id ): array {
		if ( ! self::is_active() ) {
			return array();
		}

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

		if ( ! $form instanceof \WPCF7_ContactForm ) {
			return array();
		}

		$names = array();

		foreach ( $form->scan_form_tags() as $tag ) {
			$name = is_object( $tag ) ? (string) $tag->name : (string) ( $tag['name'] ?? '' );

			// Submit buttons and the like carry no name and hold no answer.
			if ( '' !== $name ) {
				$names[] = $name;
			}
		}

		return array_values( array_unique( $names ) );
	}

	/**
	 * {@inheritDoc}
	 */
	public function register(): void {
		add_action( 'wpcf7_mail_sent', array( $this, 'submitted' ) );
	}

	/**
	 * Normalise a submission and hand it on.
	 *
	 * @param \WPCF7_ContactForm $form The form that was submitted.
	 */
	public function submitted( $form ): void {
		if ( ! class_exists( 'WPCF7_Submission' ) || ! is_object( $form ) ) {
			return;
		}

		$submission = \WPCF7_Submission::get_instance();

		if ( ! $submission ) {
			return;
		}

		Forms::handle(
			new Submission(
				self::slug(),
				(string) $form->id(),
				(string) $form->title(),
				self::flatten( (array) $submission->get_posted_data() )
			)
		);
	}

	/**
	 * Reduce posted data to one string per field.
	 *
	 * Checkbox and multi-select fields arrive as arrays. They are joined rather
	 * than dropped, because a merchant reasonably expects a multi-choice answer
	 * to appear in the message, and a consent checkbox arrives this way too — as
	 * a one-element array whose emptiness is the whole signal.
	 *
	 * @param array<string, mixed> $posted Posted data.
	 * @return array<string, string>
	 */
	private static function flatten( array $posted ): array {
		$fields = array();

		foreach ( $posted as $name => $value ) {
			if ( ! is_string( $name ) ) {
				continue;
			}

			if ( is_array( $value ) ) {
				$value = implode( ', ', array_filter( array_map( 'strval', $value ), 'strlen' ) );
			}

			if ( is_scalar( $value ) ) {
				$fields[ $name ] = (string) $value;
			}
		}

		return $fields;
	}
}
