<?php

declare(strict_types=1);

namespace Xentral\Modules\GoogleCalendar\Client;

use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use Xentral\Components\HttpClient\Response\ServerResponse;
use Xentral\Components\Logger\LoggerAwareTrait;
use Xentral\Modules\GoogleApi\Client\GoolgeApiClientInterface;
use Xentral\Modules\GoogleApi\Data\GoogleAccountData;
use Xentral\Modules\GoogleCalendar\Data\GoogleCalendarColorCollection;
use Xentral\Modules\GoogleCalendar\Data\GoogleCalendarEventData;
use Xentral\Modules\GoogleCalendar\Data\GoogleCalendarListItem;
use Xentral\Modules\GoogleCalendar\Exception\GoogleCalendarApiException;
use Xentral\Modules\GoogleCalendar\Exception\GoogleCalendarNotFoundException;
use Xentral\Modules\GoogleCalendar\Exception\InvalidArgumentException;

final class GoogleCalendarClient implements GoogleCalendarClientInterface
{
    use LoggerAwareTrait;

    /** @var string CALENDAR_PRIMARY */
    public const CALENDAR_PRIMARY = 'primary';

    /** @var string BASE_URL */
    private const BASE_URL = 'https://www.googleapis.com/calendar/v3';

    /** @var GoolgeApiClientInterface $googleApiClient */
    private $googleApiClient;

    /**
     * @param GoolgeApiClientInterface $googleApiClient
     */
    public function __construct(
        GoolgeApiClientInterface $googleApiClient
    ) {
        $this->googleApiClient = $googleApiClient;
    }

    /**
     * @param array $filters
     *
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarListItem[]
     */
    public function getCalendarList(array $filters = []): array
    {
        $uri = $this->createUri('users/me/calendarList', $filters);
        try {
            $result = $this->googleApiClient->sendRequest('GET', $uri);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }
        $list = [];
        if (isset($result['items']) && is_array($result['items'])) {
            foreach ($result['items'] as $item) {
                $list[] = GoogleCalendarListItem::fromArray($item);
            }
        }

        return $list;
    }

    /**
     * @throws GoogleCalendarNotFoundException
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarListItem
     */
    public function getPrimaryCalendar(): GoogleCalendarListItem
    {
        $ownedCalendars = $this->getCalendarList(['minAccessRole' => 'owner']);
        foreach ($ownedCalendars as $calendarListItem) {
            if ($calendarListItem->isPrimary()) {
                return $calendarListItem;
            }
        }
        throw new GoogleCalendarNotFoundException(
            sprintf('Cannot get primary calendar of user "id=%s"', $this->getAccount()->getUserId())
        );
    }

    /**
     * @param string            $calendar
     * @param DateTimeInterface $modifiedSince
     *
     * @return GoogleCalendarEventData[]
     */
    public function getModifiedEvents(string $calendar, DateTimeInterface $modifiedSince): array
    {
        $modifiedTimestamp = $modifiedSince->format(DateTimeInterface::RFC3339);
        $filters = [
            'updatedMin' => $modifiedTimestamp,
        ];
        $now = new DateTimeImmutable();
        $now = $now->setTimestamp(time());
        $from = $now->sub(new DateInterval('P1W'));
        $to = $now->add(new DateInterval('P3W'));
        $filters['timeMax'] = $to->format(DateTimeInterface::RFC3339);
        $filters['timeMin'] = $from->format(DateTimeInterface::RFC3339);

        return $this->getEventList($calendar, $filters);
    }

    /**
     * @param string            $calendar
     * @param DateTimeInterface $from
     * @param DateTimeInterface $to
     *
     * @return GoogleCalendarEventData[]
     */
    public function getAbsoluteEvents(string $calendar, DateTimeInterface $from, DateTimeInterface $to): array
    {
        $filters = [];
        $filters['timeMax'] = $to->format(DateTimeInterface::RFC3339);
        $filters['timeMin'] = $from->format(DateTimeInterface::RFC3339);

        return $this->getEventList($calendar, $filters);
    }

