<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-only
 */

namespace OCA\Mail\IMAP;

use Exception;
use Horde_Imap_Client_Password_Xoauth2;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Cache\HordeCacheFactory;
use OCA\Mail\Events\BeforeImapClientCreated;
use OCA\Mail\Exception\ServiceException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IMemcache;
use OCP\Security\ICrypto;
use function hash;
use function implode;
use function json_encode;

class IMAPClientFactory {
	/** @var array<string, int> */
	private array $loginCounts = [];

	/** @var ICrypto */
	private $crypto;

	/** @var IConfig */
	private $config;

	/** @var ICacheFactory */
	private $cacheFactory;

	/** @var IEventDispatcher */
	private $eventDispatcher;

	private ITimeFactory $timeFactory;
	private HordeCacheFactory $hordeCacheFactory;

	public function __construct(ICrypto $crypto,
		IConfig $config,
		ICacheFactory $cacheFactory,
		IEventDispatcher $eventDispatcher,
		ITimeFactory $timeFactory,
		HordeCacheFactory $hordeCacheFactory) {
		$this->crypto = $crypto;
		$this->config = $config;
		$this->cacheFactory = $cacheFactory;
		$this->eventDispatcher = $eventDispatcher;
		$this->timeFactory = $timeFactory;
		$this->hordeCacheFactory = $hordeCacheFactory;
	}

	/**
	 * Get the connection object for the given account
	 *
	 * Connections are not closed until destruction, so the caller site is
	 * responsible to log out as soon as possible to keep the number of open
	 * (and stale) connections at a minimum.
	 *
	 * @param Account $account
	 * @param bool $useCache
	 *
	 * @return Horde_Imap_Client_Socket
	 * @throws ServiceException
	 */
	public function getClient(Account $account, bool $useCache = true): Horde_Imap_Client_Socket {
		$this->eventDispatcher->dispatchTyped(
			new BeforeImapClientCreated($account)
		);
		$host = $account->getMailAccount()->getInboundHost();
		$user = $account->getMailAccount()->getInboundUser();
		$decryptedPassword = null;
		if ($account->getMailAccount()->getInboundPassword() !== null) {
			$decryptedPassword = $this->crypto->decrypt($account->getMailAccount()->getInboundPassword());
		}
		$port = $account->getMailAccount()->getInboundPort();
		$sslMode = $account->getMailAccount()->getInboundSslMode();
		if ($sslMode === 'none') {
			$sslMode = false;
		}

		$params = [
			'username' => $user,
			'password' => $decryptedPassword,
			'hostspec' => $host,
			'port' => $port,
			'secure' => $sslMode,
			'timeout' => (int)$this->config->getSystemValue('app.mail.imap.timeout', 5),
			'context' => [
				'ssl' => [
					'verify_peer' => $this->config->getSystemValueBool('app.mail.verify-tls-peer', true),
					'verify_peer_name' => $this->config->getSystemValueBool('app.mail.verify-tls-peer', true),
				],
			],
		];
		if ($account->getMailAccount()->getAuthMethod() === 'xoauth2') {
			try {
				$oauthAccessToken = $account->getMailAccount()->getOauthAccessToken();
				if ($oauthAccessToken === null) {
					throw new ServiceException('Missing access token for xoauth2 account');
				}
				$decryptedAccessToken = $this->crypto->decrypt($oauthAccessToken);
			} catch (Exception $e) {
				throw new ServiceException('Could not decrypt account access token: ' . $e->getMessage(), 0, $e);
			}

			$params['password'] = $decryptedAccessToken; // Not used, but Horde wants this
			$params['xoauth2_token'] = new Horde_Imap_Client_Password_Xoauth2(
				$account->getEmail(),
				$decryptedAccessToken,
			);
		}
		$paramHash = hash(
			'sha512',
			implode('-', [
				$this->config->getSystemValueString('secret'),
				$account->getId(),
				json_encode($params)
			]),
		);
		if ($useCache) {
			$params['cache'] = [
				'backend' => $this->hordeCacheFactory->newCache($account),
			];
		}
		if ($account->getMailAccount()->getDebug() || $this->config->getSystemValueBool('app.mail.debug')) {
			$fn = "mail-{$account->getUserId()}-{$account->getId()}-imap.log";
			$params['debug'] = $this->config->getSystemValue('datadirectory') . '/' . $fn;
		}

		$client = new HordeImapClient($params, $this);

		$rateLimitingCache = $this->cacheFactory->createDistributed('mail_imap_ratelimit');
		if ($rateLimitingCache instanceof IMemcache) {
			$client->enableRateLimiter($rateLimitingCache, $paramHash, $this->timeFactory);
		}

		return $client;
	}

	public function recordLogin(string $host): void {
		$this->loginCounts[$host] = ($this->loginCounts[$host] ?? 0) + 1;
	}

	/** @return array<string, int> */
	public function getLoginStats(): array {
		return $this->loginCounts;
	}
}
