<?php

namespace Kirby\Content;

use Kirby\Cache\Cache;
use Kirby\Cms\App;
use Kirby\Cms\Files;
use Kirby\Cms\ModelWithContent;
use Kirby\Cms\Pages;
use Kirby\Cms\Users;
use Kirby\Toolkit\A;

/**
 * The Changes class tracks changed models
 * in the Site's changes field.
 *
 * @package   Kirby Content
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 */
class Changes
{
	protected App $kirby;

	public function __construct()
	{
		$this->kirby = App::instance();
	}

	/**
	 * Access helper for the cache, in which changes are stored
	 */
	public function cache(): Cache
	{
		return $this->kirby->cache('changes');
	}

	/**
	 * Returns whether the cache has been populated
	 */
	public function cacheExists(): bool
	{
		return $this->cache()->get('__updated__') !== null;
	}

	/**
	 * Returns the cache key for a given model
	 */
	public function cacheKey(ModelWithContent $model): string
	{
		return $model::CLASS_ALIAS . 's';
	}

	/**
	 * Verify that the tracked model still really has changes.
	 * If not, untrack and remove from collection.
	 *
	 * @template T of \Kirby\Cms\Files|\Kirby\Cms\Pages|\Kirby\Cms\Users
	 * @param T $tracked
	 * @return T
	 */
	public function ensure(Files|Pages|Users $tracked): Files|Pages|Users
	{
		foreach ($tracked as $model) {
			if ($model->version('changes')->exists('*') === false) {
				$this->untrack($model);
				$tracked->remove($model);
			}
		}

		return $tracked;
	}

	/**
	 * Return all files with unsaved changes
	 */
	public function files(): Files
	{
		$files = new Files([]);

		foreach ($this->read('files') as $id) {
			if ($file = $this->kirby->file($id)) {
				$files->add($file);
			}
		}

		return $this->ensure($files);
	}

	/**
	 * Rebuilds the cache by finding all models with changes version
	 */
	public function generateCache(): void
	{
		$models = [
			'files' => [],
			'pages' => [],
			'users' => []
		];

		foreach ($this->kirby->models() as $model) {
			if ($model->version('changes')->exists('*') === true) {
				$models[$this->cacheKey($model)][] = (string)($model->uuid() ?? $model->id());
			}
		}

		foreach ($models as $key => $changes) {
			$this->update($key, $changes);
		}
	}

	/**
	 * Return all pages with unsaved changes
	 */
	public function pages(): Pages
	{
		/**
		 * @var \Kirby\Cms\Pages $pages
		 *
		 * Always pass at least two arguments even if the
		 * data is empty so that `$site->find()` always
		 * returns a collection, not a single page
		 */
		$pages = $this->kirby->site()->find(
			false,
			false,
			...$this->read('pages')
		);

		return $this->ensure($pages);
	}

	/**
	 * Read the changes for a given model type
	 */
	public function read(string $key): array
	{
		return $this->cache()->get($key) ?? [];
	}

	/**
	 * Add a new model to the list of unsaved changes
	 */
	public function track(ModelWithContent $model): void
	{
		$key = $this->cacheKey($model);

		$changes   = $this->read($key);
		$changes[] = (string)($model->uuid() ?? $model->id());

		$this->update($key, $changes);
	}

	/**
	 * Remove a model from the list of unsaved changes
	 */
	public function untrack(ModelWithContent $model): void
	{
		// get the cache key for the model type
		$key = $this->cacheKey($model);

		// remove the model from the list of changes
		$changes = A::filter(
			$this->read($key),
			fn ($id) => $id !== (string)($model->uuid() ?? $model->id())
		);

		$this->update($key, $changes);
	}

	/**
	 * Update the changes field
	 */
	public function update(string $key, array $changes): void
	{
		$changes = array_unique($changes);
		$changes = array_values($changes);

		$this->cache()->set($key, $changes);
		$this->cache()->set('__updated__', time());
	}

	/**
	 * Return all users with unsaved changes
	 */
	public function users(): Users
	{
		/**
		 * @var \Kirby\Cms\Users $users
		 *
		 * Always pass at least two arguments even if the
		 * data is empty so that `$users->find()` always
		 * returns a collection, not a single user
		 */
		$users = $this->kirby->users()->find(
			false,
			false,
			...$this->read('users')
		);

		return $this->ensure($users);
	}
}
