<?php

namespace SugarCalendarBookings\BookingForm;

use DateTime;
use SugarCalendarBookings\Antispam\AntispamService;
use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Appointments\CancellationTokenService;
use SugarCalendarBookings\Integrations\IntegrationCapabilityRegistry;
use SugarCalendarBookings\Integrations\MeetingProviderInterface;
use SugarCalendarBookings\Locations\LocationsHelpers;
use SugarCalendarBookings\Locations\OnlineLocation;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Payments\Gateways\Onsite\OnSitePaymentGateway;
use SugarCalendarBookings\Payments\Gateways\PaymentGatewayRegistry;
use SugarCalendarBookings\Payments\PaymentsHelpers;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Services\ServiceModel;
use SugarCalendarBookings\Services\SmartTags\ServiceDurationSmartTag;
use SugarCalendarBookings\Services\SmartTags\ServiceLocationsSmartTag;
use SugarCalendarBookings\Services\SmartTags\ServiceNameSmartTag;
use SugarCalendarBookings\Services\SmartTags\ServicePriceSmartTag;
use SugarCalendarBookings\Support\Helpers\Date;
use SugarCalendarBookings\Support\Helpers\Time;
use SugarCalendarBookings\Support\Helpers\WP;
use SugarCalendarBookings\Support\RequestType;
use SugarCalendarBookings\Support\SmartTags\AdminEmailSmartTag;
use SugarCalendarBookings\Support\SmartTags\SiteNameSmartTag;
use SugarCalendarBookings\Support\SmartTags\SmartTagProcessor;

/**
 * Booking form service.
 *
 * @since 1.0.0
 */
class BookingFormService {

	/**
	 * Format service data for booking form.
	 *
	 * @since 1.0.0
	 *
	 * @param ServiceModel $service Service model instance.
	 *
	 * @return array Formatted service data.
	 */
	protected function format_service_data( $service ): array {

		// Process all locations.
		$locations = $this->process_service_locations( $service );

		// Process employees.
		$employees = array_map(
			fn( $employee ) => [
				'id'   => $employee->id,
				'name' => $employee->name,
			],
			$service->employees
		);

		// Process tags.
		$tags = array_map( fn( $tag ) => $tag->id, $service->tags );

		// Process feature image.
		$feature_image_data = $this->process_feature_image( $service->feature_image_id );
		$duration           = Time::convert_duration( $service->duration );
		$payment_gateways   = $this->get_service_payment_gateways( $service );

		return [
			'id'                 => $service->id,
			'name'               => $service->name,
			'color'              => $service->color,
			'duration'           => $duration['value'],
			'duration_unit'      => $duration['unit'],
			'locations'          => $locations,
			'employees'          => $employees,
			'online_locations'   => $service->online_locations,
			'price'              => html_entity_decode(
				PaymentsHelpers::format_currency( $service->price ),
				ENT_QUOTES,
				'UTF-8'
			),
			'tags'               => $tags,
			'description'        => $this->process_service_description( $service ),
			'feature_image_id'   => $feature_image_data['id'],
			'feature_image_url'  => $feature_image_data['url'],
			'feature_image_name' => $feature_image_data['name'],
			'enabled'            => $service->status === 'enabled',
			'payment_gateways'   => $payment_gateways,
		];
	}

