<?php
/**
 * Lead Form Generator — Schema
 *
 * Defines the canonical shape of a lead form (steps → questions → conditional
 * branching), provides defaults, and handles sanitization/validation of the
 * admin-supplied JSON payload.
 *
 * Schema:
 *   array(
 *     'enabled'            => bool,
 *     'mode'               => 'chat' | 'card',
 *     'trigger'            => 'immediate' | 'after_delay' | 'after_first_user_msg',
 *     'trigger_delay_ms'   => int,
 *     'partial_submit'     => bool,   // email/phone reached → send lead even if flow not done
 *     'completion_text'    => string,
 *     'completion_media_id'=> int,
 *     'steps'              => array(
 *         array(
 *             'id'        => string (slug, unique within form),
 *             'label'     => string,
 *             'questions' => array(
 *                 array(
 *                     'id'          => string (slug, unique within form),
 *                     'label'       => string,
 *                     'type'        => 'text'|'textarea'|'email'|'phone'|'number'|'choice_single'|'choice_multi'|'appointment',
 *                     'required'    => bool,
 *                     'placeholder' => string,
 *                     'options'     => array( array('value' => string, 'label' => string), ... ),
 *                     'validation'  => array( 'min' => int|null, 'max' => int|null, 'regex' => string ),
 *                     'conditional' => array(
 *                         'enabled'         => bool,
 *                         'rules'           => array(
 *                             array(
 *                                 'match_type'    => 'equals'|'contains'|'regex'|'any_of',
 *                                 'match_value'   => string|array,
 *                                 'next_question' => string (question id) | '__complete__',
 *                             ),
 *                             ...
 *                         ),
 *                         'llm_enabled'     => bool,
 *                         'llm_instruction' => string,   // admin's guidance for smart branching
 *                         'llm_candidates'  => array,    // list of question ids LLM may choose from
 *                     ),
 *                 ),
 *                 ...
 *             ),
 *         ),
 *         ...
 *     ),
 *   )
 *
 * @package Niwa_AI
 * @since   0.12.0
 */

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

class Niwa_AI_Lead_Form_Schema {

	const MODE_CHAT = 'chat';
	const MODE_CARD = 'card';

	const TRIGGER_IMMEDIATE            = 'immediate';
	const TRIGGER_AFTER_DELAY          = 'after_delay';
	const TRIGGER_AFTER_FIRST_USER_MSG = 'after_first_user_msg';
	/**
	 * Agent-only trigger: JS auto-arm is skipped entirely; the flow starts
	 * only when the frontend Niwa chat agent calls the `activate_lead_form`
	 * tool (driven by the admin-supplied trigger_description).
	 */
	const TRIGGER_AI_ONLY              = 'ai_only';

	const QUESTION_TYPES = array(
		'text',
		'textarea',
		'email',
		'phone',
		'number',
		'choice_single',
		'choice_multi',
		'appointment',
	);

	const MATCH_TYPES = array( 'equals', 'contains', 'regex', 'any_of' );

	const COMPLETE_SENTINEL = '__complete__';

	/**
	 * Full defaults for the whole module option.
	 *
	 * @return array
	 */
	public static function defaults() {
		return array(
			'enabled'             => 0,
			'mode'                => self::MODE_CHAT,
			'trigger'             => self::TRIGGER_AFTER_FIRST_USER_MSG,
			'trigger_delay_ms'    => 3000,
			'trigger_description' => '',
			'partial_submit'      => 1,
			'completion_text'     => __( 'Hvala! Primili smo vaše odgovore i javljamo se uskoro.', 'niwa-ai' ),
			'completion_media_id' => 0,
			'steps'               => array(),
		);
	}

