<?php
/**
 * Service responsible for synchronizing employee weekday time periods,
 * and their related associations (services and locations).
 */
namespace RadiusTheme\RadiusBooking\Services\EmployeeServices;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}
use RadiusTheme\RadiusBooking\Core\Support\Collection;
use RadiusTheme\RadiusBooking\Models\EmployeeWeekdayTimePeriod;
use RadiusTheme\RadiusBooking\Models\EmployeeWeekdayTimePeriodLocation;
use RadiusTheme\RadiusBooking\Models\EmployeeWeekdayTimePeriodService;
use RadiusTheme\RadiusBooking\Repositories\UserRepository;

/**
 * Class TimePeriodSyncService
 */
class TimePeriodSyncService {

	/**
	 * The user repository instance.
	 *
	 * @var UserRepository $userRepo
	 */
	private UserRepository $userRepo;

	/**
	 * The time period service sync service instance.
	 *
	 * @var TimePeriodServiceSyncService $syncService
	 */
	private TimePeriodServiceSyncService $syncService;
	/**
	 * The time period location sync service instance.
	 *
	 * @var TimePeriodLocationSyncService $syncLocationService
	 */
	private TimePeriodLocationSyncService $syncLocationService;

	/**
	 * Constructor to initialize dependencies.
	 *
	 * @param UserRepository $userRepo Repository to manage user-related operations.
	 * @param TimePeriodServiceSyncService $syncService Service to synchronize time period services.
	 * @param TimePeriodLocationSyncService $syncLocationService Service to synchronize time period locations.
	 *
	 * @return void
	 */
	public function __construct(
		UserRepository $userRepo,
		TimePeriodServiceSyncService $syncService,
		TimePeriodLocationSyncService $syncLocationService
	) {
		$this->userRepo            = $userRepo;
		$this->syncService         = $syncService;
		$this->syncLocationService = $syncLocationService;
	}

	/**
	 * Synchronizes the time periods for a specific user and weekday by validating, saving,
	 * and deleting any removed time periods.
	 *
	 * @param int $user_id The ID of the user whose time periods are being synchronized.
	 * @param int $weekday_id The ID of the weekday for which the time periods are being synchronized.
	 * @param array $time_periods An array of time periods to be synchronized for the given user and weekday.
	 *
	 * @return void
	 * @throws \Exception If an error occurs during the synchronization process.
	 */
	public function syncUserTimePeriod( $user_id, $weekday_id, $time_periods ) {
		try {
			$this->validateTimePeriods( $time_periods );
			$this->deleteRemovedTimePeriods( $weekday_id, $time_periods );
			$this->saveTimePeriods( $weekday_id, $time_periods );

		} catch ( \Exception $e ) {
			error_log( 'Error syncing user time period: ' . $e->getMessage() );
			throw $e;
		}
	}

	/**
	 * Validates the provided time periods to ensure they meet the required criteria.
	 *
	 * @param array $time_periods The array of time periods to validate.
	 *
	 * @return void Throws an exception if validation fails.
	 * @throws \InvalidArgumentException If the provided time periods are not a non-empty array.
	 */
	private function validateTimePeriods( $time_periods ) {
		if ( ! is_array( $time_periods ) ) {
			throw new \InvalidArgumentException( 'Time periods must be a non-empty array' );
		}
	}

	/**
	 * Deletes time periods that are no longer associated with the given weekday or are absent in the provided list of time periods.
	 *
	 * @param int $weekday_id The ID of the weekday whose time periods are being managed.
	 * @param array $time_periods An array of time periods to be retained for the specified weekday. Each time period must include its 'id' and optionally 'weekday_id'.
	 *
	 * @return void
	 */
	private function deleteRemovedTimePeriods( $weekday_id, $time_periods ) {
		if ( empty( $time_periods ) ) {
			$existingTimePeriods = EmployeeWeekdayTimePeriod::where( 'weekday_id', $weekday_id )->get();
			if ( empty( $existingTimePeriods ) ) {
				return;
			}
			$timePeriodsIds = ( new Collection( $existingTimePeriods ) )->pluck( 'id' );

			$this->userRepo->deleteTimePeriods( $timePeriodsIds );
		}
		$timePeriodIds = array_column( $time_periods, 'id' );
		if ( empty( $timePeriodIds ) ) {
			return;
		}

		$weekday_ids         = array_column( $time_periods, 'weekday_id' );
		$existingTimePeriods = EmployeeWeekdayTimePeriod::whereIn( 'weekday_id', $weekday_ids )
														->whereNotIn( 'id', $timePeriodIds )
														->get();

		$collection            = new Collection( $existingTimePeriods );
		$existingTimePeriods   = $collection->toArray();
		$existingTimePeriodIds = array_column( $existingTimePeriods, 'id' );
		$deleteIds             = array_diff( $existingTimePeriodIds, $timePeriodIds );
		if ( ! empty( $deleteIds ) ) {
			$this->deleteTimePeriodDependencies( $deleteIds );
			EmployeeWeekdayTimePeriod::forceDeleteMany( $deleteIds );
		}
	}

