<?php
declare(strict_types=1);

/**
 * This class handles main actions of FreshRSS.
 */
class FreshRSS_index_Controller extends FreshRSS_ActionController {

	#[\Override]
	public function firstAction(): void {
		$this->view->html_url = Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root');
	}

	/**
	 * This action only redirect on the default view mode (normal or global)
	 */
	public function indexAction(): void {
		$preferred_output = FreshRSS_Context::userConf()->view_mode;
		$viewMode = FreshRSS_ViewMode::getAllModes()[$preferred_output] ?? null;

		// Fallback to 'normal' if the preferred mode was not found
		if ($viewMode === null) {
			Minz_Request::setBadNotification(_t('feedback.extensions.invalid_view_mode', $preferred_output));
			$viewMode = FreshRSS_ViewMode::getAllModes()['normal'];
		}

		Minz_Request::forward([
			'c' => $viewMode->controller(),
			'a' => $viewMode->action(),
		]);
	}

	/**
	 * @return '.future'|'.today'|'.yesterday'|''
	 */
	private static function dayRelative(int $timestamp, bool $mayBeFuture): string {
		static $today = null;
		if (!is_int($today)) {
			$today = strtotime('today') ?: 0;
		}
		if ($today <= 0) {
			return '';
		} elseif ($mayBeFuture && ($timestamp >= $today + 86400)) {
			return '.future';
		} elseif ($timestamp >= $today) {
			return '.today';
		} elseif ($timestamp >= $today - 86400) {
			return '.yesterday';
		}
		return '';
	}

	/**
	 * Content for displaying a transition between entries when sorting by specific criteria.
	 */
	public static function transition(FreshRSS_Entry $entry): string {
		return match (FreshRSS_Context::$sort) {
			'id' => _t('index.feed.received' . self::dayRelative($entry->dateAdded(raw: true), mayBeFuture: false)) .
				' — ' . timestamptodate($entry->dateAdded(raw: true), hour: false),
			'date' => _t('index.feed.published' . self::dayRelative($entry->date(raw: true), mayBeFuture: true)) .
				' — ' . timestamptodate($entry->date(raw: true), hour: false),
			'lastUserModified' => _t('index.feed.userModified' . self::dayRelative($entry->lastUserModified() ?? 0, mayBeFuture: false)) .
				' — ' . timestamptodate($entry->lastUserModified() ?? 0, hour: false),
			'c.name' => $entry->feed()?->category()?->name() ?? '',
			'f.name' => $entry->feed()?->name() ?? '',
			default => '',
		};
	}

	/**
	 * Produce a hyperlink to the next transition of entries.
	 */
	public static function transitionLink(FreshRSS_Entry $entry, int $offset = 0): string {
		if (in_array(FreshRSS_Context::$sort, ['c.name', 'f.name'], true)) {
			return Minz_Url::display(Minz_Request::modifiedCurrentRequest([
				'get' => match (FreshRSS_Context::$sort) {
					'c.name' => 'c_' . ($entry->feed()?->category()?->id() ?? '0'),
					'f.name' => 'f_' . ($entry->feed()?->id() ?? '0'),
				},
			]));
		}
		$operator = match (FreshRSS_Context::$sort) {
			'id' => 'date',
			'date' => 'pubdate',
			'lastUserModified' => 'userdate',
			default => throw new InvalidArgumentException('Unsupported sort criterion for transition: ' . FreshRSS_Context::$sort),
		};
		$offset = FreshRSS_Context::$order === 'ASC' ? $offset : -$offset;
		$timestamp = match (FreshRSS_Context::$sort) {
			'id' => $entry->dateAdded(raw: true),
			'date' => $entry->date(raw: true),
			'lastUserModified' => $entry->lastUserModified() ?? 0,
		};
		$searchString = $operator . ':' . ($offset < 0 ? '/' : '') . date('Y-m-d', $timestamp + ($offset * 86400)) . ($offset > 0 ? '/' : '');
		return Minz_Url::display(Minz_Request::modifiedCurrentRequest([
			'search' => FreshRSS_Context::$search->toString() === '' ? $searchString :
				FreshRSS_Context::$search->enforce(new FreshRSS_Search($searchString))->toString(),
			]));
	}

	/**
	 * This action displays the normal view of FreshRSS.
	 */
	public function normalAction(): void {
		if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Auth::allowAnonymous()) {
			if (Minz_Request::paramString('user') !== '') {
				Minz_Error::error(403, redirect: false);
			} else {
				Minz_Request::forward(['c' => 'auth', 'a' => 'login']);
			}
			return;
		}

