<?php

// Copyright (C) 2010-2026, the Friendica project
// SPDX-FileCopyrightText: 2010-2026 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace Friendica\Module\Calendar\Event;

use Friendica\App\Arguments;
use Friendica\App\BaseURL;
use Friendica\AppHelper;
use Friendica\BaseModule;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\Conversation;
use Friendica\Model\Event;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\User;
use Friendica\Module\Response;
use Friendica\Navigation\SystemMessages;
use Friendica\Network\HTTPException\BadRequestException;
use Friendica\Network\HTTPException\UnauthorizedException;
use Friendica\Protocol\Delivery;
use Friendica\Util\ACLFormatter;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Profiler;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;

/**
 * Basic API class for events
 * currently supports create, delete, ignore, unignore
 *
 * @todo: make create/update as REST-call instead of POST
 */
class API extends BaseModule
{
	public const ACTION_CREATE   = 'create';
	public const ACTION_DELETE   = 'delete';
	public const ACTION_IGNORE   = 'ignore';
	public const ACTION_UNIGNORE = 'unignore';

	public const ALLOWED_ACTIONS = [
		self::ACTION_CREATE,
		self::ACTION_DELETE,
		self::ACTION_IGNORE,
		self::ACTION_UNIGNORE,
	];

	/** @var IHandleUserSessions */
	protected $session;
	/** @var SystemMessages */
	protected $sysMessages;
	/** @var ACLFormatter */
	protected $aclFormatter;
	/** @var string */
	protected $timezone;

	public function __construct(L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, SystemMessages $sysMessages, ACLFormatter $aclFormatter, AppHelper $appHelper, array $server, array $parameters = [])
	{
		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);

		$this->session      = $session;
		$this->sysMessages  = $sysMessages;
		$this->aclFormatter = $aclFormatter;
		$this->timezone     = $appHelper->getTimeZone();

