<?php

namespace Planaday\Entities;

use ArrayAccess;
use Countable;
use Planaday\Api\ApiConnector;
use Planaday\Api\ApiException\InvalidResponseException;
use Planaday\Api\RegistrationApi;
use Planaday\Config\General\Api\ApiSettings;
use Planaday\Config\Internal\InternalSettings;
use Planaday\Entities\Booking\BookingDatabase;
use Planaday\Entities\Course\CourseApi;
use Planaday\Entities\Course\CourseController;
use Planaday\Entities\Course\CourseDatabase;
use Planaday\Entities\Course\DTO\Course;
use Planaday\Entities\Course\Exception\CourseNotFoundException;
use Planaday\Entities\DayPart\DayPartApi;
use Planaday\Entities\DayPart\DayPartController;
use Planaday\Entities\DayPart\DayPartDatabase;
use Planaday\Entities\DayPart\DTO\DayPart;
use Planaday\Entities\DayPart\Exception\DayPartNotFoundException;
use Planaday\Entities\Location\DTO\Location;
use Planaday\Entities\Location\Exception\LocationNotFoundException;
use Planaday\Entities\Location\LocationApi;
use Planaday\Entities\Location\LocationController;
use Planaday\Entities\Location\LocationDatabase;
use Planaday\Platform\PlatformAbstraction\PlatformService;
use Planaday\Utility\DataFilters\InvalidException;
use Planaday\Utility\DataFilters\TypeValidator;
use Planaday\Utility\DataGrouping\Collection;
use Planaday\Utility\DateAndTimeUtility;
use Planaday\Utility\Debug\Log\Logging;
use Planaday\Utility\DependencyInjection\Container;
use Planaday\Utility\DependencyInjection\InjectionImpossibleException;
use Planaday\Utility\Endpoints\ApiController;
use Planaday\Utility\Endpoints\ApiEndpoint;
use Planaday\Utility\Endpoints\ApiExceptionMapping;
use Planaday\Utility\Endpoints\BackofficeEndpoint;
use Planaday\Utility\Endpoints\RequestData;
use Traversable;

#[ApiController(1, 'refresh')]
class DatabaseRefresher {
	use Logging;

	function __construct(
		protected ApiSettings $apiSettings,
		protected InternalSettings $internalSettings,
		protected Container $registry
	) {}

	protected function checkIfRefreshIsNeeded(bool $calledFromApi = false): bool {
		//API switch
		try{
			$lastUsedAPI = $this->internalSettings->getLastSelectedApi();
			$apiConnector = $this->registry->getInstance(ApiConnector::class);
			if (empty($lastUsedAPI) || $lastUsedAPI != $apiConnector->getApiName()) {
				$this->logInfo('API switch detected, refreshing databases');
				return true;
			}
		} catch (InjectionImpossibleException $e){
			$this->logError('Could not get current api, injection failed ('.$e->getMessage().')');
			return true;
		}

		//timeout
		if ( $this->internalSettings->getLastRefreshCourse() <=
		    time() - $this->apiSettings->getRefreshIntervalCourses() * DateAndTimeUtility::SECONDS_IN_HOUR) {
			$this->logInfo('Databases refresh timeout, Queueing refresh');

			try{
				$this->registry->getInstance(PlatformService::class)->queueApiRefresh();
			} catch (InjectionImpossibleException){
				$this->logError('Could not queue api refresh, injection failed');
			}

			return $calledFromApi;
		}

		$this->logInfo('Databases are up to date');
		return false;
	}

