<?php

namespace XF\Repository;

use XF\Entity\Ip;
use XF\Entity\User;
use XF\Finder\UserFinder;
use XF\Mvc\Entity\Repository;

use function count, intval, strlen;

class IpRepository extends Repository
{
	/**
	 * Logs the specified IP. This will return null if the IP couldn't be logged for any reason.
	 *
	 * @param integer $userId
	 * @param string $ip String IP representation; binary version also supported
	 * @param string $contentType
	 * @param int $contentId
	 * @param string $action
	 *
	 * @return null|Ip If the insert fails (generally for an invalid IP address), null.
	 */
	public function logIp($userId, $ip, $contentType, $contentId, $action = '')
	{
		$entity = $this->em->create(Ip::class);
		$entity->user_id = $userId;
		$entity->ip = $ip;
		$entity->content_type = $contentType;
		$entity->content_id = $contentId;
		$entity->action = $action;

		if ($entity->save(false))
		{
			return $entity;
		}
		else
		{
			return null;
		}
	}

	public function logCookieLoginIfNeeded($userId, $ip)
	{
		$userId = intval($userId);
		if (!$userId)
		{
			return null;
		}

		$binaryIp = \XF\Util\Ip::stringToBinary($ip, false);
		if ($binaryIp === false)
		{
			return null;
		}

		// only log this if we haven't seen this IP in the last 6 hours
		$cutOff = \XF::$time - 6 * 3600;
		$recentLog = $this->db()->fetchOne("
			SELECT 1
			FROM xf_ip
			WHERE user_id = ?
				AND log_date > ?
				AND ip = ?
			LIMIT 1
		", [$userId, $cutOff, $binaryIp]);
		if ($recentLog)
		{
			return null;
		}

		return $this->logIp($userId, $ip, 'user', $userId, 'cookie_login');
	}

	public function pruneIps($cutOff = null)
	{
		if ($cutOff === null)
		{
			if (!$this->options()->ipLogCleanUp['enabled'])
			{
				return 0;
			}

			$cutOff = \XF::$time - 86400 * $this->options()->ipLogCleanUp['delay'];
		}

		$db = $this->db();
		$totalDeleted = 0;

		do
		{
			$deleted = $db->delete('xf_ip', 'log_date < ?', $cutOff, '', '', 1000);
			$totalDeleted += $deleted;
		}
		while ($deleted >= 1000);

		return $totalDeleted;
	}

	public function getLoggedIp($contentType, $contentId, $action, $userId = null)
	{
		if ($contentType === 'user')
		{
			$userId = $contentId;
		}

		$params = [$contentType, $contentId, $action];
		if ($userId !== null)
		{
			$params[] = $userId;
			$indexHint = 'user_id_log_date';
		}
		else
		{
			$indexHint = 'content_type_content_id';
		}

		return $this->db()->fetchOne("
			SELECT ip
			FROM xf_ip FORCE INDEX(" . $indexHint . ")
			WHERE content_type = ?
				AND content_id = ?
				AND action = ?
				" . ($userId !== null ? 'AND user_id = ?' : '') . "
			ORDER BY log_date DESC
			LIMIT 1
		", $params);
	}

	public function getSharedIpUsers($userId, $logDays)
	{
		$db = $this->db();

		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		$cutOff = \XF::$time - $logDays * 86400;

		// written this way due to mysql's ridiculous sub-query performance
		$recentIps = $db->fetchAllColumn("
			SELECT DISTINCT ip
			FROM xf_ip
			WHERE user_id = ?
				AND log_date > ?
			ORDER BY log_date DESC
			LIMIT 500
		", [$userId, $cutOff]);
		if (!$recentIps)
		{
			return [];
		}

		$ipLogs = $db->fetchAll('
			SELECT user_id,
				ip,
				MIN(log_date) AS first_date,
				MAX(log_date) AS last_date,
				COUNT(*) AS total
			FROM xf_ip
			WHERE ip IN (' . $db->quote($recentIps) . ')
				AND user_id <> ?
				AND user_id > 0
				AND log_date > ?
			GROUP BY user_id, ip
			ORDER BY log_date DESC, ip, user_id
			LIMIT 1000
		', [$userId, $cutOff]);

		$userIpLogs = [];
		foreach ($ipLogs AS $ipLog)
		{
			$userIpLogs[$ipLog['user_id']][$ipLog['ip']] = [
				'ip' => $ipLog['ip'],
				'first_date' => $ipLog['first_date'],
				'last_date' => $ipLog['last_date'],
				'total' => $ipLog['total'],
			];
		}

		if (!$userIpLogs)
		{
			return [];
		}

		$users = $this->em->findByIds(User::class, array_keys($userIpLogs));
		$output = [];

		foreach ($ipLogs AS $ipLog)
		{
			$user = $users[$ipLog['user_id']] ?? null;
			if (!$user)
			{
				continue;
			}

			$output[$ipLog['user_id']] = [
				'user_id' => $ipLog['user_id'],
				'user' => $user,
				'ips' => $userIpLogs[$ipLog['user_id']],
			];
		}

		return $output;
	}

	public function getUsersByIpRange($lowerBound, $upperBound)
	{
		$ips = $this->db()->fetchAllKeyed("
			SELECT user_id,
				GROUP_CONCAT(DISTINCT ip ORDER BY ip SEPARATOR '  ') AS ips,
				MIN(log_date) AS first_date,
				MAX(log_date) AS last_date,
				COUNT(*) AS total
			FROM xf_ip
			WHERE ip >= ? AND ip <= ? AND LENGTH(ip) = ?
			GROUP BY user_id
		", 'user_id', [$lowerBound, $upperBound, strlen($lowerBound)]);
		if (!$ips)
		{
			return [];
		}

		$userIds = array_column($ips, 'user_id');
		$userIds = array_unique($userIds);

		$userFinder = $this->finder(UserFinder::class)
			->where('user_id', $userIds)
			->order('username');

		$users = $userFinder->fetch();
		$output = [];
		foreach ($users AS $user)
		{
			$ipInfo = $ips[$user->user_id];

			$matchIps = explode('  ', $ipInfo['ips']);

			$output[$user->user_id] = [
				'user_id' => $ipInfo['user_id'],
				'ips' => $matchIps,
				'ip_total' => count($matchIps),
				'first_date' => $ipInfo['first_date'],
				'last_date' => $ipInfo['last_date'],
				'total' => $ipInfo['total'],
				'user' => $user,
			];
		}

		return $output;
	}

	public function getUsersByIp($baseIp)
	{
		$ip = \XF\Util\Ip::stringToBinary($baseIp, false);
		if ($ip === false)
		{
			$baseIp = preg_replace('/[^\x20-\x7F]/', '?', $baseIp);
			throw new \InvalidArgumentException("Cannot convert IP '$baseIp' to binary");
		}

		$ips = $this->db()->fetchAllKeyed("
			SELECT user_id,
				ip,
				MIN(log_date) AS first_date,
				MAX(log_date) AS last_date,
				COUNT(*) AS total
			FROM xf_ip
			WHERE ip = ?
			GROUP BY user_id
		", 'user_id', $ip);
		if (!$ips)
		{
			return [];
		}

		$userIds = array_column($ips, 'user_id');
		$userIds = array_unique($userIds);

		$userFinder = $this->finder(UserFinder::class)
			->where('user_id', $userIds)
			->order('username');

		$users = $userFinder->fetch();
		$output = [];
		foreach ($users AS $user)
		{
			$ipInfo = $ips[$user->user_id];

			$output[$user->user_id] = [
				'user_id' => $ipInfo['user_id'],
				'ips' => [$ipInfo['ip']],
				'ip_total' => 1,
				'first_date' => $ipInfo['first_date'],
				'last_date' => $ipInfo['last_date'],
				'total' => $ipInfo['total'],
				'user' => $user,
			];
		}

		return $output;
	}

	public function getIpsByUser($userId, $limit = 150, $offset = 0)
	{
		if ($userId instanceof User)
		{
			$userId = $userId->user_id;
		}

		return $this->db()->fetchAllKeyed($this->db()->limit("
			SELECT ip,
				MIN(log_date) AS first_date,
				MAX(log_date) AS last_date,
				COUNT(*) AS total
			FROM xf_ip
			WHERE user_id = ?
			GROUP BY ip
			ORDER BY last_date DESC
		", $limit, $offset), 'ip', $userId);
	}

}
