<?php
/**
 * Lead Capture Settings & Contacts Admin Page
 *
 * Combined admin page that shows lead capture configuration,
 * contacts stats dashboard, contacts table with search/pagination,
 * and CSV export functionality.
 *
 * @package Niwa_AI
 * @since   0.7.6
 */

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

class Niwa_AI_Lead_Capture_Settings {

	use Niwa_AI_Admin_Field_Helpers;

	const OPTION_KEY = Niwa_AI_Constants::OPT_LEAD_CAPTURE;
	const LEGACY_OPTION_KEY = Niwa_AI_Constants::OPT_SETTINGS;

	/**
	 * @var Niwa_AI_Contact_Service
	 */
	private $contact_service;

	/**
	 * Appointment service, resolved lazily so the Lead Capture page keeps
	 * working during upgrades where the scheduler runtime is not loaded yet.
	 *
	 * @var Niwa_AI_Appointment_Service|null
	 */
	private $appointment_service;

	/**
	 * Whether appointment service resolution has already been attempted.
	 *
	 * @var bool
	 */
	private $appointment_service_resolved = false;

	/**
	 * @param Niwa_AI_Contact_Service $contact_service Contact service instance.
	 */
	public function __construct( Niwa_AI_Contact_Service $contact_service ) {
		$this->contact_service = $contact_service;
	}