		$id = Minz_Request::paramInt('id');
		if ($id !== 0) {
			if (Minz_Request::paramString('type') === 'tag') {
				$tagDAO = FreshRSS_Factory::createTagDao();
				$tag = $tagDAO->searchById($id);
				$this->view->tag = $tag;
			} else {
				$feedDAO = FreshRSS_Factory::createFeedDao();
				$feed = $feedDAO->searchById($id);
				$this->view->feed = $feed;
			}
			$this->view->displaySlider = true;
			$this->view->cfrom = Minz_Request::actionName();
		}

		try {
			FreshRSS_Context::updateUsingRequest(true);
		} catch (FreshRSS_Context_Exception $e) {
			Minz_Error::error(404);
		}

		$this->_csp([
			'default-src' => "'self'",
			'frame-src' => '*',
			'img-src' => '* data: blob:',
			'frame-ancestors' => FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'",
			'media-src' => '*',
		]);

		$this->view->categories = FreshRSS_Context::categories();

		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
		$title = FreshRSS_Context::$name;
		$search = FreshRSS_Context::$search->toString(expandUserQueries: false);
		if ($search !== '') {
			$title = '“' . htmlspecialchars($search, ENT_COMPAT, 'UTF-8') . '”';
		}
		if (FreshRSS_Context::userConf()->show_title_unread && FreshRSS_Context::$get_unread > 0) {
			$title = '(' . format_number(FreshRSS_Context::$get_unread) . ') ' . $title;
		}
		if (strlen($title) > 0) {
			FreshRSS_View::prependTitle($title . ' · ');
		}

		if (FreshRSS_Context::$id_max === '0') {
			FreshRSS_Context::$id_max = uTimeString();
		}

		$this->view->callbackBeforeFeeds = static function (FreshRSS_View $view) {
			$view->nbUnreadTags = 0;
			if (Minz_Request::paramBoolean('ajax')) {
				// Disable label counts for AJAX requests: faster and not needed
				$view->tags = FreshRSS_Context::labels(precounts: false);
				return;
			}
			$view->tags = FreshRSS_Context::labels(precounts: true);
			foreach ($view->tags as $tag) {
				$view->nbUnreadTags += $tag->nbUnread();
			}
		};

		$this->view->callbackBeforeEntries = static function (FreshRSS_View $view) {
			try {
				// +1 to account for paging logic
				$view->entries = FreshRSS_index_Controller::listEntriesByContext(FreshRSS_Context::$number + 1);
				if (!$view->entries->valid()) {	// Init the generator to catch potential exceptions
					$view->entries = new EmptyIterator();
				}
				ob_start();	//Buffer "one entry at a time"
			} catch (FreshRSS_EntriesGetter_Exception $e) {
				Minz_Log::notice($e->getMessage());
				Minz_Error::error(404);
			}
		};

