<?php

namespace Kirby\Cms;

use Closure;
use Kirby\Content\Field;
use Kirby\Content\VersionId;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\Dir;
use Kirby\Http\Response;
use Kirby\Panel\Page as Panel;
use Kirby\Template\Template;
use Kirby\Toolkit\A;
use Kirby\Toolkit\BlockCollectionAccess;
use Kirby\Toolkit\LazyValue;
use Kirby\Toolkit\Str;
use Throwable;

/**
 * The `$page` object is the heart and
 * soul of Kirby. It is used to construct
 * pages and all their dependencies like
 * children, files, content, etc.
 *
 * @package   Kirby Cms
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 *
 * @use \Kirby\Cms\HasSiblings<\Kirby\Cms\Pages>
 * @method \Kirby\Uuid\PageUuid uuid()
 */
class Page extends ModelWithContent
{
	use HasChildren;
	use HasFiles;
	use HasMethods;
	use HasModels;
	use HasSiblings;
	use PageActions;
	use PageSiblings;

	public const CLASS_ALIAS = 'page';

	/**
	 * All registered page methods
	 * @todo Remove when support for PHP 8.2 is dropped
	 */
	public static array $methods = [];

	/**
	 * The PageBlueprint object
	 */
	protected PageBlueprint|null $blueprint = null;

	/**
	 * Nesting level
	 */
	protected int $depth;

	/**
	 * Sorting number + slug
	 */
	protected string|null $dirname;

	/**
	 * Path of dirnames
	 */
	protected string|null $diruri = null;

	/**
	 * Draft status flag
	 */
	protected bool $isDraft;

	/**
	 * The Page id
	 */
	protected string|null $id = null;

	/**
	 * The template, that should be loaded
	 * if it exists
	 */
	protected Template|null $intendedTemplate = null;

	protected array|null $inventory = null;

	/**
	 * The sorting number
	 */
	protected int|null $num;

	/**
	 * The parent page
	 */
	protected Page|null $parent;

	/**
	 * Absolute path to the page directory
	 */
	protected string|null $root;

	/**
	 * The URL-appendix aka slug
	 */
	protected string $slug;

	/**
	 * The intended page template
	 */
	protected Template|null $template = null;

	/**
	 * The page url
	 */
	protected string|null $url;

	/**
	 * Creates a new page object
	 */
	public function __construct(array $props)
	{
		if (isset($props['slug']) === false || $props['slug'] === '') {
			throw new InvalidArgumentException(
				message: 'The page slug is required'
			);
		}

		$this->slug    = $props['slug'];
		// Sets the dirname manually, which works
		// more reliable in connection with the inventory
		// than computing the dirname afterwards
		$this->dirname = $props['dirname'] ?? null;
		$this->isDraft = $props['isDraft'] ?? false;
		$this->num     = $props['num'] ?? null;
		$this->parent  = $props['parent'] ?? null;
		$this->root    = $props['root'] ?? null;

		// Set blueprint before setting content
		// or translations in the parent constructor.
		// Otherwise, the blueprint definition cannot be
		// used when creating the right field values
		// for the content.
		$this->setBlueprint($props['blueprint'] ?? null);

		parent::__construct($props);

		$this->setChildren($props['children'] ?? null);
		$this->setDrafts($props['drafts'] ?? null);
		$this->setFiles($props['files'] ?? null);
		$this->setTemplate($props['template'] ?? null);
		$this->setUrl($props['url'] ?? null);
	}

	/**
	 * Magic caller
	 */
	public function __call(string $method, array $arguments = []): mixed
	{
		// public property access
		if (isset($this->$method) === true) {
			return $this->$method;
		}

		// page methods
		if ($this->hasMethod($method)) {
			return $this->callMethod($method, $arguments);
		}

		// return page content otherwise
		return $this->content()->get($method);
	}

	/**
	 * Improved `var_dump` output
	 * @codeCoverageIgnore
	 */
	public function __debugInfo(): array
	{
		return [
			...$this->toArray(),
			'content'      => $this->content(),
			'children'     => $this->children(),
			'siblings'     => $this->siblings(),
			'translations' => $this->translations(),
			'files'        => $this->files(),
		];
	}

