<?php

declare(strict_types=1);

namespace App\Module\ProjectManagement\Infrastructure\Service;

use App\Module\Integration\Domain\Repository\ZimbraConfigurationRepositoryInterface;
use App\Module\ProjectManagement\Domain\Entity\Task;
use App\Module\ProjectManagement\Domain\Entity\TaskRecurrence;
use App\Module\ProjectManagement\Domain\Enum\RecurrenceType;
use App\Shared\Infrastructure\Service\TokenEncryptor;
use DateTimeZone;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCalendar;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

use const ENT_HTML5;
use const ENT_QUOTES;

final class CalDavService
{
    public function __construct(
        private readonly ZimbraConfigurationRepositoryInterface $configRepository,
        private readonly TokenEncryptor $tokenEncryptor,
        private readonly HttpClientInterface $httpClient,
        private readonly LoggerInterface $logger,
    ) {}

    public function isConfigured(): bool
    {
        $config = $this->configRepository->findSingleton();

        return null !== $config && $config->isEnabled();
    }

    public function testConnection(): bool
    {
        $config = $this->configRepository->findSingleton();

        if (null === $config || !$config->isEnabled()) {
            return false;
        }

        try {
            $response = $this->httpClient->request('PROPFIND', $this->getCalendarUrl(), [
                'timeout'    => 5,
                'auth_basic' => [
                    $config->getUsername(),
                    $this->tokenEncryptor->decrypt((string) $config->getEncryptedPassword()),
                ],
                'headers' => [
                    'Depth' => '0',
                ],
            ]);

            $statusCode = $response->getStatusCode();

            return $statusCode >= 200 && $statusCode < 300 || 207 === $statusCode;
        } catch (Throwable $e) {
            $this->logger->error('CalDAV connection test failed: '.$e->getMessage());

            return false;
        }
    }

    public function createEvent(Task $task): ?string
    {
        $uid      = $this->generateUid();
        $calendar = $this->buildEventCalendar($task, $uid);

        if (!$this->makeRequest('PUT', $this->getCalendarUrl().$uid.'.ics', $calendar->serialize())) {
            return null;
        }

        return $uid;
    }

    public function createTodo(Task $task): ?string
    {
        $uid      = $this->generateUid();
        $calendar = $this->buildTodoCalendar($task, $uid);

        if (!$this->makeRequest('PUT', $this->getCalendarUrl().$uid.'.ics', $calendar->serialize())) {
            return null;
        }

        return $uid;
    }

    public function updateEvent(Task $task): bool
    {
        $uid = $task->getCalendarEventUid();

        if (null === $uid) {
            return false;
        }

        $calendar = $this->buildEventCalendar($task, $uid);

        return $this->makeRequest('PUT', $this->getCalendarUrl().$uid.'.ics', $calendar->serialize());
    }

    public function updateTodo(Task $task): bool
    {
        $uid = $task->getCalendarTodoUid();

        if (null === $uid) {
            return false;
        }

        $calendar = $this->buildTodoCalendar($task, $uid);

        return $this->makeRequest('PUT', $this->getCalendarUrl().$uid.'.ics', $calendar->serialize());
    }

    public function deleteEvent(?string $uid): bool
    {
        if (null === $uid) {
            return true;
        }

        return $this->makeRequest('DELETE', $this->getCalendarUrl().$uid.'.ics');
    }

    public function deleteTodo(?string $uid): bool
    {
        if (null === $uid) {
            return true;
        }

        return $this->makeRequest('DELETE', $this->getCalendarUrl().$uid.'.ics');
    }