	/**
	 * Register WordPress hooks.
	 */
	public function register_hooks() {
		add_action( 'admin_menu', array( $this, 'add_submenu' ), 20 );
		add_action( 'admin_init', array( $this, 'register_settings' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_form_generator_assets' ) );
		add_action( 'wp_ajax_niwa_ai_delete_contact', array( $this, 'ajax_delete_contact' ) );
		add_action( 'wp_ajax_niwa_ai_update_contact_details', array( $this, 'ajax_update_contact_details' ) );
		add_action( 'wp_ajax_niwa_ai_export_contacts_csv', array( $this, 'ajax_export_csv' ) );
	}

	/**
	 * Register settings for the lead capture fields.
	 */
	public function register_settings() {
		register_setting( 'niwa_ai_lead_capture_group', self::OPTION_KEY, array( $this, 'sanitize' ) );
		register_setting(
			'niwa_ai_lead_form_generator_group',
			Niwa_AI_Constants::OPT_LEAD_FORM_GENERATOR,
			array( 'Niwa_AI_Lead_Form_Schema', 'sanitize' )
		);

		if ( class_exists( 'Niwa_AI_Appointment_Service' ) && defined( 'Niwa_AI_Appointment_Service::OPTION_KEY' ) ) {
			register_setting(
				'niwa_ai_scheduler_group',
				Niwa_AI_Appointment_Service::OPTION_KEY,
				array( 'Niwa_AI_Appointment_Service', 'sanitize_settings' )
			);
		}
	}

	/**
	 * Enqueue tab-specific assets on the Lead Capture page.
	 *
	 * @param string $hook_suffix Current admin page hook.
	 * @return void
	 */
	public function enqueue_form_generator_assets( $hook_suffix ) {
		if ( 'niwa-ai_page_niwa-ai-lead-capture' !== $hook_suffix ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'settings';
		if ( in_array( $tab, array( 'contacts', 'scheduler' ), true ) ) {
			$css_rel  = 'assets/css/niwa-admin-scheduler.css';
			$js_rel   = 'assets/js/niwa-admin-scheduler.js';
			$css_path = NIWA_AI_DIR . $css_rel;
			$js_path  = NIWA_AI_DIR . $js_rel;

			wp_enqueue_style(
				'niwa-admin-scheduler',
				NIWA_AI_URL . $css_rel,
				array(),
				file_exists( $css_path ) ? (string) filemtime( $css_path ) : NIWA_AI_VERSION
			);
			wp_enqueue_script(
				'niwa-admin-scheduler',
				NIWA_AI_URL . $js_rel,
				array(),
				file_exists( $js_path ) ? (string) filemtime( $js_path ) : NIWA_AI_VERSION,
				true
			);
			wp_localize_script(
				'niwa-admin-scheduler',
				'niwaAdminScheduler',
				array(
					'ajaxUrl' => admin_url( 'admin-ajax.php' ),
					'nonce'   => wp_create_nonce( 'niwa_ai_contacts_nonce' ),
					'i18n'    => array(
						'confirmRemove' => __( 'Remove this item?', 'niwa-ai' ),
						'confirmDelete' => __( 'Are you sure you want to delete this contact?', 'niwa-ai' ),
						'saving'       => __( 'Saving...', 'niwa-ai' ),
						'saved'        => __( 'Details saved.', 'niwa-ai' ),
						'saveFailed'   => __( 'Details could not be saved.', 'niwa-ai' ),
						'editDetails'  => __( 'Edit details', 'niwa-ai' ),
						'addDetails'   => __( 'Add details', 'niwa-ai' ),
						'unnamedMember' => __( 'Unnamed member', 'niwa-ai' ),
						'addMembersFirst' => __( 'Add team members first.', 'niwa-ai' ),
						'saveMemberBeforeCalendar' => __( 'Save this member and email address before sending a calendar connection.', 'niwa-ai' ),
						'copied'        => __( 'Copied', 'niwa-ai' ),
						'copyFailed'    => __( 'Copy failed', 'niwa-ai' ),
					),
				)
			);
		}

		if ( 'form_generator' !== $tab ) {
			return;
		}

		wp_enqueue_media();

		$css_rel  = 'assets/css/niwa-admin-lead-form-builder.css';
		$js_rel   = 'assets/js/niwa-admin-lead-form-builder.js';
		$css_path = NIWA_AI_DIR . $css_rel;
		$js_path  = NIWA_AI_DIR . $js_rel;
		$css_ver  = file_exists( $css_path ) ? (string) filemtime( $css_path ) : NIWA_AI_VERSION;
		$js_ver   = file_exists( $js_path ) ? (string) filemtime( $js_path ) : NIWA_AI_VERSION;

		wp_enqueue_style(
			'niwa-admin-lead-form-builder',
			NIWA_AI_URL . $css_rel,
			array(),
			$css_ver
		);

		wp_enqueue_script(
			'niwa-admin-lead-form-builder',
			NIWA_AI_URL . $js_rel,
			array( 'jquery', 'jquery-ui-sortable' ),
			$js_ver,
			true
		);

		wp_localize_script(
			'niwa-admin-lead-form-builder',
			'niwaLeadFormBuilder',
			array(
				'schema'           => Niwa_AI_Lead_Form_Schema::get(),
				'schedulerConfig'  => $this->get_lead_form_scheduler_config(),
				'matchTypes'       => Niwa_AI_Lead_Form_Schema::MATCH_TYPES,
				'questionTypes'    => Niwa_AI_Lead_Form_Schema::QUESTION_TYPES,
				'completeSentinel' => Niwa_AI_Lead_Form_Schema::COMPLETE_SENTINEL,
				'nonce'            => wp_create_nonce( 'niwa_ai_lead_form_builder' ),
				'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
				'i18n'             => array(
					'Untitled step'                                      => __( 'Untitled step', 'niwa-ai' ),
					'Untitled question'                                  => __( 'Untitled question', 'niwa-ai' ),
					'questions'                                          => __( 'questions', 'niwa-ai' ),
					'Delete this step and all its questions?'            => __( 'Delete this step and all its questions?', 'niwa-ai' ),
					'Delete this step?'                                  => __( 'Delete this step?', 'niwa-ai' ),
					'Delete this question?'                              => __( 'Delete this question?', 'niwa-ai' ),
					'Every step needs a label.'                          => __( 'Every step needs a label.', 'niwa-ai' ),
					'Every question needs a label.'                      => __( 'Every question needs a label.', 'niwa-ai' ),
					'Choice-type questions need at least one option.'    => __( 'Choice-type questions need at least one option.', 'niwa-ai' ),
					'Form is enabled but has no steps. Add at least one step or disable the form.' => __( 'Form is enabled but has no steps. Add at least one step or disable the form.', 'niwa-ai' ),
					'select next question'                               => __( 'select next question', 'niwa-ai' ),
					'Complete form'                                      => __( 'Complete form', 'niwa-ai' ),
					'Choose completion media'                            => __( 'Choose completion media', 'niwa-ai' ),
					'Use this media'                                     => __( 'Use this media', 'niwa-ai' ),
					'Please choose an image, GIF, or video file.'        => __( 'Please choose an image, GIF, or video file.', 'niwa-ai' ),
				),
			)
		);
	}

	/**
	 * Provide the lead-form builder with only the public scheduler choices it
	 * needs for appointment steps.
	 *
	 * @return array
	 */
	private function get_lead_form_scheduler_config() {
		if ( class_exists( 'Niwa_AI_Appointment_Service' ) && is_callable( array( 'Niwa_AI_Appointment_Service', 'get_frontend_config' ) ) ) {
			try {
				$config = Niwa_AI_Appointment_Service::get_frontend_config();
				if ( is_array( $config ) ) {
					return $config;
				}
			} catch ( Throwable $exception ) {
				// Fall through to the settings-derived compatibility payload.
			}
		}

		$settings = $this->get_scheduler_settings();
		$members  = isset( $settings['team_members'] ) && is_array( $settings['team_members'] ) ? $settings['team_members'] : ( isset( $settings['members'] ) && is_array( $settings['members'] ) ? $settings['members'] : array() );
		$services = isset( $settings['services'] ) && is_array( $settings['services'] ) ? $settings['services'] : array();

		return array(
			'enabled'     => class_exists( 'Niwa_AI_Appointment_Service' ) && ! empty( $settings['enabled'] ),
			'teamMembers' => array_values( array_filter( $members, 'is_array' ) ),
			'services'    => array_values( array_filter( $services, 'is_array' ) ),
		);
	}

	/**
	 * Get lead capture settings.
	 *
	 * On the first call after upgrade, any relevant keys still sitting in the
	 * legacy shared option are copied into the dedicated key, then the legacy
	 * copies are removed so no runtime fallback remains.
	 *
	 * @return array
	 */
	public static function get_settings() {
		$defaults = self::get_defaults_static();
		$stored   = get_option( self::OPTION_KEY, array() );
		$stored   = is_array( $stored ) ? $stored : array();

		// One-time migration from legacy shared option.
		if ( empty( $stored ) ) {
			$legacy = get_option( self::LEGACY_OPTION_KEY, array() );
			$legacy = is_array( $legacy ) ? $legacy : array();

			$migrated = array();
			foreach ( array_keys( $defaults ) as $key ) {
				if ( array_key_exists( $key, $legacy ) ) {
					$migrated[ $key ] = $legacy[ $key ];
				}
			}

			if ( ! empty( $migrated ) ) {
				update_option( self::OPTION_KEY, $migrated );
				$stored = $migrated;

				// Clean lead-capture keys from shared option.
				$changed = false;
				foreach ( array_keys( $defaults ) as $key ) {
					if ( array_key_exists( $key, $legacy ) ) {
						unset( $legacy[ $key ] );
						$changed = true;
					}
				}
				if ( $changed ) {
					update_option( self::LEGACY_OPTION_KEY, $legacy, false );
				}
			}
		}

		return array_merge( $defaults, $stored );
	}

	/**
	 * Sanitize lead capture settings — merges with existing options.
	 *
	 * @param array $input Raw input.
	 * @return array Sanitized merged options.
	 */
	public function sanitize( $input ) {
		$existing = get_option( self::OPTION_KEY, array() );
		$out      = is_array( $existing ) ? $existing : array();

		// Only process if our form was submitted.
		if ( ! isset( $input['_niwa_lead_capture_form'] ) ) {
			return is_array( $input ) ? $input : $out;
		}

		// Email Capture.
		$out['email_optin_enabled']  = ! empty( $input['email_optin_enabled'] ) ? 1 : 0;
		$out['email_ask_name_again'] = ! empty( $input['email_ask_name_again'] ) ? 1 : 0;

		// Email Start Chat Cacher (mode #2).
		$out['email_start_chat_cacher_enabled'] = ! empty( $input['email_start_chat_cacher_enabled'] ) ? 1 : 0;
		$out['email_start_chat_cacher_prompt']  = isset( $input['email_start_chat_cacher_prompt'] )
			? sanitize_textarea_field( $input['email_start_chat_cacher_prompt'] )
			: '';

		// Phone Capture.
		$out['phone_capture_enabled'] = ! empty( $input['phone_capture_enabled'] ) ? 1 : 0;
		$phone_mode = isset( $input['phone_capture_mode'] ) ? sanitize_text_field( $input['phone_capture_mode'] ) : 'email_phone';
		$out['phone_capture_mode'] = in_array( $phone_mode, array( 'email_phone', 'phone_only', 'email_only' ), true ) ? $phone_mode : 'email_phone';

		// Conversion Busters Code Email Hunt.
		$out['cb_code_email_hunt_enabled'] = ! empty( $input['cb_code_email_hunt_enabled'] ) ? 1 : 0;
		$out['cb_code_email_hunt_prompt']  = isset( $input['cb_code_email_hunt_prompt'] )
			? sanitize_textarea_field( $input['cb_code_email_hunt_prompt'] )
			: '';
		$out['cb_code_email_hunt_email_subject'] = isset( $input['cb_code_email_hunt_email_subject'] )
			? sanitize_text_field( $input['cb_code_email_hunt_email_subject'] )
			: '';
		$out['cb_code_email_hunt_email_body'] = isset( $input['cb_code_email_hunt_email_body'] )
			? sanitize_textarea_field( $input['cb_code_email_hunt_email_body'] )
			: '';
		$out['cb_code_email_hunt_reminder_subject'] = isset( $input['cb_code_email_hunt_reminder_subject'] )
			? sanitize_text_field( $input['cb_code_email_hunt_reminder_subject'] )
			: '';
		$out['cb_code_email_hunt_reminder_body'] = isset( $input['cb_code_email_hunt_reminder_body'] )
			? sanitize_textarea_field( $input['cb_code_email_hunt_reminder_body'] )
			: '';

		// Lead Notification.
		$out['lead_notification_email'] = isset( $input['lead_notification_email'] ) ? sanitize_email( $input['lead_notification_email'] ) : '';

		// Missing Product Waitlist.
		$out['missing_product_enabled'] = ! empty( $input['missing_product_enabled'] ) ? 1 : 0;
		// The agent trigger is hard-coded inside the tool description
		// (class-rlm-engine.php). No admin-editable trigger field is exposed.
		unset( $out['missing_product_trigger_description'] );

		return $out;
	}

	/**
	 * Add the Lead Capture submenu page.
	 */
	public function add_submenu() {
		add_submenu_page(
			'niwa-ai',
			__( 'Lead Capture', 'niwa-ai' ),
			__( 'Lead Capture', 'niwa-ai' ),
			'manage_options',
			'niwa-ai-lead-capture',
			array( $this, 'render_page' )
		);
	}

	/**
	 * Render the combined Lead Capture admin page.
	 */
	public function render_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( 'Forbidden' );
		}

		$opts = self::get_settings();

		echo '<div class="wrap niwa-wrap">';

		// Page header.
		echo '<div class="niwa-page-header">';
		echo '<div class="niwa-page-header__icon">';
		echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
		echo '</div>';
		echo '<div class="niwa-page-header__text">';
		echo '<h1 class="niwa-page-header__title">' . esc_html__( 'Lead Capture', 'niwa-ai' ) . '</h1>';
		echo '<p class="niwa-page-header__subtitle">' . esc_html__( 'Capture visitor contacts during chat — manage leads, export data, and track engagement.', 'niwa-ai' ) . '</p>';
		echo '</div>';
		echo '</div>';

		// Tab navigation.
		$active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'settings';
		if ( ! in_array( $active_tab, array( 'settings', 'contacts', 'form_generator', 'scheduler' ), true ) ) {
			$active_tab = 'settings';
		}

		$settings_url       = add_query_arg( array( 'page' => 'niwa-ai-lead-capture', 'tab' => 'settings' ), admin_url( 'admin.php' ) );
		$contacts_url       = add_query_arg( array( 'page' => 'niwa-ai-lead-capture', 'tab' => 'contacts' ), admin_url( 'admin.php' ) );
		$form_generator_url = add_query_arg( array( 'page' => 'niwa-ai-lead-capture', 'tab' => 'form_generator' ), admin_url( 'admin.php' ) );
		$scheduler_url      = add_query_arg( array( 'page' => 'niwa-ai-lead-capture', 'tab' => 'scheduler' ), admin_url( 'admin.php' ) );

		echo '<div class="niwa-tabs">';
		echo '<a href="' . esc_url( $settings_url ) . '" class="niwa-tabs__tab' . ( 'settings' === $active_tab ? ' niwa-tabs__tab--active' : '' ) . '">' . esc_html__( 'Settings', 'niwa-ai' ) . '</a>';
		echo '<a href="' . esc_url( $contacts_url ) . '" class="niwa-tabs__tab' . ( 'contacts' === $active_tab ? ' niwa-tabs__tab--active' : '' ) . '">' . esc_html__( 'Contacts', 'niwa-ai' ) . '</a>';
		echo '<a href="' . esc_url( $form_generator_url ) . '" class="niwa-tabs__tab' . ( 'form_generator' === $active_tab ? ' niwa-tabs__tab--active' : '' ) . '">' . esc_html__( 'Form Generator', 'niwa-ai' ) . '</a>';
		echo '<a href="' . esc_url( $scheduler_url ) . '" class="niwa-tabs__tab' . ( 'scheduler' === $active_tab ? ' niwa-tabs__tab--active' : '' ) . '">' . esc_html__( 'Scheduler', 'niwa-ai' ) . '</a>';
		echo '</div>';

		if ( 'settings' === $active_tab ) {
			$this->render_settings_tab( $opts );
		} elseif ( 'form_generator' === $active_tab ) {
			$this->render_form_generator_tab( $opts );
		} elseif ( 'scheduler' === $active_tab ) {
			$this->render_scheduler_tab();
		} else {
			$this->render_contacts_tab();
		}

		echo '</div>';
	}

	/**
	 * Render the Form Generator tab by delegating to the builder UI class.
	 *
	 * @param array $opts Current lead capture options (unused — builder reads its own option).
	 * @return void
	 */
	private function render_form_generator_tab( $opts ) {
		unset( $opts );
		Niwa_AI_Lead_Form_Builder_UI::render();
	}

	/**
	 * Resolve the appointment service without making Lead Capture depend on it
	 * during a partial plugin upgrade.
	 *
	 * @return Niwa_AI_Appointment_Service|null
	 */
	private function get_appointment_service() {
		if ( $this->appointment_service_resolved ) {
			return $this->appointment_service;
		}

		$this->appointment_service_resolved = true;
		if ( ! class_exists( 'Niwa_AI_Appointment_Service' ) ) {
			return null;
		}

		try {
			$reflection  = new ReflectionClass( 'Niwa_AI_Appointment_Service' );
			$constructor = $reflection->getConstructor();
			if ( null === $constructor ) {
				$this->appointment_service = $reflection->newInstance();
			} elseif ( $constructor->getNumberOfParameters() >= 1 && $constructor->getNumberOfRequiredParameters() <= 1 ) {
				$this->appointment_service = $reflection->newInstance( $this->contact_service );
			} elseif ( 0 === $constructor->getNumberOfRequiredParameters() ) {
				$this->appointment_service = $reflection->newInstance();
			}
		} catch ( Throwable $exception ) {
			$this->appointment_service = null;
		}

		return $this->appointment_service;
	}

	/**
	 * Read scheduler configuration with safe defaults.
	 *
	 * @return array
	 */
	private function get_scheduler_settings() {
		if ( ! class_exists( 'Niwa_AI_Appointment_Service' ) || ! is_callable( array( 'Niwa_AI_Appointment_Service', 'get_settings' ) ) ) {
			return array(
				'team_members'   => array(),
				'services'       => array(),
				'excluded_dates' => array(),
				'buffer_minutes' => 10,
			);
		}

		try {
			$settings = Niwa_AI_Appointment_Service::get_settings();
			return is_array( $settings ) ? $settings : array();
		} catch ( Throwable $exception ) {
			return array();
		}
	}

	/**
	 * Render team availability, services, exclusions and buffer settings.
	 *
	 * @return void
	 */
	private function render_scheduler_tab() {
		if ( ! class_exists( 'Niwa_AI_Appointment_Service' ) || ! defined( 'Niwa_AI_Appointment_Service::OPTION_KEY' ) ) {
			echo '<div class="notice notice-warning inline"><p>' . esc_html__( 'Scheduler setup is temporarily unavailable because the appointment service is not loaded. Contacts remain available.', 'niwa-ai' ) . '</p></div>';
			return;
		}

		$settings       = $this->get_scheduler_settings();
		$option_key     = Niwa_AI_Appointment_Service::OPTION_KEY;
		$members        = isset( $settings['team_members'] ) && is_array( $settings['team_members'] ) ? $settings['team_members'] : ( isset( $settings['members'] ) && is_array( $settings['members'] ) ? $settings['members'] : array() );
		$services       = isset( $settings['services'] ) && is_array( $settings['services'] ) ? $settings['services'] : array();
		$buffer_minutes = isset( $settings['buffer_minutes'] ) ? absint( $settings['buffer_minutes'] ) : ( isset( $settings['buffer'] ) ? absint( $settings['buffer'] ) : 10 );
		$enabled        = ! empty( $settings['enabled'] );
		$contact_fields = isset( $settings['contact_fields'] ) && is_array( $settings['contact_fields'] ) ? $settings['contact_fields'] : array( 'name', 'phone', 'email' );
		$confirmation_channels = isset( $settings['confirmation_channels'] ) && is_array( $settings['confirmation_channels'] ) ? $settings['confirmation_channels'] : array( 'email', 'whatsapp' );

		if ( empty( $members ) ) {
			$members[] = array( 'id' => 'member_1', 'name' => '', 'email' => '', 'working_hours' => array() );
		}
		if ( empty( $services ) ) {
			$services[] = array( 'id' => 'service_1', 'name' => '', 'description' => '', 'duration_minutes' => 60, 'team_member_ids' => array() );
		}
		if ( class_exists( 'Niwa_AI_Calendar_Integration_UI' ) ) {
			Niwa_AI_Calendar_Integration_UI::render_notices();
			Niwa_AI_Calendar_Integration_UI::render_admin();
		}
		echo '<form method="post" action="options.php" class="niwa-scheduler-form">';
		settings_fields( 'niwa_ai_scheduler_group' );
		$this->render_scheduler_usage_card();
		echo '<div class="niwa-card niwa-scheduler-status-card"><div class="niwa-card__body"><div class="niwa-scheduler-status-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="m9 16 2 2 4-4"/></svg></div><div class="niwa-scheduler-status-card__copy"><label class="niwa-toggle"><input type="checkbox" name="' . esc_attr( $option_key . '[enabled]' ) . '" value="1" ' . checked( $enabled, true, false ) . ' /> <strong>' . esc_html__( 'Enable frontend scheduling', 'niwa-ai' ) . '</strong></label><span class="niwa-field__hint">' . esc_html__( 'Allows the frontend assistant and lead forms to recommend services and offer available appointment times.', 'niwa-ai' ) . '</span></div></div></div>';
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header niwa-scheduler-intro"><div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21a8 8 0 0 0-16 0"/><circle cx="12" cy="7" r="4"/><path d="M17 11h5"/><path d="M19.5 8.5v5"/></svg></div><div><h2 class="niwa-card__title">' . esc_html__( 'Booking information and confirmations', 'niwa-ai' ) . '</h2><p class="niwa-field__hint">' . esc_html__( 'Choose which details visitors must provide and how Niwa confirms their reservation.', 'niwa-ai' ) . '</p></div></div>';
		echo '<div class="niwa-card__body"><div class="niwa-scheduler-grid">';
		echo '<div class="niwa-field"><span class="niwa-field__label">' . esc_html__( 'Information to collect', 'niwa-ai' ) . '</span>';
		echo '<input type="hidden" name="' . esc_attr( $option_key . '[contact_fields][]' ) . '" value="" />';
		foreach ( array( 'name' => __( 'Name', 'niwa-ai' ), 'phone' => __( 'Phone', 'niwa-ai' ), 'email' => __( 'Email', 'niwa-ai' ) ) as $field => $label ) {
			echo '<label class="niwa-toggle"><input type="checkbox" name="' . esc_attr( $option_key . '[contact_fields][]' ) . '" value="' . esc_attr( $field ) . '" ' . checked( in_array( $field, $contact_fields, true ), true, false ) . ' /> ' . esc_html( $label ) . '</label>';
		}
		echo '<span class="niwa-field__hint">' . esc_html__( 'All three fields are enabled by default. Disabled fields are not shown or stored by Scheduler.', 'niwa-ai' ) . '</span></div>';
		echo '<div class="niwa-field"><span class="niwa-field__label">' . esc_html__( 'Visitor confirmation channels', 'niwa-ai' ) . '</span>';
		echo '<input type="hidden" name="' . esc_attr( $option_key . '[confirmation_channels][]' ) . '" value="" />';
		foreach ( array( 'email' => __( 'Email confirmation', 'niwa-ai' ), 'whatsapp' => __( 'WhatsApp confirmation', 'niwa-ai' ) ) as $channel => $label ) {
			echo '<label class="niwa-toggle"><input type="checkbox" name="' . esc_attr( $option_key . '[confirmation_channels][]' ) . '" value="' . esc_attr( $channel ) . '" ' . checked( in_array( $channel, $confirmation_channels, true ), true, false ) . ' /> ' . esc_html( $label ) . '</label>';
		}
		echo '<span class="niwa-field__hint">' . esc_html__( 'A channel is skipped when its contact detail is not collected or WhatsApp is not configured.', 'niwa-ai' ) . '</span></div>';
		echo '</div></div></div>';

		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header niwa-scheduler-intro">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6"/><path d="M22 11h-6"/></svg></div><div><h2 class="niwa-card__title">' . esc_html__( 'Team availability', 'niwa-ai' ) . '</h2><p class="niwa-field__hint">' . esc_html__( 'Add the people who can receive appointments, their regular working hours, and breaks for each weekday.', 'niwa-ai' ) . '</p></div>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		echo '<div id="niwa-scheduler-members" class="niwa-scheduler-repeatables">';
		foreach ( array_values( $members ) as $member_index => $member ) {
			$this->render_scheduler_member( is_array( $member ) ? $member : array(), $member_index, $option_key );
		}
		echo '</div>';
		echo '<button type="button" class="button niwa-btn niwa-btn--secondary niwa-scheduler-add niwa-scheduler-add-row" data-target="#niwa-scheduler-members" data-row=".niwa-scheduler-member" data-collection="team_members">' . esc_html__( 'Add team member', 'niwa-ai' ) . '</button>';
		echo '</div></div>';

		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header niwa-scheduler-intro"><div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div><div><h2 class="niwa-card__title">' . esc_html__( 'Services', 'niwa-ai' ) . '</h2><p class="niwa-field__hint">' . esc_html__( 'Define each bookable service, its duration, and the team members who can provide it.', 'niwa-ai' ) . '</p></div></div>';
		echo '<div class="niwa-card__body">';
		echo '<div id="niwa-scheduler-services" class="niwa-scheduler-repeatables">';
		foreach ( array_values( $services ) as $service_index => $service ) {
			$this->render_scheduler_service( is_array( $service ) ? $service : array(), $service_index, $option_key, $members );
		}
		echo '</div>';
		echo '<button type="button" class="button niwa-btn niwa-btn--secondary niwa-scheduler-add niwa-scheduler-add-row" data-target="#niwa-scheduler-services" data-row=".niwa-scheduler-service" data-collection="services">' . esc_html__( 'Add service', 'niwa-ai' ) . '</button>';
		echo '</div></div>';

		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header niwa-scheduler-intro"><div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></div><div><h2 class="niwa-card__title">' . esc_html__( 'Buffer time', 'niwa-ai' ) . '</h2><p class="niwa-field__hint">' . esc_html__( 'Keep preparation time between consecutive appointments.', 'niwa-ai' ) . '</p></div></div>';
		echo '<div class="niwa-card__body"><div class="niwa-field">';
		echo '<label class="niwa-field__label" for="niwa-scheduler-buffer">' . esc_html__( 'Time between appointments', 'niwa-ai' ) . '</label>';
		echo '<select id="niwa-scheduler-buffer" name="' . esc_attr( $option_key . '[buffer_minutes]' ) . '">';
		foreach ( array( 5, 10, 15, 30 ) as $buffer_option ) {
			echo '<option value="' . esc_attr( (string) $buffer_option ) . '" ' . selected( $buffer_minutes, $buffer_option, false ) . '>' . esc_html( sprintf( __( '%d minutes', 'niwa-ai' ), $buffer_option ) ) . '</option>';
		}
		echo '</select><span class="niwa-field__hint">' . esc_html__( 'Reserved after every service so the assigned team member can prepare for the next appointment.', 'niwa-ai' ) . '</span>';
		echo '</div></div>';
		echo '<div class="niwa-card__footer">';
		submit_button( __( 'Save Scheduler', 'niwa-ai' ), 'primary niwa-btn niwa-btn--primary', 'submit', false );
		echo '</div></div>';

		echo '</form>';
	}

	/**
	 * Explain the public booking entry points where users configure Scheduler.
	 *
	 * @return void
	 */
	private function render_scheduler_usage_card() {
		$examples = array(
			array(
				'label'       => __( 'Button shortcode', 'niwa-ai' ),
				'description' => __( 'Adds a ready-made booking button to any page or post.', 'niwa-ai' ),
				'value'       => '[niwa_booking]',
			),
			array(
				'label'       => __( 'Text link shortcode', 'niwa-ai' ),
				'description' => __( 'Adds an inline booking link with your chosen label.', 'niwa-ai' ),
				'value'       => '[niwa_booking type="link" label="Book an appointment"]',
			),
			array(
				'label'       => __( 'Existing button or menu link', 'niwa-ai' ),
				'description' => __( 'Use this as the URL of any existing button, link, or menu item.', 'niwa-ai' ),
				'value'       => '#niwa-booking',
			),
		);

		echo '<div class="niwa-card niwa-scheduler-usage-card">';
		echo '<div class="niwa-card__header niwa-scheduler-intro"><div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></div><div><h2 class="niwa-card__title">' . esc_html__( 'Open Scheduler from anywhere', 'niwa-ai' ) . '</h2><p class="niwa-field__hint">' . esc_html__( 'Use one of these options on your site. Each opens the same complete booking flow outside the chat.', 'niwa-ai' ) . '</p></div></div>';
		echo '<div class="niwa-card__body"><div class="niwa-scheduler-usage-grid">';
		foreach ( $examples as $example ) {
			echo '<div class="niwa-scheduler-usage-option"><strong>' . esc_html( $example['label'] ) . '</strong><span>' . esc_html( $example['description'] ) . '</span><div class="niwa-scheduler-usage-code"><code>' . esc_html( $example['value'] ) . '</code><button type="button" class="button niwa-scheduler-copy-usage" data-copy-value="' . esc_attr( $example['value'] ) . '" aria-live="polite">' . esc_html__( 'Copy', 'niwa-ai' ) . '</button></div></div>';
		}
		echo '</div></div></div>';
	}

	/**
	 * Render one repeatable team member.
	 *
	 * @param array  $member     Member configuration.
	 * @param int    $index      Member index.
	 * @param string $option_key Scheduler option name.
	 * @return void
	 */
	private function render_scheduler_member( $member, $index, $option_key ) {
		$member_id     = isset( $member['id'] ) && '' !== trim( (string) $member['id'] ) ? (string) $member['id'] : 'member_' . ( $index + 1 );
		$working_hours = isset( $member['working_hours'] ) && is_array( $member['working_hours'] ) ? $member['working_hours'] : ( isset( $member['schedule'] ) && is_array( $member['schedule'] ) ? $member['schedule'] : array() );
		$breaks_by_day = isset( $member['breaks'] ) && is_array( $member['breaks'] ) ? $member['breaks'] : array();
		$excluded_dates = isset( $member['excluded_dates'] ) && is_array( $member['excluded_dates'] ) && ! empty( $member['excluded_dates'] ) ? array_values( $member['excluded_dates'] ) : array( '' );
		$excluded_ranges = isset( $member['excluded_ranges'] ) && is_array( $member['excluded_ranges'] ) && ! empty( $member['excluded_ranges'] ) ? array_values( $member['excluded_ranges'] ) : array( array( 'start' => '', 'end' => '', 'label' => '' ) );
		$weekdays      = array(
			'monday'    => array( 'short' => __( 'Mon', 'niwa-ai' ), 'full' => __( 'Monday', 'niwa-ai' ) ),
			'tuesday'   => array( 'short' => __( 'Tue', 'niwa-ai' ), 'full' => __( 'Tuesday', 'niwa-ai' ) ),
			'wednesday' => array( 'short' => __( 'Wed', 'niwa-ai' ), 'full' => __( 'Wednesday', 'niwa-ai' ) ),
			'thursday'  => array( 'short' => __( 'Thu', 'niwa-ai' ), 'full' => __( 'Thursday', 'niwa-ai' ) ),
			'friday'    => array( 'short' => __( 'Fri', 'niwa-ai' ), 'full' => __( 'Friday', 'niwa-ai' ) ),
			'saturday'  => array( 'short' => __( 'Sat', 'niwa-ai' ), 'full' => __( 'Saturday', 'niwa-ai' ) ),
			'sunday'    => array( 'short' => __( 'Sun', 'niwa-ai' ), 'full' => __( 'Sunday', 'niwa-ai' ) ),
		);

		echo '<section class="niwa-scheduler-repeatable niwa-scheduler-member" data-index="' . esc_attr( (string) $index ) . '">';
		echo '<div class="niwa-scheduler-repeatable__header"><div class="niwa-scheduler-repeatable__title"><span class="niwa-scheduler-repeatable__number">' . esc_html( (string) ( $index + 1 ) ) . '</span><div><strong>' . esc_html__( 'Team member', 'niwa-ai' ) . '</strong><span>' . esc_html__( 'Identity, weekly availability and time off', 'niwa-ai' ) . '</span></div></div><button type="button" class="button niwa-btn niwa-btn--danger niwa-scheduler-remove" data-row=".niwa-scheduler-member" data-collection="team_members">' . esc_html__( 'Remove', 'niwa-ai' ) . '</button></div>';
		echo '<div class="niwa-scheduler-repeatable__body">';
		echo '<input type="hidden" class="niwa-scheduler-member-id" name="' . esc_attr( $option_key . '[team_members][' . $index . '][id]' ) . '" value="' . esc_attr( $member_id ) . '" />';
		echo '<div class="niwa-scheduler-subsection"><div class="niwa-scheduler-subsection__heading"><div><h3>' . esc_html__( 'Member details', 'niwa-ai' ) . '</h3><p>' . esc_html__( 'Used internally and when assigning services.', 'niwa-ai' ) . '</p></div></div><div class="niwa-scheduler-grid">';
		echo '<div class="niwa-field"><label class="niwa-field__label">' . esc_html__( 'Name', 'niwa-ai' ) . '</label><input type="text" class="niwa-input--wide niwa-scheduler-member-name" name="' . esc_attr( $option_key . '[team_members][' . $index . '][name]' ) . '" value="' . esc_attr( $member['name'] ?? '' ) . '" required /></div>';
		echo '<div class="niwa-field"><label class="niwa-field__label">' . esc_html__( 'Email', 'niwa-ai' ) . '</label><input type="email" class="niwa-input--wide" name="' . esc_attr( $option_key . '[team_members][' . $index . '][email]' ) . '" value="' . esc_attr( $member['email'] ?? '' ) . '" required />';
		if ( class_exists( 'Niwa_AI_Calendar_Integration_UI' ) ) {
			Niwa_AI_Calendar_Integration_UI::render_member( $member );
		}
		echo '</div>';
		echo '</div></div>';
		echo '<div class="niwa-scheduler-subsection niwa-scheduler-subsection--schedule"><div class="niwa-scheduler-subsection__heading"><div><h3>' . esc_html__( 'Weekly schedule', 'niwa-ai' ) . '</h3><p>' . esc_html__( 'Turn days on or off, then set working hours and as many breaks as needed.', 'niwa-ai' ) . '</p></div></div><div class="niwa-scheduler-week">';

		foreach ( $weekdays as $day_key => $day_label ) {
			$windows    = isset( $working_hours[ $day_key ] ) && is_array( $working_hours[ $day_key ] ) ? array_values( $working_hours[ $day_key ] ) : array();
			$window     = isset( $windows[0] ) && is_array( $windows[0] ) ? $windows[0] : array();
			$is_enabled = ! empty( $windows );
			if ( empty( $working_hours ) ) {
				$is_enabled = ! in_array( $day_key, array( 'saturday', 'sunday' ), true );
			}
			$breaks = isset( $breaks_by_day[ $day_key ] ) && is_array( $breaks_by_day[ $day_key ] ) && ! empty( $breaks_by_day[ $day_key ] ) ? array_values( $breaks_by_day[ $day_key ] ) : array( array( 'start' => '', 'end' => '' ) );

			echo '<div class="niwa-scheduler-day" data-day-key="' . esc_attr( $day_key ) . '">';
			echo '<div class="niwa-scheduler-day__control"><label class="niwa-toggle" title="' . esc_attr( $day_label['full'] ) . '"><input type="checkbox" class="niwa-scheduler-day-enabled" ' . checked( $is_enabled, true, false ) . ' /> <strong>' . esc_html( $day_label['short'] ) . '</strong></label></div>';
			echo '<div class="niwa-scheduler-day__hours-column"><div class="niwa-scheduler-day__hours"><input type="time" step="300" aria-label="' . esc_attr__( 'Start time', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][working_hours][' . $day_key . '][0][start]' ) . '" value="' . esc_attr( $window['start'] ?? '09:00' ) . '" ' . disabled( $is_enabled, false, false ) . ' /><span>&ndash;</span><input type="time" step="300" aria-label="' . esc_attr__( 'End time', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][working_hours][' . $day_key . '][0][end]' ) . '" value="' . esc_attr( $window['end'] ?? '17:00' ) . '" ' . disabled( $is_enabled, false, false ) . ' /></div></div>';
			echo '<div class="niwa-scheduler-day__actions"><button type="button" class="button niwa-schedule-row-action niwa-scheduler-add-break" aria-label="' . esc_attr__( 'Add break', 'niwa-ai' ) . '" title="' . esc_attr__( 'Add break', 'niwa-ai' ) . '" ' . disabled( $is_enabled, false, false ) . '><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span></button><button type="button" class="button niwa-schedule-row-action niwa-scheduler-copy-day" aria-label="' . esc_attr__( 'Copy schedule to the next day', 'niwa-ai' ) . '" title="' . esc_attr__( 'Copy schedule to the next day', 'niwa-ai' ) . '" ' . disabled( $is_enabled, false, false ) . '><span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button></div>';
			echo '<div class="niwa-scheduler-day__break-column"><span class="niwa-scheduler-day__column-label">' . esc_html__( 'Breaks', 'niwa-ai' ) . '</span><div class="niwa-scheduler-breaks">';
			foreach ( array_values( $breaks ) as $break_index => $break ) {
				$break = is_array( $break ) ? $break : array();
				$is_placeholder = '' === trim( (string) ( $break['start'] ?? '' ) ) && '' === trim( (string) ( $break['end'] ?? '' ) );
				echo '<div class="niwa-scheduler-break' . ( $is_placeholder ? ' is-placeholder' : '' ) . '"' . ( $is_placeholder ? ' hidden' : '' ) . '><span class="screen-reader-text">' . esc_html__( 'Break', 'niwa-ai' ) . '</span><input type="time" step="300" aria-label="' . esc_attr__( 'Break start', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][breaks][' . $day_key . '][' . $break_index . '][start]' ) . '" value="' . esc_attr( $break['start'] ?? '' ) . '" ' . disabled( $is_enabled, false, false ) . ' /><span>&ndash;</span><input type="time" step="300" aria-label="' . esc_attr__( 'Break end', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][breaks][' . $day_key . '][' . $break_index . '][end]' ) . '" value="' . esc_attr( $break['end'] ?? '' ) . '" ' . disabled( $is_enabled, false, false ) . ' /><button type="button" class="button niwa-scheduler-icon-button niwa-scheduler-remove-break" aria-label="' . esc_attr__( 'Remove break', 'niwa-ai' ) . '" ' . disabled( $is_enabled, false, false ) . '>&times;</button></div>';
			}
			echo '</div></div>';
			echo '</div>';
		}

