<?php

declare(strict_types=1);

namespace Ddeboer\Imap;

use Ddeboer\Imap\Exception\CreateMailboxException;
use Ddeboer\Imap\Exception\DeleteMailboxException;
use Ddeboer\Imap\Exception\ImapGetmailboxesException;
use Ddeboer\Imap\Exception\ImapNumMsgException;
use Ddeboer\Imap\Exception\ImapQuotaException;
use Ddeboer\Imap\Exception\MailboxDoesNotExistException;
use Ddeboer\Imap\Exception\SubscribeMailboxException;

/**
 * A connection to an IMAP server that is authenticated for a user.
 */
final class Connection implements ConnectionInterface
{
    private ImapResourceInterface $resource;
    private string $server;
    /**
     * @var null|MailboxInterface[]
     */
    private ?array $mailboxes = null;
    /**
     * @var null|array<int|string, \stdClass>
     */
    private ?array $mailboxNames = null;

    /**
     * Constructor.
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(ImapResourceInterface $resource, string $server)
    {
        $this->resource = $resource;
        $this->server   = $server;
    }

    public function getResource(): ImapResourceInterface
    {
        return $this->resource;
    }

    public function expunge(): bool
    {
        return \imap_expunge($this->resource->getStream());
    }

    public function close(int $flag = 0): bool
    {
        $stream = $this->resource->getStream();

        $this->resource->clearLastMailboxUsedCache();

        return \imap_close($stream, $flag);
    }

    public function getQuota(string $root = 'INBOX'): array
    {
        $errorMessage = null;
        $errorNumber  = 0;
        \set_error_handler(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool {
            $errorMessage = $message;
            $errorNumber  = $nr;

            return true;
        });

        $return = \imap_get_quotaroot($this->resource->getStream(), $root);

        \restore_error_handler();

        if (false === $return || null !== $errorMessage) {
            throw new ImapQuotaException(
                \sprintf(
                    'IMAP Quota request failed for "%s"%s',
                    $root,
                    null !== $errorMessage ? ': ' . $errorMessage : ''
                ),
                $errorNumber
            );
        }

        return $return;
    }

    public function getMailboxes(): array
    {
        $this->initMailboxNames();
        \assert(null !== $this->mailboxNames);

        if (null === $this->mailboxes) {
            $this->mailboxes = [];
            foreach ($this->mailboxNames as $mailboxName => $mailboxInfo) {
                $this->mailboxes[(string) $mailboxName] = $this->getMailbox((string) $mailboxName);
            }
        }

        return $this->mailboxes;
    }

    public function hasMailbox(string $name): bool
    {
        $this->initMailboxNames();
        \assert(null !== $this->mailboxNames);

        return isset($this->mailboxNames[$name]);
    }

    public function getMailbox(string $name): MailboxInterface
    {
        if (false === $this->hasMailbox($name)) {
            throw new MailboxDoesNotExistException(\sprintf('Mailbox name "%s" does not exist', $name));
        }
        \assert(isset($this->mailboxNames[$name]));

        return new Mailbox($this->resource, $name, $this->mailboxNames[$name]);
    }

    #[\ReturnTypeWillChange]
    public function count()
    {
        $return = \imap_num_msg($this->resource->getStream());

        if (false === $return) {
            throw new ImapNumMsgException('imap_num_msg failed');
        }

        return $return;
    }

    public function ping(): bool
    {
        return \imap_ping($this->resource->getStream());
    }

    public function createMailbox(string $name): MailboxInterface
    {
        if (false === \imap_createmailbox($this->resource->getStream(), $this->server . \mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'))) {
            throw new CreateMailboxException(\sprintf('Can not create "%s" mailbox at "%s"', $name, $this->server));
        }

        $this->mailboxNames = $this->mailboxes = null;
        $this->resource->clearLastMailboxUsedCache();

        return $this->getMailbox($name);
    }

    public function deleteMailbox(MailboxInterface $mailbox): void
    {
        if (false === \imap_deletemailbox($this->resource->getStream(), $mailbox->getFullEncodedName())) {
            throw new DeleteMailboxException(\sprintf('Mailbox "%s" could not be deleted', $mailbox->getName()));
        }

        $this->mailboxes = $this->mailboxNames = null;
        $this->resource->clearLastMailboxUsedCache();
    }

    private function initMailboxNames(): void
    {
        if (null !== $this->mailboxNames) {
            return;
        }

        $this->mailboxNames = [];
        $mailboxesInfo      = \imap_getmailboxes($this->resource->getStream(), $this->server, '*');
        if (!\is_array($mailboxesInfo)) {
            throw new ImapGetmailboxesException('imap_getmailboxes failed');
        }

        foreach ($mailboxesInfo as $mailboxInfo) {
            $name = \mb_convert_encoding(\str_replace($this->server, '', $mailboxInfo->name), 'UTF-8', 'UTF7-IMAP');
            \assert(\is_string($name));

            $this->mailboxNames[$name] = $mailboxInfo;
        }
    }

    public function subscribeMailbox(string $name): void
    {
        if (false === \imap_subscribe($this->resource->getStream(), $this->server . \mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'))) {
            throw new SubscribeMailboxException(\sprintf('Can not subscribe to "%s" mailbox at "%s"', $name, $this->server));
        }
    }
}