    public function syncTask(Task $task): void
    {
        // No CalDAV server configured/enabled: skip all remote interaction so a
        // task save never triggers a doomed HTTP request nor stores a sync error.
        if (!$this->isConfigured()) {
            return;
        }

        if (!$task->isSyncToCalendar()) {
            $this->deleteEvent($task->getCalendarEventUid());
            $this->deleteTodo($task->getCalendarTodoUid());
            $task->setCalendarEventUid(null);
            $task->setCalendarTodoUid(null);
            $task->setCalendarSyncError(null);

            return;
        }

        $hasStart    = null !== $task->getScheduledStart();
        $hasDeadline = null !== $task->getDeadline();

        if (!$hasStart && !$hasDeadline) {
            return;
        }

        $syncError = null;

        if ($hasStart) {
            if (null !== $task->getCalendarEventUid()) {
                $success = $this->updateEvent($task);
            } else {
                $uid = $this->createEvent($task);
                if (null !== $uid) {
                    $task->setCalendarEventUid($uid);
                    $success = true;
                } else {
                    $success = false;
                }
            }

            if (!$success) {
                $syncError = 'Failed to sync event to calendar.';
            }
        } elseif (null !== $task->getCalendarEventUid()) {
            $this->deleteEvent($task->getCalendarEventUid());
            $task->setCalendarEventUid(null);
        }

        if ($hasDeadline) {
            if (null !== $task->getCalendarTodoUid()) {
                $success = $this->updateTodo($task);
            } else {
                $uid = $this->createTodo($task);
                if (null !== $uid) {
                    $task->setCalendarTodoUid($uid);
                    $success = true;
                } else {
                    $success = false;
                }
            }

            if (!$success) {
                $syncError = ($syncError ?? '').'Failed to sync todo to calendar.';
            }
        } elseif (null !== $task->getCalendarTodoUid()) {
            $this->deleteTodo($task->getCalendarTodoUid());
            $task->setCalendarTodoUid(null);
        }

        $task->setCalendarSyncError($syncError);
    }

    private function buildEventCalendar(Task $task, string $uid): VCalendar
    {
        $project     = $task->getProject();
        $projectCode = null !== $project ? $project->getCode() : '';
        $summary     = sprintf('[%s-%s] %s', $projectCode, $task->getNumber(), $task->getTitle());
        $description = $this->descriptionToPlainText($task->getDescription())."\n\nLesstime task";

        $vcalendar = new VCalendar();
        $vcalendar->add('VEVENT', [
            'UID'         => $uid,
            'SUMMARY'     => $summary,
            'DTSTART'     => $task->getScheduledStart(),
            'DTEND'       => $task->getScheduledEnd(),
            'DESCRIPTION' => $description,
        ]);

        $recurrence = $task->getRecurrence();

        if (null !== $recurrence) {
            $vevent = $vcalendar->VEVENT;
            $vevent->add('RRULE', $this->buildRRule($recurrence));
        }

        return $vcalendar;
    }

    private function buildTodoCalendar(Task $task, string $uid): VCalendar
    {
        $project     = $task->getProject();
        $projectCode = null !== $project ? $project->getCode() : '';
        $summary     = sprintf('[%s-%s] %s (deadline)', $projectCode, $task->getNumber(), $task->getTitle());
        $description = $this->descriptionToPlainText($task->getDescription())."\n\nLesstime task";

        $vcalendar = new VCalendar();
        $vcalendar->add('VTODO', [
            'UID'         => $uid,
            'SUMMARY'     => $summary,
            'DUE'         => $task->getDeadline(),
            'DESCRIPTION' => $description,
        ]);

        return $vcalendar;
    }

