<?php

namespace SugarCalendarBookings\Services;

use Exception;
use SugarCalendarBookings\Employees\EmployeeModel;
use SugarCalendarBookings\Locations\OnlineLocation;

/**
 * Service logic for Service model.
 *
 * @since 1.0.0
 */
class ServiceService {

	/**
	 * Create a new Service.
	 *
	 * @since 1.0.0
	 *
	 * @param CreateServiceDTO $dto Data for service creation.
	 *
	 * @return ServiceModel|false The created Service model or false on failure.
	 */
	public function create( $dto ) {

		$service = new ServiceModel();

		$this->fill_service( $service, $dto );

		$saved = $service->save();

		if ( ! $saved ) {
			return false;
		}

		/*
		 * If some critical relation (e.g. locations) creation fails,
		 * we need to fail the entire operation.
		 */
		try {
			$this->save_relations( $service, $dto );
		} catch ( Exception $e ) {
			$service->delete();

			return false;
		}

		return $service;
	}

	/**
	 * Update an existing Service.
	 *
	 * @since 1.0.0
	 *
	 * @param ServiceModel $service The service model to update.
	 * @param ServiceDTO   $dto     Data for service update.
	 *
	 * @return ServiceModel|false The updated Service model or false on failure.
	 */
	public function update( $service, $dto ) {

		$this->fill_service( $service, $dto );

		$saved = $service->save();

		if ( ! $saved ) {
			return false;
		}

		$this->save_relations( $service, $dto, true );

		return $service;
	}

	/**
	 * Delete a Service.
	 *
	 * @since 1.0.0
	 *
	 * @param ServiceModel $service The service model to delete.
	 *
	 * @return bool
	 */
	public function delete( $service ) {

		return $service->delete();
	}

	/**
	 * Fill a Service model from DTO.
	 *
	 * @since 1.0.0
	 * @since 1.5.0 Added slug generation.
	 *
	 * @param ServiceModel $service The service model to fill.
	 * @param ServiceDTO   $dto     Data transfer object.
	 */
	protected function fill_service( $service, $dto ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Generic.Metrics.CyclomaticComplexity.MaxExceeded

		if ( isset( $dto->name ) ) {
			$service->name = $dto->name;
			$service->slug = $this->generate_unique_slug( (string) $dto->name, $service->id );
		}

		if ( isset( $dto->duration ) ) {
			$service->duration = $dto->duration;
		}

		if ( isset( $dto->color ) ) {
			$service->color = $dto->color;
		}

		if ( isset( $dto->description ) ) {
			$service->description = $dto->description;
		}

		if ( isset( $dto->price ) ) {
			$service->price = $dto->price;
		}

		if ( isset( $dto->feature_image_id ) ) {
			$service->feature_image_id = $dto->feature_image_id;
		}

		if ( isset( $dto->enabled ) ) {
			$service->status = $dto->enabled ? 'enabled' : 'disabled';
		}

		if ( isset( $dto->override_payment_methods ) ) {
			$service->override_payment_methods = $dto->override_payment_methods;
		}

		if ( isset( $dto->payment_gateways ) ) {
			$service->payment_gateways = $dto->payment_gateways;
		}

		if ( isset( $dto->override_email_notifications ) ) {
			$service->override_email_notifications = $dto->override_email_notifications;
		}

		if ( isset( $dto->use_default_working_schedule ) ) {
			$service->use_default_working_schedule = $dto->use_default_working_schedule;
		}

		if ( isset( $dto->working_schedule ) ) {
			$service->working_schedule = $dto->working_schedule;
		}

		if ( isset( $dto->emails ) ) {
			$service->emails = $dto->emails;
		}
	}