	/**
	 * Get global payment gateway defaults.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_global_payment_gateways_defaults() {

		$options             = Plugin::get( Options::class );
		$registered_gateways = Plugin::get( PaymentGatewayRegistry::class )->get_registered();
		$gateways            = [];

		foreach ( $registered_gateways as $gateway ) {
			$gateway_id = $gateway::get_id();

			if ( (bool) $options->get( "payments.gateways.{$gateway_id}.enabled" ) && $gateway->is_configured() ) {
				$gateways[ $gateway_id ] = [];
			}
		}

		// Fallback to On-Site if no gateways are enabled.
		if ( empty( $gateways ) ) {
			$gateways[ OnSitePaymentGateway::get_id() ] = [];
		}

		return $gateways;
	}

	/**
	 * Get service payment gateways.
	 *
	 * @since 1.0.0
	 *
	 * @param ServiceModel $service Service model instance.
	 *
	 * @return array Payment gateways configuration.
	 */
	protected function get_service_payment_gateways( $service ) {

		// Start with global defaults.
		$payment_gateways = $this->get_global_payment_gateways_defaults();

		/**
		 * Filter service payment gateways data to add service-specific configuration.
		 *
		 * @since 1.0.0
		 *
		 * @param array        $gateways Gateways data.
		 * @param ServiceModel $service  Service model instance.
		 */
		$payment_gateways = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
			'sugar_calendar_bookings_booking_form_service_payment_gateways',
			$payment_gateways,
			$service
		);

