<?php

declare(strict_types=1);

namespace App\Module\Mail\Application\Service;

use App\Module\Mail\Application\Dto\MailSyncReport;
use App\Module\Mail\Domain\Entity\MailFolder;
use App\Module\Mail\Domain\Entity\MailMessage;
use App\Module\Mail\Domain\Exception\MailProviderException;
use App\Module\Mail\Domain\Provider\MailProviderInterface;
use App\Module\Mail\Domain\Repository\MailConfigurationRepositoryInterface;
use App\Module\Mail\Domain\Repository\MailFolderRepositoryInterface;
use App\Module\Mail\Domain\Repository\MailMessageRepositoryInterface;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\LockFactory;
use Throwable;

final class MailSyncService
{
    private const int FLAGS_RESYNC_LIMIT = 200;
    private const string LOCK_NAME       = 'mail.sync';
    private const float LOCK_TTL         = 600.0;

    public function __construct(
        private readonly MailProviderInterface $provider,
        private readonly MailConfigurationRepositoryInterface $configRepository,
        private readonly MailFolderRepositoryInterface $folderRepository,
        private readonly MailMessageRepositoryInterface $messageRepository,
        private readonly EntityManagerInterface $entityManager,
        private readonly LockFactory $lockFactory,
        private readonly LoggerInterface $logger,
        private readonly ManagerRegistry $managerRegistry,
    ) {}

    public function syncAll(): MailSyncReport
    {
        $startedAt = new DateTimeImmutable();

        $config = $this->configRepository->findSingleton();

        if (null === $config || !$config->isEnabled()) {
            $this->logger->info('mail.sync skipped: mail config is disabled or missing');

            return $this->emptyReport($startedAt, []);
        }

        $lock = $this->lockFactory->createLock(self::LOCK_NAME, ttl: self::LOCK_TTL, autoRelease: true);

        if (!$lock->acquire()) {
            $this->logger->info('mail.sync skipped: another sync in progress');

            return $this->emptyReport($startedAt, ['lock_not_acquired']);
        }

        try {
            return $this->doSyncAll($startedAt);
        } finally {
            $this->provider->closeConnection();
            $lock->release();
        }
    }

    public function syncFolderStructure(): void
    {
        try {
            $remoteFolders = $this->provider->listFolders();
        } catch (MailProviderException $e) {
            $this->logger->error('syncFolderStructure: listFolders failed: '.$e->getMessage());

            return;
        }

        $remotePathSet = [];

        foreach ($remoteFolders as $dto) {
            $remotePathSet[$dto->path] = true;
            $folder                    = $this->folderRepository->findByPath($dto->path);

            if (null === $folder) {
                $folder = new MailFolder();
                $folder->setPath($dto->path);
            }

            $folder->setDisplayName($dto->displayName);
            $folder->setParentPath($dto->parentPath);
            $folder->setUnreadCount($dto->unreadCount);
            $folder->setTotalCount($dto->totalCount);

            $this->entityManager->persist($folder);
        }

        $this->entityManager->flush();

        $allDbFolders = $this->folderRepository->findAllOrderedByPath();

        foreach ($allDbFolders as $dbFolder) {
            if (!isset($remotePathSet[$dbFolder->getPath()])) {
                $this->logger->warning(sprintf(
                    'syncFolderStructure: folder "%s" no longer exists on server — keeping in DB for safety',
                    $dbFolder->getPath()
                ));
            }
        }
    }