	/**
	 * Save related data (locations, tags) for a Service.
	 *
	 * @since 1.0.0
	 * @since 1.2.1 Refactor default employee attachment logic.
	 *
	 * @param ServiceModel $service   The service model.
	 * @param ServiceDTO   $dto       Data transfer object.
	 * @param bool         $is_update Whether this is an update operation.
	 *
	 * @throws Exception If some critical relation creation fails on create.
	 */
	protected function save_relations( $service, $dto, $is_update = false ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Generic.Metrics.CyclomaticComplexity.MaxExceeded

		// Attach in-person locations.
		if ( isset( $dto->locations ) ) {
			$attach_locations            = [];
			$locations_data_placeholders = [ 'location_order' => '%d' ];

			foreach ( $dto->locations as $location ) {
				$attach_locations[ $location['id'] ] = [
					'location_order' => $location['order'],
				];
			}

			if ( $is_update ) {
				$service->locations()
					->sync( $attach_locations, $locations_data_placeholders );
			} elseif ( ! empty( $attach_locations ) ) {
				$is_attached = $service->locations()
					->attach( $attach_locations, [], $locations_data_placeholders );

				/*
				 * Locations is a critical part of the service, so if it
				 * fails, we need to fail the entire operation.
				 */
				if ( ! $is_attached ) {
					throw new Exception( esc_html__( 'Failed to attach locations.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
				}
			}
		}

		if ( isset( $dto->online_locations ) ) {
			$service->online_locations = array_filter(
				array_map(
					function ( $location ) use ( $service ) {

						// Generate a unique ID for online location.
						if ( empty( $location['uid'] ) ) {
							$location['uid'] = uniqid( $service->id . ':' . $location['order'] );
						}

						// Convert data to a valid online location.
						return OnlineLocation::from_array( $location )->to_array();
					},
					$dto->online_locations
				)
			);

			$service->save();
		}

		// Attach tags.
		if ( isset( $dto->tags ) ) {
			if ( $is_update ) {
				$service->tags()->sync( $dto->tags );
			} elseif ( ! empty( $dto->tags ) ) {
				$service->tags()->attach( $dto->tags );
			}
		}

		$this->maybe_attach_default_employee( $service, $dto, $is_update );
	}

	/**
	 * Maybe attach default employee to service.
	 *
	 * Attaches a default employee if:
	 * - It's a new service (not an update), OR
	 * - It's an update but the service has no employees, AND
	 * - No explicit employee_ids are provided in the DTO.
	 *
	 * @since 1.2.1
	 *
	 * @param ServiceModel $service   The service model.
	 * @param ServiceDTO   $dto       Data transfer object.
	 * @param bool         $is_update Whether this is an update operation.
	 *
	 * @throws Exception If failed to create default employee.
	 */
	protected function maybe_attach_default_employee( $service, $dto, $is_update = false ) {

		// Skip if employee_ids are explicitly provided in the DTO.
		if ( ! empty( $dto->employee_ids ) ) {
			return;
		}

		// Only attach default employee for new services or updates with no employees.
		if ( ! $is_update || empty( $service->employees ) ) {
			$default_employee = $this->get_default_employee();

			$service->employees()->attach( $default_employee );
		}
	}

	/**
	 * Get the default employee.
	 *
	 * Temporary solution until we have a proper employee management system.
	 *
	 * @since 1.0.0
	 *
	 * @return EmployeeModel
	 *
	 * @throws Exception If failed to create default employee.
	 */
	private function get_default_employee() {

		$employee = EmployeeModel::query()->order_by( 'id', 'asc' )->first();

		if ( $employee ) {
			return $employee;
		}

		$employee = new EmployeeModel();

		$user  = wp_get_current_user();
		$name  = esc_html__( 'Owner', 'sugar-calendar-bookings-scheduling-appointments-lite' );
		$email = get_option( 'admin_email' );

		if ( $user->ID > 0 ) {
			$name  = ! empty( $user->first_name ) ? $user->first_name : $user->nickname;
			$email = $user->user_email;
		}

		$employee->name    = $name;
		$employee->email   = $email;
		$employee->user_id = $user->ID;

		$is_saved = $employee->save();

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

		return $employee;
	}

	/**
	 * Generate a unique slug for a Service.
	 *
	 * Produces sanitize_title( $name ), then appends -2, -3, ... until the
	 * resulting slug does not collide with any existing service row.
	 *
	 * @since 1.5.0
	 *
	 * @param string   $name      Service name to sluggify.
	 * @param int|null $ignore_id Service row to exclude from the collision check.
	 *
	 * @return string
	 */
	protected function generate_unique_slug( string $name, ?int $ignore_id = null ): string {

		$base = sanitize_title( $name );

		if ( $base === '' ) {
			$base = 'service';
		}

		$query = ServiceModel::query()->where(
			function ( $q ) use ( $base ) {
				$q->where( 'slug', '=', $base, '%s' )
					->or_where( 'slug', 'LIKE', $base . '-%', '%s' );
			}
		);

		if ( $ignore_id !== null ) {
			$query->where( 'id', '!=', $ignore_id, '%d' );
		}

		$taken = array_column( $query->select( [ 'slug' ] )->get(), 'slug' );

		if ( ! in_array( $base, $taken, true ) ) {
			return $base;
		}

		$suffix = 2;

		while ( in_array( $base . '-' . $suffix, $taken, true ) ) {
			++$suffix;
		}

		return $base . '-' . $suffix;
	}
}
