<?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\Location\DTO\Location;
use Planaday\Entities\Location\Exception\LocationNotFoundException;
use Planaday\Entities\Location\LocationApi;
use Planaday\Utility\DataFilters\InvalidException;
use Planaday\Utility\DataFilters\TypeValidator;
use Planaday\Utility\DataGrouping\Collection;
use Planaday\Utility\Debug\Log\Logging;
use Traversable;

class LocationApiV1Controller implements LocationApi {
	use Logging;

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

	/**
	 * @param Collection<string> $locationIds
	 *
	 * @return ArrayAccess<int|string, Location>&Traversable<int|string, Location>&Countable
	 * @throws InvalidResponseException
	 * @throws LocationNotFoundException
	 */
	public function getLocations(Countable&Traversable&ArrayAccess $locationIds): ArrayAccess&Traversable&Countable {
		if (count($locationIds) === 0){
			return Collection::fromArray([]);
		}

		$locations = [];
		for($i = 0; $i < count($locationIds); $i += 100) {
			$locationIdsChunk = array_slice( [...$locationIds], $i, 100 );
			try {
				$response = $this->client->callEndpoint(
					'location/' . substr($locationIdsChunk[0], 1),
					[
						"type" => str_starts_with($locationIdsChunk[0], 'c') ? 'company' : 'location',
						...(count($locationIdsChunk) > 1 ? ["batch" => implode( ' ', $locationIdsChunk )] : [])
					]
				);
			} catch ( ApiErrorResponseException|ClientNotInitializedException $e ) {
				throw new InvalidResponseException( $e->getMessage(), $e->getCode(), $e );
			}

			if ( $response->getStatus() === 404 ) {
				$this->logWarning( "Locations with IDs " . implode( ', ', [...$locationIds] ) . " not found" );
				throw new LocationNotFoundException( "Locations with IDs " . implode( ', ', [...$locationIds] ) . " not found" );
			}

			if ( $response->getStatus() === 401 ) {
				$this->logWarning( "Locations with IDs " . implode( ', ', [...$locationIds] ) . " not found (No Access Right)" );
				throw new LocationNotFoundException( "Locations with IDs " . implode( ', ', [...$locationIds] ) . " not found (No Access Right)" );
			}

			try {
				$responseData = $this->typeValidator->array( $response->getData() );

				if ( isset( $responseData['id'] ) ) {
					$locations[] = $this->getLocationFromApi( $responseData );
				} else {
					foreach ( $responseData as $value ) {
						$locations[] = $this->getLocationFromApi( $this->typeValidator->array( $value ) );
					}
				}

			} catch ( InvalidException ) {
				$this->logError( "could not parse response from server to locations", $response);
				throw new InvalidResponseException();
			}
		}
		return Collection::fromArray($locations);
	}

	/**
	 * @param $locationId
	 *
	 * @return Location
	 * @throws InvalidResponseException
	 * @throws LocationNotFoundException
	 */
	public function getLocation($locationId): Location {
		$location = $this->getLocations(Collection::fromArray([$locationId]));
		if (count($location) === 0){
			throw new LocationNotFoundException();
		}
		return $location[0];
	}

	/**
	 * @param array<mixed> $apiResponse
	 *
	 * @return Location
	 * @throws InvalidException
	 */
	private function getLocationFromApi(array $apiResponse): Location {
		$typeIs = $this->typeValidator;
		$costsData = $this->getCostsDataFromApi($typeIs->array($apiResponse['costs'] ?? []));
		$apiResponse['address'] ??= [];
		$apiResponse['contact_info'] ??= [];

		$locationType = $typeIs->str($apiResponse['type'] ?? null, "type was not a string");
		$idPrefix = 'l';
		if ($locationType === 'company') $idPrefix = 'c';

		return (new Location($idPrefix . $typeIs->int($apiResponse['id'] ?? null) ?? throw new InvalidException("id is not set")))
			->setName($typeIs->str($apiResponse['name'] ?? ''))
			->setCode($typeIs->str($apiResponse['code'] ?? ''))
			->setStreet1($typeIs->str($apiResponse['address']['street_1'] ?? ''))
			->setStreet2($typeIs->str($apiResponse['address']['street_2'] ?? ''))
			->setHouseNumber($typeIs->str($apiResponse['address']['housenumber'] ?? ''))
			->setHouseNumberExtension($typeIs->str($apiResponse['address']['housenumber_extention'] ?? ''))
			->setZipcode($typeIs->str($apiResponse['address']['zipcode'] ?? ''))
			->setCity($typeIs->str($apiResponse['address']['city'] ?? ''))
			->setCountry($typeIs->str($apiResponse['address']['country'] ?? ''))
			->setLat($typeIs->str($apiResponse['address']['lat'] ?? ''))
			->setLng($typeIs->str($apiResponse['address']['lng'] ?? ''))
			->setPhoneNumber($typeIs->str($apiResponse['contact_info']['phonenumber_1'] ?? ''))
			->setEmail($typeIs->str($apiResponse['contact_info']['email'] ?? ''))
			->setWebsite($typeIs->str($apiResponse['contact_info']['website'] ?? ''))
			->setDescription($typeIs->str($apiResponse['description'] ?? ''))
			->setCapacity($typeIs->int($apiResponse['capacity'] ?? 0))
			->setOverbookable($typeIs->bool($apiResponse['overbookable'] ?? false))
			->setIsVirtual($typeIs->bool($apiResponse['is_virtual'] ?? false))
			->setIsExternal($typeIs->bool($apiResponse['is_external'] ?? false))
			->setCostData($costsData);
	}

	/**
	 * @param array<mixed> $costsApiResponse
	 *
	 * @return CostsData
	 * @throws InvalidException
	 */
	private function getCostsDataFromApi(array $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));
	}
}