    public function syncFolder(MailFolder $folder): MailSyncReport
    {
        $startedAt     = new DateTimeImmutable();
        $createdCount  = 0;
        $updatedCount  = 0;
        $deletedCount  = 0;
        $errors        = [];
        $remoteHeaders = null;

        try {
            $lastUid       = $this->messageRepository->findMaxUidInFolder($folder);
            $remoteHeaders = $this->provider->listMessages($folder->getPath(), limit: 5000, offset: 0);

            foreach ($remoteHeaders as $header) {
                if ($header->uid <= $lastUid) {
                    continue;
                }

                $existing = $this->messageRepository->findByFolderAndUid($folder, $header->uid);
                if (null !== $existing) {
                    continue;
                }

                $message = new MailMessage();
                $message->setFolder($folder);
                $message->setUid($header->uid);
                $message->setMessageId($header->messageId);
                $message->setSubject($header->subject);
                $message->setFromAddress($header->fromAddress);
                $message->setFromName($header->fromName);
                $message->setToAddresses($header->toAddresses);
                $message->setCcAddresses($header->ccAddresses);
                $message->setSentAt($header->sentAt);
                $message->setIsRead($header->isRead);
                $message->setIsFlagged($header->isFlagged);
                $message->setHasAttachments($header->hasAttachments);
                $message->setSnippet($header->snippet);
                $message->setSyncedAt(new DateTimeImmutable());

                $this->entityManager->persist($message);
                ++$createdCount;
            }

            $this->entityManager->flush();
        } catch (MailProviderException $e) {
            $this->logger->error(sprintf('syncFolder[%s] listMessages failed: %s', $folder->getPath(), $e->getMessage()));
            $errors[] = $e->getMessage();
        } catch (Throwable $e) {
            $this->logger->error(sprintf('syncFolder[%s] unexpected error: %s', $folder->getPath(), $e->getMessage()));
            $errors[] = $e->getMessage();
        }

        try {
            $recentMessages = $this->messageRepository->findLastNByFolder($folder, self::FLAGS_RESYNC_LIMIT);

            if (null !== $recentMessages && [] !== $recentMessages) {
                $remoteByUid = [];
                if (null !== $remoteHeaders) {
                    foreach ($remoteHeaders as $h) {
                        $remoteByUid[$h->uid] = $h;
                    }
                }

                foreach ($recentMessages as $dbMessage) {
                    $remote = $remoteByUid[$dbMessage->getUid()] ?? null;
                    if (null === $remote) {
                        continue;
                    }

                    $changed = false;

                    if ($dbMessage->isRead() !== $remote->isRead) {
                        $dbMessage->setIsRead($remote->isRead);
                        $changed = true;
                    }

                    if ($dbMessage->isFlagged() !== $remote->isFlagged) {
                        $dbMessage->setIsFlagged($remote->isFlagged);
                        $changed = true;
                    }

                    if ($changed) {
                        ++$updatedCount;
                    }
                }

                $this->entityManager->flush();
            }
        } catch (Throwable $e) {
            $this->logger->warning(sprintf('syncFolder[%s] flag resync failed: %s', $folder->getPath(), $e->getMessage()));
        }

        try {
            $dbUids = $this->messageRepository->findAllUidsByFolder($folder);

            if ([] !== $dbUids) {
                if (null === $remoteHeaders) {
                    $remoteHeaders = $this->provider->listMessages($folder->getPath(), limit: 5000, offset: 0);
                }

                $remoteUidSet = [];
                foreach ($remoteHeaders as $h) {
                    $remoteUidSet[$h->uid] = true;
                }

                $toDelete      = array_filter($dbUids, static fn (int $uid) => !isset($remoteUidSet[$uid]));
                $toDeleteCount = count($toDelete);
                $dbTotal       = count($dbUids);

                if ($toDeleteCount > (int) ($dbTotal * 0.5)) {
                    $warningMsg = sprintf(
                        'syncFolder[%s] suppression guard triggered: %d/%d would be deleted (>50%%) — aborting deletions',
                        $folder->getPath(),
                        $toDeleteCount,
                        $dbTotal
                    );
                    $this->logger->warning($warningMsg);
                    $errors[] = $warningMsg;
                } else {
                    foreach ($toDelete as $uid) {
                        $dbMessage = $this->messageRepository->findByFolderAndUid($folder, $uid);

                        if (null !== $dbMessage) {
                            $this->entityManager->remove($dbMessage);
                            ++$deletedCount;
                        }
                    }

                    $this->entityManager->flush();
                }
            }
        } catch (MailProviderException $e) {
            $this->logger->error(sprintf('syncFolder[%s] deletion detection failed: %s', $folder->getPath(), $e->getMessage()));
            $errors[] = $e->getMessage();
        }

        $finishedAt = new DateTimeImmutable();

        return new MailSyncReport(
            createdCount: $createdCount,
            updatedCount: $updatedCount,
            deletedCount: $deletedCount,
            foldersScanned: 1,
            errors: $errors,
            durationSeconds: (float) ($finishedAt->getTimestamp() - $startedAt->getTimestamp()),
            startedAt: $startedAt,
            finishedAt: $finishedAt,
        );
    }