    /**
     * @param string $eventId
     *
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarEventData
     */
    public function getEvent($eventId): GoogleCalendarEventData
    {
        $path = sprintf('calendars/%s/events/%s', self::CALENDAR_PRIMARY, $eventId);
        $url = $this->createUri($path);
        try {
            $result = $this->googleApiClient->sendRequest('GET', $url);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }

        return GoogleCalendarEventData::fromArray($result);
    }

    /**
     * @param string $calendar calendar identifier
     * @param array  $filters
     *
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarEventData[]
     */
    public function getEventList(string $calendar, $filters = []): array
    {
        $path = sprintf('calendars/%s/events', $calendar);
        $filters['singleEvents'] = 'true';
        $url = $this->createUri($path, $filters);
        try {
            $result = $this->googleApiClient->sendRequest('GET', $url);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }
        $events = [];
        foreach ($result['items'] as $event) {
            $events[] = GoogleCalendarEventData::fromArray($event);
        }

        return $events;
    }

    /**
     * @param GoogleCalendarEventData $event
     * @param string                  $sendUpdates
     *
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarEventData
     */
    public function insertEvent(
        GoogleCalendarEventData $event,
        $sendUpdates = self::SENDUPDATES_DEFAULT
    ): GoogleCalendarEventData {
        $this->validateSendUpdatesParam($sendUpdates);
        $queryParams = [];
        if ($sendUpdates !== self::SENDUPDATES_DEFAULT) {
            $queryParams = ['sendUpdates' => $sendUpdates];
        }
        $url = $this->createUri(sprintf('calendars/%s/events', self::CALENDAR_PRIMARY), $queryParams);
        try {
            $result = $this->googleApiClient->sendRequest('POST', $url, $event->toArray());
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }

        return GoogleCalendarEventData::fromArray($result);
    }

    /**
     * @param GoogleCalendarEventData $event
     * @param string                  $sendUpdates
     *
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarEventData
     */
    public function updateEvent(
        GoogleCalendarEventData $event,
        $sendUpdates = self::SENDUPDATES_DEFAULT
    ): GoogleCalendarEventData {
        $this->validateSendUpdatesParam($sendUpdates);
        $postData = $event->toArray();
        $queryParams = [];
        if ($sendUpdates !== self::SENDUPDATES_DEFAULT) {
            $queryParams = ['sendUpdates' => $sendUpdates];
        }
        $url = $this->createUri(
            sprintf(
                'calendars/%s/events/%s',
                self::CALENDAR_PRIMARY,
                $event->getId()
            ),
            $queryParams
        );
        try {
            $result = $this->googleApiClient->sendRequest('PUT', $url, $postData);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }

        return GoogleCalendarEventData::fromArray($result);
    }

    /**
     * @param GoogleCalendarEventData $event
     * @param string                  $targetCalendar
     * @param string                  $sendUpdates
     *
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarEventData
     */
    public function moveEvent(
        GoogleCalendarEventData $event,
        $targetCalendar,
        $sendUpdates = self::SENDUPDATES_DEFAULT
    ): GoogleCalendarEventData {
        $this->validateSendUpdatesParam($sendUpdates);
        $queryParams = ['destination' => $targetCalendar];
        if ($sendUpdates !== self::SENDUPDATES_DEFAULT) {
            $queryParams['sendUpdates'] = $sendUpdates;
        }
        $url = $this->createUri(
            sprintf('calendars/%s/events/%s/move', self::CALENDAR_PRIMARY, $event->getId()),
            $queryParams
        );
        try {
            $result = $this->googleApiClient->sendRequest('POST', $url);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }

        return GoogleCalendarEventData::fromArray($result);
    }