	public function refreshDatabases(bool $force = true, bool $fromApiCall = false): void {
		if (!$force && !$this->checkIfRefreshIsNeeded($fromApiCall)) {
			return;
		}
		$this->logInfo('Refreshing databases');

		$this->internalSettings->setStartKnownTimeSlot(null);
		$this->internalSettings->setEndKnownTimeSlot(null);

		try {
			$bookingDatabase = $this->registry->getInstance(BookingDatabase::class);
			$apiConnector = $this->registry->getInstance(ApiConnector::class);

			$this->logInfo('Clearing old bookings');
			$bookingDatabase->removeOldBookings('-12days');
		} catch (InjectionImpossibleException $e){
			$this->logError('Could not clear bookings database, injection failed ('.$e->getMessage().')');
		}

		$this->logInfo('Registering url at backoffice');

		try{
			$this->registry->getInstance(RegistrationApi::class)->register();
		} catch (InjectionImpossibleException $e){
			$this->logError('Could not register at API, injection failed ('.$e->getMessage().')');
		}

		$this->logInfo('Starting refresh');
		$refreshAmount = '+' . $this->apiSettings->getRefreshSizeCourses() . 'months';

		$refreshToTime = strtotime($refreshAmount, time());
		if ($refreshToTime === false ){
			$refreshToTime = time() + DateAndTimeUtility::SECONDS_IN_DAY * 31 * 6;
		}

		try{
			$success = $this->refreshTimeSlot(time(), $refreshToTime, true);
			if ( $success ){
				$this->internalSettings->setLastRefreshCourse(time());
				if ( isset($apiConnector)) $this->internalSettings->setLastSelectedApi($apiConnector->getApiName());
				$this->logInfo('Databases refreshed');
			}
		} catch (InvalidResponseException $e){
			$this->logError('Could not refresh courses, invalid response from API ('.$e->getMessage().')');
		}

	}

	/**
	 * @param int $startTime
	 * @param int $endTime
	 * @param bool $wipeDB
	 *
	 * @return bool
	 * @throws InvalidResponseException
	 */
	public function refreshTimeSlot(int $startTime, int $endTime, bool $wipeDB = false): bool{
		$previousStartTime = $this->internalSettings->getStartKnownTimeSlot();
		$previousEndTime = $this->internalSettings->getEndKnownTimeSlot();

		$this->logDebug('Requesting refreshing of courses from ' . date('d.m.Y H:i', $startTime) . ' to ' . date('d.m.Y H:i', $endTime));
		if ($previousStartTime !== null && $startTime > $previousStartTime)
			$startTime = $previousEndTime ?? $startTime;
		if ($previousEndTime !== null && $endTime < $previousEndTime)
			$endTime = $previousStartTime ?? $endTime;

		if ($startTime >= $endTime) {
			$this->logDebug('No courses to refresh');
			return true;
		}
		$this->logDebug('Actually refreshing courses from ' . date('d.m.Y H:i', $startTime) . ' to ' . date('d.m.Y H:i', $endTime));

		try {
			$locationApiController = $this->registry->getInstance(LocationApi::class);
			$dayPartApiController = $this->registry->getInstance(DayPartApi::class);
			$courseApiController = $this->registry->getInstance(CourseApi::class);
			$locationDatabase = $this->registry->getInstance(LocationDatabase::class);
			$dayPartDatabase = $this->registry->getInstance(DayPartDatabase::class);
			$courseDatabase = $this->registry->getInstance(CourseDatabase::class);
		} catch (InjectionImpossibleException $e){
			$this->logError('Could not refresh databases, injection failed ('.$e->getMessage().')');
			return false;
		}

		$completeCourseList = $this->setAllCoursesBatch($startTime, $endTime, $courseApiController, $courseDatabase, $wipeDB);
		$this->logDebug('Refreshed ' . count($completeCourseList) . ' courses');
		$completeDayPartList = $this->setAllDayPartsBatch($completeCourseList, $dayPartApiController, $dayPartDatabase, $wipeDB);
		$this->logDebug('Refreshed ' . count($completeDayPartList) . ' day parts');
		$this->setAllLocationsBatch($completeDayPartList, $locationApiController, $locationDatabase, $wipeDB);


		if ($previousStartTime == null || $startTime < $previousStartTime)
			$this->internalSettings->setStartKnownTimeSlot($startTime);
		if ($previousEndTime == null || $endTime > $previousEndTime)
			$this->internalSettings->setEndKnownTimeSlot($endTime);
		return true;
	}

