<?php

namespace Kirby\Cms;

use Kirby\Exception\DuplicateException;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Exception\PermissionException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;

/**
 * Validators for all page actions
 *
 * @package   Kirby Cms
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 */
class PageRules
{
	/**
	 * Validates if the sorting number of the page can be changed
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the given number is invalid
	 */
	public static function changeNum(Page $page, int|null $num = null): void
	{
		if ($num !== null && $num < 0) {
			throw new InvalidArgumentException(key: 'page.num.invalid');
		}
	}

	/**
	 * Validates if the slug for the page can be changed
	 *
	 * @throws \Kirby\Exception\DuplicateException If a page with this slug already exists
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the slug
	 */
	public static function changeSlug(Page $page, string $slug): void
	{
		if ($page->permissions()->can('changeSlug') !== true) {
			throw new PermissionException(
				key: 'page.changeSlug.permission',
				data: ['slug' => $page->slug()]
			);
		}

		self::validateSlugLength($slug);
		self::validateSlugProtectedPaths($page, $slug);

		$siblings = $page->parentModel()->children();
		$drafts   = $page->parentModel()->drafts();

		if ($siblings->find($slug)?->is($page) === false) {
			throw new DuplicateException(
				key: 'page.duplicate',
				data: ['slug' => $slug]
			);
		}

		if ($drafts->find($slug)?->is($page) === false) {
			throw new DuplicateException(
				key: 'page.draft.duplicate',
				data: ['slug' => $slug]
			);
		}
	}

	/**
	 * Validates if the status for the page can be changed
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the given status is invalid
	 */
	public static function changeStatus(
		Page $page,
		string $status,
		int|null $position = null
	): void {
		if (isset($page->blueprint()->status()[$status]) === false) {
			throw new InvalidArgumentException(key: 'page.status.invalid');
		}

		match ($status) {
			'draft'     => static::changeStatusToDraft($page),
			'listed'    => static::changeStatusToListed($page, $position),
			'unlisted'  => static::changeStatusToUnlisted($page),
			default     => throw new InvalidArgumentException(
				key: 'page.status.invalid'
			)
		};
	}

	/**
	 * Validates if a page can be converted to a draft
	 *
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the page cannot be converted to a draft
	 */
	public static function changeStatusToDraft(Page $page): void
	{
		if ($page->permissions()->can('changeStatus') !== true) {
			throw new PermissionException(
				key: 'page.changeStatus.permission',
				data: ['slug' => $page->slug()]
			);
		}

		if ($page->isHomeOrErrorPage() === true) {
			throw new PermissionException(
				key: 'page.changeStatus.toDraft.invalid',
				data: ['slug' => $page->slug()]
			);
		}
	}

	/**
	 * Validates if the status of a page can be changed to listed
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the given position is invalid
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the status for the page cannot be changed by any user
	 */
	public static function changeStatusToListed(Page $page, int $position): void
	{
		// no need to check for status changing permissions,
		// instead we need to check for sorting permissions
		if ($page->isListed() === true) {
			if ($page->isSortable() !== true) {
				throw new PermissionException(
					key: 'page.sort.permission',
					data: ['slug' => $page->slug()]
				);
			}

			return;
		}

		static::publish($page);

		if ($position !== null && $position < 0) {
			throw new InvalidArgumentException(key: 'page.num.invalid');
		}
	}

	/**
	 * Validates if the status of a page can be changed to unlisted
	 *
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status
	 */
	public static function changeStatusToUnlisted(Page $page)
	{
		static::publish($page);
	}

	/**
	 * Validates if the template of the page can be changed
	 *
	 * @throws \Kirby\Exception\LogicException If the template of the page cannot be changed at all
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the template
	 */
	public static function changeTemplate(Page $page, string $template): void
	{
		if ($page->permissions()->can('changeTemplate') !== true) {
			throw new PermissionException(
				key: 'page.changeTemplate.permission',
				data: ['slug' => $page->slug()]
			);
		}

		$blueprints = $page->blueprints();

		if (
			count($blueprints) <= 1 ||
			in_array($template, array_column($blueprints, 'name'), true) === false
		) {
			throw new LogicException(
				key: 'page.changeTemplate.invalid',
				data: ['slug' => $page->slug()]
			);
		}
	}

	/**
	 * Validates if the title of the page can be changed
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the new title is empty
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title
	 */
	public static function changeTitle(Page $page, string $title): void
	{
		if ($page->permissions()->can('changeTitle') !== true) {
			throw new PermissionException(
				key: 'page.changeTitle.permission',
				data: ['slug' => $page->slug()]
			);
		}

		static::validateTitleLength($title);
	}

	/**
	 * Validates if the page can be created
	 *
	 * @throws \Kirby\Exception\DuplicateException If the same page or a draft already exists
	 * @throws \Kirby\Exception\InvalidArgumentException If the slug is invalid
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to create this page
	 */
	public static function create(Page $page): void
	{
		if ($page->permissions()->can('create') !== true) {
			throw new PermissionException(
				key: 'page.create.permission',
				data: ['slug' => $page->slug()]
			);
		}

		// creating a non-draft bypasses the normal publish flow;
		// enforce the same rules
		if ($page->isDraft() === false) {
			self::publish($page);
		}

		self::validateSlugLength($page->slug());
		self::validateSlugProtectedPaths($page, $page->slug());

		if ($page->exists() === true) {
			throw new DuplicateException(
				key: 'page.draft.duplicate',
				data: ['slug' => $page->slug()]
			);
		}

		$siblings = $page->parentModel()->children();
		$drafts   = $page->parentModel()->drafts();
		$slug     = $page->slug();

		if ($siblings->find($slug)) {
			throw new DuplicateException(
				key: 'page.duplicate',
				data: ['slug' => $slug]
			);
		}

		if ($drafts->find($slug)) {
			throw new DuplicateException(
				key: 'page.draft.duplicate',
				data: ['slug' => $slug]
			);
		}
	}

