<?php
/**
 * What a merchant configured for one form.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Forms;

defined( 'ABSPATH' ) || exit;

/**
 * Per-form configuration (spec §21).
 *
 * ### Why an option, when campaign history was moved out of one
 *
 * Campaign history was moved to a table because it grew every time the merchant
 * sent something, and the option had to be trimmed — deleting records — to stay
 * a sensible size. This does not grow with use. It grows with the number of
 * forms on the site, which is a handful, changes when somebody builds a form,
 * and has a natural ceiling. An option is the right home for it.
 *
 * ### Why every form is off until configured
 *
 * A form plugin's fields have no conventions. There is no `phone` field to
 * find — a number lives in `tel-472` or `your-phone` or whatever the person who
 * built the form typed. Guessing wrong means either texting nobody, which is
 * merely useless, or texting a number out of the wrong field, which is not. So
 * nothing sends until a merchant has said which field holds the number.
 */
final class FormSettings {

	/**
	 * Option holding every form's configuration. Not autoloaded.
	 */
	public const OPTION = 'texttango_forms';

	/**
	 * The shape one form's configuration takes.
	 *
	 * @return array<string, mixed>
	 */
	public static function defaults(): array {
		return array(
			'enabled'        => false,
			'phone_field'    => '',
			'consent_field'  => '',
			'alert'          => false,
			'alert_template' => '',
			'reply'          => false,
			'reply_template' => '',
		);
	}

	/**
	 * Every configured form, keyed by `provider:form_id`.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function all(): array {
		$stored = get_option( self::OPTION, array() );

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

		$forms = array();

		foreach ( $stored as $key => $config ) {
			if ( is_string( $key ) && is_array( $config ) ) {
				$forms[ $key ] = self::normalise( $config );
			}
		}

		return $forms;
	}

	/**
	 * One form's configuration, defaulted.
	 *
	 * @param string $key Form key.
	 * @return array<string, mixed>
	 */
	public static function for_form( string $key ): array {
		return self::all()[ $key ] ?? self::defaults();
	}

	/**
	 * Fill in what is missing and drop what is not recognised.
	 *
	 * Pure. A configuration written by an older version of this screen, or hand
	 * edited, has to produce a usable shape rather than a notice on submission —
	 * which would surface as a form that stops working for the visitor.
	 *
	 * @param array<string, mixed> $config Stored configuration.
	 * @return array<string, mixed>
	 */
	public static function normalise( array $config ): array {
		$clean = self::defaults();

		foreach ( array_keys( $clean ) as $setting ) {
			if ( ! array_key_exists( $setting, $config ) ) {
				continue;
			}

			$clean[ $setting ] = is_bool( $clean[ $setting ] )
				? (bool) $config[ $setting ]
				: (string) $config[ $setting ];
		}

		return $clean;
	}

	/**
	 * Whether a reply to the person who submitted should be sent.
	 *
	 * Pure, and both halves matter. Switched on is not enough: without a field
	 * naming where the number is, there is nothing to send to, and a form left
	 * half-configured would otherwise look enabled while reaching nobody.
	 *
	 * @param array<string, mixed> $config One form's configuration.
	 */
	public static function replies( array $config ): bool {
		return ! empty( $config['enabled'] )
			&& ! empty( $config['reply'] )
			&& '' !== trim( (string) ( $config['phone_field'] ?? '' ) );
	}

	/**
	 * Whether staff should be alerted that a submission arrived.
	 *
	 * Needs no phone field: the alert goes to the numbers already configured for
	 * admin notifications, not to anything the visitor typed.
	 *
	 * @param array<string, mixed> $config One form's configuration.
	 */
	public static function alerts( array $config ): bool {
		return ! empty( $config['enabled'] ) && ! empty( $config['alert'] );
	}

	/**
	 * Whether this submission captured marketing consent.
	 *
	 * Three things have to hold, and the third is the one worth stating: the
	 * merchant nominated a consent field, that field was ticked, **and** there is
	 * a number to attach the consent to. Consent recorded against nothing is a
	 * record that cannot be honoured or withdrawn.
	 *
	 * @param array<string, mixed> $config One form's configuration.
	 * @param Submission           $submission The submission.
	 */
	public static function captures_consent( array $config, Submission $submission ): bool {
		$field = trim( (string) ( $config['consent_field'] ?? '' ) );

		return ! empty( $config['enabled'] )
			&& '' !== $field
			&& $submission->ticked( $field )
			&& '' !== trim( (string) ( $config['phone_field'] ?? '' ) );
	}

	/**
	 * One form is switched on but cannot work.
	 */
	public const STATE_ATTENTION = 'attention';