	/**
	 * @param int $startTime
	 * @param int $endTime
	 * @param CourseApi $courseApiController
	 * @param CourseDatabase $courseDatabase
	 * @param bool $wipeDB
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 * @throws InvalidResponseException
	 */
	protected function setAllCoursesBatch(int $startTime, int $endTime, CourseApi $courseApiController, CourseDatabase $courseDatabase, bool $wipeDB = false): ArrayAccess&Traversable&Countable {
		$courses = $courseApiController->getCoursesTimeSlot( $startTime, $endTime );
		$courseDatabase->setAll($courses, $wipeDB);
		return $courses;
	}

	/**
	 * @param ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable $courses
	 * @param DayPartApi $dayPartApi
	 * @param DayPartDatabase $dayPartDatabase
	 * @param bool $wipeDB
	 *
	 * @return ArrayAccess<int|string, DayPart>&Traversable<int|string, DayPart>&Countable
	 * @throws InvalidResponseException
	 */
	protected function setAllDayPartsBatch(ArrayAccess&Traversable&Countable $courses, DayPartApi $dayPartApi, DayPartDatabase $dayPartDatabase, bool $wipeDB = false): ArrayAccess&Traversable&Countable {
		$dayPartIds = [];

		foreach ($courses as $course){
			if (
				$course->getDayPartIds() !== null &&
			    count($course->getDayPartIds()) > 0
			){
				foreach ($course->getDayPartIds() as $dayPartId){
					$dayPartIds[] = $dayPartId;
				}
			}
		}

		try {
			$dayParts = $dayPartApi->getDayParts( Collection::fromArray($dayPartIds) );
		} catch ( DayPartNotFoundException $e ) {
			Throw new InvalidResponseException( $e->getMessage(), $e->getCode(), $e );
		}

		$dayPartDatabase->setAll($dayParts, $wipeDB);
		return $dayParts;
	}

	/**
	 * @param ArrayAccess<int|string, DayPart>&Traversable<int|string, DayPart>&Countable $dayParts
	 * @param LocationApi $locationApi
	 * @param LocationDatabase $locationDatabase
	 * @param bool $wipeDB
	 *
	 * @return ArrayAccess<int|string, Location>&Traversable<int|string, Location>&Countable
	 * @throws InvalidResponseException
	 */
	protected function setAllLocationsBatch(
		ArrayAccess&Traversable&Countable $dayParts,
		LocationApi $locationApi,
		LocationDatabase $locationDatabase,
		bool $wipeDB = false
	): ArrayAccess&Traversable&Countable {
		$locationIds = [];

		foreach ($dayParts as $dayPart){
			if ( count($dayPart->getLocationIds()) > 0){
				foreach ($dayPart->getLocationIds() as $location){
					$locationIds[] = $location;
				}
			}
		}

		try {
			$locations = $locationApi->getLocations( Collection::fromArray(array_unique($locationIds)) );
		} catch ( LocationNotFoundException $e ) {
			Throw new InvalidResponseException( $e->getMessage(), $e->getCode(), $e );
		}

		$locationDatabase->setAll($locations, $wipeDB);
		return $locations;
	}

	#[BackofficeEndpoint]
	#[ApiEndpoint("full")]
	public function refreshAllEndpoint(): void {
		$this->refreshDatabases();
	}

	#[ApiEndpoint("tryFull")]
	public function tryRefreshAllEndpoint(): void {
		$this->refreshDatabases(false, true);
	}

