<?php

namespace Planaday\Api\V1;

use ArrayAccess;
use Countable;
use DateTime;
use DateTimeZone;
use Planaday\Api\ApiException\ApiErrorResponseException;
use Planaday\Api\ApiException\ClientNotInitializedException;
use Planaday\Api\ApiException\InvalidResponseException;
use Planaday\Entities\CostsData;
use Planaday\Entities\Course\Exception\CourseNotFoundException;
use Planaday\Entities\DayPart\DayPartApi;
use Planaday\Entities\DayPart\DTO\DayPart;
use Planaday\Entities\DayPart\DTO\DayPartUsersData;
use Planaday\Entities\DayPart\Exception\DayPartNotFoundException;
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 DayPartApiV1Controller implements DayPartApi {
	use Logging;

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

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

	/**
	 * @param int $courseId
	 * @param ClientV1Response|null $response
	 * @param int $offset
	 * @param int $limit
	 *
	 * @return array
	 * @throws CourseNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getDayPartsCourseRaw(int $courseId, ?ClientV1Response &$response, int $offset = 0, int $limit = 100): array {
		$offset += 1;

		try {
			$response = $this->client->callEndpoint(
				'course/' . $courseId . '/dayparts',
				['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 to fetch day parts");
			throw new CourseNotFoundException("could not find course with id $courseId to fetch day parts");
		}

		$typeIs = $this->typeValidator;

		try{
			$dayParts = [];

			foreach ($typeIs->array($typeIs->array($response->getData() ?? [])['data'] ?? []) as $dayPart){
				$dayPart = $typeIs->array($dayPart);
				$dayPart['course'] = ['id' => $courseId];
				$dayParts[] = $this->getDayPartFromApi($dayPart);
			}

			return $dayParts;
		} catch (InvalidException){
			$this->logError("could not parse response from server to day parts", $response);
			throw new InvalidResponseException();
		}
	}

	/**
	 * @param Collection<int> $dayPartIds
	 *
	 * @return ArrayAccess<int|string, DayPart>&Traversable<int|string, DayPart>&Countable
	 * @throws DayPartNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getDayParts(Countable&Traversable&ArrayAccess $dayPartIds):ArrayAccess&Traversable&Countable {
		$firstDayPart = null;

		foreach ($dayPartIds as $key => $dayPartId){
			if (!empty($dayPartId)){
				$firstDayPart = $firstDayPart ?? $dayPartId;
				continue;
			}

			unset($dayPartIds[$key]);
		}

		if ($firstDayPart === null){
			$this->logWarning("No valid day part id found");
			return Collection::fromArray([]);
		}
		$dayParts = [];
		for($i = 0; $i < count($dayPartIds); $i += 100) {
			$dayPartIdsChunk = array_slice( [...$dayPartIds], $i, 100 );
			try {
				$response = $this->client->callEndpoint( "daypart/" . $dayPartIdsChunk[0], [ 'batch' => implode( "+", $dayPartIdsChunk ) ] );
			} catch ( ApiErrorResponseException|ClientNotInitializedException $e ) {
				throw new InvalidResponseException( $e->getMessage(), $e->getCode(), $e );
			}

			if ( $response->getStatus() === 404 ) {
				$this->logWarning( "could not find day part with id $firstDayPart" );
				throw new DayPartNotFoundException( "could not find day part with id $firstDayPart" );
			}

			if ( $response->getStatus() === 401 ) {
				$this->logWarning( "could not fetch day part with id $firstDayPart (No Access Right)" );
				throw new DayPartNotFoundException( "could not fetch day part with id $firstDayPart (No Access Right)" );
			}

			$handledIndex = 0;
			try {
				$data = $this->typeValidator->array( $response->getData() ?? [] );
				if ( isset( $data['id'] ) ) {
					$dayParts[] = $this->getDayPartFromApi( $data );
				} else {
					foreach ( $data as $index => $dayPart ) {
						$dayParts[] = $this->getDayPartFromApi( $this->typeValidator->array( $dayPart, "day part was not an array" ) );
						$handledIndex = $index;
					}
				}
			} catch ( InvalidException $e ) {
				$this->logError( "could not parse response from server to day parts on index $handledIndex: " . $e->getMessage(), $response );
				throw new InvalidResponseException();
			}
		}

		return Collection::fromArray($dayParts);
	}

	/**
	 * @param $dayPartId
	 *
	 * @return DayPart
	 * @throws DayPartNotFoundException
	 * @throws InvalidResponseException
	 */
	public function getDayPart($dayPartId):DayPart {
		try {
			$response = $this->client->callEndpoint('daypart/' . $dayPartId);
		} catch (ApiErrorResponseException|ClientNotInitializedException $e){
			throw new InvalidResponseException($e->getMessage(), $e->getCode(), $e);
		}

		if ($response->getStatus() === 404){
			$this->logWarning("could not find day part with id $dayPartId");
			throw new DayPartNotFoundException("could not find day part with id $dayPartId");
		}

		if ($response->getStatus() === 401){
			$this->logWarning("could not fetch day part with id $dayPartId (No Access Right)");
			throw new DayPartNotFoundException("could not fetch day part with id $dayPartId (No Access Right)");
		}

		try{
			return $this->getDayPartFromApi($this->typeValidator->array($response->getData() ?? []));
		} catch (InvalidException){
			$this->logError("could not parse response from server to day part", $response);
			throw new InvalidResponseException();
		}
	}

	/**
	 * @param array<mixed> $apiResponse
	 *
	 * @return DayPart
	 * @throws InvalidException
	 */
	private function getDayPartFromApi(array $apiResponse): DayPart {
		$locations = [];
		$typeIs = $this->typeValidator;

		foreach ($typeIs->array($apiResponse['locations'] ?? [], "locations was not an array") as $location){
			$locationIdPrefix = 'l';
			if ($typeIs->str($location['type'] ?? null, "location type was not a string") === 'company') $locationIdPrefix = 'c';
			$locations[] = $locationIdPrefix . $typeIs->int($location['id'] ?? null, "location id was not an int") ?? throw new InvalidException("location id is not set");
		}

		$instructors = [];

		foreach ($typeIs->array($apiResponse['instructors'] ?? [], "instructors was not an array") as $instructor){
			$instructors[] = $typeIs->int($instructor['id'] ?? null, "instructor id was not an int") ?? throw new InvalidException("instructor id is not set");
		}

		$usersData = $this->getDayPartUsersDataFromApi($typeIs->array($apiResponse['users'] ?? [], "users was not an array"));

		$costsData = $this->getCostsDataFromApi($typeIs->array($apiResponse['costs'] ?? [], "costs was not an array"));

		$dateString = $typeIs->str($apiResponse['date'] ?? null, "Date was not a string") ?? "19700101";
		$startTimeString = $typeIs->str($apiResponse['start_time'] ?? null, "Start time was not a string") ?? "00:00";
		$endTimeString = $typeIs->str($apiResponse["end_time"] ?? null, "End time was not a string") ?? "00:00";
		$isELearning = $typeIs->bool($apiResponse['is_elearning'] ?? null, "is_elearning was not a bool");
		$dateFinishedBefore = $typeIs->str($apiResponse['date_finish_before'] ?? null, "Date was not a string") ?? "19700101";

		$start = $this->getAPIDateTime($dateString, $startTimeString) ??
		         (new DateTime())->setTimestamp(0)->setTimezone(new DateTimeZone('Europe/Amsterdam'));
		$end = $this->getAPIDateTime($isELearning ? $dateFinishedBefore : $dateString, $endTimeString);

		return (new DayPart(
			($typeIs->int($apiResponse['id'] ?? null, "id was not an int") ?? throw new InvalidException("id is not set")),
			($typeIs->int($typeIs->array($apiResponse['course'] ?? [], "course was not an array")['id'] ?? null) ?? throw new InvalidException("course id is not set"))
		))
			->setName($typeIs->str($apiResponse['name'] ?? null, "name was not a string"))
			->setDescription($typeIs->str($apiResponse['description'] ?? null, "description was not a string"))
			->setStatus($typeIs->str($apiResponse['status'] ?? null, "status was not a string"))
			->setLocationIds($locations)
			->setInstructors($instructors)
			->setUsersData($usersData)
			->setCostsData($costsData)
			->setIsELearning($isELearning)
			->setHasCode95($typeIs->bool($apiResponse['has_code95'] ?? null, "has_code95 was not a bool"))
			->setLabels((function () use ($apiResponse, $typeIs) {
				$labels = [];
				foreach ($typeIs->array($apiResponse['labels'] ?? [], "labels is not an array") as $label){
					$labels[] = $typeIs->str($label, "label was not a string");
				}
				return $labels;
			})())
			->setVisible($typeIs->bool($apiResponse['visible'] ?? null, "visible was not a bool"))
			->setStartTime($start)
			->setEndTime($end);
	}

	/**
	 * @param $usersApiResponse
	 *
	 * @return DayPartUsersData
	 * @throws InvalidException
	 */
	private function getDayPartUsersDataFromApi($usersApiResponse): DayPartUsersData {
		$typeIs = $this->typeValidator;
		return (new DayPartUsersData())
			->setScheduled($typeIs->int($usersApiResponse['scheduled'] ?? null, "Scheduled was not an int"))
			->setAvailable($typeIs->int($usersApiResponse['available'] ?? null, "Available was not an int"))
			->setOptions($typeIs->int($usersApiResponse['options'] ?? null, "Options was not an int"))
			->setWaitinglist($typeIs->int($usersApiResponse['waitinglist'] ?? null, "Waiting list was not an int"))
			->setMin($typeIs->int($usersApiResponse['min'] ?? null, "Min was not an int"))
			->setMax($typeIs->int($usersApiResponse['limit'] ?? null, "Max was not an int"));
	}

	/**
	 * @param $costsApiResponse
	 *
	 * @return CostsData
	 * @throws InvalidException
	 */
	private function getCostsDataFromApi($costsApiResponse): CostsData {
		$typeIs = $this->typeValidator;
		return (new CostsData())
			->setCost($typeIs->float($costsApiResponse['user'] ?? $costsApiResponse['price'] ?? $costsApiResponse['selling_price'] ?? 0, "Cost was not a float"))
			->setVat($typeIs->int($costsApiResponse['vat'] ?? 0, "Vat was not an int"))
			->setVatCode($typeIs->int($costsApiResponse['vatCode'] ?? 0, "Vat Code was not an int"))
			->setRemark($typeIs->str($costsApiResponse['remark'] ?? '', "Remark was not a string"))
			->setCourse($typeIs->float($costsApiResponse['course'] ?? 0, "Course was not a float"));
	}

	private function getAPIDateTime(string $date, string $time): ?DateTime {
		$dateTime = DateTime::createFromFormat(
			'YmdH:i',
			$date . $time,
			new DateTimeZone('Europe/Amsterdam')
		);
		if ($dateTime === false)
			$dateTime = DateTime::createFromFormat(
				'YmdH:i',
				$date . '00:00',
				new DateTimeZone('Europe/Amsterdam')
			);

		if ($dateTime === false)
			return null;

		return $dateTime;
	}
}