		$this->view->callbackBeforePagination = static function (?FreshRSS_View $view, int $nbEntries, FreshRSS_Entry $lastEntry) {
			if ($nbEntries > FreshRSS_Context::$number) {
				//We have enough entries: we discard the last one to use it for the next articles' page
				ob_clean();
				FreshRSS_Context::$continuation_id = $lastEntry->id();
			} else {
				FreshRSS_Context::$continuation_id = '0';
			}
			ob_end_flush();
		};
	}

	/**
	 * This action displays the reader view of FreshRSS.
	 *
	 * @todo: change this view into specific CSS rules?
	 */
	public function readerAction(): void {
		$this->normalAction();
	}

	/**
	 * This action displays the global view of FreshRSS.
	 */
	public function globalAction(): void {
		if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Auth::allowAnonymous()) {
			if (Minz_Request::paramString('user') !== '') {
				Minz_Error::error(403, redirect: false);
			} else {
				Minz_Request::forward(['c' => 'auth', 'a' => 'login']);
			}
			return;
		}

		FreshRSS_View::appendScript(Minz_Url::display('/scripts/extra.js?' . @filemtime(PUBLIC_PATH . '/scripts/extra.js')));
		FreshRSS_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));

		try {
			FreshRSS_Context::updateUsingRequest(true);
		} catch (FreshRSS_Context_Exception) {
			Minz_Error::error(404);
		}

		$this->view->categories = FreshRSS_Context::categories();
		// Filter feed list when searching or when a restrictive state filter is active
		if (FreshRSS_Context::$search->toString() !== '' || FreshRSS_Context::isStateConsequential(FreshRSS_Context::$state)) {
			$entryDAO = FreshRSS_Factory::createEntryDao();
			$this->view->feedIdsMatching = $entryDAO->listFeedIdsMatching(FreshRSS_Context::$state, FreshRSS_Context::$search);
		}

		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
		$title = _t('index.feed.title_global');
		if (FreshRSS_Context::userConf()->show_title_unread && FreshRSS_Context::$get_unread > 0) {
			$title = '(' . format_number(FreshRSS_Context::$get_unread) . ') ' . $title;
		}
		FreshRSS_View::prependTitle($title . ' · ');

		$this->_csp([
			'default-src' => "'self'",
			'frame-src' => '*',
			'img-src' => '* data: blob:',
			'frame-ancestors' => FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'",
			'media-src' => '*',
		]);
	}

	/**
	 * This action displays the RSS feed of FreshRSS.
	 * @deprecated See user query RSS sharing instead
	 */
	public function rssAction(): void {
		// Check if user has access.
		if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Auth::allowAnonymous() && !Minz_Request::tokenIsOk()) {
			Minz_Error::error(403, redirect: false);
			return;
		}

		try {
			FreshRSS_Context::updateUsingRequest(false);
		} catch (FreshRSS_Context_Exception $e) {
			Minz_Error::error(404);
		}

		try {
			$this->view->entries = FreshRSS_index_Controller::listEntriesByContext();
			if (!$this->view->entries->valid()) {	// Init the generator to catch potential exceptions
				$this->view->entries = new EmptyIterator();
			}
		} catch (FreshRSS_EntriesGetter_Exception $e) {
			Minz_Log::notice($e->getMessage());
			Minz_Error::error(404);
		}

		$this->view->html_url = Minz_Url::display('', 'html', true);
		$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();

		$queryString = $_SERVER['QUERY_STRING'] ?? '';
		$this->view->rss_url = htmlspecialchars(
			PUBLIC_TO_INDEX_PATH . '/' . ($queryString === '' || !is_string($queryString) ? '' : '?' . $queryString), ENT_COMPAT, 'UTF-8');

		// No layout for RSS output.
		$this->view->_layout(null);
		header('Content-Type: application/rss+xml; charset=utf-8');
	}

	public function opmlAction(): void {
		// Check if user has access.
		if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Auth::allowAnonymous() && !Minz_Request::tokenIsOk()) {
			Minz_Error::error(403, redirect: false);
			return;
		}

		try {
			FreshRSS_Context::updateUsingRequest(false);
		} catch (FreshRSS_Context_Exception) {
			Minz_Error::error(404);
		}

		$get = FreshRSS_Context::currentGet(true);
		$type = (string)$get[0];
		$id = (int)$get[1];

		$this->view->excludeMutedFeeds = $type !== 'f';	// Exclude muted feeds except when we focus on a feed

		switch ($type) {
			case 'a':	// All PRIORITY_MAIN_STREAM
			case 'A':	// All except PRIORITY_HIDDEN
			case 'Z':	// All including PRIORITY_HIDDEN
				$this->view->categories = FreshRSS_Context::categories();
				break;
			case 'c':	// Category
				$cat = FreshRSS_Context::categories()[$id] ?? null;
				if ($cat == null) {
					Minz_Error::error(404);
					return;
				}
				$this->view->categories = [$cat->id() => $cat];
				break;
			case 'f':	// Feed
				// We most likely already have the feed object in cache
				$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
				if ($feed === null) {
					$feedDAO = FreshRSS_Factory::createFeedDao();
					$feed = $feedDAO->searchById($id);
					if ($feed == null) {
						Minz_Error::error(404);
						return;
					}
				}
				$this->view->feeds = [$feed->id() => $feed];
				break;
			default:
				Minz_Error::error(404);
				return;
		}

		// No layout for OPML output.
		$this->view->_layout(null);
		header('Content-Type: application/xml; charset=utf-8');
	}

	/**
	 * This method returns a list of entries based on the Context object.
	 * @param int $postsPerPage override `FreshRSS_Context::$number`
	 * @return Generator<FreshRSS_Entry>
	 * @throws FreshRSS_EntriesGetter_Exception
	 */
	public static function listEntriesByContext(?int $postsPerPage = null): Generator {
		$entryDAO = FreshRSS_Factory::createEntryDao();

		$get = FreshRSS_Context::currentGet(true);
		if (is_array($get)) {
			$type = $get[0];
			$id = (int)($get[1]);
		} else {
			$type = $get;
			$id = 0;
		}

		$id_min = '0';
		if (FreshRSS_Context::$sinceHours > 0) {
			$id_min = (time() - (FreshRSS_Context::$sinceHours * 3600)) . '000000';
		}

		$continuation_values = [];
		if (FreshRSS_Context::$continuation_id !== '0') {
			if (in_array(FreshRSS_Context::$sort, ['c.name', 'date', 'f.name', 'link', 'title', 'lastUserModified', 'length'], true)) {
				$pagingEntry = $entryDAO->searchById(FreshRSS_Context::$continuation_id);

				if ($pagingEntry !== null && in_array(FreshRSS_Context::$sort, ['c.name', 'f.name'], true)) {
					// We most likely already have the feed object in cache
					$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $pagingEntry->feedId());
					if ($feed !== null) {
						$pagingEntry->_feed($feed);
					}
				}

				$continuation_values[] = $pagingEntry === null ? 0 : match (FreshRSS_Context::$sort) {
					'c.name' => $pagingEntry->feed()?->categoryId() === FreshRSS_CategoryDAO::DEFAULTCATEGORYID ?
						FreshRSS_CategoryDAO::DEFAULT_CATEGORY_NAME : $pagingEntry->feed()?->category()?->name() ?? '',
					'date' => $pagingEntry->date(raw: true),
					'f.name' => $pagingEntry->feed()?->name(raw: true) ?? '',
					'link' => $pagingEntry->link(raw: true),
					'title' => $pagingEntry->title(),
					'lastUserModified' => $pagingEntry->lastUserModified() ?? 0,
					'length' => $pagingEntry->sqlContentLength() ?? 0,
				};
				if (FreshRSS_Context::$sort === 'c.name') {
					// Internal secondary sort criterion for category name
					$continuation_values[] = $pagingEntry?->feed()?->name(raw: true) ?? '';
				}
				if (in_array(FreshRSS_Context::$sort, ['c.name', 'f.name'], true)) {
					// User secondary sort criterion
					$continuation_values[] = $pagingEntry === null ? 0 : match (FreshRSS_Context::$secondary_sort) {
						'id' => $pagingEntry->id(),
						'date' => $pagingEntry->date(raw: true),
						'link' => $pagingEntry->link(raw: true),
						'title' => $pagingEntry->title(),
					};
				}
			} elseif (FreshRSS_Context::$sort === 'rand') {
				FreshRSS_Context::$continuation_id = '0';
			}
		}

		yield from $entryDAO->listWhere(
			$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$search,
			id_min: $id_min, id_max: FreshRSS_Context::$id_max, sort: FreshRSS_Context::$sort, order: FreshRSS_Context::$order,
			continuation_id: FreshRSS_Context::$continuation_id, continuation_values: $continuation_values,
			limit: $postsPerPage ?? FreshRSS_Context::$number, offset: FreshRSS_Context::$offset,
			secondary_sort: FreshRSS_Context::$secondary_sort, secondary_sort_order: FreshRSS_Context::$secondary_sort_order);
	}

	/**
	 * This action displays the about page of FreshRSS.
	 */
	public function aboutAction(): void {
		FreshRSS_View::prependTitle(_t('index.about.title') . ' · ');
	}

	/**
	 * This action displays the EULA/TOS (Terms of Service) page of FreshRSS.
	 * This page is enabled only if admin created a data/tos.html file.
	 * The content of the page is the content of data/tos.html.
	 * It returns 404 if there is no EULA/TOS.
	 */
	public function tosAction(): void {
		$terms_of_service = file_get_contents(TOS_FILENAME);
		if ($terms_of_service === false) {
			Minz_Error::error(404);
			return;
		}

		$this->view->terms_of_service = $terms_of_service;
		$this->view->can_register = !FreshRSS_user_Controller::max_registrations_reached();
		FreshRSS_View::prependTitle(_t('index.tos.title') . ' · ');
	}

	/**
	 * This action displays logs of FreshRSS for the current user.
	 */
	public function logsAction(): void {
		if (!FreshRSS_Auth::hasAccess()) {
			Minz_Error::error(403);
		}

		FreshRSS_View::prependTitle(_t('index.log.title') . ' · ');

		if (Minz_Request::isPost()) {
			FreshRSS_LogDAO::truncate();
		}

		$logs = FreshRSS_LogDAO::lines();	//TODO: ask only the necessary lines
		$search = trim(Minz_Request::paramString('search', plaintext: true));
		if ($search !== '') {
			$logs = array_values(array_filter($logs, static fn(FreshRSS_Log $log): bool =>
				stripos($log->level(), $search) !== false ||
				stripos($log->date(), $search) !== false ||
				stripos($log->info(), $search) !== false));
		}
		$this->view->logSearch = $search;

		//gestion pagination
		$page = Minz_Request::paramInt('page') ?: 1;
		$this->view->logsPaginator = new Minz_Paginator($logs);
		$this->view->logsPaginator->_nbItemsPerPage(50);
		$this->view->logsPaginator->_currentPage($page);
	}
}