	/**
	 * @throws InvalidResponseException
	 * @throws InjectionImpossibleException
	 * @throws CourseNotFoundException
	 * @throws InvalidException
	 * @throws LocationNotFoundException
	 */
	#[BackofficeEndpoint]
	#[ApiEndpoint("course/(?P<courseId>[0-9]+)")]
	#[ApiExceptionMapping(InvalidException::class, 400)]
	#[ApiExceptionMapping(CourseNotFoundException::class, 404, 'Course not found')]
	#[ApiExceptionMapping(InvalidResponseException::class, 500, 'Planaday API responded with an invalid response')]
	#[ApiExceptionMapping(InjectionImpossibleException::class, 500, 'Injection failed')]
	#[ApiExceptionMapping(LocationNotFoundException::class, 500, 'Location supplied by API not found')]
	public function refreshSpecificCourse(RequestData $data): void {
		$courseId = $this->registry->getInstance(TypeValidator::class)->int(
			$data->getUrlParams()['courseId'] ?? throw new InvalidException('course id is required')
		);

		$courseController = $this->registry->getInstance(CourseController::class);
		$dayPartController = $this->registry->getInstance(DayPartController::class);
		$locationController = $this->registry->getInstance(LocationController::class);

		$course = $courseController->getCourse($courseId, true, false);

		$dayParts = $dayPartController->getDayPartsCourse($course, true);

		$locationIds = [];
		foreach ($dayParts as $daypart){
			$locationIds = array_merge($locationIds, $daypart->getLocationIds());
		}

		$locationIds = array_unique($locationIds);
		$locationController->getLocations( Collection::fromArray($locationIds), true );
	}

	/**
	 * @throws InvalidResponseException
	 * @throws InjectionImpossibleException
	 * @throws InvalidException
	 * @throws DayPartNotFoundException
	 * @throws LocationNotFoundException
	 */
	#[BackofficeEndpoint]
	#[ApiEndpoint("daypart/(?P<dayPartId>[0-9]+)")]
	#[ApiExceptionMapping(InvalidException::class, 400)]
	#[ApiExceptionMapping(DayPartNotFoundException::class, 404, 'Daypart not found')]
	#[ApiExceptionMapping(InvalidResponseException::class, 500, 'Planaday API responded with an invalid response')]
	#[ApiExceptionMapping(InjectionImpossibleException::class, 500, 'Injection failed')]
	#[ApiExceptionMapping(LocationNotFoundException::class, 500, 'Location supplied by API not found')]
	public function refreshSpecificDayPart(RequestData $data): void {
		$daypartId = $this->registry->getInstance(TypeValidator::class)->int(
			$data->getUrlParams()['dayPartId'] ?? throw new InvalidException('daypart id is required')
		);

		$dayPartController = $this->registry->getInstance(DayPartController::class);
		$locationController = $this->registry->getInstance(LocationController::class);

		$daypart = $dayPartController->getDayPart($daypartId, true, false);
		$locationController->getLocations( Collection::fromArray($daypart->getLocationIds()), true );
	}

	/**
	 * @throws InvalidResponseException
	 * @throws InjectionImpossibleException
	 * @throws InvalidException
	 * @throws LocationNotFoundException
	 */
	#[BackofficeEndpoint]
	#[ApiEndpoint("location/(?P<locationId>[0-9]+)")]
	#[ApiExceptionMapping(InvalidException::class, 400)]
	#[ApiExceptionMapping(LocationNotFoundException::class, 404, 'Location not found')]
	#[ApiExceptionMapping(InvalidResponseException::class, 500, 'Planaday API responded with an invalid response')]
	#[ApiExceptionMapping(InjectionImpossibleException::class, 500, 'Injection failed')]
	public function refreshSpecificLocation(RequestData $data): void {
		$locationId = $this->registry->getInstance(TypeValidator::class)->int(
			$data->getUrlParams()['locationId'] ?? throw new InvalidException('location id is required')
		);

		$locationController = $this->registry->getInstance(LocationController::class);

		$locationController->getLocation( $locationId, true );
	}
}