		return $payment_gateways;
	}

	/**
	 * Process service description.
	 *
	 * @since 1.0.0
	 *
	 * @param ServiceModel $service Service model instance.
	 *
	 * @return string Processed service description.
	 */
	private function process_service_description( $service ): string {

		$smart_tag_processor = new SmartTagProcessor(
			[
				// Site-level placeholders.
				new SiteNameSmartTag(),
				new AdminEmailSmartTag(),

				// Service-specific smart tags.
				new ServiceNameSmartTag( $service ),
				new ServiceDurationSmartTag( $service ),
				new ServicePriceSmartTag( $service ),
				new ServiceLocationsSmartTag( $service ),
			]
		);

		return $smart_tag_processor->process( $service->description );
	}

	/**
	 * Process service locations including in-person and online.
	 *
	 * @since 1.0.0
	 *
	 * @param ServiceModel $service Service model instance.
	 *
	 * @return array Processed locations array.
	 */
	private function process_service_locations( $service ): array {

		// Process in-person locations.
		$locations = array_map(
			fn( $location ) => [
				'id'      => $location->id,
				'type'    => 'in_person',
				'order'   => (int) $location->pivot->location_order,
				'name'    => $location->name,
				'address' => $location->address,
			],
			$service->locations
		);

		// Process online locations.
		if ( ! empty( $service->online_locations ) ) {
			$capability_registry = Plugin::get( IntegrationCapabilityRegistry::class );

			foreach ( $service->online_locations as $location ) {
				$slug = $location['provider'] ?? '';

				// Skip meeting provider locations when their integration is unavailable.
				if ( $slug !== 'custom_link' ) {
					$provider = $capability_registry->find( MeetingProviderInterface::class, $slug );

					if ( ! $provider || ! $provider->is_available() ) {
						continue;
					}
				}

				$online_location = OnlineLocation::from_array( $location );

				if ( $online_location ) {
					$locations[] = [
						'id'    => null,
						'type'  => 'online',
						'uid'   => $online_location->get_uid(),
						'name'  => $online_location->get_display_name(),
						'order' => $online_location->get_order(),
					];
				}
			}
		}

		// Sort locations by order.
		usort(
			$locations,
			fn( $a, $b ) => $a['order'] - $b['order']
		);

		return $locations;
	}

	/**
	 * Process feature image data.
	 *
	 * @since 1.0.0
	 *
	 * @param int|null $feature_image_id Feature image ID.
	 *
	 * @return array Feature image data.
	 */
	private function process_feature_image( $feature_image_id ): array {

		$feature_image_url  = null;
		$feature_image_name = null;

		if ( $feature_image_id ) {
			$feature_image      = wp_get_attachment_image_src( $feature_image_id );
			$feature_image_url  = $feature_image[0] ?? null;
			$feature_image_name = $feature_image !== false ? get_the_title( $feature_image_id ) : null;
		}

		return [
			'id'   => $feature_image_id,
			'url'  => $feature_image_url,
			'name' => $feature_image_name,
		];
	}

	/**
	 * Get services list data.
	 *
	 * @since 1.0.0
	 *
	 * @return array Services list data.
	 */
	public function get_services_list_data(): array {

		$services = ServiceModel::query()
			->where( 'status', '=', 'enabled', '%s' )
			->order_by( 'name' )
			->get();

		return array_map(
			fn( $service ) => [
				'id'   => $service->id,
				'name' => $service->name,
			],
			$services
		);
	}

	/**
	 * Get services data formatted for booking form.
	 *
	 * @since 1.0.0
	 *
	 * @param array $service_ids Optional array of service IDs to filter by.
	 *
	 * @return array Formatted services data.
	 */
	public function get_booking_form_data( array $service_ids = [] ): array {

		$query = ServiceModel::query()
			->with(
				[
					'locations' => fn( $q ) => $q->select( [ 'id', 'name', 'address' ] )->where( 'status', '=', 'enabled', '%s' ),
					'employees' => fn( $q ) => $q->select( [ 'id', 'name' ] )->where( 'status', '=', 'enabled', '%s' )->order_by( 'name' ),
					'tags'      => fn( $q ) => $q->select( [ 'id' ] ),
					'meta'      => fn( $q ) => $q->where(
						'meta_key',
						'IN',
						[
							'online_locations',
							'feature_image_id',
							'working_schedule',
							'use_default_working_schedule',
							'override_payment_methods',
							'payment_gateways',
						],
						'%s'
					),
				]
			)
			->where( 'status', '=', 'enabled', '%s' );

		if ( ! empty( $service_ids ) ) {
			$query->where( 'id', 'IN', $service_ids, '%d' );
		}

		$services = array_values(
			array_filter(
				array_map(
					fn( $service ) => $this->format_service_data( $service ),
					$query->get()
				),
				fn( $service ) => ! empty( $service['locations'] ) && ! empty( $service['employees'] )
			)
		);

		/**
		 * Filter form payment gateways data.
		 *
		 * @since 1.0.0
		 *
		 * @param array $gateways Gateways data.
		 */
		$payment_gateways = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
			'sugar_calendar_bookings_booking_form_payment_gateways',
			[]
		);

		/**
		 * Filter form data.
		 *
		 * @since 1.0.0
		 *
		 * @param array $form_data Form data.
		 */
		$form_data = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
			'sugar_calendar_bookings_booking_form_data',
			[
				'services'         => $services,
				'payment_gateways' => $payment_gateways,
				'is_pro'           => Plugin::instance()->is_pro(),
			]
		);

		return $form_data;
	}

	/**
	 * Get appointment form data for the booking-form / confirmation / cancellation UI.
	 *
	 * Customer fields default to the loaded record but callers in public flows
	 * MUST pass $overrides['customer'] with the submitted values to avoid a
	 * phone→email PII oracle: when the public booking-form auto-merges a
	 * phone-only lookup against an existing customer record, echoing the
	 * matched record's email back to the (unauthenticated) caller turns the
	 * endpoint into a phone-to-email lookup. Callers that have already
	 * verified ownership (e.g. tokenized cancellation page) may omit the
	 * override and receive the DB record values.
	 *
	 * @since 1.0.0
	 * @since 1.4.1 Added $overrides — public callers must pass a customer override.
	 *
	 * @param AppointmentModel $appointment Appointment model instance.
	 * @param array            $overrides   Optional. Field overrides. Supports the `customer`
	 *                                      key with `name` / `email` / `phone` sub-keys.
	 *
	 * @return array
	 */
	public function get_appointment_form_data( $appointment, $overrides = [] ) {

		$location = $appointment->get_location();

		$customer = $overrides['customer'] ?? [
			'name'  => $appointment->customer->name,
			'email' => $appointment->customer->email,
			'phone' => $appointment->customer->phone,
		];

		return [
			'id'                 => $appointment->id,
			'code'               => $appointment->code,
			'cancellation_token' => Plugin::get( CancellationTokenService::class )->generate_cancellation_token( $appointment ),
			'status'             => $appointment->status,
			'start_at'           => $appointment->start_at->format( DateTime::ATOM ),
			'end_at'             => $appointment->end_at->format( DateTime::ATOM ),
			'duration'           => $appointment->get_duration(),
			'price'              => html_entity_decode(
				PaymentsHelpers::format_currency( $appointment->get_price() ),
				ENT_QUOTES,
				'UTF-8'
			),
			'customer'           => $customer,
			'service'            => [
				'name' => $appointment->service->name,
			],
			'location'           => $location ? [
				'type'            => $location->get_type(),
				'name'            => $location->get_display_name(),
				'address'         => $location->get_address(),
				'google_maps_url' => LocationsHelpers::get_google_maps_url( $location->get_model() ),
				'provider'        => $appointment->online_location['provider'] ?? null,
				'join_url'        => esc_url_raw( $appointment->online_location['join_url'] ?? $appointment->online_location['url'] ?? '' ) ?: null,
			] : null,
			'employee'           => [
				'name' => $appointment->employee->name,
			],
		];
	}

	/**
	 * Get services list data for block editor.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_services_list(): array {

		$services = ServiceModel::query()
			->select( [ 'id', 'name' ] )
			->where( 'status', '=', 'enabled', '%s' )
			->order_by( 'name' )
			->get();

		return array_map(
			fn( $service ) => [
				'id'   => $service->id,
				'name' => $service->name,
			],
			$services
		);
	}

	/**
	 * Get script data.
	 *
	 * @since 1.0.0
	 * @since 1.2.0 Add is_pro flag.
	 *
	 * @return array
	 */
	public function get_script_data(): array {

		$options = Plugin::get( Options::class );

		$data = [
			'ajax_url'              => WP::ajax_url(),
			'build_url'             => SCBOOKINGS_PLUGIN_URL . 'assets/dist/',
			'display_start_of_week' => $options->get( 'display.start_of_week' ),
			'display_date_format'   => $options->get( 'display.date_format' ),
			'display_time_format'   => $options->get( 'display.time_format' ),
			'display_timezone'      => Date::get_default_timezone_string(),
			'is_pro'                => Plugin::instance()->is_pro(),
		];

		// Include services data for block editor to avoid loading delay.
		if ( Plugin::get( RequestType::class )->is_block_editor() ) {
			$data['services']        = $this->get_services_list();
			$data['bookingFormData'] = $this->get_booking_form_data();
		}

		// Append antispam configuration when enabled. Secret keys are never exposed.
		$antispam_block = $this->build_antispam_script_data();

		if ( $antispam_block !== null ) {
			$data['antispam'] = $antispam_block;
		}

		return $data;
	}

	/**
	 * Build the antispam configuration block for the localized booking-form data.
	 *
	 * Returns null when antispam is disabled so the block is omitted entirely.
	 * Secret keys are never included.
	 *
	 * @since 1.5.0
	 *
	 * @return array|null
	 */
	private function build_antispam_script_data(): ?array {

		$antispam = Plugin::get( AntispamService::class );
		$active   = $antispam->get_active_provider();

		if ( $active === null ) {
			return null;
		}

		return [
			'enabled'         => true,
			'method'          => (string) Plugin::get( Options::class )->get( 'antispam.method' ),
			'check_on_stripe' => $antispam->should_check( 'stripe' ),
			'no_conflict'     => $antispam->is_no_conflict_enabled(),
			'fail_message'    => $active->get_fail_message(),
			'strings'         => [
				'please_complete' => esc_html__(
					'Please complete the verification before continuing.',
					'sugar-calendar-bookings-scheduling-appointments-lite'
				),
			],
			'config'          => $active->get_frontend_config(),
		];
	}
}