	/**
	 * Default shape for a new question (used by the admin builder).
	 *
	 * @param string $id Optional predetermined id.
	 * @return array
	 */
	public static function question_defaults( $id = '' ) {
		return array(
			'id'          => $id !== '' ? $id : self::generate_id( 'q' ),
			'label'       => '',
			'type'        => 'text',
			'required'    => 1,
			'placeholder' => '',
			'options'     => array(),
			'appointment' => array(
				'service_ids'     => array(),
				'team_member_ids' => array(),
			),
			'validation'  => array(
				'min'   => null,
				'max'   => null,
				'regex' => '',
			),
			'conditional' => array(
				'enabled'         => 0,
				'rules'           => array(),
				'llm_enabled'     => 0,
				'llm_instruction' => '',
				'llm_candidates'  => array(),
			),
		);
	}

	/**
	 * Default shape for a new step.
	 *
	 * @param string $id Optional.
	 * @return array
	 */
	public static function step_defaults( $id = '' ) {
		return array(
			'id'        => $id !== '' ? $id : self::generate_id( 's' ),
			'label'     => '',
			'questions' => array(),
		);
	}

	/**
	 * Generate a short unique id for steps/questions.
	 *
	 * @param string $prefix Prefix.
	 * @return string
	 */
	public static function generate_id( $prefix = 'id' ) {
		return $prefix . '_' . substr( md5( microtime( true ) . wp_rand() ), 0, 8 );
	}

	/**
	 * Full sanitize+normalize of the option payload from admin form POST.
	 *
	 * Unknown keys are dropped. Unknown enum values fall back to defaults.
	 * Structural integrity is enforced (steps is array, questions is array, etc.).
	 *
	 * @param mixed $input Raw input (likely array, possibly string JSON from a hidden field).
	 * @return array Sanitized schema.
	 */
	public static function sanitize( $input ) {
		$defaults = self::defaults();

		// Admin builder may POST the whole schema as a JSON string in a hidden field.
		if ( is_string( $input ) ) {
			$decoded = json_decode( $input, true );
			$input   = is_array( $decoded ) ? $decoded : array();
		}

		if ( ! is_array( $input ) ) {
			return $defaults;
		}

		$out                        = $defaults;
		$out['enabled']             = ! empty( $input['enabled'] ) ? 1 : 0;
		$out['partial_submit']      = array_key_exists( 'partial_submit', $input ) ? ( ! empty( $input['partial_submit'] ) ? 1 : 0 ) : $defaults['partial_submit'];
		$out['mode']                = in_array( isset( $input['mode'] ) ? $input['mode'] : '', array( self::MODE_CHAT, self::MODE_CARD ), true )
			? $input['mode']
			: $defaults['mode'];
		$out['trigger']             = in_array(
			isset( $input['trigger'] ) ? $input['trigger'] : '',
			array(
				self::TRIGGER_IMMEDIATE,
				self::TRIGGER_AFTER_DELAY,
				self::TRIGGER_AFTER_FIRST_USER_MSG,
				self::TRIGGER_AI_ONLY,
			),
			true
		) ? $input['trigger'] : $defaults['trigger'];
		$out['trigger_delay_ms']    = isset( $input['trigger_delay_ms'] ) ? max( 0, min( 60000, (int) $input['trigger_delay_ms'] ) ) : $defaults['trigger_delay_ms'];
		$out['trigger_description'] = isset( $input['trigger_description'] ) ? sanitize_textarea_field( (string) $input['trigger_description'] ) : $defaults['trigger_description'];
		$out['completion_text']     = isset( $input['completion_text'] ) ? sanitize_textarea_field( (string) $input['completion_text'] ) : $defaults['completion_text'];
		$out['completion_media_id'] = isset( $input['completion_media_id'] ) ? absint( $input['completion_media_id'] ) : 0;

		$raw_steps    = isset( $input['steps'] ) && is_array( $input['steps'] ) ? $input['steps'] : array();
		$out['steps'] = self::sanitize_steps( $raw_steps );

		return $out;
	}

