<?php

namespace SugarCalendarBookings\BookingForm;

use DateTime;
use Exception;
use SugarCalendarBookings\Antispam\AntispamService;
use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Appointments\AppointmentService;
use SugarCalendarBookings\Appointments\CancellationTokenService;
use SugarCalendarBookings\Appointments\CreateAppointmentDTO;
use SugarCalendarBookings\Customers\CreateCustomerDTO;
use SugarCalendarBookings\Customers\CustomerService;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Ajax\AbstractController;

/**
 * Booking form controller.
 *
 * @since 1.0.0
 */
class BookingFormController extends AbstractController {

	/**
	 * Create appointment.
	 *
	 * @since 1.0.0
	 */
	public function create() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$data = $this->get_input_data( $this->get_input_rules() );

		$this->verify_antispam_or_die( $data );

		$customer_dto = new CreateCustomerDTO(
			[
				'name'  => $data['customer_name'],
				'email' => $data['customer_email'] ?? null,
				'phone' => $data['customer_phone'] ?? null,
			]
		);

		$customer_service = Plugin::get( CustomerService::class );

		try {
			$customer = $customer_service->get_existing_customer( $customer_dto );

			if ( ! $customer ) {
				$customer = $customer_service->create( $customer_dto );
			}
			// Existing-customer hit: associate the new appointment without
			// overwriting name / phone / notes. The public flow has no proof
			// the booker is the same person as the matched record — an
			// email-or-phone match alone is not authentication. Overwriting
			// would create a phone→email PII oracle and let any caller
			// silently rewrite a registered customer's contact details.
		} catch ( Exception $e ) {
			wp_send_json_error( $e->getMessage() );
		}

		$data['customer_id'] = $customer->id;
		$data['status']      = AppointmentModel::STATUS_DRAFT;
		$appointment_dto     = new CreateAppointmentDTO( $data );

		try {
			$appointment = Plugin::get( AppointmentService::class )->create( $appointment_dto );
		} catch ( Exception $e ) {
			wp_send_json_error( $e->getMessage() );
		}

		// Public response — strip DB-loaded customer fields so a phone-only
		// lookup that hits an existing record does not leak the matched
		// customer's email back to the caller. Echo the submitted values.
		$data = Plugin::get( BookingFormService::class )->get_appointment_form_data(
			$appointment,
			[
				'customer' => [
					'name'  => $data['customer_name'],
					'email' => $data['customer_email'] ?? '',
					'phone' => $data['customer_phone'] ?? '',
				],
			]
		);

		wp_send_json_success( $data );
	}

	/**
	 * Cancel appointment.
	 *
	 * @since 1.0.0
	 */
	public function cancel() {

		$rules = [
			'code'  => 'required|string|max:20',
			'token' => 'required|string|min:24|max:24',
		];

		$data = $this->get_input_data( $rules );

		$appointment = AppointmentModel::query()
			->with( [ 'payments', 'meta' ] )
			->where( 'code', '=', $data['code'], '%s' )
			->first();

		if ( ! $appointment ) {
			wp_send_json_error( esc_html__( 'Appointment not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		// Token gate: previously this path accepted code-only auth, which made
		// it a free mass-cancellation primitive for any caller that learned an
		// appointment's `code` (e.g. via the prior payments_capture IDOR).
		if ( ! Plugin::get( CancellationTokenService::class )->verify_cancellation_token( $data['token'], $appointment ) ) {
			wp_send_json_error( esc_html__( 'Invalid cancellation token.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		if ( $appointment->status === 'cancelled' ) {
			wp_send_json_error( esc_html__( 'Appointment is already cancelled.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$cancelled = Plugin::get( AppointmentService::class )->cancel( $appointment );

		if ( ! $cancelled ) {
			wp_send_json_error( esc_html__( 'Failed to cancel appointment.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		wp_send_json_success(
			[
				'message' => esc_html__( 'Appointment cancelled successfully.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			]
		);
	}

	/**
	 * Get services data formatted for booking form block editor.
	 *
	 * @since 1.0.0
	 */
	public function get_booking_form_data() {

		$rules = [
			'service_ids'   => 'array',
			'service_ids.*' => 'id',
		];

		$input_data  = $this->get_input_data( $rules );
		$service_ids = $input_data['service_ids'] ?? [];

		$data = Plugin::get( BookingFormService::class )->get_booking_form_data( $service_ids );

		wp_send_json_success( $data );
	}

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

		$data = Plugin::get( BookingFormService::class )->get_services_list_data();

		wp_send_json_success( $data );
	}

	/**
	 * Input validation rules for the create-appointment request.
	 *
	 * Exposed as a protected method so Pro can extend the ruleset without
	 * duplicating the shared keys.
	 *
	 * @since 1.5.0
	 *
	 * @return array
	 */
	protected function get_input_rules(): array {

		return [
			'service_id'        => 'required|id',
			'location'          => 'required|array',
			'location.type'     => 'required|string|in:in_person,online',
			'location.id'       => 'required_if:type,in_person|id',
			'location.uid'      => 'required_if:type,online|string',
			'employee_id'       => 'required|id',
			'customer_name'     => 'required|string|max:255',
			'customer_email'    => 'string|email|max:255',
			'customer_phone'    => 'string|max:255',
			'customer_notes'    => 'string|max:1000|trim',
			'customer_timezone' => 'required|string|timezone',
			'start_at'          => 'required|string|date_format:' . DateTime::ATOM,
			'payment_method'    => 'required|string|in:onsite,stripe',
			'antispam_token'    => 'string|max:5000',
		];
	}

	/**
	 * Run the antispam guard for the current request.
	 *
	 * Short-circuits if the configured method or chosen payment gateway does not
	 * require verification (issue #86 — Stripe is opt-in via filter). Otherwise
	 * verifies the submitted token; on failure, sends the configured fail message
	 * via `wp_send_json_error()` and terminates the request.
	 *
	 * @since 1.5.0
	 *
	 * @param array $data Validated input data from `get_input_data()`.
	 */
	protected function verify_antispam_or_die( array $data ): void {

		$antispam = Plugin::get( AntispamService::class );

		if ( ! $antispam->should_check( $data['payment_method'] ) ) {
			return;
		}

		if ( ! $antispam->verify( $data['antispam_token'] ?? '', 'booking_form_create' ) ) {
			wp_send_json_error( $antispam->get_fail_message() );
		}
	}
}
