<?php

namespace Planaday\Html\Tags\Course\CourseListShortcode;

use ArrayAccess;
use Countable;
use Exception;
use Planaday\Api\ApiException\InvalidResponseException;
use Planaday\Config\Course\CourseSettings;
use Planaday\Config\Course\General\GeneralSettings;
use Planaday\Config\Course\Overview\OverviewSettings;
use Planaday\Entities\Course\CourseController;
use Planaday\Entities\Course\CourseUtility;
use Planaday\Entities\Course\DTO\Course;
use Planaday\Entities\Course\DTO\CourseSearch;
use Planaday\Entities\Course\PostRequest\CourseSearchFieldEnum;
use Planaday\Entities\DayPart\DayPartController;
use Planaday\Entities\PostEntities;
use Planaday\Html\HtmlParser;
use Planaday\Html\Tags\Course\CourseListItem\CourseListItem;
use Planaday\Html\Tags\Tag;
use Planaday\Platform\PlatformAbstraction\DataHandler;
use Planaday\Platform\PlatformAbstraction\LocalizationController;
use Planaday\Utility\DataGrouping\Collection;
use Planaday\Utility\DataGrouping\LazyFetchModifiable;
use Planaday\Utility\DependencyInjection\Container;
use Planaday\Utility\DependencyInjection\InjectionImpossibleException;
use ReflectionClass;
use Traversable;

class CourseListShortcode extends Tag {
	const TAG = 'course-list';
	const EMPTY_LIST_ERROR = 'emptyListError';
	const SERVER_ERROR = 'serverError';
	const NO_SEARCH_ERROR = 'noSearchError';
	const FILTERS_ALL_ERROR = 'filtersAllError';
	const NO_SETTINGS_ERROR = 'noSettingsError';
	const NOTHING_FOUND_ERROR = 'nothingFoundError';
	const DEFAULT_TEMPLATE = 'courseList';
	const FILTER_ATTRIBUTE = 'filter';
	const SEARCH_ATTRIBUTE = 'search';

	protected LocalizationController $localizationController;
	protected DayPartController $dayPartController;
	protected CourseController $courseController;
	protected DataHandler $dataHandler;
	protected CourseUtility $courseUtility;
	private CourseSettings $courseSettings;
	protected OverviewSettings $overviewSettings;
	protected GeneralSettings $generalSettings;

	/**
	 * @param Container $registry
	 * @param string|null $dir
	 * @param string|null $template
	 *
	 * @throws InjectionImpossibleException
	 */
	public function __construct(Container $registry , ?string $dir = null, ?string $template = null) {
		$dir = $dir ?? __DIR__;
		$template = $template ?? self::DEFAULT_TEMPLATE;

		$this->courseSettings    = $registry->getInstance(CourseSettings::class);
		$this->dayPartController = $registry->getInstance(DayPartController::class);
		$this->localizationController = $registry->getInstance(LocalizationController::class);
		$this->courseController = $registry->getInstance(CourseController::class);
		$this->dataHandler = $registry->getInstance(DataHandler::class);
		$this->courseUtility = $registry->getInstance(CourseUtility::class);
		$this->overviewSettings = $this->courseSettings->getOverviewSettings();
		$this->generalSettings = $this->courseSettings->getGeneralSettings();
		parent::__construct($dir, $template, $registry);
	}

	public function buildTag(): string {
		$parser = $this->getNewParser();

		try{
			$attributeSearchArguments = $this->getAttributeSearchArgs();
			$allSearchArgs = $this->getAllSearchArgs($this->getPostSearchArgs(), $attributeSearchArguments);

			$courses = $this->getCourseList($allSearchArgs);

			$this->initializeCourseData($courses, $parser, $listItems);

			if ($listItems == ''){
				$this->checkAndSetNoCoursesError($parser, $allSearchArgs, $attributeSearchArguments);
			}

			$template = $this->getTemplate();
		}
		catch ( InvalidResponseException $e ) {
			$this->initializeExceptionData($e, $parser);
			$template = $this->getServerErrorTemplate();
		}
		return $parser->parse($template);
	}