	/**
	 * Returns the url to the api endpoint
	 * @internal
	 */
	#[BlockCollectionAccess]
	public function apiUrl(bool $relative = false): string
	{
		if ($relative === true) {
			return 'pages/' . $this->panel()->id();
		}

		return $this->kirby()->url('api') . '/pages/' . $this->panel()->id();
	}

	/**
	 * Returns the blueprint object
	 */
	public function blueprint(): PageBlueprint
	{
		return $this->blueprint ??= PageBlueprint::factory(
			'pages/' . $this->intendedTemplate(),
			'pages/default',
			$this
		);
	}

	/**
	 * Returns an array with all blueprints that are available for the page
	 */
	public function blueprints(string|null $inSection = null): array
	{
		if ($inSection !== null) {
			return $this->blueprint()->section($inSection)->blueprints();
		}

		if ($this->blueprints !== null) {
			return $this->blueprints;
		}

		$blueprints  = [];
		$templates   = $this->blueprint()->changeTemplate() ?? null;
		$templates ??= $this->blueprint()->options()['changeTemplate'] ?? [];

		$currentTemplate = $this->intendedTemplate()->name();

		if (is_array($templates) === false) {
			$templates = [];
		}

		// add the current template to the array if it's not already there
		if (in_array($currentTemplate, $templates, true) === false) {
			array_unshift($templates, $currentTemplate);
		}

		// make sure every template is only included once
		$templates = array_unique($templates);

		foreach ($templates as $template) {
			try {
				$props = Blueprint::load('pages/' . $template);

				$blueprints[] = [
					'name'  => basename($props['name']),
					'title' => $props['title'],
				];
			} catch (Exception) {
				// skip invalid blueprints
			}
		}

		return $this->blueprints = array_values($blueprints);
	}

	/**
	 * Builds the cache id for the page
	 */
	protected function cacheId(string $contentType, VersionId $versionId): string
	{
		$cacheId = [$this->id()];

		if ($this->kirby()->multilang() === true) {
			$cacheId[] = $this->kirby()->language()->code();
		}

		$cacheId[] = $versionId->value();
		$cacheId[] = $contentType;

		return implode('.', $cacheId);
	}

	/**
	 * Prepares the content for the write method
	 * @internal
	 */
	public function contentFileData(
		array $data,
		string|null $languageCode = null
	): array {
		return A::prepend($data, [
			'title' => $data['title'] ?? null,
			'slug'  => $data['slug']  ?? null
		]);
	}

	/**
	 * Call the page controller
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby`, `site`, `pages` or `page`
	 */
	#[BlockCollectionAccess]
	public function controller(
		array $data = [],
		string $contentType = 'html'
	): array {
		// create the template data
		$data = [
			...$data,
			'kirby' => $kirby = $this->kirby(),
			'site'  => $site  = $this->site(),
			'pages' => new LazyValue(fn () => $site->children()),
			'page'  => new LazyValue(fn () => $site->visit($this))
		];

		// call the template controller if there's one.
		$controllerData = $kirby->controller(
			$this->template()->name(),
			$data,
			$contentType
		);

		// merge controller data with original data safely
		// to provide original data to template even if
		// it wasn't returned by the controller explicitly
		if ($controllerData !== []) {
			$classes = [
				'kirby' => App::class,
				'site'  => Site::class,
				'pages' => Pages::class,
				'page'  => Page::class
			];

			foreach ($controllerData as $key => $value) {
				$data[$key] = match (true) {
					// original data wasn't overwritten
					array_key_exists($key, $classes) === false => $value,
					// original data was overwritten, but matches expected type
					$value instanceof $classes[$key] => $value,
					// throw error if data was overwritten with wrong type
					default => throw new InvalidArgumentException(
						message: 'The returned variable "' . $key . '" from the controller "' . $this->template()->name() . '" is not of the required type "' . $classes[$key] . '"'
					)
				};
			}
		}

		// unwrap remaining lazy values in data
		// (happens if the controller didn't override an original lazy Kirby object)
		$data = LazyValue::unwrap($data);

		return $data;
	}

	/**
	 * Returns a number indicating how deep the page
	 * is nested within the content folder
	 */
	public function depth(): int
	{
		return $this->depth ??= (substr_count($this->id(), '/') + 1);
	}

