<?php

namespace SugarCalendarBookings\Appointments;

use Exception;
use SugarCalendarBookings\Availability\AvailabilityService;
use SugarCalendarBookings\Customers\CustomerModel;
use SugarCalendarBookings\Employees\EmployeeModel;
use SugarCalendarBookings\Locations\LocationFactory;
use SugarCalendarBookings\Notifications\AppointmentNotifier;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Services\ServiceModel;
use SugarCalendarBookings\Support\Helpers\Date;

/**
 * Appointment logic for Appointment model.
 *
 * @since 1.0.0
 */
class AppointmentService {

	/**
	 * Create a new Appointment.
	 *
	 * @since 1.0.0
	 *
	 * @param CreateAppointmentDTO $dto Data for appointment creation.
	 *
	 * @return AppointmentModel The created Appointment model.
	 * @throws Exception If appointment creation fails.
	 */
	public function create( $dto ) {

		$appointment = new AppointmentModel();

		if ( ! isset( $dto->status ) ) {
			$dto = $dto->update(
				[
					'status' => Plugin::get( Options::class )->get( 'appointments.default_status' ),
				]
			);
		}

		$this->validate_appointment( $appointment, $dto );

		$this->fill_appointment( $appointment, $dto );

		$saved = $appointment->save();

		if ( ! $saved ) {
			throw new Exception( esc_html__( 'Failed to create appointment.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		/**
		 * Fires after an appointment is created and saved, before notifications are sent.
		 *
		 * @since 1.4.0
		 *
		 * @param AppointmentModel $appointment The created appointment.
		 */
		do_action( 'sugar_calendar_bookings_appointment_created', $appointment ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		// Send booking notifications (skip for draft - payment processor will trigger after final status is set).
		if ( $appointment->status !== AppointmentModel::STATUS_DRAFT ) {
			Plugin::get( AppointmentNotifier::class )->notify( $appointment );
		}

		return $appointment;
	}

	/**
	 * Update an existing Appointment.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The appointment model to update.
	 * @param AppointmentDTO   $dto         Data for appointment update.
	 *
	 * @return AppointmentModel The updated Appointment model.
	 * @throws Exception If appointment update fails.
	 */
	public function update( $appointment, $dto ) {

		$this->validate_appointment( $appointment, $dto, true );

		$this->fill_appointment( $appointment, $dto );

		$saved = $appointment->save();

		if ( ! $saved ) {
			throw new Exception( esc_html__( 'Failed to update appointment.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		/**
		 * Fires after an appointment is updated and saved, before notifications are sent.
		 *
		 * @since 1.4.0
		 *
		 * @param AppointmentModel $appointment The updated appointment.
		 */
		do_action( 'sugar_calendar_bookings_appointment_updated', $appointment ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		// Send appropriate notifications based on what changed.
		Plugin::get( AppointmentNotifier::class )->notify( $appointment );

		return $appointment;
	}

	/**
	 * Delete a Appointment.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The appointment model to delete.
	 *
	 * @return bool
	 */
	public function delete( $appointment ) {

		// Routes through AbstractModel::delete() → DeleteManager::delete_appointments(),
		// which is the canonical place that hydrates meta and fires
		// `sugar_calendar_bookings_appointment_deleted`. Going through
		// DeleteManager ensures cascade deletions (service-delete →
		// appointments, employee-delete → appointments) reach the same hook
		// listeners that direct API calls do.
		return $appointment->delete();
	}

	/**
	 * Cancel an Appointment.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The appointment model to cancel.
	 *
	 * @return bool
	 */
	public function cancel( $appointment ) {

		$appointment->status = 'cancelled';

		$saved = $appointment->save();

		if ( $saved ) {
			/**
			 * Fires after an appointment is cancelled and saved, before notifications are sent.
			 *
			 * @since 1.4.0
			 *
			 * @param AppointmentModel $appointment The cancelled appointment.
			 */
			do_action( 'sugar_calendar_bookings_appointment_cancelled', $appointment ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

			// Send cancellation notifications.
			Plugin::get( AppointmentNotifier::class )->notify( $appointment );
		}

		return $saved;
	}

	/**
	 * Validate an appointment.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The appointment model.
	 * @param AppointmentDTO   $dto         Data transfer object.
	 * @param bool             $is_update   Whether this is an update operation.
	 *
	 * @throws Exception If appointment validation fails.
	 */
	private function validate_appointment( $appointment, $dto, $is_update = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Generic.Metrics.CyclomaticComplexity.MaxExceeded

		$service = isset( $dto->service_id ) ? ServiceModel::get( $dto->service_id ) : $appointment->service;

		if ( ! $service ) {
			throw new Exception( esc_html__( 'Service not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$location = isset( $dto->location ) ? LocationFactory::from_array( $dto->location ) : LocationFactory::from_appointment( $appointment );

		if ( ! $location ) {
			throw new Exception( esc_html__( 'Location not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$employee = isset( $dto->employee_id ) ? EmployeeModel::get( $dto->employee_id ) : $appointment->employee;

		if ( ! $employee ) {
			throw new Exception( esc_html__( 'Employee not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		$customer = isset( $dto->customer_id ) ? CustomerModel::get( $dto->customer_id ) : $appointment->customer;

		if ( ! $customer ) {
			throw new Exception( esc_html__( 'Customer not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		Plugin::get( AppointmentValidationService::class )->validate_relations( $service, $location, $employee );

		if ( ! isset( $dto->ignore_availability ) || ! $dto->ignore_availability ) {
			$start_at = isset( $dto->start_at ) ? Date::to_utc( $dto->start_at ) : $appointment->start_at;
			$end_at   = isset( $dto->end_at ) ? Date::to_utc( $dto->end_at ) : $appointment->end_at;

			if ( empty( $end_at ) ) {
				$end_at = $start_at->modify( sprintf( '+%d minutes', $service->duration ) );
			}

			$is_available = Plugin::get( AvailabilityService::class )->is_slot_available( $start_at, $end_at, $service, $location, $employee, $is_update ? [ $appointment->id ] : [] );

			if ( ! $is_available ) {
				throw new Exception( esc_html__( 'Slot is not available.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
			}
		}
	}

	/**
	 * Fill a Appointment model from DTO.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment The appointment model to fill.
	 * @param AppointmentDTO   $dto         Data transfer object.
	 *
	 * @throws Exception If data is invalid.
	 */
	protected function fill_appointment( $appointment, $dto ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Generic.Metrics.CyclomaticComplexity.TooHigh

		if ( isset( $dto->service_id ) ) {
			$service = ServiceModel::get( $dto->service_id );

			$appointment->service()->associate( $service );
			$appointment->service = $service;
		}

		if ( isset( $dto->location ) ) {
			$location = LocationFactory::from_array( $dto->location );

			// Capture existing online location before reset to preserve integration data.
			$existing_online_location = $appointment->online_location;

			// Reset the location relationship.
			$appointment->location()->dissociate();
			$appointment->online_location = null;

			if ( $location->get_type() === 'in_person' ) {
				$appointment->location()->associate( $location->get_model() );
			} elseif ( $location->get_type() === 'online' ) {
				$new_online_location = $location->to_array();

				// Preserve integration-managed data (e.g. meeting_id, join_url) when the provider hasn't changed.
				if (
					is_array( $existing_online_location ) &&
					( $existing_online_location['provider'] ?? '' ) === ( $new_online_location['provider'] ?? '' )
				) {
					$new_online_location = array_merge( $existing_online_location, $new_online_location );
				}

				$appointment->online_location = $new_online_location;
			}
		}

		if ( isset( $dto->employee_id ) ) {
			$employee = EmployeeModel::get( $dto->employee_id );

			$appointment->employee()->associate( $employee );
		}

		if ( isset( $dto->customer_id ) ) {
			$customer = CustomerModel::get( $dto->customer_id );

			$appointment->customer()->associate( $customer );
		}

		if ( isset( $dto->status ) ) {
			$appointment->status = $dto->status;
		}

		if ( isset( $dto->start_at ) ) {
			$appointment->start_at = Date::to_utc( $dto->start_at );
		}

		// TODO: Think of a better way to handle this.
		if ( isset( $dto->end_at ) ) {
			$appointment->end_at = Date::to_utc( $dto->end_at );
		} elseif ( empty( $appointment->end_at ) && isset( $dto->start_at ) ) {
			$appointment->end_at = $appointment->start_at->modify( sprintf( '+%d minutes', $appointment->service->duration ) );
		}

		if ( isset( $dto->ignore_availability ) ) {
			$appointment->ignore_availability = $dto->ignore_availability;
		}

		if ( isset( $dto->customer_timezone ) ) {
			$appointment->customer_timezone = $dto->customer_timezone;
		}

		if ( isset( $dto->internal_notes ) ) {
			$appointment->internal_notes = $dto->internal_notes;
		}

		if ( isset( $dto->customer_notes ) ) {
			$appointment->customer_notes = $dto->customer_notes;
		}
	}

	/**
	 * Finalize appointment after payment processing.
	 *
	 * Sets the final appointment status and sends appropriate emails.
	 * Called by payment processors after successful payment.
	 *
	 * @since 1.0.0
	 *
	 * @param AppointmentModel $appointment Appointment model.
	 */
	public function finalize_after_payment( $appointment ) {

		// Set final status from settings.
		$appointment->status = Plugin::get( Options::class )->get( 'appointments.default_status' );

		$appointment->save();

		/**
		 * Fires after an appointment is finalized following payment, before notifications are sent.
		 *
		 * @since 1.4.0
		 *
		 * @param AppointmentModel $appointment The finalized appointment.
		 */
		do_action( 'sugar_calendar_bookings_appointment_finalized', $appointment ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		// Send booking notifications based on final status.
		Plugin::get( AppointmentNotifier::class )->notify( $appointment );
	}
}
