<?php
/**
 * Conditional field visibility for Basic Forms — packed only in the Pro overlay.
 *
 * Secure design: Lite treats every field as visible when this class is absent.
 * Stored show_if rules are preserved on Lite save and applied only when the
 * overlay is present and licensed.
 *
 * @package BasicForms
 */

namespace BasicForms;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Evaluates show_if rules on the server during submission.
 */
final class ConditionalFields {

	/** @var list<string> */
	private const OPERATORS = [ 'equals', 'not_equals', 'contains', 'not_empty', 'empty' ];

	/**
	 * Normalize a single rule into {field_id, operator, value} or null when incomplete.
	 *
	 * @param array<string, mixed> $rule
	 * @return array{field_id: string, operator: string, value: string}|null
	 */
	private static function normalizeRule( array $rule ): ?array {
		$field_id = isset( $rule['field_id'] ) ? sanitize_key( (string) $rule['field_id'] ) : '';
		if ( $field_id === '' ) {
			return null;
		}

		$operator = isset( $rule['operator'] ) ? sanitize_key( (string) $rule['operator'] ) : 'equals';
		if ( ! in_array( $operator, self::OPERATORS, true ) ) {
			$operator = 'equals';
		}

		return [
			'field_id' => $field_id,
			'operator' => $operator,
			'value'    => isset( $rule['value'] ) ? sanitize_text_field( (string) $rule['value'] ) : '',
		];
	}

	/**
	 * Normalize a show_if definition into {relation, rules[]}.
	 *
	 * Accepts both the legacy single-rule shape ({field_id, operator, value})
	 * and the multi-rule shape ({relation, rules:[...]}) for backward compatibility.
	 *
	 * @param array<string, mixed> $show_if
	 * @return array{relation: string, rules: list<array{field_id: string, operator: string, value: string}>}|null
	 */
	public static function normalizeShowIf( array $show_if ): ?array {
		$relation = isset( $show_if['relation'] ) && 'or' === strtolower( (string) $show_if['relation'] ) ? 'or' : 'and';

		$raw_rules = [];
		if ( isset( $show_if['rules'] ) && is_array( $show_if['rules'] ) ) {
			$raw_rules = $show_if['rules'];
		} elseif ( isset( $show_if['field_id'] ) ) {
			// Legacy single-rule format.
			$raw_rules = [ $show_if ];
		}

		$rules = [];
		foreach ( $raw_rules as $raw_rule ) {
			if ( ! is_array( $raw_rule ) ) {
				continue;
			}
			$normalized = self::normalizeRule( $raw_rule );
			if ( null !== $normalized ) {
				$rules[] = $normalized;
			}
		}

		if ( empty( $rules ) ) {
			return null;
		}

		return [
			'relation' => $relation,
			'rules'    => $rules,
		];
	}

	/**
	 * @param array<string, mixed>  $field
	 * @param list<array<string, mixed>> $fields
	 * @param array<string, mixed>  $post_data
	 */
	public static function isVisible( array $field, array $fields, array $post_data ): bool {
		if ( empty( $field['show_if'] ) || ! is_array( $field['show_if'] ) ) {
			return true;
		}

		$config = self::normalizeShowIf( $field['show_if'] );
		if ( null === $config ) {
			return true;
		}

		$is_or  = 'or' === $config['relation'];
		$result = ! $is_or; // AND starts true, OR starts false.

		foreach ( $config['rules'] as $rule ) {
			$value = self::getControllerValue( $fields, $rule['field_id'], $post_data );
			$match = self::matchesRule( $rule['operator'], $value, $rule['value'] );

			if ( $is_or ) {
				$result = $result || $match;
				if ( $result ) {
					break;
				}
			} else {
				$result = $result && $match;
				if ( ! $result ) {
					break;
				}
			}
		}

		return $result;
	}

	/**
	 * @param list<array<string, mixed>> $fields
	 */
	public static function getControllerValue( array $fields, string $field_id, array $post_data ): string {
		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$candidate_id = isset( $field['field_id'] ) ? sanitize_key( (string) $field['field_id'] ) : '';
			if ( $candidate_id !== $field_id ) {
				continue;
			}

			$post_key = 'field_' . ( $field['id'] ?? '' );
			$type     = (string) ( $field['type'] ?? 'text' );

			if ( $type === 'checkbox' ) {
				$raw = $post_data[ $post_key ] ?? null;
				if ( is_array( $raw ) ) {
					return implode(
						', ',
						array_map(
							static fn( $item ) => sanitize_text_field( (string) $item ),
							$raw
						)
					);
				}

				return ! empty( $raw ) ? '1' : '';
			}

			if ( $type === 'file' ) {
				return ! empty( $_FILES[ $post_key ]['name'] ) ? '1' : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- visibility only.
			}

			$raw = $post_data[ $post_key ] ?? '';
			if ( is_array( $raw ) ) {
				return implode(
					', ',
					array_map(
						static fn( $item ) => sanitize_text_field( (string) $item ),
						$raw
					)
				);
			}

			return sanitize_text_field( (string) $raw );
		}

		return '';
	}

	private static function matchesRule( string $operator, string $actual, string $expected ): bool {
		// Equality and containment compare case-insensitively so "Other" matches "other".
		$actual_ci   = function_exists( 'mb_strtolower' ) ? mb_strtolower( $actual ) : strtolower( $actual );
		$expected_ci = function_exists( 'mb_strtolower' ) ? mb_strtolower( $expected ) : strtolower( $expected );

		return match ( $operator ) {
			'not_equals' => $actual_ci !== $expected_ci,
			'contains'   => $expected_ci !== '' && str_contains( $actual_ci, $expected_ci ),
			'not_empty'  => $actual !== '',
			'empty'      => $actual === '',
			default      => $actual_ci === $expected_ci,
		};
	}
}