	/**
	 * @return string[]
	 */
	protected function getAttributeSearchArgs():array {
		$searchArguments = [];

		foreach ($this->attributes as $field => $value){
			$constName = strtoupper($field);
			$fieldName = (new ReflectionClass(CourseSearchFieldEnum::class))->getConstant($constName);

			if ($field == 'start') $fieldName = CourseSearchFieldEnum::DATE_MIN;
			if ($field == 'end') $fieldName = CourseSearchFieldEnum::DATE_MAX;
			if ($field == 'label') $fieldName = CourseSearchFieldEnum::LABELS;
			if ($field == 'templateid') $fieldName = CourseSearchFieldEnum::TEMPLATE_IDS;

			if ($fieldName !== false){
				$searchArguments[$fieldName] = $value;
			}
		}

		return $searchArguments;
	}

	/**
	 * @return string[]
	 */
	protected function getPostSearchArgs():array {
		$searchArguments = [];

		if (isset($this->attributes[self::FILTER_ATTRIBUTE]) || isset($this->attributes[self::SEARCH_ATTRIBUTE])){
			$postData = $this->dataHandler->getPostField(PostEntities::SEARCH_FORM);
			$searchableFields = CourseSearchFieldEnum::getEnumArray();

			if (is_array($postData))
				foreach ($postData as $postField => $value){
					if(in_array($postField, $searchableFields)){
						$searchArguments[$postField] = $value;
					}
				}
		}

		return $searchArguments;
	}

	/**
	 * @param string[] $postSearchArgs
	 * @param string[] $attrSearchArgs
	 *
	 * @return string[]
	 */
	protected function getAllSearchArgs(array $postSearchArgs, array $attrSearchArgs):array {
		$secondaryArray = isset($this->attributes[self::SEARCH_ATTRIBUTE]) ? $attrSearchArgs : $postSearchArgs;
		$primaryArray = isset($this->attributes[self::SEARCH_ATTRIBUTE]) ? $postSearchArgs : $attrSearchArgs;

		foreach ($primaryArray as $field => $value){
			$secondaryArray[$field] = $value;
		}

		return $secondaryArray;
	}



	/**
	 * @param string[] $searchArguments
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 * @throws InvalidResponseException
	 */
	protected function getCourseList(array $searchArguments):ArrayAccess&Traversable&Countable {
		if (count($searchArguments) > 0){
			$courses = $this->courseController->getCoursesBySearch(new CourseSearch($searchArguments));
		} else {
			$courses = new Collection();
		}

		if (!isset($this->attributes[self::SEARCH_ATTRIBUTE])){
			$courses = $this->filterList($courses, $searchArguments[CourseSearchFieldEnum::DATE_MIN] ?? null);
		}

		return $courses;
	}

	/**
	 * @param ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable $courses
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 */
	protected function filterList(ArrayAccess&Traversable&Countable $courses, $startDate = null):ArrayAccess&Traversable&Countable {
		if ($courses instanceof LazyFetchModifiable){
			$courses->addFetchAction(
				function (Course $course) use ($startDate) {
					if ( $this->coursePassesFilter( $course, $startDate))
						return $course;
					else
						return null;
				}
			);

			return $courses;
		}

		$courseArray = [];

		foreach ($courses as $course){
			if ( $this->coursePassesFilter( $course, $startDate ))
				$courseArray[] = $course;
		}

		return Collection::fromArray($courseArray);
	}

	protected function coursePassesFilter(Course $course, $startDate):bool {
		$includeOptionsAvailability = $this->courseSettings->getGeneralSettings()->isIncludeOptionInCount();
		$overviewSettings = $this->courseSettings->getOverviewSettings();

		if (
			$this->courseSettings->getGeneralSettings()->isSkipELearningOnly() &&
			$this->courseUtility->getDayPartCount($course, false) == 0
		) return false;
		if (
			!($this->attributeIsTruthy("show-full") ?? $overviewSettings->isIncludeFullCourses()) &&
			$course->getUsersData()->getAvailableSpots($includeOptionsAvailability) <= 0
		) return false;

		if (
			! (
				$this->attributeIsTruthy( "show-past" ) ??
				$this->overviewSettings->isIncludePartiallyPassedCourses()
			) && $startDate
		){
			$startDaypart = $course->getEarliestDayPart($this->generalSettings->isELearningCountsAsStart());
			if (
				$startDaypart->getStartTime()->getTimestamp() < strtotime( $startDate )
			) return false;
		}

		return true;
	}