    private function buildRRule(TaskRecurrence $recurrence): string
    {
        $parts    = [];
        $interval = $recurrence->getInterval();

        match ($recurrence->getType()) {
            RecurrenceType::Daily  => $parts[] = 'FREQ=DAILY;INTERVAL='.$interval,
            RecurrenceType::Weekly => (function () use (&$parts, $interval, $recurrence): void {
                $dayMap     = $this->getDayMap();
                $daysOfWeek = $recurrence->getDaysOfWeek() ?? [];
                $byDay      = implode(',', array_map(fn (string $d) => $dayMap[$d] ?? $d, $daysOfWeek));
                $rule       = 'FREQ=WEEKLY;INTERVAL='.$interval;
                if ('' !== $byDay) {
                    $rule .= ';BYDAY='.$byDay;
                }
                $parts[] = $rule;
            })(),
            RecurrenceType::Monthly => (function () use (&$parts, $interval, $recurrence): void {
                $dayOfMonth  = $recurrence->getDayOfMonth();
                $weekOfMonth = $recurrence->getWeekOfMonth();
                $daysOfWeek  = $recurrence->getDaysOfWeek() ?? [];

                if (null !== $dayOfMonth) {
                    $parts[] = 'FREQ=MONTHLY;INTERVAL='.$interval.';BYMONTHDAY='.$dayOfMonth;
                } elseif (null !== $weekOfMonth && [] !== $daysOfWeek) {
                    $dayMap  = $this->getDayMap();
                    $day     = $dayMap[$daysOfWeek[0]] ?? $daysOfWeek[0];
                    $parts[] = 'FREQ=MONTHLY;INTERVAL='.$interval.';BYDAY='.$weekOfMonth.$day;
                } else {
                    $parts[] = 'FREQ=MONTHLY;INTERVAL='.$interval;
                }
            })(),
            RecurrenceType::Yearly => $parts[] = 'FREQ=YEARLY;INTERVAL='.$interval,
            default                => $parts[] = 'FREQ=DAILY;INTERVAL='.$interval,
        };

        $rule = $parts[0] ?? 'FREQ=DAILY;INTERVAL=1';

        $endDate        = $recurrence->getEndDate();
        $maxOccurrences = $recurrence->getMaxOccurrences();

        if (null !== $endDate) {
            $rule .= ';UNTIL='.$endDate->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z');
        } elseif (null !== $maxOccurrences) {
            $rule .= ';COUNT='.$maxOccurrences;
        }

        return $rule;
    }

    private function getCalendarUrl(): string
    {
        $config = $this->configRepository->findSingleton();

        if (null === $config) {
            return '';
        }

        return rtrim((string) $config->getServerUrl(), '/').'/'.ltrim((string) $config->getCalendarPath(), '/').'/';
    }

    private function makeRequest(string $method, string $url, ?string $body = null, string $contentType = 'text/calendar'): bool
    {
        $config = $this->configRepository->findSingleton();

        if (null === $config) {
            return false;
        }

        try {
            $options = [
                'timeout'    => 5,
                'auth_basic' => [
                    $config->getUsername(),
                    $this->tokenEncryptor->decrypt((string) $config->getEncryptedPassword()),
                ],
            ];

            if (null !== $body) {
                $options['headers'] = ['Content-Type' => $contentType];
                $options['body']    = $body;
            }

            $response   = $this->httpClient->request($method, $url, $options);
            $statusCode = $response->getStatusCode();

            return $statusCode >= 200 && $statusCode < 300 || 207 === $statusCode;
        } catch (Throwable $e) {
            $this->logger->error(sprintf('CalDAV %s request to %s failed: %s', $method, $url, $e->getMessage()));

            return false;
        }
    }

    private function generateUid(): string
    {
        return sprintf('%s@lesstime', bin2hex(random_bytes(16)));
    }

    private function descriptionToPlainText(?string $value): string
    {
        if (null === $value || '' === $value) {
            return '';
        }

        $stripped = strip_tags($value);
        $decoded  = html_entity_decode($stripped, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        return trim((string) preg_replace('/[ \t]+/', ' ', $decoded));
    }

    /** @return array<string, string> */
    private function getDayMap(): array
    {
        return [
            'monday'    => 'MO',
            'tuesday'   => 'TU',
            'wednesday' => 'WE',
            'thursday'  => 'TH',
            'friday'    => 'FR',
            'saturday'  => 'SA',
            'sunday'    => 'SU',
        ];
    }
}