	/**
	 * Validates if the page can be deleted
	 *
	 * @throws \Kirby\Exception\LogicException If the page has children and should not be force-deleted
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the page
	 */
	public static function delete(Page $page, bool $force = false): void
	{
		if ($page->permissions()->can('delete') !== true) {
			throw new PermissionException(
				key: 'page.delete.permission',
				data: ['slug' => $page->slug()]
			);
		}

		if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) {
			throw new LogicException(key: 'page.delete.hasChildren');
		}
	}

	/**
	 * Validates if the page can be duplicated
	 *
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to duplicate the page
	 */
	public static function duplicate(
		Page $page,
		string $slug,
		array $options = []
	): void {
		if ($page->permissions()->can('duplicate') !== true) {
			throw new PermissionException(
				key: 'page.duplicate.permission',
				data: ['slug' => $page->slug()]
			);
		}

		self::validateSlugLength($slug);
	}

	/**
	 * Check if the page can be moved
	 * to the given parent
	 */
	public static function move(Page $page, Site|Page $parent): void
	{
		// if nothing changes, there's no need for checks
		if ($parent->is($page->parent()) === true) {
			return;
		}

		if ($page->permissions()->can('move') !== true) {
			throw new PermissionException(
				key: 'page.move.permission',
				data: ['slug' => $page->slug()]
			);
		}

		// the page cannot be moved into itself
		if (
			$parent instanceof Page &&
			(
				$page->is($parent) === true ||
				$page->isAncestorOf($parent) === true
			)
		) {
			throw new LogicException(key: 'page.move.ancestor');
		}

		// check for duplicates
		if ($parent->childrenAndDrafts()->find($page->slug())) {
			throw new DuplicateException(
				key: 'page.move.duplicate',
				data: ['slug' => $page->slug()]
			);
		}

		$allowed = [];

		// collect all allowed subpage templates
		// from all pages sections in the blueprint
		// (only consider page sections that list pages
		// of the targeted new parent page)
		$sections = array_filter(
			$parent->blueprint()->sections(),
			fn ($section) =>
				$section->type() === 'pages' &&
				$section->parent()->is($parent)
		);

		// check if the parent has at least one pages section
		if ($sections === []) {
			throw new LogicException([
				'key'  => 'page.move.noSections',
				'data' => [
					'parent' => $parent->id() ?? '/',
				]
			]);
		}

		// go through all allowed templates and
		// add the name to the allowlist
		foreach ($sections as $section) {
			foreach ($section->templates() as $template) {
				$allowed[] = $template;
			}
		}

		// check if the template of this page is allowed as subpage type
		// for the potential new parent
		if (
			$allowed !== [] &&
			in_array($page->intendedTemplate()->name(), $allowed) === false
		) {
			throw new PermissionException(
				key: 'page.move.template',
				data: [
					'template' => $page->intendedTemplate()->name(),
					'parent'   => $parent->id() ?? '/',
				]
			);
		}
	}

	/**
	 * Check if the page can be published
	 * (status change from draft to listed or unlisted)
	 */
	public static function publish(Page $page): void
	{
		if ($page->permissions()->can('changeStatus') !== true) {
			throw new PermissionException(
				key: 'page.changeStatus.permission',
				data: [
					'slug' => $page->slug()
				]
			);
		}

		if ($page->isDraft() === true && empty($page->errors()) === false) {
			throw new PermissionException(
				key:  'page.changeStatus.incomplete',
				details: $page->errors()
			);
		}
	}

	/**
	 * Validates if the page can be updated
	 *
	 * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the page
	 */
	public static function update(Page $page, array $content = []): void
	{
		if ($page->permissions()->can('update') !== true) {
			throw new PermissionException(
				key: 'page.update.permission',
				data: ['slug' => $page->slug()]
			);
		}
	}

	/**
	 * Ensures that the slug is not empty and doesn't exceed the maximum length
	 * to make sure that the directory name will be accepted by the filesystem
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the slug is empty or too long
	 */
	public static function validateSlugLength(string $slug): void
	{
		$slugLength = Str::length($slug);

		if ($slugLength === 0) {
			throw new InvalidArgumentException(key: 'page.slug.invalid');
		}

		if ($slugsMaxlength = App::instance()->option('slugs.maxlength', 255)) {
			$maxlength = (int)$slugsMaxlength;

			if ($slugLength > $maxlength) {
				throw new InvalidArgumentException(
					key: 'page.slug.maxlength',
					data: ['length' => $maxlength]
				);
			}
		}
	}


	/**
	 * Ensure that a top-level page path does not start with one of
	 * the reserved URL paths, e.g. for API or the Panel
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the page ID starts as one of the disallowed paths
	 */
	protected static function validateSlugProtectedPaths(
		Page $page,
		string $slug
	): void {
		if ($page->parent() === null) {
			$paths = A::map(
				['api', 'assets', 'media', 'panel'],
				fn ($url) => $page->kirby()->url($url, true)->path()->toString()
			);

			$index = array_search($slug, $paths);

			if ($index !== false) {
				throw new InvalidArgumentException(
					key: 'page.changeSlug.reserved',
					data: ['path' => $paths[$index]]
				);
			}
		}
	}

	/**
	 * Ensures that the page title is not empty
	 *
	 * @throws \Kirby\Exception\InvalidArgumentException If the title is empty
	 */
	public static function validateTitleLength(string $title): void
	{
		if (Str::length($title) === 0) {
			throw new InvalidArgumentException(key: 'page.changeTitle.empty');
		}
	}
}
