<?php

namespace Planaday\Entities\Course;

use ArrayAccess;
use Countable;
use Planaday\Api\ApiException\InvalidResponseException;
use Planaday\Config\General\Api\ApiSettings;
use Planaday\Config\Internal\InternalSettings;
use Planaday\Entities\Course\DTO\Course;
use Planaday\Entities\Course\DTO\CourseMaterial;
use Planaday\Entities\Course\DTO\CourseSearch;
use Planaday\Entities\Course\DTO\CourseTemplate;
use Planaday\Entities\Course\Exception\CourseNotFoundException;
use Planaday\Entities\DayPart\DayPartController;
use Planaday\Entities\Location\LocationController;
use Planaday\Utility\DataGrouping\Collection;
use Planaday\Utility\DataGrouping\LazyCollection;
use Planaday\Entities\DatabaseRefresher;
use Planaday\Utility\DateAndTimeUtility;
use Planaday\Utility\Debug\Log\Logging;
use Planaday\Utility\DependencyInjection\Attributes\InjectedProperty;
use Traversable;

class CourseController {
	use Logging;

	#[InjectedProperty]
	protected LocationController $locationController;

	public function __construct(
		protected CourseApi $api,
		protected CourseDatabase $database,
		protected CourseCache $cache,
		protected ApiSettings $apiSettings,
		protected InternalSettings $internalSettings,
		protected DayPartController $dayPartController,
		protected DatabaseRefresher $databaseRefresher
	) {}

	/**
	 * @param int $courseID
	 * @param bool $forceCall
	 * @param bool $allowCache
	 *
	 * @return Course
	 * @throws CourseNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getCourse(int $courseID, bool $forceCall = false, bool $allowCache = true):Course {
		if ($allowCache && $this->cache->contains($courseID)) return $this->cache->get($courseID);

		if (!$forceCall){
			$courseDatabase = $this->database->getCourseById($courseID);
			$maxAllowedAge = $this->apiSettings->getRefreshIntervalCourses() * DateAndTimeUtility::SECONDS_IN_HOUR;

			if (isset($courseDatabase) && $courseDatabase->getAge() < $maxAllowedAge) {
				$this->addDaypartsToCourse($courseDatabase);
				$this->addMaterialsToCourse($courseDatabase);
				return $courseDatabase;
			}
		}

		$apiCourse = $this->api->getCourse($courseID);

		$this->addDaypartsToCourse($apiCourse, $forceCall);
		$this->addMaterialsToCourse($apiCourse);
		$this->cache->set($apiCourse);
		$this->database->setCourse($apiCourse);

		return $apiCourse;
	}

	/**
	 * @param int $courseId
	 *
	 * @return ArrayAccess<int|string, CourseMaterial>&Traversable<int|string, CourseMaterial>&Countable
	 * @throws CourseNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getMaterialsCourse(int $courseId):ArrayAccess&Traversable&Countable {
		return $this->api->getMaterialsCourse($courseId);
	}

	/**
	 * @param int $courseId
	 *
	 * @return bool
	 */
	public function courseHasMaterials(int $courseId):bool {
		try {
			return count($this->getMaterialsCourse($courseId)) > 0;
		} catch ( InvalidResponseException|CourseNotFoundException ) {
			return false;
		}
	}

	/**
	 * @param int $startTime
	 * @param int $endTime
	 * @param bool $forceCall
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 * @throws InvalidResponseException
	 */
	public function getCoursesTimeSlot(int $startTime, int $endTime, bool $forceCall = false):ArrayAccess&Traversable&Countable {
		if ($forceCall || !$this->checkIfValidSearchWindow($startTime, $endTime))
			$this->databaseRefresher->refreshTimeSlot($startTime, $endTime);

		$courses = $this->database->getCoursesByTimeSlot($startTime, $endTime);
		foreach ($courses as $course){
			$this->addDaypartsToCourse($course);
			$this->addMaterialsToCourse($course);
		}
		return $courses;
	}

	/**
	 * @param CourseSearch $search
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 * @throws InvalidResponseException
	 */
	public function getCoursesBySearch(CourseSearch $search): ArrayAccess&Traversable&Countable {
		$searchDateMin = $search->getDateMin();
		$searchDateMax = $search->getDateMax();

		if ( $searchDateMin !== null && $searchDateMax !== null){
			$startTime = strtotime($searchDateMin);
			$endTime = strtotime($searchDateMax);
			if ($startTime === false || $endTime === false) throw new InvalidResponseException();
			if (!$this->checkIfValidSearchWindow($startTime, $endTime)) $this->getCoursesTimeSlot($startTime, $endTime, true);
		}

		$courses = $this->database->getCoursesBySearch($search);
		foreach ($courses as $course){
			$this->addDaypartsToCourse($course);
			$this->addMaterialsToCourse($course);
		}

		return $courses;
	}

	/**
	 * @return ArrayAccess<int|string, CourseTemplate>&Traversable<int|string, CourseTemplate>&Countable
	 * @throws InvalidResponseException
	 */
	public function getCourseTemplateList():ArrayAccess&Traversable&Countable {
		return $this->api->getCourseTemplateList();
	}

	/**
	 * @return ArrayAccess<int|string, string>&Traversable<int|string, string>&Countable
	 */
	public function getUniqueLabelsCourse(): ArrayAccess&Traversable&Countable{
		return $this->database->getUniqueLabelsCourse();
	}

	/**
	 * @return Collection<string>
	 */
	public function getUniqueCitiesCourse(): Collection{
		return $this->locationController->getAllCities();
	}

	protected function checkIfValidSearchWindow(int $startTime, int $endTime):bool {
		$maxAllowedAge = $this->apiSettings->getRefreshIntervalCourses() * DateAndTimeUtility::SECONDS_IN_HOUR;
		$refreshTime = $this->internalSettings->getLastRefreshCourse();

		$startKnownTimeSlot = $this->internalSettings->getStartKnownTimeSlot();
		$endKnownTimeSlot = $this->internalSettings->getEndKnownTimeSlot();

		return ( time() - $refreshTime < $maxAllowedAge &&
		     ($endTime <= $endKnownTimeSlot || date('dmY', $endTime) == date('dmY', $endKnownTimeSlot)) &&
		     ($startTime >= $startKnownTimeSlot || date('dmY', $startTime) == date('dmY', $startKnownTimeSlot))
		);
	}

	protected function addDaypartsToCourse(Course $course, bool $forceCall = false): void {
		$course->setDayParts(
			new LazyCollection(
				function () use ($course, $forceCall) {
					try {
						return $this->dayPartController->getDayPartsCourse( $course, $forceCall);
					} catch (InvalidResponseException $e) {
						$this->logError("Failed to fetch day parts for course with id {$course->getId()}", $e);
						return Collection::fromArray([]);
					}
				}
			)
		);
	}

	protected function addMaterialsToCourse(Course $course): void {
		$course->setMaterials(
			new LazyCollection(
				function () use ($course) {
					try {
						return $this->getMaterialsCourse($course->getId());
					} catch (InvalidResponseException $e) {
						$this->logError("Failed to fetch materials for course with id {$course->getId()}", $e);
						return Collection::fromArray([]);
					}
				}
			)
		);
	}
}