	/**
	 * Deletes dependencies associated with the specified time period IDs, including locations and services.
	 *
	 * @param array $deleteIds An array of time period IDs whose dependencies should be deleted.
	 *
	 * @return void
	 */
	private function deleteTimePeriodDependencies( $deleteIds ) {
		$this->deleteTimePeriodLocations( $deleteIds );
		$this->deleteTimePeriodServices( $deleteIds );
	}

	/**
	 * Deletes locations associated with the specified time period IDs.
	 *
	 * @param array $deleteIds An array of time period IDs whose associated locations need to be deleted.
	 *
	 * @return void
	 */
	private function deleteTimePeriodLocations( $deleteIds ) {
		$timePeriodLocations = EmployeeWeekdayTimePeriodLocation::whereIn( 'weekday_time_period_id', $deleteIds )->get();

		if ( ! empty( $timePeriodLocations ) ) {
			$locationIds = array_column( $timePeriodLocations, 'id' );
			EmployeeWeekdayTimePeriodLocation::forceDeleteMany( $locationIds );
		}
	}

	/**
	 * Deletes all services associated with the specified time period IDs.
	 *
	 * @param array $deleteIds An array of IDs representing the time periods whose associated services need to be deleted.
	 *
	 * @return void
	 */
	private function deleteTimePeriodServices( $deleteIds ) {
		$timePeriodServices = EmployeeWeekdayTimePeriodService::whereIn( 'weekday_time_period_id', $deleteIds )->get();

		if ( ! empty( $timePeriodServices ) ) {
			$serviceIds = array_column( $timePeriodServices, 'id' );
			EmployeeWeekdayTimePeriodService::forceDeleteMany( $serviceIds );
		}
	}

	/**
	 * Saves the provided time periods for the specified weekday and handles their associations.
	 *
	 * @param int $weekday_id The ID of the weekday for which the time periods are being saved.
	 * @param array $time_periods An array of time period data to be saved. Each time period must include the required details for validation, saving, and association.
	 *
	 * @return void
	 */
	private function saveTimePeriods( $weekday_id, $time_periods ) {
		foreach ( $time_periods as $period ) {
			$this->validateTimePeriodData( $period );
			$timePeriodId = $this->saveTimePeriod( $weekday_id, $period );
			$this->syncTimePeriodAssociations( $timePeriodId, $period );
		}
	}

	/**
	 * Validates the provided time period data to ensure required fields are present.
	 *
	 * @param array $period The time period data to validate. It must contain 'start_time' and 'end_time' keys.
	 *
	 * @return void
	 *
	 * @throws \InvalidArgumentException If either 'start_time' or 'end_time' is missing from the provided data.
	 */
	private function validateTimePeriodData( $period ) {
		if ( ! isset( $period['start_time'] ) || ! isset( $period['end_time'] ) ) {
			throw new \InvalidArgumentException( 'Start time and end time are required for time periods' );
		}
	}

	/**
	 * Saves or updates a time period associated with a specific weekday.
	 *
	 * @param int $weekday_id The ID of the weekday to associate with the time period.
	 * @param array $period An associative array representing the time period. Must include 'start_time' and 'end_time', and may include 'id' if updating an existing record.
	 *
	 * @return int The ID of the saved or updated time period.
	 *
	 * @throws \RuntimeException If the time period cannot be saved successfully.
	 */
	private function saveTimePeriod( $weekday_id, $period ) {
		$employeeTimePeriod = isset( $period['id'] )
			? EmployeeWeekdayTimePeriod::findOrNew( $period['id'] )
			: new EmployeeWeekdayTimePeriod();

		$employeeTimePeriod->fill(
			array(
				'weekday_id' => $weekday_id,
				'start_time' => $period['start_time'],
				'end_time'   => $period['end_time'],
			)
		);

		$saved = $employeeTimePeriod->save();
		if ( ! $saved ) {
			throw new \RuntimeException( 'Failed to save employee weekday time period' );
		}

		return $employeeTimePeriod->get_id();
	}

	/**
	 * Synchronizes associations between a time period and its related services and locations.
	 *
	 * @param int $timePeriodId The ID of the time period being synchronized.
	 * @param array $period An array containing the data to synchronize, including services and locations.
	 *                      The 'services' key associates with a list of services, and the 'locations' key associates with a list of locations.
	 *
	 * @return void
	 * @throws \Exception If an error occurs during the synchronization of services or locations.
	 */
	private function syncTimePeriodAssociations( $timePeriodId, $period ) {
		if ( isset( $period['services'] ) ) {
			$this->syncService->syncUserWeekdayTimePeriodService( $timePeriodId, $period['services'] );
		}

		if ( isset( $period['locations'] ) ) {
			$this->syncLocationService->syncUserWeekdayTimePeriodLocation( $timePeriodId, $period['locations'] );
		}
	}
}
