<?php

/**
 * Chill is a software for social workers.
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;

use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Convert Chill Calendar event to Remote MS Graph event, and MS Graph
 * event to RemoteEvent.
 */
class RemoteEventConverter
{
    /**
     * valid when the remote string contains also a timezone, like in
     * lastModifiedDate.
     */
    final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P';

    /**
     * Same as above, but sometimes the date is expressed with only 6 milliseconds.
     */
    final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP';

    private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';

    private const REMOTE_DATETIME_WITHOUT_TZ_FORMAT = 'Y-m-d\TH:i:s.u?';

    private readonly \DateTimeZone $defaultDateTimeZone;

    private readonly \DateTimeZone $remoteDateTimeZone;

    public function __construct(
        private readonly \Twig\Environment $engine,
        private readonly LocationConverter $locationConverter,
        private readonly LoggerInterface $logger,
        private readonly PersonRenderInterface $personRender,
        private readonly TranslatorInterface $translator,
    ) {
        $this->defaultDateTimeZone = (new \DateTimeImmutable())->getTimezone();
        $this->remoteDateTimeZone = self::getRemoteTimeZone();
    }

    /**
     * Transform a CalendarRange into a representation suitable for storing into MSGraph.
     *
     * @return array an array representation for event in MS Graph
     */
    public function calendarRangeToEvent(CalendarRange $calendarRange): array
    {
        return [
            'subject' => $this->translator->trans('remote_calendar.calendar_range_title'),
            'start' => [
                'dateTime' => $calendarRange->getStartDate()->setTimezone($this->remoteDateTimeZone)
                    ->format(self::REMOTE_DATE_FORMAT),
                'timeZone' => 'UTC',
            ],
            'end' => [
                'dateTime' => $calendarRange->getEndDate()->setTimezone($this->remoteDateTimeZone)
                    ->format(self::REMOTE_DATE_FORMAT),
                'timeZone' => 'UTC',
            ],
            'attendees' => [
                [
                    'emailAddress' => [
                        'address' => $calendarRange->getUser()->getEmailCanonical(),
                        'name' => $calendarRange->getUser()->getLabel(),
                    ],
                ],
            ],
            'isReminderOn' => false,
            'location' => $this->locationConverter->locationToRemote($calendarRange->getLocation()),
        ];
    }

    public function calendarToEvent(Calendar $calendar): array
    {
        $result = array_merge(
            [
                'subject' => '[Chill] '.
                    implode(
                        ', ',
                        $calendar->getPersons()->map(fn (Person $p) => $this->personRender->renderString($p, []))->toArray()
                    ),
                'start' => [
                    'dateTime' => $calendar->getStartDate()->setTimezone($this->remoteDateTimeZone)
                        ->format(self::REMOTE_DATE_FORMAT),
                    'timeZone' => 'UTC',
                ],
                'end' => [
                    'dateTime' => $calendar->getEndDate()->setTimezone($this->remoteDateTimeZone)
                        ->format(self::REMOTE_DATE_FORMAT),
                    'timeZone' => 'UTC',
                ],
                'allowNewTimeProposals' => false,
                'transactionId' => 'calendar_'.$calendar->getId(),
                'body' => [
                    'contentType' => 'text',
                    'content' => $this->engine->render(
                        '@ChillCalendar/MSGraph/calendar_event_body.html.twig',
                        ['calendar' => $calendar]
                    ),
                ],
                'responseRequested' => true,
                'isReminderOn' => false,
            ],
            $this->calendarToEventAttendeesOnly($calendar)
        );

        if ($calendar->hasLocation()) {
            $result['location'] = $this->locationConverter->locationToRemote($calendar->getLocation());
        }

        return $result;
    }

    public function calendarToEventAttendeesOnly(Calendar $calendar): array
    {
        return [
            'attendees' => $calendar->getInvites()->map(
                fn (Invite $i) => $this->buildInviteToAttendee($i)
            )->toArray(),
        ];
    }

    public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent
    {
        $startDate =
            \DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'], $this->remoteDateTimeZone)
                ->setTimezone($this->defaultDateTimeZone);
        $endDate =
            \DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
                ->setTimezone($this->defaultDateTimeZone);

        return new RemoteEvent(
            uniqid('generated_'),
            $this->translator->trans('remote_ms_graph.freebusy_statuses.'.$event['status']),
            '',
            $startDate,
            $endDate
        );
    }

    public static function convertStringDateWithoutTimezone(string $date): \DateTimeImmutable
    {
        $d = \DateTimeImmutable::createFromFormat(
            self::REMOTE_DATETIME_WITHOUT_TZ_FORMAT,
            $date,
            self::getRemoteTimeZone()
        );

        if (false === $d) {
            throw new \RuntimeException("could not convert string date to datetime: {$date}");
        }

        return $d->setTimezone((new \DateTimeImmutable())->getTimezone());
    }

    public static function convertStringDateWithTimezone(string $date): \DateTimeImmutable
    {
        $d = \DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $date);

        if (false === $d) {
            throw new \RuntimeException("could not convert string date to datetime: {$date}");
        }

        $d->setTimezone((new \DateTimeImmutable())->getTimezone());

        return $d;
    }

    public function convertToRemote(array $event): RemoteEvent
    {
        $startDate =
            \DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'], $this->remoteDateTimeZone)
                ->setTimezone($this->defaultDateTimeZone);
        $endDate =
            \DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
                ->setTimezone($this->defaultDateTimeZone);

        return new RemoteEvent(
            $event['id'],
            $event['subject'],
            '',
            $startDate,
            $endDate,
            $event['isAllDay']
        );
    }

    public function getLastModifiedDate(array $event): \DateTimeImmutable
    {
        $date = \DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $event['lastModifiedDateTime']);

        if (false === $date) {
            $date = \DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT_ALT, $event['lastModifiedDateTime']);
        }

        if (false === $date) {
            $this->logger->error(self::class.' Could not convert lastModifiedDate', [
                'actual' => $event['lastModifiedDateTime'],
                'format' => self::REMOTE_DATETIMEZONE_FORMAT,
                'format_alt' => self::REMOTE_DATETIMEZONE_FORMAT_ALT,
            ]);

            throw new \RuntimeException(sprintf('could not convert lastModifiedDate: %s, expected format: %s', $event['lastModifiedDateTime'], self::REMOTE_DATETIMEZONE_FORMAT.' and '.self::REMOTE_DATETIMEZONE_FORMAT_ALT));
        }

        return $date;
    }

    /**
     * Return a string which format a DateTime to string. To be used in POST requests,.
     */
    public static function getRemoteDateTimeSimpleFormat(): string
    {
        return 'Y-m-d\TH:i:s';
    }

    public static function getRemoteTimeZone(): \DateTimeZone
    {
        return new \DateTimeZone('UTC');
    }

    private function buildInviteToAttendee(Invite $invite): array
    {
        return [
            'emailAddress' => [
                'address' => $invite->getUser()->getEmail(),
                'name' => $invite->getUser()->getLabel(),
            ],
            'type' => 'Required',
        ];
    }
}