		if (!$this->session->getLocalUserId()) {
			throw new UnauthorizedException($this->t('Permission denied.'));
		}
	}

	protected function post(array $request = [])
	{
		$this->createEvent($request);
	}

	protected function rawContent(array $request = [])
	{
		if (empty($this->parameters['action']) || !in_array($this->parameters['action'], self::ALLOWED_ACTIONS)) {
			throw new BadRequestException($this->t('Invalid Request'));
		}

		// CREATE is done per POSt, so nothing to do left
		if ($this->parameters['action'] === static::ACTION_CREATE) {
			return;
		}

		if (empty($this->parameters['id'])) {
			throw new BadRequestException($this->t('Event id is missing.'));
		}

		$returnPath = $request['return_path'] ?? 'calendar';

		switch ($this->parameters['action']) {
			case self::ACTION_IGNORE:
				Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id']);
				break;
			case self::ACTION_UNIGNORE:
				Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id'], false);
				break;
			case self::ACTION_DELETE:
				// Remove an event from the calendar and its related items
				$event = Event::getByIdAndUid($this->session->getLocalUserId(), $this->parameters['id']);

				// Delete only real events (no birthdays)
				if (!empty($event) && $event['type'] == 'event') {
					Item::deleteForUser(['id' => $event['itemid']], $this->session->getLocalUserId());
				}

				if (Post::exists(['id' => $event['itemid']])) {
					$this->sysMessages->addNotice($this->t('Failed to remove event'));
				}
				break;
			default:
				throw new BadRequestException($this->t('Invalid Request'));
		}

		$this->baseUrl->redirect($returnPath);
	}

	protected function createEvent(array $request)
	{
		$eventId = !empty($request['event_id']) ? intval($request['event_id']) : 0;
		$uid     = (int) $this->session->getLocalUserId();
		// No overwriting event.cid on edit
		$cid = !empty($request['cid']) && !$eventId ? intval($request['cid']) : 0;

		$strStartDateTime  = Strings::escapeHtml($request['start_text'] ?? '');
		$strFinishDateTime = Strings::escapeHtml($request['finish_text'] ?? '');

		$noFinish = intval($request['nofinish'] ?? 0);

		$share     = intval($request['share'] ?? 0);
		$isPreview = intval($request['preview'] ?? 0);

		$start = DateTimeFormat::convert($strStartDateTime, 'UTC', $this->timezone);
		if (!$noFinish) {
			$finish = DateTimeFormat::convert($strFinishDateTime, 'UTC', $this->timezone);
		} else {
			$finish = DBA::NULL_DATETIME;
		}

		// Don't allow the event to finish before it begins.
		// It won't hurt anything, but somebody will file a bug report,
		// and we'll waste a bunch of time responding to it. Time that
		// could've been spent doing something else.

		$summary  = trim($request['summary'] ?? '');
		$desc     = trim($request['desc'] ?? '');
		$location = trim($request['location'] ?? '');
		$type     = 'event';

		$params = [
			'summary'  => $summary,
			'desc'     => $desc,
			'location' => $location,
			'start'    => $strStartDateTime,
			'finish'   => $strFinishDateTime,
			'nofinish' => $noFinish,
		];

		$action          = empty($eventId) ? 'new' : 'edit/' . $eventId;
		$redirectOnError = 'calendar/event/' . $action . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);

		if (strcmp($finish, $start) < 0 && !$noFinish) {
			if ($isPreview) {
				$this->earlyHttpExit($this->t('Event can not end before it has started.'));
			} else {
				$this->sysMessages->addNotice($this->t('Event can not end before it has started.'));
				$this->baseUrl->redirect($redirectOnError);
			}
		}

		if (empty($summary) || ($start === DBA::NULL_DATETIME)) {
			if ($isPreview) {
				$this->earlyHttpExit($this->t('Event title and start time are required.'));
			} else {
				$this->sysMessages->addNotice($this->t('Event title and start time are required.'));
				$this->baseUrl->redirect($redirectOnError);
			}
		}

		$self = Contact::getPublicIdByUserId($uid);

		$aclFormatter = $this->aclFormatter;

		if ($share) {
			$user = User::getById($uid, ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);
			if (empty($user)) {
				$this->logger->warning('Cannot find user for an event.', ['uid' => $uid, 'event' => $eventId]);
				$this->response->setStatus(500);
				return;
			}

			$strAclContactAllow = isset($request['contact_allow']) ? $aclFormatter->toString($request['contact_allow']) : $user['allow_cid'] ?? '';
			$strAclCircleAllow  = isset($request['circle_allow'])  ? $aclFormatter->toString($request['circle_allow'])  : $user['allow_gid'] ?? '';
			$strContactDeny     = isset($request['contact_deny'])  ? $aclFormatter->toString($request['contact_deny'])  : $user['deny_cid']  ?? '';
			$strCircleDeny      = isset($request['circle_deny'])   ? $aclFormatter->toString($request['circle_deny'])   : $user['deny_gid']  ?? '';

			$visibility = $request['visibility'] ?? '';
			if ($visibility === 'public') {
				// The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected
				$strAclContactAllow = $strAclCircleAllow = $strContactDeny = $strCircleDeny = '';
			} elseif ($visibility === 'custom') {
				// Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL
				// case that would make it public. So we always append the author's contact id to the allowed contacts.
				// See https://github.com/friendica/friendica/issues/9672
				$strAclContactAllow .= $aclFormatter->toString((string) $self);
			}
		} else {
			$strAclContactAllow = $aclFormatter->toString((string) $self);
			$strAclCircleAllow  = '';
			$strContactDeny     = '';
			$strCircleDeny      = '';
		}

		$datarray = [
			'start'     => $start,
			'finish'    => $finish,
			'summary'   => $summary,
			'desc'      => $desc,
			'location'  => $location,
			'type'      => $type,
			'nofinish'  => $noFinish,
			'uid'       => $uid,
			'cid'       => $cid,
			'allow_cid' => $strAclContactAllow,
			'allow_gid' => $strAclCircleAllow,
			'deny_cid'  => $strContactDeny,
			'deny_gid'  => $strCircleDeny,
			'id'        => $eventId,
		];

		if (intval($request['preview'])) {
			$this->earlyHttpExit(Event::getHTML($datarray));
		}

		$eventId = Event::store($datarray);

		$newItem = Event::getItemArrayForId($eventId, [
			'network'   => Protocol::DFRN,
			'protocol'  => Conversation::PARCEL_DIRECT,
			'direction' => Conversation::PUSH,
		]);
		if (Item::insert($newItem)) {
			$uriId = (int) $newItem['uri-id'];
		} else {
			$uriId = 0;
		}

		if (!$cid && $uriId) {
			Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::POST, $uriId, $uid);
		}

		$this->baseUrl->redirect('calendar');
	}
}