	/**
	 * Returns the directory name (UID with optional sorting number)
	 */
	public function dirname(): string
	{
		if ($this->dirname !== null) {
			return $this->dirname;
		}

		if ($this->num() !== null) {
			return $this->dirname = $this->num() . Dir::$numSeparator . $this->uid();
		}

		return $this->dirname = $this->uid();
	}

	/**
	 * Returns the directory path relative to the `content` root
	 * (including optional sorting numbers and draft directories)
	 */
	public function diruri(): string
	{
		if (is_string($this->diruri) === true) {
			return $this->diruri;
		}

		$dirname = match ($this->isDraft()) {
			true  => '_drafts/' . $this->dirname(),
			false => $this->dirname()
		};

		if ($parent = $this->parent()) {
			return $this->diruri = $parent->diruri() . '/' . $dirname;
		}

		return $this->diruri = $dirname;
	}

	/**
	 * Checks if the page exists on disk
	 */
	public function exists(): bool
	{
		return is_dir($this->root()) === true;
	}

	/**
	 * Constructs a Page object and also
	 * takes page models into account.
	 */
	public static function factory($props): static
	{
		return static::model($props['model'] ?? $props['template'] ?? 'default', $props);
	}

	/**
	 * Redirects to this page,
	 * wrapper for the `go()` helper
	 *
	 * @since 3.4.0
	 *
	 * @param array $options Options for `Kirby\Http\Uri` to create URL parts
	 * @param int $code HTTP status code
	 */
	#[BlockCollectionAccess]
	public function go(array $options = [], int $code = 302): void
	{
		Response::go($this->url($options), $code);
	}

	/**
	 * Checks if the intended template
	 * for the page exists.
	 */
	public function hasTemplate(): bool
	{
		return $this->intendedTemplate() === $this->template();
	}

	/**
	 * Returns the Page Id
	 */
	public function id(): string
	{
		if ($this->id !== null) {
			return $this->id;
		}

		// set the id, depending on the parent
		if ($parent = $this->parent()) {
			return $this->id = $parent->id() . '/' . $this->uid();
		}

		return $this->id = $this->uid();
	}

	/**
	 * Returns the template that should be
	 * loaded if it exists.
	 */
	public function intendedTemplate(): Template
	{
		if ($this->intendedTemplate !== null) {
			return $this->intendedTemplate;
		}

		return $this
			->setTemplate($this->inventory()['template'])
			->intendedTemplate();
	}

	/**
	 * Returns the inventory of files children and content files
	 */
	#[BlockCollectionAccess]
	public function inventory(): array
	{
		if ($this->inventory !== null) {
			return $this->inventory;
		}

		$kirby = $this->kirby();

		return $this->inventory = Dir::inventory(
			$this->root(),
			$kirby->contentExtension(),
			$kirby->contentIgnore(),
			$kirby->multilang()
		);
	}

	/**
	 * Compares the current object with the given page object
	 *
	 * @param \Kirby\Cms\Page|string $page
	 */
	public function is($page): bool
	{
		if ($page instanceof self === false) {
			if (is_string($page) === false) {
				return false;
			}

			$page = $this->kirby()->page($page);
		}

		if ($page instanceof self === false) {
			return false;
		}

		return $this->id() === $page->id();
	}

	/**
	 * Checks if the page is accessible to the current user
	 * This permission depends on the `read` option until v6
	 */
	public function isAccessible(): bool
	{
		// TODO: remove this check when `read` option deprecated in v6
		if ($this->isReadable() === false) {
			return false;
		}

		return PagePermissions::canFromCache($this, 'access');
	}

	/**
	 * Checks if the page is the current page
	 */
	public function isActive(): bool
	{
		return $this->site()->page()?->is($this) === true;
	}

	/**
	 * Checks if the page is a direct or indirect ancestor
	 * of the given $page object
	 */
	public function isAncestorOf(Page $child): bool
	{
		return $child->parents()->has($this->id()) === true;
	}

