<?php

namespace Velis;

use DateTime;
use DateTimeZone;
use Velis\Filter as Filter;
use Velis\Exception as VelisException;

/**
 * Class ICS
 * @author Maciej Borowiec <maciej.borowiec@velistech.com>
 * @description Class for generating of ics - calendar invitation file
 * More information:
 * - https://www.kanzaki.com/docs/ical/
 * - https://www.ietf.org/rfc/rfc2445.txt
 */
class ICS
{
    private const DATE_TIME_ZONE = 'UTC';
    private const DATE_TIME_FORMAT = 'Ymd\THis';

    private const AVAILABLE_PARAMS = [
        'start_at',
        'end_at',
        'title',
        'location',
        'description',
        'url',
        'recipients',
    ];

    private const REQUIRED_PARAMS = [
        'start_at',
        'end_at',
        'title',
    ];

    private User $user;

    private string $eventStartDate;
    private string $eventEndDate;
    private ?string $location;
    private ?string $description;
    private ?string $url;

    /**
     * @var array|null ['full_name', 'email']
     */
    private ?array $recipients;
    private string $summary;        // title/name of the event

    private $utcTimeZone;
    private $appTimeZone;
    private $offsetTimeZone;

    /**
     * @param User $organizerUser
     * @param array $params
     * @throws VelisException
     */
    public function __construct(User $organizerUser, array $params)
    {
        $filteredParams = $this->filterParams($params);

        if (!$filteredParams) {
            throw new VelisException('Incorrect params for ICS file', 409);
        }

        $this->checkRequiredParams($filteredParams);

        $this->user = $organizerUser;
        $this->setTimeZoneProperties();
        $this->setStartEventDateTime($params['start_at']);
        $this->setEndEventDateTime($params['end_at']);
        $this->setSummary($params['title']);

        if (array_key_exists('location', $params)) {
            $this->setLocation($params['location']);
        }

        if (array_key_exists('description', $params)) {
            $this->setDescription($params['description']);
        }

        if (array_key_exists('url', $params)) {
            $this->setUrl($params['url']);
        }

        if (array_key_exists('recipients', $params)) {
            $this->setRecipients($params['recipients']);
        }

        $this->setData();
    }

    /**
     * @param $params
     * @description Params ($params) injected into the constructor are filtered according to
     * the list of allowed parameters (AVAILABLE_PARAMS)
     * @return array
     */
    private function filterParams($params): array
    {
        return array_intersect_key(array_keys($params), self::AVAILABLE_PARAMS);
    }

    /**
     * @param array $params
     * @throws VelisException
     */
    private function checkRequiredParams(array $params): void
    {
        foreach (self::REQUIRED_PARAMS as $requireParam) {
            if (array_key_exists($requireParam, $params)) {
                throw new VelisException('No required params.', 422);
            }
        }
    }

    /**
     * @throws Exception
     * @description set Timezone properties required to set event data format
     */
    private function setTimeZoneProperties(): void
    {
        // Application time zone
        $this->appTimeZone = $this->user->getDateTimeZone();

        // Application current time
        $appTime = new DateTime('now', $this->appTimeZone);

        // UTC time zone
        $this->utcTimeZone = new DateTimeZone(self::DATE_TIME_ZONE);

        $this->offsetTimeZone = $this->appTimeZone->getOffset($appTime) / 3600;
    }

    private function getTZname(): string
    {
        return 'GMT' . $this->getTZoffset();
    }

    private function getTZoffset(): string
    {
        $offset = $this->offsetTimeZone;

        return (($offset < 0) ? '' : '+') . (($offset > -10 && $offset < 10) ? '0' : '') . $offset . '00';
    }

    /**
     * @param string $dateTime
     * @return string
     * @throws Exception|\Exception
     */
    private function convToUTC(string $dateTime): string
    {
        // Create visit time with current application time zone
        $eventTime = new DateTime($dateTime);

        // Set UTC time zone to be used in invitation
        $eventTime->setTimezone($this->utcTimeZone);

        // Get UTC date string for invitation
        return $eventTime->format(self::DATE_TIME_FORMAT) . 'Z';
    }

    /**
     * @param string $startDate
     * @throws Exception
     */
    protected function setStartEventDateTime(string $startDate): void
    {
        $this->eventStartDate = $this->convToUTC($startDate);
    }

