<?php


namespace Planaday\Api\V1;

use ArrayAccess;
use Countable;
use Planaday\Api\ApiException\ApiErrorResponseException;
use Planaday\Api\ApiException\ClientNotInitializedException;
use Planaday\Api\ApiException\InvalidResponseException;
use Planaday\Entities\CostsData;
use Planaday\Entities\Course\CourseApi;
use Planaday\Entities\Course\DTO\ApiAttribute;
use Planaday\Entities\Course\DTO\ApiAttributeValue;
use Planaday\Entities\Course\DTO\Course;
use Planaday\Entities\Course\DTO\CourseMaterial;
use Planaday\Entities\Course\DTO\CourseTemplate;
use Planaday\Entities\Course\DTO\CourseUsersData;
use Planaday\Entities\Course\DTO\DayPartDisplayTypes;
use Planaday\Entities\Course\Exception\CourseNotFoundException;
use Planaday\Utility\DataFilters\InvalidException;
use Planaday\Utility\DataFilters\TypeValidator;
use Planaday\Utility\DataGrouping\Collection;
use Planaday\Utility\DataGrouping\PagedResponse;
use Planaday\Utility\Debug\Log\Logging;
use Traversable;

class CourseApiV1Controller implements CourseApi {
	use Logging;

	public function __construct(
		protected ClientV1 $client,
		protected TypeValidator $typeValidator
	) {}

	/**
	 * @param int $courseId
	 *
	 * @return Course
	 * @throws CourseNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getCourse( int $courseId ): Course {
		try {
			$response = $this->client->callEndpoint(
				'course/' . $courseId
			);
		} catch (ApiErrorResponseException|ClientNotInitializedException $e){
			throw new InvalidResponseException($e->getMessage(), $e->getCode(), $e);
		}

		if ($response->getStatus() === 404) {
			$this->logWarning("could not find course with id $courseId");
			throw new CourseNotFoundException("could not find course with id $courseId");
		}

		if ($response->getStatus() === 401) {
			$this->logWarning("could not fetch course with id $courseId, (No Access Right)");
			throw new CourseNotFoundException("could not fetch course with id $courseId, (No Access Right)");
		}

		try{
			return $this->getObjectFromAPIResponse( $response->getData() );
		} catch (InvalidException){
			$this->logError("could not parse response from server to course with (id $courseId)", $response);
			throw new InvalidResponseException();
		}
	}

	/**
	 * @param int $courseId
	 * @param int $offset
	 * @param int $limit
	 *
	 * @return Countable&Traversable<CourseMaterial>&ArrayAccess<CourseMaterial>
	 * @throws CourseNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getMaterialsCourse( int $courseId, int $offset = 0, int $limit = 100): ArrayAccess&Traversable&Countable {
		return (new PagedResponse($this->getMaterialsCourseRaw( $courseId, $offset, $limit, $response)))
			->setFetchPosition( $offset - 1 + $limit)
			->setPageSize( $limit )
			->setTotalPossibleEntries( $response?->getData()['meta']['records'] ?? 0 )
			->setLoadValuesCallable( function (int $offset, int $limit) use ( $courseId ) {
				try {
					return $this->getMaterialsCourseRaw($courseId, $offset, $limit);
				} catch ( CourseNotFoundException | InvalidResponseException $e ) {
					$this->logError("could not load further materials for course with id $courseId", $e);
					return [];
				}
			});
	}

	/**
	 * @param int $courseId
	 * @param int $offset
	 * @param int $limit
	 * @param mixed|null $response
	 *
	 * @return Countable&Traversable<CourseMaterial>&ArrayAccess<CourseMaterial>
	 * @throws CourseNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getMaterialsCourseRaw( int $courseId, int $offset = 0, int $limit = 100, ?ClientV1Response &$response = null): ArrayAccess&Traversable&Countable {
		$materials = [];
		$offset += 1;

		try{
			$response = $this->client->callEndpoint(
				'course/' . $courseId . '/materials',
				[
					'offset' => $offset,
					'limit'  => $limit
				]
			);
		} catch (ApiErrorResponseException|ClientNotInitializedException $e){
			throw new InvalidResponseException($e->getMessage(), $e->getCode(), $e);
		}

		if ($response->getStatus() === 404) {
			$this->logWarning("could not find course with id $courseId");
			throw new CourseNotFoundException("could not find course with id $courseId");
		}

		if ($response->getStatus() === 401) {
			$this->logWarning("could not fetch course materials with id $courseId, (No Access Right)");
			throw new CourseNotFoundException("could not fetch course materials with id $courseId, (No Access Right)");
		}

		try {
			$responseAsArray = $this->typeValidator->array($response->getData());
			if (!isset($responseAsArray['data'])){
				throw new InvalidException();
			}

			foreach ( $responseAsArray['data'] as $material ) {
				$materials[] = $this->getMaterialFromApi( $material );
			}
		} catch (InvalidException){
			$this->logError("could not parse response from server to course materials, response:", $response);
			throw new InvalidResponseException();
		}

		return Collection::fromArray($materials);
	}

	/**
	 * @param int $startDate
	 * @param int $endDate
	 * @param int $offset
	 * @param int $limit
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 * @throws InvalidResponseException
	 */
	public function getCoursesTimeSlot( int $startDate, int $endDate, int $offset = 0, int $limit = 100): ArrayAccess&Traversable&Countable {
		return $this->getCourseList( [
			'start'  => date( 'Ymd', $startDate ),
			'end'    => date( 'Ymd', $endDate ),
			'offset' => $offset,
			'limit'  => $limit
		] );
	}