	/**
	 * @param HtmlParser $parser
	 * @param string[] $allSearchArgs
	 * @param string[] $attributeSearchArguments
	 *
	 * @return void
	 */
	protected function checkAndSetNoCoursesError(HtmlParser $parser , array $allSearchArgs, array $attributeSearchArguments): void {
		if (isset($this->attributes[self::SEARCH_ATTRIBUTE]) && count($allSearchArgs) == 0){
			$parser->setUnsafeData('message', $this->getErrorTemplate('', self::NO_SEARCH_ERROR, __DIR__));
		}
		elseif (isset($this->attributes[self::SEARCH_ATTRIBUTE])) {
			$parser->setUnsafeData('message', $this->getErrorTemplate('', self::NOTHING_FOUND_ERROR, __DIR__));
		}
		elseif (count($attributeSearchArguments) == 0) {
			$parser->setUnsafeData('message', $this->getErrorTemplate('', self::NO_SETTINGS_ERROR, __DIR__));
		}
		elseif (count($attributeSearchArguments) == count($allSearchArgs)){
			$parser->setUnsafeData('message', $this->getErrorTemplate('', self::EMPTY_LIST_ERROR, __DIR__));
			$parser->setData('errorText', $this->courseSettings->getOverviewSettings()->getNoCourseText());
		}
		elseif (isset($this->attributes[self::FILTER_ATTRIBUTE])) {
			$parser->setUnsafeData('message', $this->getErrorTemplate('', self::FILTERS_ALL_ERROR, __DIR__));
		}
	}

	protected function getServerErrorTemplate():string {
		return $this->getErrorTemplate('', self::SERVER_ERROR, __DIR__);
	}

	/**
	 * @param ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable $courses
	 * @param HtmlParser $parser
	 *
	 * @return void
	 */
	private function initializeCourseData(ArrayAccess&Traversable&Countable $courses, HtmlParser $parser, &$listItems = ''): void {
		$overviewSettings = $this->courseSettings->getOverviewSettings();
		$parser->setCondition('showDayParts', $overviewSettings->isShowDayPartCount());
		$parser->setCondition('showLocation', $overviewSettings->isShowFirstLocation());
		$parser->setCondition('showPrice', ($this->attributes['showprice'] ?? $overviewSettings->isShowPrice()) == 1);
		$parser->setCondition('showDetailButton', $overviewSettings->isShowCourseButton());

		$itemTemplate = ($this->attributes[self::TEMPLATE_ATTRIBUTE] ?? self::DEFAULT_TEMPLATE) . 'Item';

		$listItems = '';
		$showClosedCourses = $this->attributeIsTruthy('show-closed-courses');
		$hideOpenCourses = $this->attributeIsTruthy("hide-open-courses");
		foreach ($courses as $course){
			if ($course->getType() !== 'open' && !$showClosedCourses && ($this->attributes["company_id"] ?? null) === null) continue;
			if ($hideOpenCourses && $course->getType() === 'open') continue;
			if (
				isset($this->attributes["company_id"]) &&
				$course->getCompanies()->count() > 0 &&
				!$course->getCompanies()->contains($this->attributes["company_id"])
			) continue;

			$passThroughAtt                               = $this->attributes;
			$passThroughAtt[Tag::TEMPLATE_ATTRIBUTE]     = $itemTemplate;
			$passThroughAtt[CourseListItem::ID_ATTRIBUTE] = $course->getId();

			$listItems .= "<pad-" . CourseListItem::TAG . " ";

			foreach ($passThroughAtt as $key => $value){
				$listItems .= $key . '="' . $value . '" ';
			}

			$listItems .= '/>';
		}

		$parser->setUnsafeData('courseListItems', $listItems);
	}

	protected function initializeExceptionData( Exception $e, HtmlParser $parser): void {
		if ($e instanceof InvalidResponseException){
			$parser->setData('error-title', $this->localizationController->translate('Iets is mis gegaan'));
		}

		$parser->setData('error-message', $e->getMessage() . '. Error Code: ' . $e->getCode());
	}

	protected function attributeIsTruthy(string $attribute):?bool {
		$attribute = $this->attributes[$attribute] ?? null;
		if ($attribute === null) return null;
		return $attribute == '1' || $attribute == 'true' || $attribute == 'yes' || $attribute == 'ja' || $attribute == 'y';
	}
}