    /**
     * @param string $endDate
     * @throws Exception
     */
    protected function setEndEventDateTime(string $endDate): void
    {
        $this->eventEndDate = $this->convToUTC($endDate);
    }

    protected function setLocation(string $location = ''): void
    {
        $this->location = Filter::filterStripTags($location);
    }

    /**
     * @description Lines of text SHOULD NOT be longer than 75 octets, excluding the line break.
     * More info: https://www.ietf.org/rfc/rfc2445.txt -> 4.1
     * @param ?string $description
     */
    protected function setDescription(?string $description = ''): void
    {
        $description = Filter::filterStripTags($description);

        $this->description = wordwrap($description, 74, '\,');
    }

    /**
     * @param string|null $url
     */
    protected function setUrl(?string $url = ''): void
    {
        $this->url = (string)$url;
    }

    /**
     * @param array $recipients - contians User objcets
     * @throws Exception
     */
    protected function setRecipients(array $recipients): void
    {
        foreach ($recipients as $recipient) {
            $this->recipients[] = [
                'full_name' => $recipient->getFullName(),
                'email' => $recipient->email,
            ];
        }
    }

    /**
     * @param string $company
     * @param string $product
     * @return string
     * @description Set PRODID parameter
     */
    protected function getProdIdValue(string $company = 'VELIS Real Estate Tech', string $product = 'Singu FM'): string
    {
        return sprintf('-//%s//NONSGML %s//EN', $company, $product);
    }

    /**
     * @param string $title
     * @description Set SUMMARY parameter based on title
     */
    protected function setSummary(string $title): void
    {
        $this->summary = Filter::filterStripTags($title);
    }

    /**
     * @return string
     */
    private function getOrganizerValue(): string
    {
        return sprintf('CN=%s;EMAIL=%s:mailto:%s;', $this->user->getFullName(), $this->user['email'], $this->user['email']);
    }

    /**
     * @description Dynamic ICS body of file
     * @throws Exception
     * @todo - Refactor: objective implementation
     */
    private function setData(): void
    {
        $veventArray = [
            'TZNAME:' . $this->getTZname(),
            'TZOFFSETTO:' . $this->getTZoffset(),
            'DTSTART:' . $this->eventStartDate,
            'DTEND:' . $this->eventEndDate,
            'ORGANIZER;' . $this->getOrganizerValue(), // the semicolon is intentional
            'CREATED:' . date('Ymd\THis'),
            'UID:' . md5(date('YmdHis')),
            'SEQUENCE:' . '0',
            'TRANSP:' . 'OPAQUE',
        ];

        if ($this->summary && !empty($this->summary)) {
            $veventArray[] = 'SUMMARY:' . $this->summary;
        }

        if (isset($this->url) && !empty($this->url)) {
            $veventArray[] = 'URL:' . $this->url;
        }

        if (isset($this->location) && !empty($this->location)) {
            $veventArray[] = 'LOCATION:' . $this->location;
        }

        if (isset($this->description) && !empty($this->description)) {
            $veventArray[] = 'DESCRIPTION:' . $this->description;
        }

        if (!empty($this->getAttendees())) {
            $veventArray[] = $this->getAttendees();
        }

        $vcalendarSource = [
            'BEGIN:VCALENDAR',
            'VERSION:2.0',
            'CALSCALE:GREGORIAN',
            'METHOD:REQUEST',
            'TZID:' . $this->user->getTimezone(),
            'PRODID:' . $this->getProdIdValue(),
            'BEGIN:VEVENT',
            implode(PHP_EOL, $veventArray),
            'END:VEVENT',
            'END:VCALENDAR'
        ];

        $this->data = implode(PHP_EOL, $vcalendarSource);
    }

    /**
     * @description Return list of ATTENDEE params for recipients list
     * @return string
     */
    private function getAttendees(): string
    {
        if (!isset($this->recipients)) {
            return '';
        }

        $attendees = [];

        foreach ($this->recipients as $key => $recipient) {
            $attendees[] = sprintf(
                'ATTENDEE;CN="%s";X-NUM-GUESTS=%d:mailto:%s',
                $recipient['full_name'],
                $key,
                $recipient['email']
            );
        }

        return implode(PHP_EOL, $attendees);
    }

    /**
     * @return string
     * @description Returns the invitation body of the ics file
     */
    public function getInviteICS(): string
    {
        return $this->data;
    }
}