	/**
	 * @param array<string, int|string> $arguments
	 *
	 * @return ArrayAccess<int|string, Course>&Traversable<int|string, Course>&Countable
	 * @throws InvalidResponseException
	 */
	public function getCourseList( array $arguments ): ArrayAccess&Traversable&Countable {
		return (new PagedResponse($this->getCourseListRaw( $arguments, $response )))
			->setFetchPosition($arguments['offset'] - 1 + $arguments['limit'])
			->setPageSize($arguments['limit'])
			->setTotalPossibleEntries($response?->getData()['meta']['records'] ?? 0)
			->setLoadValuesCallable( function (int $offset, int $limit) use ( $arguments ) {
				$arguments['offset'] = $offset;
				$arguments['limit'] = $limit;
				try {
					return $this->getCourseListRaw($arguments);
				} catch ( InvalidResponseException $e ) {
					$this->logError("could not load courses for time slot", $e);
					return [];
				}
			});
	}

	/**
	 * @param array $arguments
	 * @param ClientV1Response|null $response
	 *
	 * @return Countable&Traversable&ArrayAccess
	 * @throws InvalidResponseException
	 */
	public function getCourseListRaw( array $arguments, ?ClientV1Response &$response = null ): ArrayAccess&Traversable&Countable {
		$courses = [];
		$arguments['offset'] = 1 + ((int)$arguments['offset']);
		$arguments['limit'] ??= 100;

		try {
			$response = $this->client->callEndpoint(
				'course/list',
				$arguments
			);
		} catch (ApiErrorResponseException|ClientNotInitializedException $e){
			throw new InvalidResponseException($e->getMessage(), $e->getCode(), $e);
		}

		if ($response->getStatus() === 404) {
			$this->logWarning("could not find courses with arguments: ", $arguments);
			return Collection::fromArray([]);
		}

		if ($response->getStatus() === 401) {
			$this->logWarning("could not fetch courses (No Access Right) with arguments: ", $arguments);
			throw new InvalidResponseException("could not fetch courses (No Access Right)");
		}

		try{
			$responseData = $this->typeValidator->array($response->getData());
			if (!isset($responseData['data'])){
				$this->logError("response from server was not course list as expected but the following: ", $response);
				throw new InvalidException();
			}

			foreach ( $responseData['data'] as $course ) {
				$courses[] =  $this->getObjectFromAPIResponse( $course );
			}
		} catch (InvalidException){
			$this->logError("could not parse response from server to courses, response:", $response);
			throw new InvalidResponseException();
		}

		return Collection::fromArray($courses);
	}

	/**
	 * @return ArrayAccess<int|string, CourseTemplate>&Traversable<int|string, CourseTemplate>&Countable
	 * @throws InvalidResponseException
	 */
	public function getCourseTemplateList(): ArrayAccess&Traversable&Countable {
		try {
			$response = $this->client->callEndpoint('coursetemplate/list');
		} catch (ApiErrorResponseException|ClientNotInitializedException $e){
			throw new InvalidResponseException($e->getMessage(), $e->getCode(), $e);
		}

		if ($response->getStatus() === 401) {
			$this->logWarning("could not fetch course templates (No Access Right)");
			throw new InvalidResponseException("could not fetch course templates (No Access Right)");
		}

		$returnArray = [];

		try{
			$responseData = $this->typeValidator->array($response->getData());
			if (!isset($responseData['coursetemplates'])){
				$this->logError("response from server was not course templates as expected but the following: (data)", $response);
				throw new InvalidException();
			}

			foreach ( $responseData['coursetemplates'] as $template ) {
				$returnArray[] = $this->getTemplateFromApi( $template );
			}
		} catch (InvalidException){
			$this->logError("could not parse response from server to course templates, response:", $response);
			throw new InvalidResponseException();
		}

		return new PagedResponse(Collection::fromArray($returnArray));
	}

