<?php
/**
 * @copyright Copyright (C) 2010-2024, the Friendica project
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Friendica\Protocol\Diaspora\Repository;

use Friendica\BaseRepository;
use Friendica\Database\Database;
use Friendica\Database\Definition\DbaDefinition;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Diaspora\Entity;
use Friendica\Protocol\Diaspora\Factory;
use Friendica\Protocol\WebFingerUri;
use Friendica\Util\DateTimeFormat;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;

class DiasporaContact extends BaseRepository
{
	const ALWAYS_UPDATE                 = true;
	const NEVER_UPDATE                  = false;
	const UPDATE_IF_MISSING_OR_OUTDATED = null;

	protected static $table_name = 'diaspora-contact-view';

	/** @var Factory\DiasporaContact */
	protected $factory;
	/** @var DbaDefinition */
	private $definition;

	public function __construct(DbaDefinition $definition, Database $database, LoggerInterface $logger, Factory\DiasporaContact $factory)
	{
		parent::__construct($database, $logger, $factory);

		$this->definition = $definition;
	}

	/**
	 * @param array $condition
	 * @param array $params
	 * @return Entity\DiasporaContact
	 * @throws HTTPException\NotFoundException
	 */
	public function selectOne(array $condition, array $params = []): Entity\DiasporaContact
	{
		return parent::_selectOne($condition, $params);
	}

	/**
	 * @param int $uriId
	 * @return Entity\DiasporaContact
	 * @throws HTTPException\NotFoundException
	 */
	public function selectOneByUriId(int $uriId): Entity\DiasporaContact
	{
		return $this->selectOne(['uri-id' => $uriId]);
	}

	/**
	 * @param UriInterface $uri
	 * @return Entity\DiasporaContact
	 * @throws HTTPException\NotFoundException
	 */
	public function selectOneByUri(UriInterface $uri): Entity\DiasporaContact
	{
		try {
			return $this->selectOne(['url' => (string) $uri]);
		} catch (HTTPException\NotFoundException $e) {
		}

		try {
			return $this->selectOne(['addr' => (string) $uri]);
		} catch (HTTPException\NotFoundException $e) {
		}

		return $this->selectOne(['alias' => (string) $uri]);
	}

	/**
	 * @param WebFingerUri $uri
	 * @return Entity\DiasporaContact
	 * @throws HTTPException\NotFoundException
	 */
	public function selectOneByAddr(WebFingerUri $uri): Entity\DiasporaContact
	{
		return $this->selectOne(['addr' => $uri->getAddr()]);
	}

	/**
	 * @param int $uriId
	 * @return bool
	 * @throws \Exception
	 */
	public function existsByUriId(int $uriId): bool
	{
		return $this->db->exists(self::$table_name, ['uri-id' => $uriId]);
	}

	public function save(Entity\DiasporaContact $DiasporaContact): Entity\DiasporaContact
	{
		$uriId = $DiasporaContact->uriId ?? ItemURI::insert(['uri' => $DiasporaContact->url, 'guid' => $DiasporaContact->guid]);

		$fields = [
			'uri-id'            => $uriId,
			'addr'              => $DiasporaContact->addr,
			'alias'             => (string)$DiasporaContact->alias,
			'nick'              => $DiasporaContact->nick,
			'name'              => $DiasporaContact->name,
			'given-name'        => $DiasporaContact->givenName,
			'family-name'       => $DiasporaContact->familyName,
			'photo'             => (string)$DiasporaContact->photo,
			'photo-medium'      => (string)$DiasporaContact->photoMedium,
			'photo-small'       => (string)$DiasporaContact->photoSmall,
			'batch'             => (string)$DiasporaContact->batch,
			'notify'            => (string)$DiasporaContact->notify,
			'poll'              => (string)$DiasporaContact->poll,
			'subscribe'         => (string)$DiasporaContact->subscribe,
			'searchable'        => $DiasporaContact->searchable,
			'pubkey'            => $DiasporaContact->pubKey,
			'gsid'              => $DiasporaContact->gsid,
			'created'           => $DiasporaContact->created->format(DateTimeFormat::MYSQL),
			'updated'           => DateTimeFormat::utcNow(),
			'interacting_count' => $DiasporaContact->interacting_count,
			'interacted_count'  => $DiasporaContact->interacted_count,
			'post_count'        => $DiasporaContact->post_count,
		];

		// Limit the length on incoming fields
		$fields = $this->definition->truncateFieldsForTable('diaspora-contact', $fields);

		$this->db->insert('diaspora-contact', $fields, Database::INSERT_UPDATE);

		return $this->selectOneByUriId($uriId);
	}

	/**
	 * Fetch a Diaspora profile from a given WebFinger address and updates it depending on the mode
	 *
	 * @param WebFingerUri $uri    Profile address
	 * @param boolean      $update true = always update, false = never update, null = update when not found or outdated
	 * @return Entity\DiasporaContact
	 * @throws HTTPException\NotFoundException
	 */
	public function getByAddr(WebFingerUri $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact
	{
		if ($update !== self::ALWAYS_UPDATE) {
			try {
				$dcontact = $this->selectOneByAddr($uri);
				if ($update === self::NEVER_UPDATE) {
					return $dcontact;
				}
			} catch (HTTPException\NotFoundException $e) {
				if ($update === self::NEVER_UPDATE) {
					throw $e;
				}

				// This is necessary for Contact::getByURL in case the base contact record doesn't need probing,
				// but we still need the result of a probe to create the missing diaspora-contact record.
				$update = self::ALWAYS_UPDATE;
			}
		}

		$contact = Contact::getByURL($uri, $update, ['uri-id']);
		if (empty($contact['uri-id'])) {
			throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found');
		}

		return self::selectOneByUriId($contact['uri-id']);
	}

	/**
	 * Fetch a Diaspora profile from a given profile URL and updates it depending on the mode
	 *
	 * @param UriInterface $uri    Profile URL
	 * @param boolean      $update true = always update, false = never update, null = update when not found or outdated
	 * @return Entity\DiasporaContact
	 * @throws HTTPException\NotFoundException
	 */
	public function getByUrl(UriInterface $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact
	{
		if ($update !== self::ALWAYS_UPDATE) {
			try {
				$dcontact = $this->selectOneByUriId(ItemURI::getIdByURI($uri));
				if ($update === self::NEVER_UPDATE) {
					return $dcontact;
				}
			} catch (HTTPException\NotFoundException $e) {
				if ($update === self::NEVER_UPDATE) {
					throw $e;
				}

				// This is necessary for Contact::getByURL in case the base contact record doesn't need probing,
				// but we still need the result of a probe to create the missing diaspora-contact record.
				$update = self::ALWAYS_UPDATE;
			}
		}

		$contact = Contact::getByURL($uri, $update, ['uri-id']);
		if (empty($contact['uri-id'])) {
			throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found');
		}

		return self::selectOneByUriId($contact['uri-id']);
	}

	/**
	 * Update or create a diaspora-contact entry via a probe array
	 *
	 * @param array $data Probe array
	 * @return Entity\DiasporaContact
	 * @throws \Exception
	 */
	public function updateFromProbeArray(array $data): Entity\DiasporaContact
	{
		if (empty($data['url'])) {
			throw new \InvalidArgumentException('Missing url key in Diaspora probe data array');
		}

		if (empty($data['guid'])) {
			throw new \InvalidArgumentException('Missing guid key in Diaspora probe data array');
		}

		if (empty($data['pubkey'])) {
			throw new \InvalidArgumentException('Missing pubkey key in Diaspora probe data array');
		}

		$uriId = ItemURI::insert(['uri' => $data['url'], 'guid' => $data['guid']]);

		$contact   = Contact::getByUriId($uriId, ['id', 'created']);
		$apcontact = APContact::getByURL($data['url'], false);
		if (!empty($apcontact)) {
			$interacting_count = $apcontact['followers_count'];
			$interacted_count  = $apcontact['following_count'];
			$post_count        = $apcontact['statuses_count'];
		} elseif (!empty($contact['id'])) {
			$last_interaction = DateTimeFormat::utc('now - 180 days');

			$interacting_count = $this->db->count('contact-relation', ["`relation-cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]);
			$interacted_count  = $this->db->count('contact-relation', ["`cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]);
			$post_count        = $this->db->count('post', ['author-id' => $contact['id'], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]);
		}

		$DiasporaContact = $this->factory->createfromProbeData(
			$data,
			$uriId,
			new \DateTime($contact['created'] ?? 'now', new \DateTimeZone('UTC')),
			$interacting_count ?? 0,
			$interacted_count ?? 0,
			$post_count ?? 0
		);

		$DiasporaContact = $this->save($DiasporaContact);

		$this->logger->info('Updated diaspora-contact', ['url' => (string) $DiasporaContact->url]);

		return $DiasporaContact;
	}

	/**
	 * get a url (scheme://domain.tld/u/user) from a given contact guid
	 *
	 * @param mixed $guid Hexadecimal string guid
	 *
	 * @return string the contact url or null
	 * @throws \Exception
	 */
	public function getUrlByGuid(string $guid): ?string
	{
		$diasporaContact = $this->db->selectFirst(self::$table_name, ['url'], ['guid' => $guid]);

		return $diasporaContact['url'] ?? null;
	}
}