	/**
	 * Sanitize an array of steps.
	 *
	 * @param array $steps Raw.
	 * @return array
	 */
	private static function sanitize_steps( array $steps ) {
		$seen_ids = array();
		$out      = array();

		foreach ( $steps as $step ) {
			if ( ! is_array( $step ) ) {
				continue;
			}

			$clean          = self::step_defaults();
			$clean['id']    = self::sanitize_slug( isset( $step['id'] ) ? $step['id'] : '', 's' );
			$clean['label'] = isset( $step['label'] ) ? sanitize_text_field( (string) $step['label'] ) : '';

			// Enforce unique step ids within the form.
			if ( isset( $seen_ids[ $clean['id'] ] ) ) {
				$clean['id'] = self::generate_id( 's' );
			}
			$seen_ids[ $clean['id'] ] = true;

			$raw_questions      = isset( $step['questions'] ) && is_array( $step['questions'] ) ? $step['questions'] : array();
			$clean['questions'] = self::sanitize_questions( $raw_questions, $seen_ids );
			$out[]              = $clean;
		}

		return $out;
	}

	/**
	 * Sanitize an array of questions.
	 *
	 * @param array $questions Raw.
	 * @param array &$seen_ids Global id set (shared across steps+questions for uniqueness).
	 * @return array
	 */
	private static function sanitize_questions( array $questions, array &$seen_ids ) {
		$out = array();

		foreach ( $questions as $q ) {
			if ( ! is_array( $q ) ) {
				continue;
			}

			$clean = self::question_defaults();

			$clean['id'] = self::sanitize_slug( isset( $q['id'] ) ? $q['id'] : '', 'q' );
			if ( isset( $seen_ids[ $clean['id'] ] ) ) {
				$clean['id'] = self::generate_id( 'q' );
			}
			$seen_ids[ $clean['id'] ] = true;

			$clean['label']       = isset( $q['label'] ) ? sanitize_text_field( (string) $q['label'] ) : '';
			$clean['type']        = in_array( isset( $q['type'] ) ? $q['type'] : '', self::QUESTION_TYPES, true ) ? $q['type'] : 'text';
			$clean['required']    = ! empty( $q['required'] ) ? 1 : 0;
			$clean['placeholder'] = isset( $q['placeholder'] ) ? sanitize_text_field( (string) $q['placeholder'] ) : '';

			$clean['options']     = self::sanitize_options( isset( $q['options'] ) ? $q['options'] : array() );
			$clean['appointment'] = self::sanitize_appointment( isset( $q['appointment'] ) ? $q['appointment'] : array() );
			$clean['validation']  = self::sanitize_validation( isset( $q['validation'] ) ? $q['validation'] : array() );
			$clean['conditional'] = self::sanitize_conditional( isset( $q['conditional'] ) ? $q['conditional'] : array() );

			$out[] = $clean;
		}

		return $out;
	}

	/**
	 * Sanitize Scheduler restrictions for an appointment question.
	 *
	 * Empty lists intentionally mean "all currently active" so a lead form does
	 * not have to be edited every time a service or team member is added.
	 *
	 * @param mixed $appointment Raw appointment settings.
	 * @return array{service_ids:array,team_member_ids:array}
	 */
	private static function sanitize_appointment( $appointment ) {
		$out = array(
			'service_ids'     => array(),
			'team_member_ids' => array(),
		);

		if ( ! is_array( $appointment ) ) {
			return $out;
		}

		foreach ( array_keys( $out ) as $key ) {
			$values = isset( $appointment[ $key ] ) && is_array( $appointment[ $key ] )
				? $appointment[ $key ]
				: array();
			foreach ( $values as $value ) {
				$value = substr( sanitize_text_field( (string) $value ), 0, 100 );
				if ( '' !== $value && ! in_array( $value, $out[ $key ], true ) ) {
					$out[ $key ][] = $value;
				}
			}
		}

		return $out;
	}