	/**
	 * @param mixed $apiResponse
	 *
	 * @return Course
	 * @throws InvalidException
	 */
	protected function getObjectFromAPIResponse(mixed $apiResponse ): Course {
		$typeIs = $this->typeValidator;
		$apiResponse = $typeIs->array($apiResponse);
		$course = (
			new Course($typeIs->int($apiResponse['id'] ?? null) ?? throw new InvalidException("id is not set"))
		)->setUsersData($this->getUsersDataFromApi( $typeIs->array($apiResponse['users'] ?? [])))
		 ->setCostsData($this->getCostsDataFromApi( $typeIs->array($apiResponse['costs'] ?? [])))
		 ->setName($typeIs->str($apiResponse['name'] ?? ''))
		 ->setCode($typeIs->str($apiResponse['code'] ?? null))
		 ->setDescription($typeIs->str($apiResponse['description'] ?? null))
		 ->setType($typeIs->str($apiResponse['type'] ?? null))
		 ->setStatus($typeIs->str($apiResponse['status'] ?? null))
		 ->setDaypartAmount($typeIs->int($apiResponse['daypart_amount'] ?? null))
		 ->setDayPartDisplayType(DayPartDisplayTypes::tryFrom( $typeIs->str($apiResponse['daypart_display_type'] ?? null) ?? "" ) ?? DayPartDisplayTypes::AllSelectedImmutable )
		 ->setLanguage($typeIs->str($apiResponse['language'] ?? null))
		 ->setHasCode95($typeIs->bool($apiResponse['has_code95'] ?? null))
		 ->setHasElearning($typeIs->bool($apiResponse['has_elearning'] ?? null))
		 ->setHasSoob($typeIs->bool($apiResponse['has_soob'] ?? null))
		 ->setHasStap($typeIs->bool($apiResponse['has_stap'] ?? null))
		 ->setStapOnly($typeIs->bool($apiResponse['stap_only'] ?? null))
		 ->setStartGuaranteed($typeIs->bool($apiResponse['start_guaranteed'] ?? null))
		 ->setMoneybackGuaranteed($typeIs->bool($apiResponse['moneyback_guaranteed'] ?? null))
		 ->setTemplateId($typeIs->int($apiResponse['coursetemplate']['id'] ?? null))
		 ->setLevel($typeIs->str($apiResponse['level'] ?? null))
		 ->setLevelDescription($typeIs->str($apiResponse['level_description'] ?? null))
		 ->setAttributes($this->getAttributesFromApi($typeIs->array($apiResponse['attributes'] ?? [])))
		 ->setPriceFromDayParts($typeIs->str($apiResponse['costs']['type'] ?? '') === 'daypart');

		if (($daypartIds = $typeIs->array($apiResponse['daypart_ids'] ?? null)) !== null){
			foreach ($daypartIds as $id => $dayPart){
				$daypartIds[$id] = $typeIs->int($dayPart);
			}
			$course->setDayPartIds($daypartIds);
		}
		if (($labels = $typeIs->array($apiResponse['labels'] ?? null)) !== null) {
			foreach ($labels as $index => $label){
				$labels[$index] = $typeIs->str($label);
			}
			$course->setLabels($labels);
		}
		if (($companies = $typeIs->array($apiResponse['connected_companies'] ?? null)) !== null) {
			foreach ($companies as $index => $company){
				$companies[$index] = $typeIs->int($company);
			}
			$course->setCompanies(Collection::fromArray($companies));
		}

		return $course;
	}

	/**
	 * @param array<mixed> $attributesApi
	 *
	 * @return ApiAttribute[]
	 * @throws InvalidException
	 */
	protected function getAttributesFromApi( array $attributesApi ): array {
		$typeIs = $this->typeValidator;
		$attributes = [];
		foreach ($attributesApi as $name => $data){
			$values = [];
			$data = $typeIs->array($data);
			foreach ($typeIs->array($data['values'] ?? []) as $valueName => $valueInfo){
				$valueInfo = $typeIs->array($valueInfo);
				$values[] = (new ApiAttributeValue())
					->setName($typeIs->str($valueName))
					->setValue($typeIs->str($valueInfo['value'] ?? null))
					->setDescription($typeIs->str($valueInfo['description'] ?? ''));
			}

			if ($code = $typeIs->str($data['code'] ?? null))
				$attributes[] = (new ApiAttribute())
					->setName($typeIs->str($name))
					->setCode($code)
					->setIsRequired($typeIs->bool($data['is_required'] ?? false))
					->setForBooking($typeIs->bool($data['for_booking'] ?? false))
					->setMultipleValuesAllowed($typeIs->bool($data['multiple_values_allowed'] ?? false))
					->setIsFinancial($typeIs->bool($data['is_financial'] ?? false))
					->setDescription($typeIs->str($data['description'] ?? ''))
					->setValues($values);
		}
		return $attributes;
	}

