<?php

namespace SHServ\Controllers;

use \SHServ\Models\Devices;
use \SHServ\Tools\DeviceScanner;
use \SHServ\Entities\Device;
use \SHServ\Entities\Area;
use \SHServ\Integrations\GAuth\AuthControllerTrait;

class DevicesRESTAPIController extends \SHServ\Middleware\Controller {
	use AuthControllerTrait;
	protected function withScanLock(callable $callback) {
		$tmpDir   = sys_get_temp_dir();
		$lockFile = $tmpDir . '/shserv-scan.lock';
		$debugLog = $tmpDir . '/shserv-scan-debug.log';

		$log = function($msg) use ($debugLog) {
			$line = date('Y-m-d H:i:s') . ' [' . getmypid() . '] ' . $msg . PHP_EOL;
			@file_put_contents($debugLog, $line, FILE_APPEND | LOCK_EX);
		};

		if (file_exists($lockFile) && (time() - filemtime($lockFile)) > 120) {
			$log('stale lock detected, unlinking');
			@unlink($lockFile);
		}

		$fp = @fopen($lockFile, 'c+');
		if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
			$log('lock busy (fp=' . var_export($fp, true) . '), returning 400');
			return $this -> utils() -> response_error("scan_in_progress");
		}

		ftruncate($fp, 0);
		fwrite($fp, getmypid());
		fflush($fp);
		touch($lockFile);

		$lockReleased = false;
		$releaseLock = function() use ($fp, $lockFile, $log, &$lockReleased) {
			if ($lockReleased) return;
			$lockReleased = true;
			flock($fp, LOCK_UN);
			fclose($fp);
			@unlink($lockFile);
			$log('lock released');
		};
		register_shutdown_function($releaseLock);

