<?php

namespace XF\Spam\Checker;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Request;
use XF\Entity\User;

class Akismet extends AbstractProvider implements ContentCheckerInterface
{
	protected $client;

	/**
	 * @var array Map of XenForo content_type to Akismet comment_type.
	 *            See https://akismet.com/development/api/#comment-check
	 */
	protected $commentTypes = [
		'thread'                => 'forum-post',
		'post'                  => 'reply',
		'profile_post'          => 'blog',
		'profile_post_comment'  => 'comment',
		'user'                  => 'signup',
		'conversation_message'  => 'message',
		'contact'               => 'contact-form',

		// * - These are (probably) not officially supported by Akismet

		'user_signature'        => 'user-signature',    // *

		'media'                 => 'multi-media',       // *
		'album'                 => 'album',             // *
		'media_comment'         => 'comment',
		'media_album_comment'   => 'comment',

		'resource_rating'       => 'review',            // *
		'resource_update'       => 'blog-post',
	];

	protected function getType()
	{
		return 'Akismet';
	}

	public function check(User $user, $message, array $extraParams = [])
	{
		$decision = 'allowed';

		try
		{
			$isSpam = $this->isSpam($user, $message, $extraParams);
			if ($isSpam)
			{
				$decision = 'moderated';

				$this->logDetail('akismet_matched');
			}

			$this->logParam('akismetIsSpam', $isSpam);
			$this->logParam('akismet', $this->getParams($user, $message, $extraParams));
		}
		catch (TransferException $e)
		{
			$this->app()->logException($e, false, 'Akismet HTTP error: ');
		}
		catch (\InvalidArgumentException $e)
		{
			$this->app()->logException($e, false, 'Akismet service error: ');
		}

		$this->logDecision($decision);
	}

	public function submitSpam($contentType, $contentIds)
	{
		foreach ($this->getContentSpamCheckParams($contentType, $contentIds) AS $contentId => $params)
		{
			if ($params)
			{
				$this->_submitSpam($params);
			}
		}
	}

	protected function _submitSpam($params)
	{
		if (!empty($params['akismetIsSpam']))
		{
			return; // Akismet already told us this is spam, so do not submit again.
		}

		if (!empty($params['akismet']))
		{
			try
			{
				$params = [
					'headers' => [
						'User-Agent' => $this->getUserAgent(),
					],
					'form_params' => $params['akismet'],
				];

				$response = $this->getHttpClient()->post('https://' . $this->getApiKey() . '.rest.akismet.com/1.1/submit-spam', $params);
				$response = trim($response->getBody()->getContents());

				if ($response == 'invalid')
				{
					throw new \InvalidArgumentException("Invalid API key");
				}
			}
			catch (TransferException $e)
			{
				$this->app()->logException($e, false, 'Akismet HTTP error: ');
			}
			catch (\InvalidArgumentException $e)
			{
				$this->app()->logException($e, false, 'Akismet service error: ');
			}
		}
	}

	public function submitHam($contentType, $contentIds)
	{
		foreach ($this->getContentSpamCheckParams($contentType, $contentIds) AS $contentId => $params)
		{
			if ($params)
			{
				$this->_submitHam($params);
			}
		}
	}

	protected function _submitHam($params)
	{
		if (empty($params['akismetIsSpam']))
		{
			return; // Akismet already told us this wasn't spam, so do not submit as ham.
		}

		if (!empty($params['akismet']))
		{
			try
			{
				$params = [
					'headers' => [
						'User-Agent' => $this->getUserAgent(),
					],
					'form_params' => $params['akismet'],
				];

				$response = $this->getHttpClient()->post('https://' . $this->getApiKey() . '.rest.akismet.com/1.1/submit-ham', $params);
				$response = trim($response->getBody()->getContents());

				if ($response == 'invalid')
				{
					throw new \InvalidArgumentException("Invalid API key");
				}
			}
			catch (TransferException $e)
			{
				$this->app()->logException($e, false, 'Akismet HTTP error: ');
			}
			catch (\InvalidArgumentException $e)
			{
				$this->app()->logException($e, false, 'Akismet service error: ');
			}
		}
	}

	protected function getParams(User $user, $message, $extraParams)
	{
		$options = $this->app()->options();
		$request = $this->app()->request();

		$params = [
			'blog' => $options->boardUrl,
			'user_ip' => $request->getIp(),
			'user_agent' => $request->getUserAgent(),
			'referrer' => $request->getReferrer() ?: '',
			'comment_type' => 'comment',
			'comment_author' => $user->username,
			'comment_author_email' => $user->email,
			'comment_author_url' => $user->Profile ? $user->Profile->website : '',
			'comment_content' => $message,
		];
		if (isset($extraParams['content_type']))
		{
			$params['comment_type'] = $this->getCommentType($extraParams['content_type']);
		}
		if (isset($extraParams['permalink']))
		{
			$params['permalink'] = $extraParams['permalink'];
		}

		return $params;
	}

	protected function isSpam(User $user, $message, $extraParams)
	{
		$params = [
			'headers' => [
				'User-Agent' => $this->getUserAgent(),
			],
			'form_params' => $this->getParams($user, $message, $extraParams),
		];

		$response = $this->getHttpClient()->post('https://' . $this->getApiKey() . '.rest.akismet.com/1.1/comment-check', $params);
		$response = trim($response->getBody()->getContents());

		if ($response == 'invalid')
		{
			throw new \InvalidArgumentException("Invalid API key");
		}

		return ($response == 'true');
	}

	/**
	 * @return Client
	 */
	protected function getHttpClient()
	{
		if (!$this->client)
		{
			$this->client = $this->app->http()->client();
		}

		return $this->client;
	}

	/**
	 * @return Request
	 */
	protected function getHttpRequest($url = null, $method = 'POST')
	{
		$client = $this->getHttpClient();
		$request = new Request($method, $url, [
			'headers' => [
				'User-Agent' => $this->getUserAgent(),
			],
		]);

		return $request;
	}

	protected function getUserAgent()
	{
		$version = \XF::$version;
		return 'XenForo/' . $version . ' | Akismet/' . $version;
	}

	protected function getCommentType($contentType)
	{
		return $this->commentTypes[$contentType] ?? 'comment';
	}

	protected function getApiKey()
	{
		return trim($this->app()->options()->akismetKey);
	}
}