    /**
     * @param string $eventId
     * @param string $sendUpdates
     *
     * @return bool
     */
    public function deleteEvent(
        $eventId,
        $sendUpdates = self::SENDUPDATES_DEFAULT
    ): bool {
        $this->validateSendUpdatesParam($sendUpdates);
        $queryParams = [];
        if ($sendUpdates !== self::SENDUPDATES_DEFAULT) {
            $queryParams = ['sendUpdates' => $sendUpdates];
        }
        $url = $this->createUri(
            sprintf('calendars/%s/events/%s', self::CALENDAR_PRIMARY, $eventId),
            $queryParams
        );
        try {
            $this->googleApiClient->sendRequest('DELETE', $url, null, []);
        } catch (Exception $e) {
            $httpCode = $e->getCode();
            if ($httpCode> 399 && $httpCode < 500) {
                return false;
            }
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }

        return true;
    }

    /**
     * @param string $calendar
     *
     * @throws GoogleCalendarApiException
     *
     * @return bool
     */
    public function canAccessCalendar(string $calendar): bool
    {
        $url = $this->createUri(sprintf('calendars/%s', $calendar));
        try {
            $this->googleApiClient->sendRequest('GET', $url);
        } catch (Exception $e) {
            $httpCode = $e->getCode();
            if ($httpCode > 399 && $httpCode < 500) {
                return false;
            }
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }

        return true;
    }

    /**
     * @throws GoogleCalendarApiException
     *
     * @return array
     */
    public function getUserSettings(): array
    {
        $url = $this->createUri('users/me/settings');
        try {
            $result = $this->googleApiClient->sendRequest('GET', $url);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }
        $settings = [];
        if (isset($result['items']) && is_array($result['items'])) {
            foreach ($result['items'] as $setting) {
                $settings[$setting['id']] = $setting['value'];
            }
        }

        return $settings;
    }

    /**
     * @throws GoogleCalendarApiException
     *
     * @return GoogleCalendarColorCollection
     */
    public function getAvailableColors(): GoogleCalendarColorCollection
    {
        $url = $this->createUri('colors');
        try {
            $result = $this->googleApiClient->sendRequest('GET', $url);
        } catch (Exception $e) {
            throw new GoogleCalendarApiException($e->getMessage(), $e->getCode(), $e);
        }
        $colors = GoogleCalendarColorCollection::createFromJsonArray($result);
        $default = $this->getDefaultColorId();
        $colors->setDefaultColorId($default);

        return $colors;
    }

    /**
     * @return GoogleAccountData
     */
    public function getAccount(): GoogleAccountData
    {
        return $this->googleApiClient->getAccount();
    }

    /**
     * @param string $uri
     * @param array  $queryParams
     *
     * @return string
     */
    private function createUri(string $uri, array $queryParams = []): string
    {
        $url = sprintf('%s/%s', self::BASE_URL, $uri);
        if (!empty($queryParams)) {
            $url .= '?' . http_build_query($queryParams);
        }

        return $url;
    }

    /**
     * @param string $sendUpdates
     *
     * @throws InvalidArgumentException
     *
     * @return bool
     */
    private function validateSendUpdatesParam(string $sendUpdates): bool
    {
        if (
            $sendUpdates !== self::SENDUPDATES_DEFAULT
            && $sendUpdates !== self::SENDUPDATES_EXTERNALONLY
            && $sendUpdates !== self::SENDUPDATES_NONE
            && $sendUpdates !== self::SENDUPDATES_ALL
        ) {
            throw new InvalidArgumentException('Ivalid value for query parameter "sendUpdates".');
        }

        return true;
    }

    /**
     * @throws GoogleCalendarApiException
     *
     * @return string
     */
    private function getDefaultColorId(): string
    {
        $colorId = '';
        $filters = [];
        $filters['minAccessRole'] = 'owner';
        $calendars = $this->getCalendarList($filters);
        foreach ($calendars as $calendar) {
            if ($calendar->isPrimary()) {
                $colorId = $calendar->getColorId();
            }
        }

        return $colorId;
    }
}