	/**
	 * Sanitize option rows for a choice question.
	 *
	 * @param mixed $options Raw.
	 * @return array
	 */
	private static function sanitize_options( $options ) {
		if ( ! is_array( $options ) ) {
			return array();
		}

		$out = array();
		foreach ( $options as $opt ) {
			if ( ! is_array( $opt ) ) {
				continue;
			}
			$value = isset( $opt['value'] ) ? sanitize_text_field( (string) $opt['value'] ) : '';
			$label = isset( $opt['label'] ) ? sanitize_text_field( (string) $opt['label'] ) : '';

			if ( $value === '' && $label === '' ) {
				continue;
			}
			if ( $value === '' ) {
				$value = $label;
			}
			$out[] = array(
				'value' => $value,
				'label' => $label !== '' ? $label : $value,
			);
		}
		return $out;
	}

	/**
	 * Sanitize validation block.
	 *
	 * @param mixed $v Raw.
	 * @return array
	 */
	private static function sanitize_validation( $v ) {
		$default = array(
			'min'   => null,
			'max'   => null,
			'regex' => '',
		);
		if ( ! is_array( $v ) ) {
			return $default;
		}
		$min = isset( $v['min'] ) && $v['min'] !== '' && $v['min'] !== null ? (int) $v['min'] : null;
		$max = isset( $v['max'] ) && $v['max'] !== '' && $v['max'] !== null ? (int) $v['max'] : null;

		return array(
			'min'   => $min,
			'max'   => $max,
			'regex' => isset( $v['regex'] ) ? sanitize_text_field( (string) $v['regex'] ) : '',
		);
	}

	/**
	 * Sanitize conditional branching block.
	 *
	 * @param mixed $c Raw.
	 * @return array
	 */
	private static function sanitize_conditional( $c ) {
		$default = array(
			'enabled'         => 0,
			'rules'           => array(),
			'llm_enabled'     => 0,
			'llm_instruction' => '',
			'llm_candidates'  => array(),
		);
		if ( ! is_array( $c ) ) {
			return $default;
		}

		$out                    = $default;
		$out['enabled']         = ! empty( $c['enabled'] ) ? 1 : 0;
		$out['llm_enabled']     = ! empty( $c['llm_enabled'] ) ? 1 : 0;
		$out['llm_instruction'] = isset( $c['llm_instruction'] ) ? sanitize_textarea_field( (string) $c['llm_instruction'] ) : '';

		$candidates = isset( $c['llm_candidates'] ) && is_array( $c['llm_candidates'] ) ? $c['llm_candidates'] : array();
		foreach ( $candidates as $cand ) {
			$slug = self::sanitize_slug( (string) $cand, '' );
			if ( $slug !== '' ) {
				$out['llm_candidates'][] = $slug;
			}
		}

		$rules = isset( $c['rules'] ) && is_array( $c['rules'] ) ? $c['rules'] : array();
		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}
			$match_type = in_array( isset( $rule['match_type'] ) ? $rule['match_type'] : '', self::MATCH_TYPES, true )
				? $rule['match_type']
				: 'equals';

			$match_value_raw = isset( $rule['match_value'] ) ? $rule['match_value'] : '';
			if ( is_array( $match_value_raw ) ) {
				$match_value = array_map( 'sanitize_text_field', array_map( 'strval', $match_value_raw ) );
			} else {
				$match_value = sanitize_text_field( (string) $match_value_raw );
			}

			$next = isset( $rule['next_question'] ) ? (string) $rule['next_question'] : '';
			if ( $next === self::COMPLETE_SENTINEL ) {
				$next_clean = self::COMPLETE_SENTINEL;
			} else {
				$next_clean = self::sanitize_slug( $next, '' );
			}

			if ( $next_clean === '' ) {
				continue;
			}