	/**
	 * @param mixed $usersApiResponse
	 *
	 * @return CourseUsersData
	 * @throws InvalidException
	 */
	protected function getUsersDataFromApi( mixed $usersApiResponse ): CourseUsersData {
		$typeIs = $this->typeValidator;
		return (new CourseUsersData())
			->setMin($typeIs->int($usersApiResponse['min'] ?? 0))
			->setMax($typeIs->int($usersApiResponse['max'] ?? 0))
			->setScheduled($typeIs->int($usersApiResponse['scheduled'] ?? 0))
			->setAvailable($typeIs->int($usersApiResponse['available'] ?? 0))
			->setOptions($typeIs->int($usersApiResponse['options'] ?? 0));
	}

	/**
	 * @param mixed $costsApiResponse
	 *
	 * @return CostsData
	 * @throws InvalidException
	 */
	protected function getCostsDataFromApi(mixed $costsApiResponse ): CostsData {
		$typeIs = $this->typeValidator;
		return (new CostsData())
			->setCost($typeIs->float($costsApiResponse['user'] ?? $costsApiResponse['price'] ?? $costsApiResponse['selling_price'] ?? 0))
			->setVat($typeIs->int($costsApiResponse['vat'] ?? 0))
			->setVatCode($typeIs->int($costsApiResponse['vatCode'] ?? 0))
			->setRemark($typeIs->str($costsApiResponse['remark'] ?? ''))
			->setCourse($typeIs->float($costsApiResponse['course'] ?? 0));
	}

	/**
	 * @param mixed $apiResponse
	 *
	 * @return CourseMaterial
	 * @throws InvalidException
	 */
	protected function getMaterialFromApi( mixed $apiResponse ): CourseMaterial {
		$typeIs = $this->typeValidator;
		return (new CourseMaterial( $typeIs->int($apiResponse['id'] ?? null) ?? throw new InvalidException("id is not set")))
			->setCode($typeIs->str($apiResponse['code'] ?? ''))
			->setName($typeIs->str($apiResponse['name'] ?? ''))
			->setCostsData($this->getCostsDataFromApi( $apiResponse ))
			->setDescription($typeIs->str($apiResponse['description'] ?? ''))
			->setInvoicedPerStudent($typeIs->bool($apiResponse['invoiced_per_student'] ?? false))
			->setInvoicedPerCompany($typeIs->bool($apiResponse['invoiced_per_company'] ?? false))
			->setAmount($typeIs->int($apiResponse['amount'] ?? null));
	}

	/**
	 * @param mixed $apiResponse
	 *
	 * @return CourseTemplate
	 * @throws InvalidException
	 */
	protected function getTemplateFromApi( mixed $apiResponse ): CourseTemplate {
		$typeIs = $this->typeValidator;
		return (new CourseTemplate( $typeIs->int($apiResponse['id'] ?? null) ?? throw new InvalidException("id is not set")))
			->setCode($typeIs->str($apiResponse['code'] ?? ''))
			->setName($typeIs->str($apiResponse['name'] ?? ''))
			->setDescription($typeIs->str($apiResponse['description'] ?? ''))
			->setDaypartAmount($typeIs->int($apiResponse['daypart_amount'] ?? 0))
			->setHasElearning($typeIs->bool($apiResponse['has_elearning'] ?? false))
			->setHasCode95($typeIs->bool($apiResponse['has_code95'] ?? false))
			->setHasSoob($typeIs->bool($apiResponse['has_soob'] ?? false))
			->setHasStap($typeIs->bool($apiResponse['has_stap'] ?? false))
			->setStapOnly($typeIs->bool($apiResponse['only_stap'] ?? false))
			->setLanguage($typeIs->str($apiResponse['language'] ?? ''))
			->setLabels((function () use (&$apiResponse, $typeIs) {
				$labels = [];
				foreach ($typeIs->array($apiResponse['labels'] ?? []) as $label){
					$labels[] = $typeIs->str($label);
				}
				return $labels;
			})());
	}
}