		try {
			$log('lock acquired, starting scan');
			$result = $callback();
			$log('scan finished normally');
			return $result;
		} catch (\Throwable $e) {
			$log('scan exception: ' . $e -> getMessage());
			throw $e;
		} finally {
			$releaseLock();
		}
	}

	public function scanning__ready_to_setup() {
		if ($auth = $this -> require_permission('devices.scan')) { return $auth; }
		return $this -> withScanLock(function() {
			$device_model = new Devices();
			$devices = $device_model -> get_unregistered_devices();

			logging() -> debug('php:Devices', 'Scan ready_to_setup finished', ['found' => count($devices)]);

			return $this -> utils() -> response_success([
				"devices" => $devices
			]);
		});
	}

	public function scanning__all() {
		if ($auth = $this -> require_permission('devices.scan')) { return $auth; }
		return $this -> withScanLock(function() {
			$device_model = new Devices();
			$devices = $device_model -> scanning_localnet(FCONF["device_ip_range"][0], FCONF["device_ip_range"][1]);

			// Update IP / connection_status for active devices that changed their address
			$device_model -> reconcile_scan_results($devices);

			logging() -> debug('php:Devices', 'Scan all finished', ['found' => count($devices)]);

			return $this -> utils() -> response_success([
				"devices" => $devices
			]);
		});
	}

	public function setup_new_device($device_ip, $alias, $name, $description) {
		if ($auth = $this -> require_permission('devices.setup')) { return $auth; }
		$devices_model = new \SHServ\Models\Devices();

		$host = $device_ip;
		if(strpos($device_ip, ':') !== false) {
			list($host, ) = explode(':', $device_ip, 2);
		}
		if(!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
			logging() -> warn('php:Devices', 'Setup new device failed: invalid IP', ['device_ip' => $device_ip]);
			return $this -> utils() -> response_error("invalid_ip", ["device_ip"]);
		}

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

		if(!strlen($name) || strlen($name) > 255) {
			logging() -> warn('php:Devices', 'Setup new device failed: empty name', ['name' => $name]);
			return $this -> utils() -> response_error("empty_field", ["name"]);
		}

		if(!$devices_model -> alias_is_uniq($alias)) {
			logging() -> warn('php:Devices', 'Setup new device failed: alias already exists', ['alias' => $alias]);
			return $this -> utils() -> response_error("alias_already_exists", ["alias"]);
		}

		logging() -> info('php:Devices', 'Setup new device attempt', ['device_ip' => $device_ip, 'alias' => $alias]);

		$result = $devices_model -> connect_new_device($device_ip, $alias, $name, $description);

		if($result and $result instanceof \SHServ\Entities\Device) {
			logging() -> info('php:Devices', 'Setup new device successful', ['device_id' => $result -> id(), 'alias' => $result -> alias]);
			$device_about = $result -> device_api() -> get_about();
			return $this -> utils() -> response_success([
				"device" => [
					"id" => $result -> id(),
					"alias" => $result -> alias,
					"name" => $result -> name,
					"description" => $result -> description,
					"device" => $device_about["data"]
				]
			]);
		}

		logging() -> warn('php:Devices', 'Setup new device failed', ['device_ip' => $device_ip, 'alias' => $alias, 'error' => $result["err_alias"] ?? 'unknown']);
		return $this -> utils() -> response_error($result["err_alias"]);
	}

	public function resetup_device($device_id) {
		if ($auth = $this -> require_permission('devices.setup')) { return $auth; }
		$devices_model = new \SHServ\Models\Devices();

		$device = $devices_model -> by_id(intval($device_id));

		if(!$device) {
			logging() -> warn('php:Devices', 'Resetup device failed: not found', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> info('php:Devices', 'Resetup device', ['device_id' => $device -> id(), 'alias' => $device -> alias]);

		$result = $devices_model -> resetup_device($device);

		if($result) {
			logging() -> info('php:Devices', 'Resetup device successful', ['device_id' => $device -> id()]);
			$device_about = $device -> device_api() -> get_about();
			return $this -> utils() -> response_success([
				"device" => [
					"id" => $device -> id(),
					"alias" => $device -> alias,
					"name" => $device -> name,
					"description" => $device -> description,
					"device" => $device_about["data"]
				]
			]);
		}

		logging() -> warn('php:Devices', 'Resetup device failed', ['device_id' => $device_id]);
		return $this -> utils() -> response_error();
	}

	public function remove_device($device_id) {
		if ($auth = $this -> require_permission('devices.delete')) { return $auth; }
		$devices_model  = new Devices();

		logging() -> warn('php:Devices', 'Remove device attempt', ['device_id' => $device_id]);
		$result = $devices_model -> remove_device(intval($device_id));

		if(!$result["result"]) {
			logging() -> warn('php:Devices', 'Remove device failed', ['device_id' => $device_id, 'error' => $result["err_alias"] ?? 'unknown', 'device_msg' => $result["device_msg"] ?? '']);
			return $this -> utils() -> response_error(
				$result["err_alias"],
				[],
				[ "device_msg" => $result["device_msg"] ]
			);
		}

		logging() -> info('php:Devices', 'Remove device successful', ['device_id' => $device_id, 'device_msg' => $result["device_msg"]]);
		return $this -> utils() -> response_success([
			"result" => $result["result"] ? true : false,
			"device_msg" => $result["device_msg"]
		]);
	}

	public function reboot_device($device_id) {
		if ($auth = $this -> require_permission('devices.control')) { return $auth; }
		$devices_model = new Devices();
		logging() -> info('php:Devices', 'Reboot device attempt', ['device_id' => $device_id]);
		$result = $devices_model -> reboot_device(intval($device_id));

		if(!$result["result"]) {
			logging() -> warn('php:Devices', 'Reboot device failed', ['device_id' => $device_id, 'error' => $result["err_alias"] ?? 'unknown']);
			return $this -> utils() -> response_error(
				$result["err_alias"],
				[],
				[ "device_msg" => $result["msg"] ?? "" ]
			);
		}

		logging() -> info('php:Devices', 'Reboot device successful', ['device_id' => $device_id]);
		return $this -> utils() -> response_success([
			"device_msg" => $result["msg"]
		]);
	}

	public function device_info($device_id) {
		if ($auth = $this -> require_permission('devices.view')) { return $auth; }
		$device_id = intval($device_id);
		$devices_model = new Devices();
		$result = $devices_model -> get_device_info($device_id);

		if(!$result) {
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> trace('php:Devices', 'Device info fetched', ['device_id' => $device_id]);
		return $this -> utils() -> response_success([ "device" => $result ]);
	}

	public function device($device_id) {
		if ($auth = $this -> require_permission('devices.view')) { return $auth; }
		$devices_model = new Devices();
		$device = $devices_model -> by_id(intval($device_id));

		if(!$device) {
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> trace('php:Devices', 'Device fetched', ['device_id' => $device -> id(), 'alias' => $device -> alias]);

		$info = [
			"id" => $device -> id(),
			"alias" => $device -> alias,
			"device_type" => $device -> device_type,
			"device_hard_id" => $device -> device_hard_id,
			"device_mac" => $device -> device_mac,
			"device_ip" => $device -> device_ip,
			"firmware_version" => $device -> firmware_version,
			"name" => $device -> name,
			"description" => $device -> description,
			"status" => $device -> status,
			"connection_status" => $device -> connection_status,
			"area_id" => $device -> area_id,
			"last_contact" => $device -> last_contact,
			"create_at" => $device -> create_at,
			"update_at" => $device -> update_at
		];

		return $this -> utils() -> response_success([ "device" => $info ]);
	}

	public function device_status($device_id) {
		if ($auth = $this -> require_permission('devices.view')) { return $auth; }
		$devices_model = new Devices();
		$device = $devices_model -> by_id(intval($device_id));

		if(!$device) {
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> trace('php:Devices', 'Device status request', ['device_id' => $device -> id(), 'alias' => $device -> alias]);

		$device_status = $device -> device_api() -> get_status();

		if(!$device_status || ($device_status["status"] ?? null) !== "ok") {
			return $this -> utils() -> response_error("device_request_fail", [], [
				"device_id" => $device -> id(),
				"connection_status" => $device -> connection_status
			]);
		}

		if($device -> connection_status != "active") {
			$device -> connection_status = "active";
			$device -> update();
		}

		$response_data = [
			"id" => $device -> id(),
			"alias" => $device -> alias,
			"device_response" => $device_status
		];

		return $this -> utils() -> response_success(["device" => $response_data]);
	}

	public function do_device_action($device_id, $action, $params) {
		if ($auth = $this -> require_permission('devices.control')) { return $auth; }
		$devices_model = new Devices();
		$device = $devices_model -> by_id(intval($device_id));

		if(!$device) {
			logging() -> warn('php:Devices', 'Device action failed: not found', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

		if(is_string($params)) {
			$decoded = json_decode($params, true);
			if(json_last_error() !== JSON_ERROR_NONE) {
				logging() -> warn('php:Devices', 'Device action failed: invalid JSON params', ['device_id' => $device_id, 'action' => $action]);
				return $this -> utils() -> response_error("invalid_json", ["params"]);
			}
			$params = $decoded;
		}

		if(!is_array($params)) {
			logging() -> warn('php:Devices', 'Device action failed: invalid params', ['device_id' => $device_id, 'action' => $action]);
			return $this -> utils() -> response_error("invalid_params", ["params"]);
		}

		if(!preg_match('/^[a-z0-9_]+$/', $action) || strlen($action) > 255) {
			logging() -> warn('php:Devices', 'Device action failed: invalid action', ['device_id' => $device_id, 'action' => $action]);
			return $this -> utils() -> response_error("invalid_action", ["action"]);
		}

		logging() -> info('php:Devices', 'Device action', ['device_id' => $device -> id(), 'alias' => $device -> alias, 'action' => $action]);

		$result = $device -> device_api() -> post_action($action, $params);
		$device_response = $result["data"] ?? [];

		if(!$device_response) {
			logging() -> warn('php:Devices', 'Device action failed: empty response', ['device_id' => $device -> id(), 'action' => $action]);
			return $this -> utils() -> response_error("device_request_fail");
		}

		if($device_response["status"] != "ok") {
			logging() -> warn('php:Devices', 'Device action failed: device error', ['device_id' => $device -> id(), 'action' => $action, 'device_status' => $device_response["status"], 'device_msg' => $device_response["message"] ?? '']);
			return $this -> utils() -> response_error("device_request_fail", [], [
				"device_status" => $device_response["status"],
				"device_error" => $device_response["status"] ?? "",
				"device_msg" => $device_response["message"] ?? ""
			]);
		}

		logging() -> info('php:Devices', 'Device action successful', ['device_id' => $device -> id(), 'alias' => $device -> alias, 'action' => $action]);
		$response_data = [
			"id" => $device -> id(),
			"alias" => $device -> alias,
			"device_response" => $device_response
		];

		return $this -> utils() -> response_success(["device" => $response_data]);
	}

	public function place_in_area($target_id, $place_in_area_id) {
		if ($auth = $this -> require_permission('devices.edit')) { return $auth; }
		if ($error = $this -> validate_positive_int_ids([
			'target_id' => $target_id,
			'place_in_area_id' => $place_in_area_id,
		])) {
			return $error;
		}

		$device = new Device(intval($target_id));
		$place_area = new Area(intval($place_in_area_id));

		if(!$device) {
			logging() -> warn('php:Devices', 'Place device in area failed: not found', ['device_id' => $target_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

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

		logging() -> info('php:Devices', 'Place device in area', ['device_id' => $device -> id(), 'alias' => $device -> alias, 'area_id' => $place_area -> id()]);

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

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

	public function unassign_from_area($target_id) {
		if ($auth = $this -> require_permission('devices.edit')) { return $auth; }
		if ($error = $this -> validate_positive_int_ids(['target_id' => $target_id])) {
			return $error;
		}

		$device = new Device(intval($target_id));

		if(!$device) {
			logging() -> warn('php:Devices', 'Unassign device from area failed: not found', ['device_id' => $target_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> info('php:Devices', 'Unassign device from area', ['device_id' => $device -> id(), 'alias' => $device -> alias]);

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

	public function devices_list($status = "active") {
		if ($auth = $this -> require_permission('devices.view')) { return $auth; }
		$status_list = Device::get_status_list();
		if(!in_array($status, $status_list)) {
			return $this -> utils() -> response_error("wrong_status_name");
		}

		$devices_model = new Devices();
		$devices = $devices_model -> get_device_list($status);

		logging() -> trace('php:Devices', 'Devices list fetched', ['status' => $status, 'total' => count($devices)]);

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

	public function update_name($device_id, $name) {
		if ($auth = $this -> require_permission('devices.edit')) { return $auth; }
		$device_id = intval($device_id);

		if($device_id < 1) {
			return $this -> utils() -> response_error("invalid_id", ["device_id"]);
		}

		$device = new Device($device_id);

		if(!$device) {
			logging() -> warn('php:Devices', 'Update device name failed: not found', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

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

		logging() -> info('php:Devices', 'Update device name', ['device_id' => $device -> id(), 'alias' => $device -> alias, 'name' => $name]);

		if(!$device -> device_api() -> set_device_name($name)) {
			logging() -> warn('php:Devices', 'Update device name failed: device error', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_request_fail");
		}

		$device -> name = $name;
		$device -> update();

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

	public function update_description($device_id, $description) {
		if ($auth = $this -> require_permission('devices.edit')) { return $auth; }
		$device_id = intval($device_id);

		if($device_id < 1) {
			return $this -> utils() -> response_error("invalid_id", ["device_id"]);
		}

		$device = new Device($device_id);

		if(!$device) {
			logging() -> warn('php:Devices', 'Update device description failed: not found', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> info('php:Devices', 'Update device description', ['device_id' => $device -> id(), 'alias' => $device -> alias]);

		$device -> description = $description;
		$device -> update();

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

	public function update_alias($device_id, $new_alias) {
		if ($auth = $this -> require_permission('devices.edit')) { return $auth; }
		$device_id = intval($device_id);

		if($device_id < 1) {
			return $this -> utils() -> response_error("invalid_id", ["device_id"]);
		}

		$device = new Device($device_id);

		if(!$device) {
			logging() -> warn('php:Devices', 'Update device alias failed: not found', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

		$devices_model = new \SHServ\Models\Devices();

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

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

		logging() -> info('php:Devices', 'Update device alias', ['device_id' => $device -> id(), 'old_alias' => $device -> alias, 'new_alias' => $new_alias]);

		$device -> alias = $new_alias;
		$device -> update();

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

	public function reset_device($device_id) {
		if ($auth = $this -> require_permission('devices.edit')) { return $auth; }
		$device_id = intval($device_id);

		if($device_id < 1) {
			return $this -> utils() -> response_error("invalid_id", ["device_id"]);
		}

		$device = new Device($device_id);

		if(!$device) {
			logging() -> warn('php:Devices', 'Reset device failed: not found', ['device_id' => $device_id]);
			return $this -> utils() -> response_error("device_not_found");
		}

		logging() -> warn('php:Devices', 'Reset device', ['device_id' => $device -> id(), 'alias' => $device -> alias]);

		$result = $device -> device_api() -> reset();

		if(($result["http_code"] ?? 0) !== 200) {
			logging() -> warn('php:Devices', 'Reset device failed', ['device_id' => $device_id, 'http_code' => $result["http_code"] ?? 0]);
			return $this -> utils() -> response_error("device_request_fail", [], [
				"device_msg" => $result["error"] ?? ""
			]);
		}

		logging() -> info('php:Devices', 'Reset device successful', ['device_id' => $device -> id(), 'alias' => $device -> alias]);
		return $this -> utils() -> response_success();
	}
}
