/*
 * Copyright (c) 2024 ETH Zürich, IT Services
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

using System;
using System.Linq;
using System.Threading.Tasks;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.System.Events;
using SafeExamBrowser.SystemComponents.Contracts.Registry;

namespace SafeExamBrowser.Monitoring.System.Components
{
	internal class Cursors
	{
		private static readonly string SYSTEM_PATH = $@"{Environment.ExpandEnvironmentVariables("%SystemRoot%")}\Cursors\";
		private static readonly string USER_PATH = $@"{Environment.ExpandEnvironmentVariables("%LocalAppData%")}\Microsoft\Windows\Cursors\";

		private readonly ILogger logger;
		private readonly IRegistry registry;

		internal event SentinelEventHandler CursorChanged;

		internal Cursors(ILogger logger, IRegistry registry)
		{
			this.logger = logger;
			this.registry = registry;
		}

		internal void StartMonitoring()
		{
			registry.ValueChanged += Registry_ValueChanged;

			if (registry.TryGetNames(RegistryValue.UserHive.Cursors_Key, out var names))
			{
				foreach (var name in names)
				{
					registry.StartMonitoring(RegistryValue.UserHive.Cursors_Key, name);
				}

				logger.Info("Started monitoring cursors.");
			}
			else
			{
				logger.Warn("Failed to start monitoring cursor registry values!");
			}
		}

		internal void StopMonitoring()
		{
			registry.ValueChanged -= Registry_ValueChanged;

			if (registry.TryGetNames(RegistryValue.UserHive.Cursors_Key, out var names))
			{
				foreach (var name in names)
				{
					registry.StopMonitoring(RegistryValue.UserHive.Cursors_Key, name);
				}

				logger.Info("Stopped monitoring cursors.");
			}
			else
			{
				logger.Warn("Failed to stop monitoring cursor registry values!");
			}
		}

		internal bool Verify()
		{
			logger.Info($"Starting cursor verification...");

			var success = registry.TryGetNames(RegistryValue.UserHive.Cursors_Key, out var cursors);

			if (success)
			{
				foreach (var cursor in cursors.Where(c => !string.IsNullOrWhiteSpace(c)))
				{
					success &= VerifyCursor(cursor);
				}

				if (success)
				{
					logger.Info("Cursor configuration successfully verified.");
				}
				else
				{
					logger.Warn("Cursor configuration is compromised!");
				}
			}
			else
			{
				logger.Error("Failed to verify cursor configuration!");
			}

			return success;
		}

		private void Registry_ValueChanged(string key, string name, object oldValue, object newValue)
		{
			if (key == RegistryValue.UserHive.Cursors_Key)
			{
				HandleCursorChange(key, name, oldValue, newValue);
			}
		}

		private void HandleCursorChange(string key, string name, object oldValue, object newValue)
		{
			var args = new SentinelEventArgs();

			logger.Warn($@"The cursor registry value '{key}\{name}' has changed from '{oldValue}' to '{newValue}'!");

			Task.Run(() => CursorChanged?.Invoke(args)).ContinueWith((_) =>
			{
				if (args.Allow)
				{
					registry.StopMonitoring(key, name);
				}
			});
		}

		private bool VerifyCursor(string cursor)
		{
			var success = true;

			success &= registry.TryRead(RegistryValue.UserHive.Cursors_Key, cursor, out var value);
			success &= !(value is string) || (value is string path && (string.IsNullOrWhiteSpace(path) || IsValidCursorPath(path)));

			if (!success)
			{
				if (value != default)
				{
					logger.Warn($"Configuration of cursor '{cursor}' is compromised: '{value}'!");
				}
				else
				{
					logger.Warn($"Failed to verify configuration of cursor '{cursor}'!");
				}
			}

			return success;
		}

		private bool IsValidCursorPath(string path)
		{
			return path.StartsWith(USER_PATH, StringComparison.OrdinalIgnoreCase) || path.StartsWith(SYSTEM_PATH, StringComparison.OrdinalIgnoreCase);
		}
	}
}