	/**
	 * Checks if the page can be cached in the
	 * pages cache. This will also check if one
	 * of the ignore rules from the config kick in.
	 */
	public function isCacheable(VersionId|null $versionId = null): bool
	{
		$kirby   = $this->kirby();
		$cache   = $kirby->cache('pages');
		$options = $cache->options();
		$ignore  = $options['ignore'] ?? null;

		// the pages cache is switched off
		if (($options['active'] ?? false) === false) {
			return false;
		}

		// updating the changes version does not flush the pages cache
		if ($versionId?->is('changes') === true) {
			return false;
		}

		// inspect the current request
		$request = $kirby->request();

		// disable the pages cache for any request types but GET or HEAD
		if (in_array($request->method(), ['GET', 'HEAD'], true) === false) {
			return false;
		}

		// disable the pages cache when there's request data
		if (empty($request->data()) === false) {
			return false;
		}

		// disable the pages cache when there are any params
		if ($request->params()->isNotEmpty()) {
			return false;
		}

		// check for a custom ignore rule
		if ($ignore instanceof Closure) {
			if ($ignore($this) === true) {
				return false;
			}
		}

		// ignore pages by id
		if (is_array($ignore) === true) {
			if (in_array($this->id(), $ignore, true) === true) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Checks if the page is a child of the given page
	 *
	 * @param \Kirby\Cms\Page|string $parent
	 */
	public function isChildOf($parent): bool
	{
		return $this->parent()?->is($parent) ?? false;
	}

	/**
	 * Checks if the page is a descendant of the given page
	 *
	 * @param \Kirby\Cms\Page|string $parent
	 */
	public function isDescendantOf($parent): bool
	{
		if (is_string($parent) === true) {
			$parent = $this->site()->find($parent);
		}

		if (!$parent) {
			return false;
		}

		return $this->parents()->has($parent->id()) === true;
	}

	/**
	 * Checks if the page is a descendant of the currently active page
	 */
	public function isDescendantOfActive(): bool
	{
		if ($active = $this->site()->page()) {
			return $this->isDescendantOf($active);
		}

		return false;
	}

	/**
	 * Checks if the current page is a draft
	 */
	public function isDraft(): bool
	{
		return $this->isDraft;
	}

	/**
	 * Checks if the page is the error page
	 */
	public function isErrorPage(): bool
	{
		return $this->id() === $this->site()->errorPageId();
	}

	/**
	 * Checks if the page is the home page
	 */
	public function isHomePage(): bool
	{
		return $this->id() === $this->site()->homePageId();
	}

	/**
	 * It's often required to check for the
	 * home and error page to stop certain
	 * actions. That's why there's a shortcut.
	 */
	public function isHomeOrErrorPage(): bool
	{
		return $this->isHomePage() === true || $this->isErrorPage() === true;
	}

	/**
	 * Check if the page can be listable by the current user
	 * This permission depends on the `read` option until v6
	 */
	public function isListable(): bool
	{
		// TODO: remove this check when `read` option deprecated in v6
		if ($this->isReadable() === false) {
			return false;
		}

		// not accessible also means not listable
		if ($this->isAccessible() === false) {
			return false;
		}

		return PagePermissions::canFromCache($this, 'list');
	}

	/**
	 * Checks if the page has a sorting number
	 */
	public function isListed(): bool
	{
		return $this->isPublished() && $this->num() !== null;
	}

	public function isMovableTo(Page|Site $parent): bool
	{
		try {
			PageRules::move($this, $parent);
			return true;
		} catch (Throwable) {
			return false;
		}
	}

	/**
	 * Checks if the page is open.
	 * Open pages are either the current one
	 * or descendants of the current one.
	 */
	public function isOpen(): bool
	{
		if ($this->isActive() === true) {
			return true;
		}

		if ($this->site()->page()?->parents()->has($this->id()) === true) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if the page is not a draft.
	 */
	public function isPublished(): bool
	{
		return $this->isDraft() === false;
	}

	/**
	 * Check if the page can be read by the current user
	 * @todo Deprecate `read` option in v6 and make the necessary changes for `access` and `list` options.
	 */
	public function isReadable(): bool
	{
		static $readable   = [];
		$role              = $this->kirby()->role()?->id() ?? '__none__';
		$template          = $this->intendedTemplate()->name();
		$readable[$role] ??= [];

		return $readable[$role][$template] ??= $this->permissions()->can('read');
	}

	/**
	 * Checks if the page is sortable
	 */
	public function isSortable(): bool
	{
		return $this->permissions()->can('sort');
	}

	/**
	 * Checks if the page has no sorting number
	 */
	public function isUnlisted(): bool
	{
		return $this->isPublished() && $this->num() === null;
	}

	/**
	 * Returns the absolute path to the media folder for the page
	 */
	#[BlockCollectionAccess]
	public function mediaDir(): string
	{
		return $this->kirby()->root('media') . '/pages/' . $this->id();
	}

	/**
	 * @see `::mediaDir`
	 */
	#[BlockCollectionAccess]
	public function mediaRoot(): string
	{
		return $this->mediaDir();
	}

	/**
	 * The page's base URL for any files
	 */
	public function mediaUrl(): string
	{
		return $this->kirby()->url('media') . '/pages/' . $this->id();
	}

	/**
	 * Returns the last modification date of the page
	 */
	public function modified(
		string|null $format = null,
		string|null $handler = null,
		string|null $languageCode = null
	): int|string|false|null {
		$modified = $this->version()->modified(
			$languageCode ?? 'current'
		);

		if ($modified === null) {
			return null;
		}

		return Str::date($modified, $format, $handler);
	}

	/**
	 * Returns the sorting number
	 */
	public function num(): int|null
	{
		return $this->num;
	}

	/**
	 * Returns the panel info object
	 */
	public function panel(): Panel
	{
		return new Panel($this);
	}

	/**
	 * Returns the parent Page object
	 */
	public function parent(): Page|null
	{
		return $this->parent;
	}

	/**
	 * Returns the parent id, if a parent exists
	 */
	public function parentId(): string|null
	{
		return $this->parent()?->id();
	}

	/**
	 * Returns the parent model,
	 * which can either be another Page
	 * or the Site
	 */
	public function parentModel(): Page|Site
	{
		return $this->parent() ?? $this->site();
	}

	/**
	 * Returns a list of all parents and their parents recursively
	 */
	public function parents(): Pages
	{
		$parents = new Pages();
		$page    = $this->parent();

		while ($page instanceof Page) {
			$parents->append($page->id(), $page);
			$page = $page->parent();
		}

		return $parents;
	}

	/**
	 * Return the permanent URL to the page using its UUID
	 * @since 3.8.0
	 */
	public function permalink(): string|null
	{
		return $this->uuid()?->toPermalink();
	}

	/**
	 * Returns the permissions object for this page
	 */
	public function permissions(): PagePermissions
	{
		return new PagePermissions($this);
	}

	/**
	 * Returns the preview URL with authentication for drafts and versions
	 * @unstable
	 */
	#[BlockCollectionAccess]
	public function previewUrl(VersionId|string $versionId = 'latest'): string|null
	{
		if ($this->permissions()->can('preview') !== true) {
			return null;
		}

		return $this->version($versionId)->url();
	}

	/**
	 * Renders the page with the given data.
	 *
	 * An optional content type can be passed to
	 * render a content representation instead of
	 * the default template.
	 *
	 * @param string $contentType
	 * @param \Kirby\Content\VersionId|string|null $versionId Optional override for the auto-detected version to render
	 * @throws \Kirby\Exception\NotFoundException If the default template cannot be found
	 */
	#[BlockCollectionAccess]
	public function render(
		array $data = [],
		$contentType = 'html',
		VersionId|string|null $versionId = null
	): string {
		$kirby = $this->kirby();
		$cache = $cacheId = $html = null;

		// if not manually overridden, first use a globally set
		// version ID (e.g. when rendering within another render),
		// otherwise auto-detect from the request and fall back to
		// the latest version if request is unauthenticated (no valid token);
		// make sure to convert it to an object no matter what happened
		$versionId ??= VersionId::$render;
		$versionId ??= $this->renderVersionFromRequest();
		$versionId ??= 'latest';
		$versionId   = VersionId::from($versionId);

		// try to get the page from cache
		if ($data === [] && $this->isCacheable($versionId) === true) {
			$cache       = $kirby->cache('pages');
			$cacheId     = $this->cacheId($contentType, $versionId);
			$result      = $cache->get($cacheId);
			$html        = $result['html'] ?? null;
			$response    = $result['response'] ?? [];
			$usesAuth    = $result['usesAuth'] ?? false;
			$usesCookies = $result['usesCookies'] ?? [];

			// if the request contains dynamic data that the cached response
			// relied on, don't use the cache to allow dynamic code to run
			if (Responder::isPrivate($usesAuth, $usesCookies) === true) {
				$html = null;
			}

			// reconstruct the response configuration
			if (empty($html) === false && empty($response) === false) {
				$kirby->response()->fromArray($response);
			}
		}

		// fetch the page regularly
		if ($html === null) {
			// set `VersionId::$render` to the intended version (only) while we render
			$html = VersionId::render($versionId, function () use ($kirby, $data, $contentType) {
				$template = match ($contentType) {
					'html'  => $this->template(),
					default => $this->representation($contentType)
				};

				if ($template->exists() === false) {
					throw new NotFoundException(
						key: 'template.default.notFound'
					);
				}

				$kirby->data = $this->controller($data, $contentType);

				// trigger before hook and apply for `data`
				$kirby->data = $kirby->apply('page.render:before', [
					'contentType' => $contentType,
					'data'        => $kirby->data,
					'page'        => $this
				], 'data');

				// render the page
				$html = $template->render($kirby->data);

				// trigger after hook and apply for `html`
				return $kirby->apply('page.render:after', [
					'contentType' => $contentType,
					'data'        => $kirby->data,
					'html'        => $html,
					'page'        => $this
				], 'html');
			});

			// cache the result
			$response = $kirby->response();
			if ($cache !== null && $response->cache() === true) {
				$cache->set($cacheId, [
					'html'        => $html,
					'response'    => $response->toCacheArray(),
					'usesAuth'    => $response->usesAuth(),
					'usesCookies' => $response->usesCookies(),
				], $response->expires() ?? 0);
			}
		}

		return $html;
	}

	/**
	 * Determines which version (if any) can be rendered
	 * based on the token authentication in the current request
	 * @unstable
	 */
	#[BlockCollectionAccess]
	public function renderVersionFromRequest(): VersionId|null
	{
		$request = $this->kirby()->request();
		$token   = $request->get('_token', '');

		try {
			$versionId = VersionId::from($request->get('_version', ''));
		} catch (InvalidArgumentException) {
			// ignore invalid enum values in the request
			$versionId = VersionId::latest();
		}

		// authenticated requests can always be trusted;
		// only resolve the expected token when one was sent
		if (
			$token !== '' &&
			hash_equals($this->version($versionId)->previewToken(), $token) === true
		) {
			return $versionId;
		}

		// published pages with published parents can render
		// the latest version without (valid) token
		if (
			$this->isPublished() === true &&
			$this->parents()->findBy('status', 'draft') === null
		) {
			return VersionId::latest();
		}

		// drafts cannot be accessed without authentication
		return null;
	}

	/**
	 * @throws \Kirby\Exception\NotFoundException If the content representation cannot be found
	 */
	#[BlockCollectionAccess]
	public function representation(mixed $type): Template
	{
		$kirby          = $this->kirby();
		$template       = $this->template();
		$representation = $kirby->template($template->name(), $type);

		if ($representation->exists() === true) {
			return $representation;
		}

		throw new NotFoundException(
			message: 'The content representation cannot be found'
		);
	}

	/**
	 * Returns the absolute root to the page directory
	 * No matter if it exists or not.
	 */
	#[BlockCollectionAccess]
	public function root(): string
	{
		return $this->root ??= $this->kirby()->root('content') . '/' . $this->diruri();
	}

	/**
	 * Returns the PageRules class instance
	 * which is being used in various methods
	 * to check for valid actions and input.
	 */
	protected function rules(): PageRules
	{
		return new PageRules();
	}

	/**
	 * Search all pages within the current page
	 */
	#[BlockCollectionAccess]
	public function search(string|null $query = null, string|array $params = []): Pages
	{
		return $this->index()->search($query, $params);
	}

	/**
	 * Sets the Blueprint object
	 *
	 * @return $this
	 */
	protected function setBlueprint(array|null $blueprint = null): static
	{
		if ($blueprint !== null) {
			$blueprint['model'] = $this;
			$this->blueprint = new PageBlueprint($blueprint);
		}

		return $this;
	}

	/**
	 * Sets the intended template
	 *
	 * @return $this
	 */
	protected function setTemplate(string|null $template = null): static
	{
		if ($template !== null) {
			$this->intendedTemplate = $this->kirby()->template(strtolower($template));
		}

		return $this;
	}

	/**
	 * Sets the Url
	 *
	 * @return $this
	 */
	protected function setUrl(string|null $url = null): static
	{
		if (is_string($url) === true) {
			$url = rtrim($url, '/');
		}

		$this->url = $url;
		return $this;
	}

	/**
	 * Returns the slug of the page
	 */
	public function slug(string|null $languageCode = null): string
	{
		if ($this->kirby()->multilang() === true) {
			$languageCode      ??= $this->kirby()->languageCode();
			$defaultLanguageCode = $this->kirby()->defaultLanguage()->code();

			if (
				$languageCode !== $defaultLanguageCode &&
				$translation = $this->translations()->find($languageCode)
			) {
				return $translation->slug() ?? $this->slug;
			}
		}

		return $this->slug;
	}

	/**
	 * Returns the page status, which
	 * can be `draft`, `listed` or `unlisted`
	 */
	public function status(): string
	{
		if ($this->isDraft() === true) {
			return 'draft';
		}

		if ($this->isUnlisted() === true) {
			return 'unlisted';
		}

		return 'listed';
	}

	/**
	 * Returns the final template
	 */
	public function template(): Template
	{
		if ($this->template !== null) {
			return $this->template;
		}

		$intended = $this->intendedTemplate();

		if ($intended->exists() === true) {
			return $this->template = $intended;
		}

		return $this->template = $this->kirby()->template('default');
	}

	/**
	 * Returns the title field or the slug as fallback
	 */
	public function title(): Field
	{
		return $this->content()->get('title')->or($this->slug());
	}

	/**
	 * Converts the most important
	 * properties to array
	 */
	#[BlockCollectionAccess]
	public function toArray(): array
	{
		return [
			...parent::toArray(),
			'children'  => $this->children()->keys(),
			'files'     => $this->files()->keys(),
			'id'        => $this->id(),
			'mediaUrl'  => $this->mediaUrl(),
			'mediaRoot' => $this->mediaRoot(),
			'num'       => $this->num(),
			'parent'    => $this->parent()?->id(),
			'slug'      => $this->slug(),
			'template'  => $this->template(),
			'uid'       => $this->uid(),
			'uri'       => $this->uri(),
			'url'       => $this->url()
		];
	}

	/**
	 * Returns the UID of the page.
	 * The UID is basically the same as the
	 * slug, but stays the same on
	 * multi-language sites. Whereas the slug
	 * can be translated.
	 *
	 * @see self::slug()
	 */
	public function uid(): string
	{
		return $this->slug;
	}

	/**
	 * The uri is the same as the id, except
	 * that it will be translated in multi-language setups
	 */
	public function uri(string|null $languageCode = null): string
	{
		// set the id, depending on the parent
		if ($parent = $this->parent()) {
			return $parent->uri($languageCode) . '/' . $this->slug($languageCode);
		}

		return $this->slug($languageCode);
	}

	/**
	 * Returns the Url
	 *
	 * @param array|string|null $options
	 */
	public function url($options = null): string
	{
		if ($this->kirby()->multilang() === true) {
			if (is_string($options) === true) {
				return $this->urlForLanguage($options);
			}

			return $this->urlForLanguage(null, $options);
		}

		if ($options !== null) {
			return Url::to($this->url(), $options);
		}

		if (is_string($this->url) === true) {
			return $this->url;
		}

		if ($this->isHomePage() === true) {
			return $this->url = $this->site()->url();
		}

		if ($parent = $this->parent()) {
			if ($parent->isHomePage() === true) {
				return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid();
			}

			return $this->url = $this->parent()->url() . '/' . $this->uid();
		}

		return $this->url = $this->kirby()->url('base') . '/' . $this->uid();
	}

	/**
	 * Builds the Url for a specific language
	 *
	 * @internal
	 * @param string|null $language
	 */
	public function urlForLanguage(
		$language = null,
		array|null $options = null
	): string {
		if ($options !== null) {
			return Url::to($this->urlForLanguage($language), $options);
		}

		if ($this->isHomePage() === true) {
			return $this->url = $this->site()->urlForLanguage($language);
		}

		if ($parent = $this->parent()) {
			if ($parent->isHomePage() === true) {
				return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language);
			}

			return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language);
		}

		return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language);
	}
}
