<?php

namespace SugarCalendarBookings\Migration\Sources\Amelia\Mappers;

use SugarCalendarBookings\Employees\EmployeeModel;
use SugarCalendarBookings\Locations\LocationModel;
use SugarCalendarBookings\Migration\AbstractMapper;
use SugarCalendarBookings\Migration\ModelRegistry;

/**
 * Appointment Mapper.
 *
 * @since 1.3.0
 */
class AppointmentMapper extends AbstractMapper {

	/**
	 * Map Amelia appointment to SCB format.
	 *
	 * @since 1.3.0
	 *
	 * @param object               $raw     Raw Amelia data.
	 * @param array<string, mixed> $context Context.
	 *
	 * @return array<string, mixed>|null Returns null if required relations not found.
	 */
	public function map( object $raw, array $context = [] ): ?array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$sanitized = $this->extract_and_sanitize(
			$raw,
			[
				'service_id'      => [
					'source'  => 'serviceId',
					'default' => 0,
					'rule'    => 'numeric',
				],
				'customer_id'     => [
					'source'  => 'customerId',
					'default' => 0,
					'rule'    => 'numeric',
				],
				'employee_id'     => [
					'source'  => 'providerId',
					'default' => 0,
					'rule'    => 'numeric',
				],
				'location_id'     => [
					'source'  => 'locationId',
					'default' => 0,
					'rule'    => 'numeric',
				],
				'booking_start'   => [
					'source'  => 'bookingStart',
					'default' => '',
					'rule'    => 'string',
				],
				'booking_end'     => [
					'source'  => 'bookingEnd',
					'default' => '',
					'rule'    => 'string',
				],
				'status'          => [
					'source'  => 'status',
					'default' => 'approved',
					'rule'    => 'string',
				],
				'internal_notes'  => [
					'source'  => 'internalNotes',
					'default' => '',
					'rule'    => 'string',
				],
				'utc_offset'      => [
					'source'  => 'utcOffset',
					'default' => null,
					'rule'    => 'nullable|numeric',
				],
				'payment_id'      => [
					'source'  => 'paymentId',
					'default' => 0,
					'rule'    => 'numeric',
				],
				'payment_amount'  => [
					'source'  => 'paymentAmount',
					'default' => 0,
					'rule'    => 'numeric',
				],
				'payment_status'  => [
					'source'  => 'paymentStatus',
					'default' => 'pending',
					'rule'    => 'string',
				],
				'payment_gateway' => [
					'source'  => 'paymentGateway',
					'default' => 'onSite',
					'rule'    => 'string',
				],
			]
		);

		// Find SCB IDs from Amelia IDs (service and customer have no fallback).
		$service_id  = ModelRegistry::find_id_by_amelia_id( 'service', (int) $sanitized['service_id'] );
		$customer_id = ModelRegistry::find_id_by_amelia_id( 'customer', (int) $sanitized['customer_id'] );

		if ( ! $service_id || ! $customer_id ) {
			return null;
		}

		// Employee is required (DB constraint).
		// Fallback to default employee if not found (handles SCB Lite and unmigrated employees).
		$employee_id = ModelRegistry::find_id_by_amelia_id(
			'employee',
			(int) $sanitized['employee_id'],
			function () {
				$default = EmployeeModel::get_or_create_default();

				return $default ? $default->id : null;
			}
		);

		// Location is optional.
		// Fallback to default location if not found (handles Amelia Lite which has no locations).
		$location_id = ModelRegistry::find_id_by_amelia_id(
			'location',
			(int) $sanitized['location_id'],
			function () {
				$default = LocationModel::get_or_create_default();

				return $default ? $default->id : null;
			}
		);

		// Skip if employee fallback failed (rare edge case - DB error creating default).
		if ( ! $employee_id ) {
			return null;
		}

		$mapped = [
			'customer_id'       => $customer_id,
			'service_id'        => $service_id,
			'employee_id'       => $employee_id,
			'location_id'       => $location_id,
			'start_at'          => $sanitized['booking_start'] ?: current_time( 'mysql' ),
			'end_at'            => $sanitized['booking_end'] ?: current_time( 'mysql' ),
			'status'            => $this->map_status( $sanitized['status'] ),
			'customer_timezone' => $this->map_timezone( $sanitized['utc_offset'] ),
			'created_at'        => current_time( 'mysql' ),
			'updated_at'        => current_time( 'mysql' ),
		];

		// Include internal notes if present.
		if ( ! empty( $sanitized['internal_notes'] ) ) {
			$mapped['internal_notes'] = wp_kses_post( $sanitized['internal_notes'] );
		}

		// Include payment data if present.
		if ( ! empty( $sanitized['payment_id'] ) ) {
			$mapped['_payment'] = [
				'amount'  => (float) $sanitized['payment_amount'],
				'status'  => $this->map_payment_status( $sanitized['payment_status'] ),
				'gateway' => $sanitized['payment_gateway'] === 'onSite' ? 'onsite' : $sanitized['payment_gateway'],
			];
		}

		return $mapped;
	}

	/**
	 * Map appointment status.
	 *
	 * @since 1.3.0
	 *
	 * @param string $status Amelia status.
	 *
	 * @return string
	 */
	private function map_status( string $status ): string {

		// SCB valid statuses: draft, pending, approved, cancelled, done.
		// Amelia statuses: approved, pending, canceled, rejected, no-show.
		$map = [
			'approved' => 'approved',
			'pending'  => 'pending',
			'canceled' => 'cancelled',
			'rejected' => 'cancelled',
			'no-show'  => 'done', // Customer didn't show, appointment time passed.
		];

		return $map[ $status ] ?? 'pending';
	}

	/**
	 * Map payment status.
	 *
	 * @since 1.3.0
	 *
	 * @param string $status Amelia payment status.
	 *
	 * @return string
	 */
	private function map_payment_status( string $status ): string {

		// SCB valid statuses: pending, completed, failed, refunded.
		// Amelia statuses: paid, pending, partiallyPaid, refunded.
		$map = [
			'paid'          => 'completed',
			'pending'       => 'pending',
			'partiallyPaid' => 'pending', // Partial payment still awaiting full amount.
			'refunded'      => 'refunded',
		];

		return $map[ $status ] ?? 'pending';
	}

	/**
	 * Map customer timezone from UTC offset.
	 *
	 * @since 1.3.0
	 *
	 * @param int|null $utc_offset UTC offset in minutes (from Amelia).
	 *
	 * @return string Timezone string.
	 */
	private function map_timezone( $utc_offset ): string {

		// If no offset provided, use site timezone.
		if ( $utc_offset === null ) {
			return wp_timezone_string();
		}

		// Convert minutes offset to timezone string.
		// Amelia stores offset in minutes, negative for west of UTC.
		$hours   = (int) floor( abs( $utc_offset ) / 60 );
		$minutes = abs( $utc_offset ) % 60;
		$sign    = $utc_offset >= 0 ? '+' : '-';

		// Format as UTC offset timezone (e.g., +05:30, -08:00).
		return sprintf( '%s%02d:%02d', $sign, $hours, $minutes );
	}
}