	/**
	 * One form is switched on and able to do what it says.
	 */
	public const STATE_ON = 'on';

	/**
	 * One form is not switched on.
	 */
	public const STATE_OFF = 'off';

	/**
	 * What a form is set to do, for its row on the settings screen.
	 *
	 * Pure, and it exists so a merchant does not have to open a panel to find
	 * out. With twenty forms the list is the screen, and a row that says only
	 * "on" says nothing worth reading.
	 *
	 * The state worth having is the third one. A form can be switched on, show
	 * ticked boxes, and still reach nobody — a reply with no field naming where
	 * the number is, or a consent checkbox with no number to attach consent to.
	 * `replies()` and `captures_consent()` already refuse in those cases, so the
	 * message silently never goes; until now nothing said so, and the screen
	 * showed a tick either way.
	 *
	 * Returns badges rather than a sentence, so the row reads as state — the same
	 * way a delivery status or the connection does — and the template renders
	 * without deciding anything.
	 *
	 * Capability badges are deliberately **not** coloured. The row already
	 * carries a green tick when a form works, and a second green thing beside it
	 * says nothing the tick did not. Keeping them quiet leaves the amber warning
	 * as the only coloured thing on a list of twenty forms, which is the entire
	 * reason the warning is worth having.
	 *
	 * @param array<string, mixed> $config One form's configuration.
	 * @return array{state: string, badges: array<int, array{label: string, tone: string, title: string}>}
	 */
	public static function describe( array $config ): array {
		if ( empty( $config['enabled'] ) ) {
			return array(
				'state'  => self::STATE_OFF,
				'badges' => array( self::badge( __( 'Off', 'texttango' ) ) ),
			);
		}

		$has_phone = '' !== trim( (string) ( $config['phone_field'] ?? '' ) );
		$consent   = '' !== trim( (string) ( $config['consent_field'] ?? '' ) );

		$badges = array();

		if ( ! empty( $config['alert'] ) ) {
			$badges[] = self::badge( __( 'Staff alert', 'texttango' ) );
		}

		if ( ! empty( $config['reply'] ) ) {
			$badges[] = self::badge( __( 'Reply', 'texttango' ) );
		}

		if ( $consent ) {
			$badges[] = self::badge( __( 'Consent', 'texttango' ) );
		}

		/*
		 * The short label goes on the badge and the full explanation in its
		 * title. "set to reply, but no field holds the phone number" is the right
		 * sentence and the wrong pill — a badge that long stops looking like one.
		 */
		$warning = '';
		$why     = '';

		if ( ! empty( $config['reply'] ) && ! $has_phone ) {
			$warning = __( 'Needs a phone field', 'texttango' );
			$why     = __( 'Set to reply, but no field holds the phone number, so nothing is sent.', 'texttango' );
		} elseif ( $consent && ! $has_phone ) {
			$warning = __( 'Needs a phone field', 'texttango' );
			$why     = __( 'Set to capture consent, but no field holds the phone number, so there is nothing to attach it to.', 'texttango' );
		} elseif ( array() === $badges ) {
			$warning = __( 'Does nothing', 'texttango' );
			$why     = __( 'Switched on, but not set to alert anyone, reply, or capture consent.', 'texttango' );
		}

		if ( '' === $warning ) {
			return array(
				'state'  => self::STATE_ON,
				'badges' => $badges,
			);
		}

		return array(
			'state'  => self::STATE_ATTENTION,
			'badges' => array( self::badge( $warning, self::TONE_WARN, $why ) ),
		);
	}

	/**
	 * The quiet tone, for what a form does.
	 */
	public const TONE_PLAIN = 'off';

	/**
	 * The loud one, for a form that cannot do what it says.
	 */
	public const TONE_WARN = 'warn';

	/**
	 * One badge.
	 *
	 * @param string $label Short text on the badge.
	 * @param string $tone  {@see self::TONE_PLAIN} or {@see self::TONE_WARN}.
	 * @param string $title The longer explanation, when there is one.
	 * @return array{label: string, tone: string, title: string}
	 */
	private static function badge( string $label, string $tone = self::TONE_PLAIN, string $title = '' ): array {
		return array(
			'label' => $label,
			'tone'  => $tone,
			'title' => $title,
		);
	}

	/**
	 * Store every form's configuration.
	 *
	 * @param array<string, array<string, mixed>> $forms Configuration by form key.
	 */
	public static function save( array $forms ): void {
		$clean = array();

		foreach ( $forms as $key => $config ) {
			if ( is_string( $key ) && is_array( $config ) ) {
				$clean[ $key ] = self::normalise( $config );
			}
		}

		update_option( self::OPTION, $clean, false );
	}
}