		echo '</div></div>';
		echo '<div class="niwa-scheduler-subsection niwa-scheduler-subsection--time-off"><div class="niwa-scheduler-subsection__heading"><div><h3>' . esc_html__( 'Time off and exceptions', 'niwa-ai' ) . '</h3><p>' . esc_html__( 'Block individual dates or longer periods without changing the regular week.', 'niwa-ai' ) . '</p></div></div><div class="niwa-scheduler-member-exclusions">';
		echo '<div class="niwa-scheduler-exclusion-panel"><span class="niwa-field__label">' . esc_html__( 'Individual days off', 'niwa-ai' ) . '</span><span class="niwa-field__hint">' . esc_html__( 'Add holidays or other single dates when this team member is unavailable.', 'niwa-ai' ) . '</span><div class="niwa-member-excluded-dates">';
		foreach ( $excluded_dates as $excluded_date ) {
			echo '<div class="niwa-member-excluded-date"><input type="date" name="' . esc_attr( $option_key . '[team_members][' . $index . '][excluded_dates][]' ) . '" value="' . esc_attr( (string) $excluded_date ) . '" /><button type="button" class="button niwa-scheduler-icon-button niwa-scheduler-remove-local" aria-label="' . esc_attr__( 'Remove date', 'niwa-ai' ) . '">&times;</button></div>';
		}
		echo '</div><button type="button" class="button niwa-btn niwa-btn--secondary niwa-scheduler-add-local" data-list=".niwa-member-excluded-dates" data-row=".niwa-member-excluded-date">' . esc_html__( '+ Add date', 'niwa-ai' ) . '</button></div>';
		echo '<div class="niwa-scheduler-exclusion-panel"><span class="niwa-field__label">' . esc_html__( 'Date ranges', 'niwa-ai' ) . '</span><span class="niwa-field__hint">' . esc_html__( 'Use a range for annual leave or a multi-day closure.', 'niwa-ai' ) . '</span><div class="niwa-member-excluded-ranges">';
		foreach ( $excluded_ranges as $range_index => $range ) {
			$range = is_array( $range ) ? $range : array();
			echo '<div class="niwa-member-excluded-range"><input type="date" aria-label="' . esc_attr__( 'Range start', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][excluded_ranges][' . $range_index . '][start]' ) . '" value="' . esc_attr( $range['start'] ?? '' ) . '" /><span>&ndash;</span><input type="date" aria-label="' . esc_attr__( 'Range end', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][excluded_ranges][' . $range_index . '][end]' ) . '" value="' . esc_attr( $range['end'] ?? '' ) . '" /><input type="text" aria-label="' . esc_attr__( 'Range label', 'niwa-ai' ) . '" placeholder="' . esc_attr__( 'Annual leave', 'niwa-ai' ) . '" name="' . esc_attr( $option_key . '[team_members][' . $index . '][excluded_ranges][' . $range_index . '][label]' ) . '" value="' . esc_attr( $range['label'] ?? '' ) . '" /><button type="button" class="button niwa-scheduler-icon-button niwa-scheduler-remove-local" aria-label="' . esc_attr__( 'Remove range', 'niwa-ai' ) . '">&times;</button></div>';
		}
		echo '</div><button type="button" class="button niwa-btn niwa-btn--secondary niwa-scheduler-add-local" data-list=".niwa-member-excluded-ranges" data-row=".niwa-member-excluded-range">' . esc_html__( '+ Add range', 'niwa-ai' ) . '</button></div>';
		echo '</div></div>';
		echo '</div></section>';
	}

	/**
	 * Render one repeatable service.
	 *
	 * @param array  $service    Service configuration.
	 * @param int    $index      Service index.
	 * @param string $option_key Scheduler option name.
	 * @param array  $members    Current team members.
	 * @return void
	 */
	private function render_scheduler_service( $service, $index, $option_key, $members ) {
		$service_id      = isset( $service['id'] ) && '' !== trim( (string) $service['id'] ) ? (string) $service['id'] : 'service_' . ( $index + 1 );
		$assigned_members = isset( $service['team_member_ids'] ) && is_array( $service['team_member_ids'] ) ? array_map( 'strval', $service['team_member_ids'] ) : ( isset( $service['assigned_member_ids'] ) && is_array( $service['assigned_member_ids'] ) ? array_map( 'strval', $service['assigned_member_ids'] ) : array() );

		echo '<section class="niwa-scheduler-repeatable niwa-scheduler-service" data-index="' . esc_attr( (string) $index ) . '">';
		echo '<div class="niwa-scheduler-repeatable__header"><div class="niwa-scheduler-repeatable__title"><span class="niwa-scheduler-repeatable__number">' . esc_html( (string) ( $index + 1 ) ) . '</span><div><strong>' . esc_html__( 'Service', 'niwa-ai' ) . '</strong><span>' . esc_html__( 'Booking duration, description and team assignment', 'niwa-ai' ) . '</span></div></div><button type="button" class="button niwa-btn niwa-btn--danger niwa-scheduler-remove" data-row=".niwa-scheduler-service" data-collection="services">' . esc_html__( 'Remove', 'niwa-ai' ) . '</button></div>';
		echo '<div class="niwa-scheduler-repeatable__body">';
		echo '<input type="hidden" class="niwa-scheduler-service-id" name="' . esc_attr( $option_key . '[services][' . $index . '][id]' ) . '" value="' . esc_attr( $service_id ) . '" />';
		echo '<div class="niwa-scheduler-subsection"><div class="niwa-scheduler-grid niwa-scheduler-grid--service">';
		echo '<div class="niwa-field"><label class="niwa-field__label">' . esc_html__( 'Service name', 'niwa-ai' ) . '</label><input type="text" class="niwa-input--wide" name="' . esc_attr( $option_key . '[services][' . $index . '][name]' ) . '" value="' . esc_attr( $service['name'] ?? '' ) . '" required /></div>';
		echo '<div class="niwa-field"><label class="niwa-field__label">' . esc_html__( 'Duration (minutes)', 'niwa-ai' ) . '</label><input type="number" min="5" max="480" step="5" name="' . esc_attr( $option_key . '[services][' . $index . '][duration_minutes]' ) . '" value="' . esc_attr( (string) ( $service['duration_minutes'] ?? 60 ) ) . '" /></div>';
		echo '</div>';
		echo '<div class="niwa-field"><label class="niwa-field__label">' . esc_html__( 'Description', 'niwa-ai' ) . '</label><textarea class="niwa-input--wide" rows="3" name="' . esc_attr( $option_key . '[services][' . $index . '][description]' ) . '">' . esc_textarea( $service['description'] ?? '' ) . '</textarea><span class="niwa-field__hint">' . esc_html__( 'The frontend assistant uses this context when recommending services.', 'niwa-ai' ) . '</span></div>';
		echo '<div class="niwa-field"><span class="niwa-field__label">' . esc_html__( 'Assigned team members', 'niwa-ai' ) . '</span><div class="niwa-service-member-list" data-field-name="' . esc_attr( $option_key . '[services][' . $index . '][team_member_ids][]' ) . '">';
		foreach ( $members as $member_index => $member ) {
			$member = is_array( $member ) ? $member : array();
			$member_id = isset( $member['id'] ) && '' !== trim( (string) $member['id'] ) ? (string) $member['id'] : 'member_' . ( $member_index + 1 );
			$label = isset( $member['name'] ) && '' !== trim( (string) $member['name'] ) ? (string) $member['name'] : __( 'Unnamed member', 'niwa-ai' );
			echo '<label class="niwa-scheduler-check"><input type="checkbox" name="' . esc_attr( $option_key . '[services][' . $index . '][team_member_ids][]' ) . '" value="' . esc_attr( $member_id ) . '" ' . checked( in_array( $member_id, $assigned_members, true ), true, false ) . ' /> ' . esc_html( $label ) . '</label>';
		}
		echo '</div></div></div>';
		echo '</div></section>';
	}

	/**
	 * Render the Settings tab.
	 *
	 * @param array $opts Current options.
	 */
	private function render_settings_tab( $opts ) {
		$n = self::OPTION_KEY;

		echo '<form method="post" action="options.php">';
		settings_fields( 'niwa_ai_lead_capture_group' );

		// Hidden marker so sanitize() knows our form was submitted.
		echo '<input type="hidden" name="' . esc_attr( $n . '[_niwa_lead_capture_form]' ) . '" value="1" />';

		// ── Card 1: Email Start Chat Cacher (mode #2) ──
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Email Start Chat Cacher', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $n . '[email_start_chat_cacher_enabled]' ) . '" value="1" ' . checked( ! empty( $opts['email_start_chat_cacher_enabled'] ), true, false ) . ' />';
		echo esc_html__( 'Lock chat behind email', 'niwa-ai' );
		echo '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'Replaces the greeting with an email capture prompt. The visitor must enter a valid email to unlock the AI assistant. Uses the localized message for the plugin language, or the custom prompt below if set.', 'niwa-ai' ) . '</span>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Custom unlock prompt (optional)', 'niwa-ai' ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'Leave empty to use the default translation ("Please enter your email to unlock the AI assistant.").', 'niwa-ai' ) . '</span>';
		echo '<textarea name="' . esc_attr( $n . '[email_start_chat_cacher_prompt]' ) . '" rows="2" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['email_start_chat_cacher_prompt'] ?? '' ) . '</textarea>';
		echo '</div>';

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card

		// ── Card 2: Conversion Busters Code Email Hunt ──
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v1"/><path d="M3 8l9 6 9-6"/><path d="M21 10v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7"/><path d="M16 14h3v3"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Conversion Busters Code Email Hunt', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		// Enable Lead Capture toggle.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $n . '[cb_code_email_hunt_enabled]' ) . '" value="1" ' . checked( ! empty( $opts['cb_code_email_hunt_enabled'] ), true, false ) . ' />';
		echo esc_html__( 'Enable CB email hunt', 'niwa-ai' );
		echo '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'After a positive CB or CB mini reply, ask for email, save the lead, send the generated coupon, and schedule the unused-code reminder at 80% of the coupon lifetime.', 'niwa-ai' ) . '</span>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Email prompt', 'niwa-ai' ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'Shown immediately after the shopper confirms they want the discount code.', 'niwa-ai' ) . '</span>';
		echo '<textarea name="' . esc_attr( $n . '[cb_code_email_hunt_prompt]' ) . '" rows="2" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['cb_code_email_hunt_prompt'] ?? '' ) . '</textarea>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Coupon email subject', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $n . '[cb_code_email_hunt_email_subject]' ) . '" value="' . esc_attr( $opts['cb_code_email_hunt_email_subject'] ?? '' ) . '" class="niwa-input--wide" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Coupon email body', 'niwa-ai' ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'Available placeholders: {coupon_code}, {duration}, {discount_amount}, {site_name}.', 'niwa-ai' ) . '</span>';
		echo '<textarea name="' . esc_attr( $n . '[cb_code_email_hunt_email_body]' ) . '" rows="5" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['cb_code_email_hunt_email_body'] ?? '' ) . '</textarea>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Reminder email subject', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $n . '[cb_code_email_hunt_reminder_subject]' ) . '" value="' . esc_attr( $opts['cb_code_email_hunt_reminder_subject'] ?? '' ) . '" class="niwa-input--wide" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Reminder email body', 'niwa-ai' ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'Sent after 80% of the coupon lifetime if the coupon is still unused. Available placeholders: {coupon_code}, {remaining_minutes}, {site_name}.', 'niwa-ai' ) . '</span>';
		echo '<textarea name="' . esc_attr( $n . '[cb_code_email_hunt_reminder_body]' ) . '" rows="5" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['cb_code_email_hunt_reminder_body'] ?? '' ) . '</textarea>';
		echo '</div>';

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card

		// ── Card: Missing Product Waitlist ──
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v3"/><path d="M11 14h.01"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Missing Product Waitlist', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		// Toggle.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $n . '[missing_product_enabled]' ) . '" value="1" ' . checked( ! empty( $opts['missing_product_enabled'] ), true, false ) . ' />';
		echo esc_html__( 'Enable missing product waitlist capture', 'niwa-ai' );
		echo '</label>';
		echo '<span class="niwa-field__hint">' . esc_html__( 'When enabled, the AI agent proactively offers to notify the visitor as soon as it detects a requested product is not in the catalog or the visitor refuses a proposed alternative. The agent formulates the ask in the visitor\'s own language and only requests an email if one has not already been captured elsewhere in the session (email capture / CBEH / lead form). On completion the email is saved as a new contact and a notification is sent to the Notification Email above. The exact trigger rules are built into the AI tool — no further configuration is required.', 'niwa-ai' ) . '</span>';
		echo '</div>';

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card

		// ── Card 2: Notifications ──
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Notifications', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		// Notification Email text input.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Notification Email', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $n . '[lead_notification_email]' ) . '" value="' . esc_attr( isset( $opts['lead_notification_email'] ) ? $opts['lead_notification_email'] : '' ) . '" class="niwa-input--wide" />';
		echo '<span class="niwa-field__hint">' . esc_html__( 'Email address that receives a notification whenever a new lead is captured. Leave empty to disable notifications. Failed emails are automatically retried via cron (max 3 retries).', 'niwa-ai' ) . '</span>';
		echo '</div>';

		echo '</div>'; // .niwa-card__body
		echo '<div class="niwa-card__footer">';
		submit_button( __( 'Save Settings', 'niwa-ai' ), 'primary niwa-btn niwa-btn--primary', 'submit', false );
		echo '</div>';
		echo '</div>'; // .niwa-card

		echo '</form>';
	}

	/**
	 * Render the Contacts tab.
	 */
	private function render_contacts_tab() {
		$search   = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
		$paged    = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1;
		$per_page = 20;

		$this->render_stats_dashboard();
		$this->render_export_section();
		$this->render_month_calendar();

		// Contacts table inside a card.
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Contacts', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		$this->render_contacts_table( $search, $paged, $per_page );

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card

		$this->render_contact_details_modal();
	}

	/**
	 * Render stats dashboard cards.
	 */
	private function render_stats_dashboard() {
		$stats = $this->contact_service->get_stats();

		echo '<div class="niwa-stats">';

		// Total Contacts.
		echo '<div class="niwa-stat niwa-stat--brand">';
		echo '<div class="niwa-stat__label">' . esc_html__( 'Total Contacts', 'niwa-ai' ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( (string) $stats['total'] ) . '</div>';
		echo '</div>';

		// Last 24h.
		echo '<div class="niwa-stat niwa-stat--success">';
		echo '<div class="niwa-stat__label">' . esc_html__( 'Last 24h', 'niwa-ai' ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( (string) $stats['last_24h'] ) . '</div>';
		echo '</div>';

		// Last 7 Days.
		echo '<div class="niwa-stat niwa-stat--brand">';
		echo '<div class="niwa-stat__label">' . esc_html__( 'Last 7 Days', 'niwa-ai' ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( (string) $stats['last_7d'] ) . '</div>';
		echo '</div>';

		// Last 30 Days.
		echo '<div class="niwa-stat niwa-stat--brand">';
		echo '<div class="niwa-stat__label">' . esc_html__( 'Last 30 Days', 'niwa-ai' ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( (string) $stats['last_30d'] ) . '</div>';
		echo '</div>';

		// Name Capture Rate.
		echo '<div class="niwa-stat niwa-stat--brand">';
		echo '<div class="niwa-stat__label">' . esc_html__( 'Name Capture', 'niwa-ai' ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( $stats['name_rate'] . '%' ) . '</div>';
		echo '</div>';

		// GDPR Consent Rate.
		echo '<div class="niwa-stat niwa-stat--brand">';
		echo '<div class="niwa-stat__label">' . esc_html__( 'GDPR Consent', 'niwa-ai' ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( $stats['consent_rate'] . '%' ) . '</div>';
		echo '</div>';

		echo '</div>'; // .niwa-stats
	}

	/**
	 * Render the CSV export section.
	 */
	private function render_export_section() {
		$nonce = wp_create_nonce( 'niwa_ai_contacts_nonce' );

		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Export CSV', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		echo '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">';

		$ranges = array(
			'24h'  => __( 'Last 24h', 'niwa-ai' ),
			'7d'   => __( 'Last 7 Days', 'niwa-ai' ),
			'1m'   => __( 'Last Month', 'niwa-ai' ),
			'2m'   => __( 'Last 2 Months', 'niwa-ai' ),
			'all'  => __( 'All Time', 'niwa-ai' ),
		);

		foreach ( $ranges as $range_key => $range_label ) {
			$url = add_query_arg(
				array(
					'action' => 'niwa_ai_export_contacts_csv',
					'nonce'  => $nonce,
					'range'  => $range_key,
					'mode'   => 'full',
				),
				admin_url( 'admin-ajax.php' )
			);
			echo '<a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $url ) . '">' . esc_html( $range_label ) . '</a>';
		}

		$emails_url = add_query_arg(
			array(
				'action' => 'niwa_ai_export_contacts_csv',
				'nonce'  => $nonce,
				'range'  => 'all',
				'mode'   => 'emails',
			),
			admin_url( 'admin-ajax.php' )
		);
		echo '<a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $emails_url ) . '">' . esc_html__( 'Emails Only', 'niwa-ai' ) . '</a>';

		echo '</div>'; // flex toolbar

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card
	}

	/**
	 * Render the contacts table with search and pagination.
	 *
	 * @param string $search   Search term.
	 * @param int    $paged    Current page.
	 * @param int    $per_page Items per page.
	 */
	private function render_contacts_table( $search, $paged, $per_page ) {
		$offset  = ( $paged - 1 ) * $per_page;
		$result  = $this->contact_service->search_contacts( $search, $per_page, $offset );
		$items   = $result['items'];
		$total   = $result['total'];
		$pages   = max( 1, (int) ceil( $total / $per_page ) );
		$nonce   = wp_create_nonce( 'niwa_ai_contacts_nonce' );

		// Search form.
		echo '<form method="get" style="display:flex;gap:8px;align-items:center;margin-bottom:16px;">';
		echo '<input type="hidden" name="page" value="niwa-ai-lead-capture" />';
		echo '<input type="hidden" name="tab" value="contacts" />';
		echo '<input type="search" name="s" value="' . esc_attr( $search ) . '" placeholder="' . esc_attr__( 'Search contacts...', 'niwa-ai' ) . '" class="niwa-input--wide" />';
		echo '<button class="button niwa-btn niwa-btn--secondary" type="submit">' . esc_html__( 'Search', 'niwa-ai' ) . '</button>';
		if ( '' !== $search ) {
			$clear_url = add_query_arg( array( 'page' => 'niwa-ai-lead-capture', 'tab' => 'contacts' ), admin_url( 'admin.php' ) );
			echo '<a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $clear_url ) . '">' . esc_html__( 'Clear', 'niwa-ai' ) . '</a>';
		}
		echo '</form>';

		echo '<p class="niwa-field__hint" style="margin-bottom:12px;">' . esc_html(
			sprintf(
				/* translators: 1: shown count, 2: total count */
				__( 'Showing %1$s of %2$s contacts', 'niwa-ai' ),
				(string) count( $items ),
				(string) $total
			)
		) . '</p>';

		echo '<div class="niwa-scheduler-table-wrap"><table class="niwa-table">';
		echo '<thead><tr>';
		echo '<th>' . esc_html__( 'ID', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Name', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Email', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Phone', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Scheduled', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Source', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Consent', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Created', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Details', 'niwa-ai' ) . '</th>';
		echo '<th>' . esc_html__( 'Actions', 'niwa-ai' ) . '</th>';
		echo '</tr></thead><tbody>';

		if ( empty( $items ) ) {
			echo '<tr><td colspan="10" style="text-align:center;padding:20px;">' . esc_html__( 'No contacts found.', 'niwa-ai' ) . '</td></tr>';
		}

		foreach ( $items as $contact ) {
			$id         = isset( $contact['id'] ) ? (int) $contact['id'] : 0;
			$c_name     = isset( $contact['name'] ) ? $contact['name'] : '';
			$c_email    = isset( $contact['email'] ) ? $contact['email'] : '';
			$c_phone    = isset( $contact['phone'] ) ? $contact['phone'] : '';
			$c_source   = isset( $contact['consent_source'] ) ? $contact['consent_source'] : '';
			$c_consent_given = ! empty( $contact['consent_given'] );
			$c_created  = isset( $contact['created_at'] ) ? $contact['created_at'] : '';
			$is_phone_only = ( 0 === strpos( $c_email, 'phone_only_' ) );
			$schedule_context = $this->get_contact_schedule_context( $id, $contact );
			$appointment = $schedule_context['appointment'];
			$details     = $schedule_context['details'];

			echo '<tr>';
			echo '<td>' . esc_html( (string) $id ) . '</td>';
			echo '<td>' . esc_html( $c_name ? $c_name : "\xE2\x80\x94" ) . '</td>';
			echo '<td>' . esc_html( $is_phone_only ? __( '(phone only)', 'niwa-ai' ) : $c_email ) . '</td>';
			echo '<td>' . esc_html( $c_phone ? $c_phone : "\xE2\x80\x94" ) . '</td>';
			echo '<td class="niwa-schedule-cell">' . $this->format_contact_schedule_html( $appointment ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo '<td>' . esc_html( $c_source ) . '</td>';
			echo '<td>';
			if ( $c_consent_given ) {
				echo '<span class="niwa-badge niwa-badge--success">' . esc_html__( 'Yes', 'niwa-ai' ) . '</span>';
			} else {
				echo '<span class="niwa-badge niwa-badge--neutral">' . esc_html__( 'No', 'niwa-ai' ) . '</span>';
			}
			echo '</td>';
			echo '<td>' . esc_html( $c_created ) . '</td>';
			echo '<td><button type="button" id="niwa-contact-details-' . esc_attr( (string) $id ) . '" class="button-link niwa-contact-details-trigger' . ( '' !== trim( $details ) ? ' has-details' : '' ) . '" data-contact-id="' . esc_attr( (string) $id ) . '">' . esc_html( '' !== trim( $details ) ? __( 'Edit details', 'niwa-ai' ) : __( 'Add details', 'niwa-ai' ) ) . '</button><textarea class="niwa-contact-details-value" hidden>' . esc_textarea( $details ) . '</textarea></td>';
			echo '<td><button class="button niwa-btn niwa-btn--danger niwa-delete-contact" data-id="' . esc_attr( (string) $id ) . '" data-nonce="' . esc_attr( $nonce ) . '">' . esc_html__( 'Delete', 'niwa-ai' ) . '</button></td>';
			echo '</tr>';
		}

		echo '</tbody></table></div>';

		// Pagination.
		if ( $pages > 1 ) {
			echo '<div style="margin-top:16px;display:flex;gap:4px;align-items:center;flex-wrap:wrap;">';

			// Previous button.
			if ( $paged > 1 ) {
				$prev_url = add_query_arg(
					array(
						'page'  => 'niwa-ai-lead-capture',
						'tab'   => 'contacts',
						'paged' => $paged - 1,
						's'     => $search,
					),
					admin_url( 'admin.php' )
				);
				echo '<a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $prev_url ) . '">&laquo; ' . esc_html__( 'Prev', 'niwa-ai' ) . '</a>';
			}

			// Page number buttons.
			for ( $p = 1; $p <= $pages; $p++ ) {
				$page_url = add_query_arg(
					array(
						'page'  => 'niwa-ai-lead-capture',
						'tab'   => 'contacts',
						'paged' => $p,
						's'     => $search,
					),
					admin_url( 'admin.php' )
				);
				if ( $p === $paged ) {
					echo '<a class="button niwa-btn niwa-btn--primary" href="' . esc_url( $page_url ) . '">' . esc_html( (string) $p ) . '</a>';
				} else {
					echo '<a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $page_url ) . '">' . esc_html( (string) $p ) . '</a>';
				}
			}

			// Next button.
			if ( $paged < $pages ) {
				$next_url = add_query_arg(
					array(
						'page'  => 'niwa-ai-lead-capture',
						'tab'   => 'contacts',
						'paged' => $paged + 1,
						's'     => $search,
					),
					admin_url( 'admin.php' )
				);
				echo '<a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $next_url ) . '">' . esc_html__( 'Next', 'niwa-ai' ) . ' &raquo;</a>';
			}

			echo '</div>';
		}
	}

	/**
	 * Load the latest booking and editable CRM profile for a contact.
	 *
	 * @param int   $contact_id Contact ID.
	 * @param array $contact    Contact table row.
	 * @return array{appointment:array,details:string}
	 */
	private function get_contact_schedule_context( $contact_id, $contact ) {
		$appointment = array();
		$profile     = array();
		$service     = $this->get_appointment_service();

		if ( $service ) {
			try {
				if ( method_exists( $service, 'get_latest_appointment_for_contact' ) ) {
					$result      = $service->get_latest_appointment_for_contact( $contact_id );
					$appointment = is_array( $result ) ? $result : array();
				} elseif ( method_exists( $service, 'get_latest_for_contact' ) ) {
					$result      = $service->get_latest_for_contact( $contact_id );
					$appointment = is_array( $result ) ? $result : array();
				}
				$contact_has_details = '' !== trim( (string) ( $contact['details'] ?? $contact['contact_details'] ?? $contact['conversation_summary'] ?? '' ) );
				if ( ! $contact_has_details && method_exists( $service, 'get_contact_profile' ) ) {
					$result  = $service->get_contact_profile( $contact_id );
					$profile = is_array( $result ) ? $result : array();
				}
			} catch ( Throwable $exception ) {
				$appointment = array();
				$profile     = array();
			}
		}

		$details_candidates = array(
			$contact['details'] ?? '',
			$contact['contact_details'] ?? '',
			$profile['details'] ?? '',
			$profile['contact_details'] ?? '',
			$contact['conversation_summary'] ?? '',
			$profile['conversation_summary'] ?? '',
			$appointment['contact_details'] ?? '',
			$appointment['conversation_summary'] ?? '',
		);
		$details = '';
		foreach ( $details_candidates as $candidate ) {
			if ( '' !== trim( (string) $candidate ) ) {
				$details = (string) $candidate;
				break;
			}
		}

		return array(
			'appointment' => $appointment,
			'details'     => $details,
		);
	}

	/**
	 * Format an appointment for the Contacts table.
	 *
	 * @param array $appointment Appointment row.
	 * @return string Safe HTML.
	 */
	private function format_contact_schedule_html( $appointment ) {
		if ( empty( $appointment ) ) {
			return '<span aria-hidden="true">&mdash;</span><span class="screen-reader-text">' . esc_html__( 'Not scheduled', 'niwa-ai' ) . '</span>';
		}

		$start = $this->get_appointment_start( $appointment );
		if ( '' === $start ) {
			return '<span aria-hidden="true">&mdash;</span><span class="screen-reader-text">' . esc_html__( 'Not scheduled', 'niwa-ai' ) . '</span>';
		}

		try {
			$start_date = new DateTimeImmutable( $start, wp_timezone() );
			$formatted  = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $start_date->getTimestamp(), wp_timezone() );
		} catch ( Throwable $exception ) {
			$formatted = $start;
		}
		$labels    = $this->get_scheduler_labels();
		$service   = $appointment['service_name'] ?? ( isset( $appointment['service_id'] ) ? ( $labels['services'][ (string) $appointment['service_id'] ] ?? '' ) : '' );
		$member    = $appointment['team_member_name'] ?? $appointment['member_name'] ?? ( isset( $appointment['team_member_id'] ) ? ( $labels['members'][ (string) $appointment['team_member_id'] ] ?? '' ) : '' );
		$meta      = trim( implode( ' · ', array_filter( array( (string) $service, (string) $member ) ) ) );

		$html = '<span class="niwa-schedule-time">' . esc_html( $formatted ) . '</span>';
		if ( '' !== $meta ) {
			$html .= '<span class="niwa-schedule-meta">' . esc_html( $meta ) . '</span>';
		}
		return $html;
	}

	/**
	 * Return local start time from any supported appointment row.
	 *
	 * @param array $appointment Appointment row.
	 * @return string
	 */
	private function get_appointment_start( $appointment ) {
		foreach ( array( 'starts_at', 'start_at', 'scheduled_at', 'start_time' ) as $key ) {
			if ( isset( $appointment[ $key ] ) && '' !== trim( (string) $appointment[ $key ] ) ) {
				return (string) $appointment[ $key ];
			}
		}
		return '';
	}

	/**
	 * Build service/member ID-to-name maps for appointment displays.
	 *
	 * @return array{members:array,services:array}
	 */
	private function get_scheduler_labels() {
		$settings = $this->get_scheduler_settings();
		$members  = array();
		$services = array();
		foreach ( (array) ( $settings['team_members'] ?? array() ) as $member ) {
			if ( is_array( $member ) && ! empty( $member['id'] ) ) {
				$members[ (string) $member['id'] ] = (string) ( $member['name'] ?? $member['id'] );
			}
		}
		foreach ( (array) ( $settings['services'] ?? array() ) as $service ) {
			if ( is_array( $service ) && ! empty( $service['id'] ) ) {
				$services[ (string) $service['id'] ] = (string) ( $service['name'] ?? $service['id'] );
			}
		}
		return array( 'members' => $members, 'services' => $services );
	}

	/**
	 * Render week and month appointment calendars.
	 *
	 * @return void
	 */
	private function render_month_calendar() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$view = isset( $_GET['calendar_view'] ) ? sanitize_key( wp_unslash( $_GET['calendar_view'] ) ) : 'week';
		$view = in_array( $view, array( 'week', 'month' ), true ) ? $view : 'week';
		$timezone = wp_timezone();

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$requested_date = isset( $_GET['calendar_date'] ) ? sanitize_text_field( wp_unslash( $_GET['calendar_date'] ) ) : '';
		$anchor         = DateTimeImmutable::createFromFormat( '!Y-m-d', $requested_date, $timezone );
		if ( false === $anchor || $anchor->format( 'Y-m-d' ) !== $requested_date ) {
			$anchor = new DateTimeImmutable( 'today', $timezone );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$requested_month = isset( $_GET['calendar_month'] ) ? sanitize_text_field( wp_unslash( $_GET['calendar_month'] ) ) : '';
		$month           = DateTimeImmutable::createFromFormat( '!Y-m', $requested_month, $timezone );
		if ( false === $month || $month->format( 'Y-m' ) !== $requested_month ) {
			$month = $anchor->modify( 'first day of this month' )->setTime( 0, 0, 0 );
		}

		$month_start = $month->setTime( 0, 0, 0 );
		$month_end   = $month_start->modify( 'first day of next month' );
		$grid_start  = $month_start->modify( 'monday this week' );
		if ( '1' === $month_start->format( 'N' ) ) {
			$grid_start = $month_start;
		}
		$last_day = $month_end->modify( '-1 day' );
		$grid_end = $last_day->modify( 'sunday this week' )->modify( '+1 day' );

		$week_start = $anchor->modify( 'monday this week' )->setTime( 0, 0, 0 );
		if ( '1' === $anchor->format( 'N' ) ) {
			$week_start = $anchor->setTime( 0, 0, 0 );
		}
		$week_end    = $week_start->modify( '+7 days' );
		$range_start = 'week' === $view ? $week_start : $grid_start;
		$range_end   = 'week' === $view ? $week_end : $grid_end;

		$appointments = array();
		$service      = $this->get_appointment_service();
		if ( $service && method_exists( $service, 'get_appointments_for_range' ) ) {
			try {
				$result       = $service->get_appointments_for_range( $range_start->format( 'Y-m-d' ), $range_end->modify( '-1 day' )->format( 'Y-m-d' ), array(), 1000, 0 );
				$appointments = is_array( $result ) ? $result : array();
			} catch ( Throwable $exception ) {
				$appointments = array();
			}
		}

		$events_by_date = array();
		foreach ( $appointments as $appointment ) {
			if ( ! is_array( $appointment ) ) {
				continue;
			}
			if ( in_array( (string) ( $appointment['status'] ?? '' ), array( 'cancelled', 'no_show' ), true ) ) {
				continue;
			}
			$start = $this->get_appointment_start( $appointment );
			if ( '' === $start ) {
				continue;
			}
			$date = substr( $start, 0, 10 );
			if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
				$events_by_date[ $date ][] = $appointment;
			}
		}

		$base_url = add_query_arg( array( 'page' => 'niwa-ai-lead-capture', 'tab' => 'contacts' ), admin_url( 'admin.php' ) );
		if ( 'week' === $view ) {
			$prev_url     = add_query_arg( array( 'calendar_view' => 'week', 'calendar_date' => $week_start->modify( '-7 days' )->format( 'Y-m-d' ) ), $base_url );
			$next_url     = add_query_arg( array( 'calendar_view' => 'week', 'calendar_date' => $week_start->modify( '+7 days' )->format( 'Y-m-d' ) ), $base_url );
			$period_label = wp_date( 'M j', $week_start->getTimestamp(), $timezone ) . ' – ' . wp_date( 'M j, Y', $week_end->modify( '-1 day' )->getTimestamp(), $timezone );
		} else {
			$prev_url     = add_query_arg( array( 'calendar_view' => 'month', 'calendar_month' => $month_start->modify( '-1 month' )->format( 'Y-m' ) ), $base_url );
			$next_url     = add_query_arg( array( 'calendar_view' => 'month', 'calendar_month' => $month_start->modify( '+1 month' )->format( 'Y-m' ) ), $base_url );
			$period_label = wp_date( 'F Y', $month_start->getTimestamp(), $timezone );
		}
		$week_url  = add_query_arg( array( 'calendar_view' => 'week', 'calendar_date' => $anchor->format( 'Y-m-d' ) ), $base_url );
		$month_url = add_query_arg( array( 'calendar_view' => 'month', 'calendar_month' => $month_start->format( 'Y-m' ) ), $base_url );
		$today     = wp_date( 'Y-m-d', null, $timezone );
		$labels    = $this->get_scheduler_labels();
		$week_layout = $this->get_week_calendar_window( $events_by_date );

		echo '<div class="niwa-card niwa-calendar-card">';
		echo '<div class="niwa-card__header"><div class="niwa-calendar-heading"><div class="niwa-card__icon"><span class="dashicons dashicons-calendar-alt" aria-hidden="true"></span></div><div><h2 class="niwa-card__title">' . esc_html__( 'Appointments calendar', 'niwa-ai' ) . '</h2><span class="niwa-field__hint">' . esc_html__( 'See every scheduled prospect by day and time.', 'niwa-ai' ) . '</span></div></div><div class="niwa-calendar-toolbar"><div class="niwa-calendar-nav"><a class="button niwa-schedule-row-action" href="' . esc_url( $prev_url ) . '" aria-label="' . esc_attr__( 'Previous period', 'niwa-ai' ) . '"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span></a><strong>' . esc_html( $period_label ) . '</strong><a class="button niwa-schedule-row-action" href="' . esc_url( $next_url ) . '" aria-label="' . esc_attr__( 'Next period', 'niwa-ai' ) . '"><span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span></a></div><div class="niwa-calendar-view-switch" role="group" aria-label="' . esc_attr__( 'Calendar view', 'niwa-ai' ) . '"><a class="' . ( 'week' === $view ? 'is-active' : '' ) . '" href="' . esc_url( $week_url ) . '">' . esc_html__( 'Week', 'niwa-ai' ) . '</a><a class="' . ( 'month' === $view ? 'is-active' : '' ) . '" href="' . esc_url( $month_url ) . '">' . esc_html__( 'Month', 'niwa-ai' ) . '</a></div><div class="niwa-calendar-time-switch" role="group" aria-label="' . esc_attr__( 'Time format', 'niwa-ai' ) . '"><button type="button" class="niwa-calendar-time-mode is-active" data-mode="12" aria-pressed="true">12h</button><button type="button" class="niwa-calendar-time-mode" data-mode="24" aria-pressed="false">24h</button></div></div></div>';
		echo '<div class="niwa-card__body niwa-scheduler-table-wrap">';
		if ( ! $service ) {
			echo '<p class="niwa-field__hint">' . esc_html__( 'Appointment data will appear here after the scheduler service is loaded.', 'niwa-ai' ) . '</p>';
		}
		if ( 'week' === $view ) {
			echo '<div class="niwa-week-calendar"><div class="niwa-week-calendar__grid">';
			echo '<aside class="niwa-week-calendar__time-axis" aria-hidden="true"><div class="niwa-week-calendar__time-corner"></div><div class="niwa-week-calendar__time-scale" style="--niwa-week-height:' . esc_attr( (string) $week_layout['height'] ) . 'px;--niwa-hour-size:' . esc_attr( (string) $week_layout['pixels_per_hour'] ) . 'px">';
			for ( $time_minutes = (int) $week_layout['start']; $time_minutes <= (int) $week_layout['end']; $time_minutes += 60 ) {
				$time_hour = (int) floor( $time_minutes / 60 ) % 24;
				$time_raw  = sprintf( '%02d:00', $time_hour );
				$time_text = (string) ( 0 === $time_hour % 12 ? 12 : $time_hour % 12 ) . ' ' . ( $time_hour >= 12 ? 'PM' : 'AM' );
				$time_top  = round( ( $time_minutes - (int) $week_layout['start'] ) / 60 * (int) $week_layout['pixels_per_hour'], 2 );
				echo '<span class="niwa-week-calendar__time-label" data-time="' . esc_attr( $time_raw ) . '" style="--niwa-time-top:' . esc_attr( (string) $time_top ) . 'px">' . esc_html( $time_text ) . '</span>';
			}
			echo '</div></aside>';
			for ( $day = $week_start; $day < $week_end; $day = $day->modify( '+1 day' ) ) {
				$date_key = $day->format( 'Y-m-d' );
				$classes  = array( 'niwa-week-calendar__day' );
				if ( $date_key === $today ) {
					$classes[] = 'is-today';
				}
				echo '<section class="' . esc_attr( implode( ' ', $classes ) ) . '"><header><span>' . esc_html( wp_date( 'D', $day->getTimestamp(), $timezone ) ) . '</span><strong>' . esc_html( $day->format( 'd' ) ) . '</strong></header><div class="niwa-week-calendar__events" style="--niwa-week-height:' . esc_attr( (string) $week_layout['height'] ) . 'px;--niwa-hour-size:' . esc_attr( (string) $week_layout['pixels_per_hour'] ) . 'px">';
				$day_events = $events_by_date[ $date_key ] ?? array();
				foreach ( $this->get_week_day_event_layouts( $day_events, $week_layout ) as $event_layout ) {
					$this->render_calendar_event_card( $event_layout['event'], $labels, $event_layout['layout'] );
				}
				echo '</div></section>';
			}
			echo '</div></div>';
		} else {
		echo '<div class="niwa-calendar"><div class="niwa-calendar__weekdays">';
		foreach ( array( __( 'Mon', 'niwa-ai' ), __( 'Tue', 'niwa-ai' ), __( 'Wed', 'niwa-ai' ), __( 'Thu', 'niwa-ai' ), __( 'Fri', 'niwa-ai' ), __( 'Sat', 'niwa-ai' ), __( 'Sun', 'niwa-ai' ) ) as $weekday ) {
			echo '<div class="niwa-calendar__weekday">' . esc_html( $weekday ) . '</div>';
		}
		echo '</div><div class="niwa-calendar__grid">';

		for ( $day = $grid_start; $day < $grid_end; $day = $day->modify( '+1 day' ) ) {
			$date_key = $day->format( 'Y-m-d' );
			$classes  = array( 'niwa-calendar__day' );
			if ( $day->format( 'Y-m' ) !== $month_start->format( 'Y-m' ) ) {
				$classes[] = 'is-outside';
			}
			if ( $date_key === $today ) {
				$classes[] = 'is-today';
			}
			echo '<div class="' . esc_attr( implode( ' ', $classes ) ) . '"><span class="niwa-calendar__date">' . esc_html( $day->format( 'j' ) ) . '</span><div class="niwa-calendar__events">';
			foreach ( $events_by_date[ $date_key ] ?? array() as $event ) {
				$this->render_calendar_event_card( $event, $labels );
			}
			echo '</div></div>';
		}

			echo '</div></div>';
		}
		echo '</div></div>';
	}

	/**
	 * Calculate a readable weekly time window around actual appointments.
	 *
	 * @param array $events_by_date Appointment rows grouped by local date.
	 * @return array{start:int,end:int,height:int,pixels_per_hour:int}
	 */
	private function get_week_calendar_window( $events_by_date ) {
		$earliest = null;
		$latest   = null;

		foreach ( $events_by_date as $events ) {
			foreach ( (array) $events as $event ) {
				if ( ! is_array( $event ) ) {
					continue;
				}
				$start = $this->get_calendar_time_minutes( $this->get_appointment_start( $event ) );
				if ( null === $start ) {
					continue;
				}
				$end      = min( 1440, $start + $this->get_calendar_event_duration( $event, $start ) );
				$earliest = null === $earliest ? $start : min( $earliest, $start );
				$latest   = null === $latest ? $end : max( $latest, $end );
			}
		}

		if ( null === $earliest || null === $latest ) {
			$window_start = 8 * 60;
			$window_end   = 18 * 60;
		} else {
			$window_start = max( 0, (int) floor( $earliest / 60 ) * 60 - 60 );
			$window_end   = min( 1440, (int) ceil( $latest / 60 ) * 60 + 60 );
			$minimum_span = 6 * 60;
			if ( $window_end - $window_start < $minimum_span ) {
				$window_end   = min( 1440, $window_start + $minimum_span );
				$window_start = max( 0, $window_end - $minimum_span );
			}
		}

		$pixels_per_hour = 48;
		return array(
			'start'           => $window_start,
			'end'             => $window_end,
			'height'          => max( 288, (int) round( ( $window_end - $window_start ) / 60 * $pixels_per_hour ) ),
			'pixels_per_hour' => $pixels_per_hour,
		);
	}

	/**
	 * Assign time positions and overlap lanes to one day's appointments.
	 *
	 * @param array $events Appointment rows for one date.
	 * @param array $window Weekly window configuration.
	 * @return array
	 */
	private function get_week_day_event_layouts( $events, $window ) {
		$prepared = array();
		foreach ( (array) $events as $event ) {
			if ( ! is_array( $event ) ) {
				continue;
			}
			$start = $this->get_calendar_time_minutes( $this->get_appointment_start( $event ) );
			if ( null === $start ) {
				continue;
			}
			$prepared[] = array(
				'event' => $event,
				'start' => $start,
				'end'   => min( 1440, $start + $this->get_calendar_event_duration( $event, $start ) ),
			);
		}

		usort(
			$prepared,
			static function ( $left, $right ) {
				return $left['start'] <=> $right['start'];
			}
		);

		$rows          = array();
		$lane_ends     = array();
		$group_rows    = array();
		$group_max_end = -1;
		$finalize      = static function () use ( &$rows, &$lane_ends, &$group_rows ) {
			$lane_count = max( 1, count( $lane_ends ) );
			foreach ( $group_rows as $row_index ) {
				$rows[ $row_index ]['lane_count'] = $lane_count;
			}
		};

		foreach ( $prepared as $item ) {
			if ( ! empty( $group_rows ) && $item['start'] >= $group_max_end ) {
				$finalize();
				$lane_ends     = array();
				$group_rows    = array();
				$group_max_end = -1;
			}

			$lane = 0;
			while ( isset( $lane_ends[ $lane ] ) && $lane_ends[ $lane ] > $item['start'] ) {
				++$lane;
			}
			$lane_ends[ $lane ] = $item['end'];
			$row_index = count( $rows );
			$rows[] = array(
				'event'      => $item['event'],
				'start'      => $item['start'],
				'end'        => $item['end'],
				'lane'       => $lane,
				'lane_count' => 1,
			);
			$group_rows[] = $row_index;
			$group_max_end = max( $group_max_end, $item['end'] );
		}
		if ( ! empty( $group_rows ) ) {
			$finalize();
		}

		$pixels_per_minute = (int) $window['pixels_per_hour'] / 60;
		$out               = array();
		foreach ( $rows as $row ) {
			$visible_start = max( (int) $window['start'], $row['start'] );
			$visible_end   = min( (int) $window['end'], $row['end'] );
			$lane_width    = 100 / max( 1, $row['lane_count'] );
			$out[]         = array(
				'event'  => $row['event'],
				'layout' => array(
					'top'    => round( max( 0, ( $visible_start - (int) $window['start'] ) * $pixels_per_minute ), 2 ),
					'height' => round( max( 30, ( $visible_end - $visible_start ) * $pixels_per_minute - 4 ), 2 ),
					'left'   => round( $row['lane'] * $lane_width, 3 ),
					'width'  => round( $lane_width, 3 ),
				),
			);
		}

		return $out;
	}

	/**
	 * Convert a local date-time string to minutes after midnight.
	 *
	 * @param string $date_time Local date-time string.
	 * @return int|null
	 */
	private function get_calendar_time_minutes( $date_time ) {
		if ( ! preg_match( '/(?:^|\s)(\d{1,2}):(\d{2})/', (string) $date_time, $matches ) ) {
			return null;
		}
		$hour   = (int) $matches[1];
		$minute = (int) $matches[2];
		return $hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59 ? $hour * 60 + $minute : null;
	}

	/**
	 * Determine appointment duration for calendar layout.
	 *
	 * @param array $event         Appointment row.
	 * @param int   $start_minutes Start minutes after midnight.
	 * @return int
	 */
	private function get_calendar_event_duration( $event, $start_minutes ) {
		$end_minutes = $this->get_calendar_time_minutes( (string) ( $event['ends_at'] ?? '' ) );
		if ( null !== $end_minutes ) {
			if ( $end_minutes <= $start_minutes ) {
				$end_minutes += 1440;
			}
			return max( 5, min( 1440, $end_minutes - $start_minutes ) );
		}
		return max( 5, min( 1440, absint( $event['duration_minutes'] ?? 60 ) ) );
	}

	/**
	 * Render one pastel appointment card shared by week and month views.
	 *
	 * @param array $event       Appointment row.
	 * @param array $labels      Scheduler labels.
	 * @param array $layout      Optional week-position layout.
	 * @return void
	 */
	private function render_calendar_event_card( $event, $labels, $layout = array() ) {
		$start        = $this->get_appointment_start( $event );
		$end          = isset( $event['ends_at'] ) ? (string) $event['ends_at'] : '';
		$start_time   = strlen( $start ) >= 16 ? substr( $start, 11, 5 ) : '';
		$end_time     = strlen( $end ) >= 16 ? substr( $end, 11, 5 ) : '';
		$contact_name = (string) ( $event['customer_name'] ?? $event['contact_name'] ?? $event['name'] ?? __( 'Appointment', 'niwa-ai' ) );
		$service_name = (string) ( $event['service_name'] ?? ( isset( $event['service_id'] ) ? ( $labels['services'][ (string) $event['service_id'] ] ?? '' ) : '' ) );
		$member_name  = (string) ( $event['team_member_name'] ?? ( isset( $event['team_member_id'] ) ? ( $labels['members'][ (string) $event['team_member_id'] ] ?? '' ) : '' ) );
		$color_key = ! empty( $event['booking_uid'] ) ? (string) $event['booking_uid'] : ( ! empty( $event['id'] ) ? (string) $event['id'] : (string) ( ( $event['service_id'] ?? '' ) . '|' . ( $event['contact_id'] ?? '' ) . '|' . $start ) );
		$color     = hexdec( substr( md5( $color_key ), 0, 4 ) ) % 4 + 1;
		$classes   = array( 'niwa-calendar-event', 'is-color-' . $color );
		$style     = '';

		if ( ! empty( $layout ) ) {
			$classes[] = 'is-week-layout';
			$style     = sprintf(
				'--niwa-event-top:%spx;--niwa-event-height:%spx;--niwa-event-left:%s%%;--niwa-event-width:%s%%',
				(float) ( $layout['top'] ?? 0 ),
				(float) ( $layout['height'] ?? 30 ),
				(float) ( $layout['left'] ?? 0 ),
				(float) ( $layout['width'] ?? 100 )
			);
		}

		echo '<article class="' . esc_attr( implode( ' ', $classes ) ) . '"' . ( '' !== $style ? ' style="' . esc_attr( $style ) . '"' : '' ) . '><strong class="niwa-calendar-event__name">' . esc_html( $contact_name ) . '</strong><span class="niwa-calendar-event__time" data-start-time="' . esc_attr( $start_time ) . '" data-end-time="' . esc_attr( $end_time ) . '">' . esc_html( trim( $start_time . ( '' !== $end_time ? ' – ' . $end_time : '' ) ) ) . '</span>';
		if ( '' !== $service_name || '' !== $member_name ) {
			echo '<span class="niwa-calendar-event__meta">' . esc_html( trim( $service_name . ( '' !== $service_name && '' !== $member_name ? ' · ' : '' ) . $member_name ) ) . '</span>';
		}
		echo '</article>';
	}

	/**
	 * Render the editable contact-details modal.
	 *
	 * @return void
	 */
	private function render_contact_details_modal() {
		echo '<div id="niwa-contact-details-modal" class="niwa-contact-details-modal" role="dialog" aria-modal="true" aria-labelledby="niwa-contact-details-title" hidden>';
		echo '<div class="niwa-contact-details-modal__backdrop" data-niwa-details-close></div><div class="niwa-contact-details-modal__dialog">';
		echo '<div class="niwa-contact-details-modal__header"><h2 id="niwa-contact-details-title">' . esc_html__( 'Prospect details', 'niwa-ai' ) . '</h2><button type="button" class="button-link" data-niwa-details-close aria-label="' . esc_attr__( 'Close', 'niwa-ai' ) . '">&times;</button></div>';
		echo '<div class="niwa-contact-details-modal__body"><p class="niwa-field__hint">' . esc_html__( 'Keep useful context about this prospect. Niwa admin can use these notes when you ask about the contact.', 'niwa-ai' ) . '</p><textarea aria-label="' . esc_attr__( 'Prospect details', 'niwa-ai' ) . '"></textarea></div>';
		echo '<div class="niwa-contact-details-modal__footer"><span class="niwa-details-status" role="status" aria-live="polite"></span><button type="button" class="button" data-niwa-details-close>' . esc_html__( 'Cancel', 'niwa-ai' ) . '</button><button type="button" class="button button-primary niwa-details-save">' . esc_html__( 'Save details', 'niwa-ai' ) . '</button></div>';
		echo '</div></div>';
	}

	/**
	 * AJAX handler: delete a contact.
	 */
	public function ajax_delete_contact() {
		check_ajax_referer( 'niwa_ai_contacts_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ) );
		}

		$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		if ( $id <= 0 ) {
			wp_send_json_error( array( 'message' => 'Invalid ID.' ) );
		}

		$deleted = $this->contact_service->delete_contact( $id );

		if ( $deleted ) {
			wp_send_json_success( array( 'message' => 'Contact deleted.' ) );
		} else {
			wp_send_json_error( array( 'message' => 'Failed to delete contact.' ) );
		}
	}

	/**
	 * AJAX handler: update contact-level CRM details.
	 *
	 * @return void
	 */
	public function ajax_update_contact_details() {
		check_ajax_referer( 'niwa_ai_contacts_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Forbidden', 'niwa-ai' ) ), 403 );
		}

		$contact_id = isset( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
		$details    = isset( $_POST['details'] ) ? sanitize_textarea_field( wp_unslash( $_POST['details'] ) ) : '';
		if ( $contact_id <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'Invalid contact ID.', 'niwa-ai' ) ), 400 );
		}

		try {
			$service = $this->get_appointment_service();
			if ( $service && method_exists( $service, 'update_contact_details' ) ) {
				$updated = $service->update_contact_details( $contact_id, $details, get_current_user_id() );
			} elseif ( method_exists( $this->contact_service, 'update_contact_details' ) ) {
				$updated = $this->contact_service->update_contact_details( $contact_id, $details );
			} else {
				$updated = false;
			}
		} catch ( Throwable $exception ) {
			$updated = false;
		}

		if ( false === $updated ) {
			wp_send_json_error( array( 'message' => __( 'Details could not be saved.', 'niwa-ai' ) ) );
		}

		wp_send_json_success(
			array(
				'message' => __( 'Details saved.', 'niwa-ai' ),
				'details' => $details,
			)
		);
	}

	/**
	 * AJAX handler: export contacts as CSV.
	 */
	public function ajax_export_csv() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$nonce = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'niwa_ai_contacts_nonce' ) ) {
			wp_die( 'Invalid nonce.' );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( 'Forbidden' );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$range = isset( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'all';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$mode  = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : 'full';

		global $wpdb;
		$table = $wpdb->prefix . 'niwa_ai_contacts';

		$where = '';
		switch ( $range ) {
			case '24h':
				$where = 'WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)';
				break;
			case '7d':
				$where = 'WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)';
				break;
			case '1m':
				$where = 'WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)';
				break;
			case '2m':
				$where = 'WHERE created_at >= DATE_SUB(NOW(), INTERVAL 2 MONTH)';
				break;
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$contacts = $wpdb->get_results( "SELECT * FROM {$table} {$where} ORDER BY created_at DESC", ARRAY_A );

		$filename = 'niwa-ai-contacts-' . $range . '-' . wp_date( 'Y-m-d' ) . '.csv';

		// Streaming CSV output.
		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Pragma: no-cache' );
		header( 'Expires: 0' );

		$output = fopen( 'php://output', 'w' );

		// UTF-8 BOM for Excel compatibility.
		fwrite( $output, "\xEF\xBB\xBF" );

		if ( 'emails' === $mode ) {
			fputcsv( $output, array( 'Email' ) );
			foreach ( $contacts as $contact ) {
				$email = isset( $contact['email'] ) ? $contact['email'] : '';
				if ( 0 === strpos( $email, 'phone_only_' ) ) {
					continue; // Skip placeholder emails.
				}
				fputcsv( $output, array( $email ) );
			}
		} else {
			fputcsv( $output, array( 'ID', 'Name', 'Email', 'Phone', 'Source', 'Consent', 'Created' ) );
			foreach ( $contacts as $contact ) {
				$email = isset( $contact['email'] ) ? $contact['email'] : '';
				if ( 0 === strpos( $email, 'phone_only_' ) ) {
					$email = '(phone only)';
				}
				fputcsv( $output, array(
					isset( $contact['id'] ) ? $contact['id'] : '',
					isset( $contact['name'] ) ? $contact['name'] : '',
					$email,
					isset( $contact['phone'] ) ? $contact['phone'] : '',
					isset( $contact['consent_source'] ) ? $contact['consent_source'] : '',
					! empty( $contact['consent_given'] ) ? 'Yes' : 'No',
					isset( $contact['created_at'] ) ? $contact['created_at'] : '',
				) );
			}
		}

		fclose( $output );
		exit;
	}

	/**
	 * Get defaults for lead capture fields.
	 *
	 * @return array
	 */
	private function get_defaults() {
		return self::get_defaults_static();
	}

	/**
	 * Static defaults for external consumers.
	 *
	 * @return array
	 */
	private static function get_defaults_static() {
		return array(
			'email_optin_enabled'     => 0,
			'email_ask_name_again'    => 0,
			'email_start_chat_cacher_enabled' => 0,
			'email_start_chat_cacher_prompt'  => '',
			'phone_capture_enabled'   => 0,
			'phone_capture_mode'      => 'email_phone',
			'cb_code_email_hunt_enabled' => 0,
			'cb_code_email_hunt_prompt'  => 'Da li mozete upisati email da vam prosledim kod za popust?',
			'cb_code_email_hunt_email_subject' => 'Vas kod za popust',
			'cb_code_email_hunt_email_body' => "Pozdrav,\n\nVas kod za popust je {coupon_code}.\nMozete ga iskoristiti na checkout stranici u narednih {duration} minuta.\n\nKliknite na dugme ispod da nastavite kupovinu.\n\nPozdrav od Niwe",
			'cb_code_email_hunt_reminder_subject' => 'Kod za popust uskoro istice',
			'cb_code_email_hunt_reminder_body' => "Vidim da niste iskoristili kod koji sam vam poslala, molim vas pozurite jer vazi jos samo {remaining_minutes} minuta.\n\nKod: {coupon_code}\n\nKliknite na dugme ispod da se vratite u kupovinu.",
			'lead_notification_email' => '',
			// Missing Product Waitlist — capture email when a requested product isn't in catalog.
			// Trigger rules are hard-coded in the AI tool description (class-rlm-engine.php).
			'missing_product_enabled' => 0,
		);
	}

}