			$out['rules'][] = array(
				'match_type'    => $match_type,
				'match_value'   => $match_value,
				'next_question' => $next_clean,
			);
		}

		return $out;
	}

	/**
	 * Sanitize an id/slug. Limits to [a-z0-9_] and length 64.
	 *
	 * @param string $raw      Raw.
	 * @param string $fallback Prefix if raw ends up empty ('s'|'q'|''). Empty string → return ''.
	 * @return string
	 */
	public static function sanitize_slug( $raw, $fallback = '' ) {
		$raw = strtolower( (string) $raw );
		$raw = preg_replace( '/[^a-z0-9_]/', '_', $raw );
		$raw = trim( (string) $raw, '_' );
		$raw = substr( $raw, 0, 64 );
		if ( $raw === '' && $fallback !== '' ) {
			return self::generate_id( $fallback );
		}
		return $raw;
	}

	/**
	 * Read stored module option (merged with defaults).
	 *
	 * @return array
	 */
	public static function get() {
		$stored = get_option( Niwa_AI_Constants::OPT_LEAD_FORM_GENERATOR, array() );
		$stored = is_array( $stored ) ? $stored : array();
		return array_replace_recursive( self::defaults(), $stored );
	}

	/**
	 * Flatten schema into a question lookup table.
	 *
	 * @param array $schema Full schema (output of get()).
	 * @return array<string, array{step_index:int,question_index:int,question:array}>
	 */
	public static function question_index( array $schema ) {
		$map   = array();
		$steps = isset( $schema['steps'] ) && is_array( $schema['steps'] ) ? $schema['steps'] : array();
		foreach ( $steps as $si => $step ) {
			if ( empty( $step['questions'] ) || ! is_array( $step['questions'] ) ) {
				continue;
			}
			foreach ( $step['questions'] as $qi => $q ) {
				if ( empty( $q['id'] ) ) {
					continue;
				}
				$map[ $q['id'] ] = array(
					'step_index'     => $si,
					'question_index' => $qi,
					'question'       => $q,
				);
			}
		}
		return $map;
	}

	/**
	 * Return the next question id in document order (ignoring conditional rules).
	 *
	 * @param array  $schema      Full schema.
	 * @param string $current_qid Current question id.
	 * @return string|null Next question id, or null at end of form.
	 */
	public static function next_in_order( array $schema, $current_qid ) {
		$index = self::question_index( $schema );
		if ( ! isset( $index[ $current_qid ] ) ) {
			return null;
		}
		$current = $index[ $current_qid ];
		$steps   = isset( $schema['steps'] ) && is_array( $schema['steps'] ) ? $schema['steps'] : array();

		// Same-step next question.
		$si = $current['step_index'];
		$qi = $current['question_index'];
		if ( isset( $steps[ $si ]['questions'][ $qi + 1 ]['id'] ) ) {
			return $steps[ $si ]['questions'][ $qi + 1 ]['id'];
		}

		// First question of next non-empty step.
		$n = count( $steps );
		for ( $i = $si + 1; $i < $n; $i++ ) {
			if ( ! empty( $steps[ $i ]['questions'][0]['id'] ) ) {
				return $steps[ $i ]['questions'][0]['id'];
			}
		}

		return null;
	}

	/**
	 * Return the first question id of the form (start of flow).
	 *
	 * @param array $schema Full schema.
	 * @return string|null
	 */
	public static function first_question_id( array $schema ) {
		$steps = isset( $schema['steps'] ) && is_array( $schema['steps'] ) ? $schema['steps'] : array();
		foreach ( $steps as $step ) {
			if ( ! empty( $step['questions'][0]['id'] ) ) {
				return $step['questions'][0]['id'];
			}
		}
		return null;
	}

	/**
	 * Total count of questions in the form.
	 *
	 * @param array $schema Full schema.
	 * @return int
	 */
	public static function total_questions( array $schema ) {
		$count = 0;
		$steps = isset( $schema['steps'] ) && is_array( $schema['steps'] ) ? $schema['steps'] : array();
		foreach ( $steps as $step ) {
			if ( ! empty( $step['questions'] ) && is_array( $step['questions'] ) ) {
				$count += count( $step['questions'] );
			}
		}
		return $count;
	}
}
