<?php

namespace SHServ\Controllers;

use \SHServ\Models\Areas;
use \SHServ\Entities\Area;
use \SHServ\Middleware\ControlScripts;
use \SHServ\Integrations\GAuth\AuthControllerTrait;

/**
 * Контроллер реализующий REST API для работы с абстрактным описанием физических областей.
 * Подчёркиваю, это Areas, а не Spaces. Реализована логика работы именно с физическим областями, а не абстрактным пространством.
 */
class AreasRESTAPIController extends \SHServ\Middleware\Controller {
	use AuthControllerTrait;
	/**
	 * Получить список пространств в системе. Всех или у родительского пространства
	 * @param  Int $area_id Area ID
	 * @return String JSON. Вернёт массив областей в поле `areas` и  количество в поле `total`
	 */
	public function areas_list($area_id = 0) {
		if ($auth = $this -> require_permission('areas.view')) { return $auth; }
		$areas_model = new Areas();

		if(!$area_id) {
			$areas = $areas_model -> get_all();
		} else {
			$parent_area = new Area(intval($area_id));
			if(!$parent_area) {
				return $this -> utils() -> response_error("parent_area_not_found");
			}

			$areas = $parent_area -> get_inner_areas();
		}

		$response = [];

		foreach($areas as $i => $area) {
			$response[] = $area -> to_array();
		}

		logging() -> trace('php:Areas', 'Areas list fetched', ['total' => count($response)]);
		return $this -> utils() -> response_success([
			"areas" => $response,
			"total" => count($response)
		]);
	}

