<?php

namespace XF\EmailBounce;

use Laminas\Mail\Exception\ExceptionInterface;
use Laminas\Mail\Storage\AbstractStorage;
use XF\App;
use XF\Entity\EmailBounceLog;
use XF\Entity\User;
use XF\Mail\Storage\Imap;
use XF\Mail\Storage\Pop3;
use XF\Repository\EmailBounceRepository;
use XF\Repository\OptionRepository;

class Processor
{
	/**
	 * @var App
	 */
	protected $app;

	/**
	 * @var Parser
	 */
	protected $bounceParser;

	/**
	 * @var EmailBounceRepository
	 */
	protected $bounceRepo;

	public function __construct(App $app, Parser $parser)
	{
		$this->app = $app;
		$this->bounceParser = $parser;
		$this->bounceRepo = $app->repository(EmailBounceRepository::class);
	}

	public function processFromStorage(AbstractStorage $storage, $maxRunTime = 0)
	{
		$s = microtime(true);

		$total = $storage->countMessages();
		if (!$total)
		{
			return true;
		}

		$finished = true;

		for ($messageId = $total; $messageId > 0; $messageId--)
		{
			$contentValid = false;

			try
			{
				$headers = $storage->getRawHeader($messageId);
				$content = $storage->getRawContent($messageId);
				$contentValid = true;
			}
			catch (\InvalidArgumentException $e)
			{
				$headers = '';
				$content = '';
				$contentValid = false;
			}
			finally
			{
				$storage->removeMessage($messageId);
			}

			$rawMessage = trim($headers) . "\r\n\r\n" . trim($content);

			if ($contentValid)
			{
				$this->processMessage($rawMessage);
			}
			else
			{
				$result = new ParseResult();
				$result->date = \XF::$time;

				$this->logBounceMessage($rawMessage, null, $result);
			}

			if ($maxRunTime && microtime(true) - $s > $maxRunTime)
			{
				$finished = false;
				break;
			}
		}

		return $finished;
	}

	public function processMessage($rawMessage, &$result = null)
	{
		try
		{
			$result = $this->bounceParser->parseMessage($rawMessage);
		}
		catch (\Exception $e)
		{
			\XF::logException($e, false, "Bounce message processing failed: ");

			$result = new ParseResult();
			$result->date = \XF::$time;

			return $this->logBounceMessage($rawMessage, null, $result);
		}

		if ($result->recipient)
		{
			/** @var User $user */
			$user = $this->app->em()->findOne(User::class, ['email' => $result->recipient]);
		}
		else
		{
			$user = null;
		}

		$action = null;

		if ($user && $result->messageType == ParseResult::TYPE_BOUNCE)
		{
			if ($result->recipientTrusted)
			{
				$bounceType = $this->bounceParser->getBounceTypeFromStatus($result->remoteStatus);
				if ($bounceType)
				{
					$action = $this->takeBounceAction($user, $bounceType, $result->date);
				}
			}
			else
			{
				$action = 'untrusted';
			}
		}

		return $this->logBounceMessage($rawMessage, $action, $result, $user);
	}

	public function takeBounceAction(User $user, $bounceType, $bounceDate)
	{
		if ($bounceType == 'hard')
		{
			$this->triggerUserBounceAction($user);
			return 'hard';
		}
		else if ($bounceType == 'soft')
		{
			$this->bounceRepo->insertSoftBounceLogEntry($user->user_id, $bounceDate);

			if ($this->hasSoftBouncedTooMuch($user->user_id))
			{
				$this->triggerUserBounceAction($user);
				return 'soft_hard';
			}

			return 'soft';
		}
		else
		{
			throw new \InvalidArgumentException("Unknown bounce type '$bounceType'");
		}
	}

	public function triggerUserBounceAction(User $user)
	{
		if ($this->canApplyUserBounceAction($user))
		{
			$this->applyUserBounceAction($user);

			return true;
		}

		return false;
	}

	public function applyUserBounceAction(User $user)
	{
		$user->user_state = 'email_bounce';
		$user->save();
	}

	protected function canApplyUserBounceAction(User $user)
	{
		return (
			$user->user_state == 'valid'
			&& !$user->is_moderator
			&& !$user->is_admin
			&& !$user->is_staff
		);
	}

	protected function hasSoftBouncedTooMuch($userId)
	{
		$bounces = $this->bounceRepo->countRecentSoftBounces($userId, 30);
		if (!$bounces['bounce_total'])
		{
			return false;
		}

		$thresholds = $this->app->options()->emailSoftBounceThreshold;
		return (
			$bounces['bounce_total'] >= $thresholds['bounce_total']
			&& $bounces['unique_days'] >= $thresholds['unique_days']
			&& $bounces['days_between'] >= $thresholds['days_between']
		);
	}

	protected function logBounceMessage($rawMessage, $action, ParseResult $result, ?User $user = null)
	{
		$bounce = $this->app->em()->create(EmailBounceLog::class);
		$bounce->bulkSet([
			'email_date' => $result->date,
			'message_type' => $result->messageType ?: ParseResult::TYPE_UNKNOWN,
			'action_taken' => $action ?: '',
			'user_id' => $user ? $user->user_id : null,
			'recipient' => $result->recipient,
			'raw_message' => substr($rawMessage, 0, 1024 * 1024), // limit logging to 1MB to prevent potential insert issues
			'status_code' => $result->remoteStatus,
			'diagnostic_info' => $result->remoteDiagnostics,
		]);
		$bounce->save();

		return $bounce;
	}

	/**
	 * @param App $app
	 *
	 * @return null|AbstractStorage
	 */
	public static function getDefaultBounceHandlerStorage(App $app)
	{
		$options = $app->options();

		if (!$options->bounceEmailAddress)
		{
			return null;
		}

		$handler = $options->emailBounceHandler;
		if (!$handler || empty($handler['enabled']))
		{
			return null;
		}

		$optionRepo = $app->repository(OptionRepository::class);
		$handler = $optionRepo->refreshEmailAccessTokenIfNeeded('emailBounceHandler');

		try
		{
			if ($handler['type'] == 'pop3')
			{
				$connection = Pop3::setupFromHandler($handler);
			}
			else if ($handler['type'] == 'imap')
			{
				$connection = Imap::setupFromHandler($handler);
			}
			else
			{
				throw new \Exception("Unknown email bounce handler $handler[type]");
			}
		}
		catch (ExceptionInterface $e)
		{
			$app->logException($e, false, "Bounce connection error: ");
			return null;
		}

		return $connection;
	}
}