    private function doSyncAll(DateTimeImmutable $startedAt): MailSyncReport
    {
        $this->syncFolderStructure();

        $totalCreated = 0;
        $totalUpdated = 0;
        $totalDeleted = 0;
        $totalFolders = 0;
        $allErrors    = [];

        $folders = $this->folderRepository->findAllOrderedByPath();

        foreach ($folders as $folder) {
            try {
                $report = $this->syncFolder($folder);
                $totalCreated += $report->createdCount;
                $totalUpdated += $report->updatedCount;
                $totalDeleted += $report->deletedCount;
                ++$totalFolders;
                $allErrors = array_merge($allErrors, $report->errors);
                // A folder error can leave the reused IMAP connection in a bad
                // selection state ("must be in SELECTED state", "empty response").
                // Drop it so the next folder reconnects on a clean session.
                if ([] !== $report->errors) {
                    $this->provider->closeConnection();
                }
            } catch (Throwable $e) {
                $this->logger->error(sprintf('doSyncAll: syncFolder[%s] threw: %s', $folder->getPath(), $e->getMessage()));
                $allErrors[] = $e->getMessage();
                $this->provider->closeConnection();
            }

            // A failed flush closes the Doctrine EntityManager; without a reset
            // every subsequent folder would fail with "EntityManager is closed".
            // Reset it via the registry and stop the run cleanly — the next cron
            // cycle resumes incrementally from where we left off.
            if (!$this->entityManager->isOpen()) {
                $this->logger->error('doSyncAll: EntityManager was closed mid-sync, resetting and aborting this run');
                $this->managerRegistry->resetManager();
                $allErrors[] = 'EntityManager closed mid-sync — run aborted, will resume next cycle';

                break;
            }
        }

        $finishedAt = new DateTimeImmutable();

        $this->logger->info(sprintf(
            'mail.sync done: %d created, %d updated, %d deleted, %d folders, %d errors, %.1fs',
            $totalCreated,
            $totalUpdated,
            $totalDeleted,
            $totalFolders,
            count($allErrors),
            (float) ($finishedAt->getTimestamp() - $startedAt->getTimestamp())
        ));

        return new MailSyncReport(
            createdCount: $totalCreated,
            updatedCount: $totalUpdated,
            deletedCount: $totalDeleted,
            foldersScanned: $totalFolders,
            errors: $allErrors,
            durationSeconds: (float) ($finishedAt->getTimestamp() - $startedAt->getTimestamp()),
            startedAt: $startedAt,
            finishedAt: $finishedAt,
        );
    }

    /**
     * @param list<string> $errors
     */
    private function emptyReport(DateTimeImmutable $startedAt, array $errors): MailSyncReport
    {
        $now = new DateTimeImmutable();

        return new MailSyncReport(
            createdCount: 0,
            updatedCount: 0,
            deletedCount: 0,
            foldersScanned: 0,
            errors: $errors,
            durationSeconds: 0.0,
            startedAt: $startedAt,
            finishedAt: $now,
        );
    }
}