	/**
	 * Добавить новую физическую область
	 * @param  String $type         Произвольный тип области. Это может быть например комната, этаж, здание, или двор. Ограничений нет, но тип должен описывать именно физическую область.
	 * @param  String $alias        Псевдоним для обращения к области из кода. Обязательно должен быть уникальным.
	 * @param  String $display_name Отображаемое название/имя области
	 * @return String               JSON. Возвращает поля созданной `area` и `alias` или ошибку.
	 */
	public function new_area($type, $alias, $display_name) {
		if ($auth = $this -> require_permission('areas.manage')) { return $auth; }
		$areas_model = new Areas();

		if(!preg_match('/^[a-z0-9_]+$/', $alias) || strlen($alias) > 255) {
			logging() -> warn('php:Areas', 'New area failed: invalid alias', ['alias' => $alias]);
			return $this -> utils() -> response_error("invalid_alias", ["alias"]);
		}

		if(!$areas_model -> alias_is_uniq($alias)) {
			logging() -> warn('php:Areas', 'New area failed: alias already exists', ['alias' => $alias]);
			return $this -> utils() -> response_error("alias_already_exists", ["alias"]);
		}

		if(!strlen($type)) {
			return $this -> utils() -> response_error("empty_field", ["type"]);
		}

		if(!strlen($display_name)) {
			return $this -> utils() -> response_error("empty_field", ["display_name"]);
		}

		$area = $areas_model -> create_new_area($type, $alias, $display_name);

		if(!$area) {
			logging() -> warn('php:Areas', 'New area failed: creation error', ['alias' => $alias]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		logging() -> info('php:Areas', 'New area created', ['area_id' => $area -> id(), 'alias' => $area -> alias, 'type' => $type]);
		return $this -> utils() -> response_success([
			"alias" => $area -> alias,
			"area" => $area -> to_array()
		]);
	}

	/**
	 * Удалить область. При этом все помещённые в неё устройства и child области будут отвязаны
	 * @param  Int $area_id Area ID
	 * @return String       JSON. Вернёт ошибку или положительный результат
	 */
	public function remove_area($area_id) {
		if ($auth = $this -> require_permission('areas.manage')) { return $auth; }
		if($area_id != intval($area_id) or intval($area_id) < 1) {
			return $this -> utils() -> response_error("invalid_id", ["area_id"]);
		}

		$area = new Area(intval($area_id));

		if(!$area) {
			logging() -> warn('php:Areas', 'Remove area failed: not found', ['area_id' => $area_id]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		logging() -> warn('php:Areas', 'Remove area', ['area_id' => $area -> id(), 'alias' => $area -> alias]);

		if(!$area -> remove()) {
			logging() -> warn('php:Areas', 'Remove area failed', ['area_id' => $area -> id()]);
			return $this -> utils() -> response_error("undefined_error");
		}

		return $this -> utils() -> response_success();
	}

	/**
	 * Поместить/вложить одну область в другую. Например комнату можна поместить в этаж, этаж в здание, а здание в глобальную область типа "дом/комплекс/т.п.".
	 * @param  Int $target_id   Area ID целевой области, которую хотим поместить
	 * @param  Int $place_in_area_id Area ID родительской области в которую хотим поместить целевую область
	 * @return String                JSON. Вернёт ошибку или положительный результат
	 */
	public function place_in_area($target_id, $place_in_area_id) {
		if ($auth = $this -> require_permission('areas.manage')) { return $auth; }
		if ($error = $this -> validate_positive_int_ids([
			'target_id' => $target_id,
			'place_in_area_id' => $place_in_area_id,
		])) {
			return $error;
		}

		$target_area = new Area(intval($target_id));
		$place_area = new Area(intval($place_in_area_id));

		if(!$target_area or !$place_area) {
			logging() -> warn('php:Areas', 'Place area in area failed: not found', ['target_id' => $target_id, 'place_in_area_id' => $place_in_area_id]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		logging() -> info('php:Areas', 'Place area in area', ['target_id' => $target_area -> id(), 'target_alias' => $target_area -> alias, 'parent_id' => $place_area -> id()]);

		if(!$target_area -> place_in_area($place_area)) {
			logging() -> warn('php:Areas', 'Place area in area failed', ['target_id' => $target_area -> id(), 'parent_id' => $place_area -> id()]);
			return $this -> utils() -> response_error("undefined_error");
		}

		return $this -> utils() -> response_success();
	}

	/**
	 * Переименовать область
	 * @param  Int $area_id         Area ID области, которую переименовываем
	 * @param  String $display_name Новое название/имя области
	 * @return String               JSON. Возвращает поля `area` с данными об изменённой области и `alias`.
	 */
	public function update_display_name($area_id, $display_name) {
		if ($auth = $this -> require_permission('areas.manage')) { return $auth; }
		if($area_id != intval($area_id) or intval($area_id) < 1) {
			return $this -> utils() -> response_error("invalid_id");
		}

		if(!strlen($display_name)) {
			return $this -> utils() -> response_error("empty_field", ["display_name"]);
		}

		$area = new Area(intval($area_id));

		if(!$area) {
			logging() -> warn('php:Areas', 'Update area display name failed: not found', ['area_id' => $area_id]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		logging() -> info('php:Areas', 'Update area display name', ['area_id' => $area -> id(), 'alias' => $area -> alias, 'display_name' => $display_name]);

		$area -> display_name = $display_name;

		return $area -> update()
			? $this -> utils() -> response_success([
					"alias" => $area -> alias,
					"area" => $area -> to_array()
				])
			: $this -> utils() -> response_error("undefined_error");
	}

	/**
	 * Измениеть псевдоним для обращения к области из кода.
	 * ОСТОРОЖНО! если область уже учавствует в написанных скриптах, это сломает работу скриптов.
	 * @param  Int $area_id   Area ID
	 * @param  String $new_alias Новый уникальный псевдоним области
	 * @return String            JSON. Возвращает поля `area` с данными об изменённой области и `alias`. 
	 */
	public function update_alias($area_id, $new_alias) {
		if ($auth = $this -> require_permission('areas.manage')) { return $auth; }
		if($area_id != intval($area_id) or intval($area_id) < 1) {
			return $this -> utils() -> response_error("invalid_id", ["area_id"]);
		}

		if(!preg_match('/^[a-z0-9_]+$/', $new_alias) || strlen($new_alias) > 255) {
			logging() -> warn('php:Areas', 'Update area alias failed: invalid alias', ['area_id' => $area_id, 'new_alias' => $new_alias]);
			return $this -> utils() -> response_error("invalid_alias", ["new_alias"]);
		}

		$areas_model = new Areas();

		if(!$areas_model -> alias_is_uniq($new_alias, intval($area_id))) {
			logging() -> warn('php:Areas', 'Update area alias failed: already exists', ['area_id' => $area_id, 'new_alias' => $new_alias]);
			return $this -> utils() -> response_error("alias_already_exists", ["alias"]);
		}

		$area = new Area(intval($area_id));

		if(!$area) {
			logging() -> warn('php:Areas', 'Update area alias failed: not found', ['area_id' => $area_id]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		logging() -> info('php:Areas', 'Update area alias', ['area_id' => $area -> id(), 'old_alias' => $area -> alias, 'new_alias' => $new_alias]);

		$area -> alias = $new_alias;

		return $area -> update()
			? $this -> utils() -> response_success([
					"alias" => $area -> alias,
					"area" => $area -> to_array()
				])
			: $this -> utils() -> response_error("undefined_error");
	}

	/**
	 * Перезагрузить все устройства, что помещены в указанную область.
	 * @param  Int $area_id Area ID области в которой нужно перезагрузить все устройства.
	 * @return String       JSON. При положительном ответе вернётся `total` - количество устройств, что были перезагружены, 
	 * и `results` - ассоциативный массив с ключами вида `area_alias:device_alias` и телом ответа от устройства в качестве значения.
	 */
	public function reboot_devices($area_id = 0) {
		if ($auth = $this -> require_permission('devices.control')) { return $auth; }
		$area = new Area(intval($area_id));

		if(!$area) {
			logging() -> warn('php:Areas', 'Reboot devices in area failed: not found', ['area_id' => $area_id]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		$devices = $area -> get_inner_devices(true);

		logging() -> info('php:Areas', 'Reboot devices in area', ['area_id' => $area -> id(), 'alias' => $area -> alias, 'count' => count($devices)]);

		$results = [];
		foreach($devices as $device) {
			$results["{$area -> alias}:{$device -> alias}"] = $device -> device_api() -> reboot();
		}

		return $this -> utils() -> response_success([
			"results" => $results,
			"total" => count($results)
		]);
	}

	/**
	 * Получить список устройств, что помещены в указанную область.
	 * @param  Int $area_id Area ID
	 * @return String       JSON. Возвращает массив устройств в поле `devices` и количество устрйоств в поле `total`
	 */
	public function devices_list($area_id) {
		if ($auth = $this -> require_permission('devices.view')) { return $auth; }
		if($area_id != intval($area_id) or intval($area_id) < 1) {
			return $this -> utils() -> response_error("invalid_id", ["area_id"]);
		}

		$area = new Area(intval($area_id));

		if(!$area) {
			return $this -> utils() -> response_error("area_not_exists");
		}

		$devices = $area -> get_inner_devices(true);

		logging() -> trace('php:Areas', 'Devices in area fetched', ['area_id' => $area_id, 'total' => count($devices)]);

		return $this -> utils() -> response_success([
			"devices" => array_map(function($device) {
				return $device -> to_array();
			}, $devices),
			"total" => count($devices)
		]);
	}

	/**
	 * Получить список скриптов, что помещены в указанную область
	 * @param  Int $area_id Area ID
	 * @return String  JSON. Массив скриптов в поле `scripts` и количество скриптов в поле `total`
	 */
	public function scripts_list($area_id) {
		if ($auth = $this -> require_permission('scripts.view')) { return $auth; }
		if($area_id != intval($area_id) or intval($area_id) < 1) {
			return $this -> utils() -> response_error("invalid_id", ["area_id"]);
		}

		$area = new Area(intval($area_id));

		if(!$area) {
			return $this -> utils() -> response_error("area_not_exists");
		}

		$scripts_base = [
			"scope" => [],
			"action" => ControlScripts::get_actions_scripts(),
			"regular" => ControlScripts::get_regular_scripts(),
		];

		$scripts = $area -> get_inner_scripts(true);
		$response_scripts = array_map(function($script) use ($scripts_base) {
			return isset($scripts_base[$script -> type][$script -> uniq_name])
				? $script -> prepare_to_view($scripts_base[$script -> type][$script -> uniq_name])
				: false;
		}, $scripts);

		$response_scripts = array_values(array_filter($response_scripts, function ($item) {
			return $item != false;
		}));

		logging() -> trace('php:Areas', 'Scripts in area fetched', ['area_id' => $area_id, 'total' => count($scripts)]);

		return $this -> utils() -> response_success([
			"scripts" => $response_scripts,
			"total" => count($scripts)
		]);
	}

	/**
	 * Отвязать указанную область от области в которую она почещена. Устройства, что помещены в эту область в ней останутся.
	 * @param  Int $target_id Area ID
	 * @return String       JSON. Вернётся ошибка или положительный ответ.
	 */
	public function unassign_from_area($target_id) {
		if ($auth = $this -> require_permission('areas.manage')) { return $auth; }
		if($target_id != intval($target_id) or intval($target_id) < 1) {
			return $this -> utils() -> response_error("invalid_id", ["target_id"]);
		}

		$area = new Area(intval($target_id));

		if(!$area) {
			logging() -> warn('php:Areas', 'Unassign area failed: not found', ['area_id' => $target_id]);
			return $this -> utils() -> response_error("area_not_exists");
		}

		logging() -> info('php:Areas', 'Unassign area', ['area_id' => $area -> id(), 'alias' => $area -> alias]);

		return $area -> place_in_area_id(0)
			? $this -> utils() -> response_success()
			: $this -> utils() -> response_error("undefined_error");
	}

	/**
	 * Получить список существующих в БД пользовательских типов областей. Это может быть полезно для выведения подсказок пользователю при добавлении новой области, 
	 * чтоб помогать не плодить множества одинаковых по смыслу, но разных по названию, типов.
	 * @return String JSON Вернёт поле `types` с массивом типов
	 */
	public function exists_types() {
		if ($auth = $this -> require_permission('areas.view')) { return $auth; }
		$types = (new Areas()) -> get_exists_types();
		logging() -> trace('php:Areas', 'Area types fetched', ['total' => count($types)]);
		return $this -> utils() -> response_success([
			"types" => $types
		]);
	}
}
