using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Launcher;
using Phantom.Agent.Minecraft.Launcher.Types;
using Phantom.Agent.Minecraft.Properties;
using Phantom.Agent.Minecraft.Server;
using Phantom.Agent.Rpc;
using Phantom.Agent.Services.Backups;
using Phantom.Common.Data;
using Phantom.Common.Data.Agent;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Logging;
using Phantom.Common.Messages.ToServer;
using Phantom.Utils.IO;
using Phantom.Utils.Tasks;
using Serilog;

namespace Phantom.Agent.Services.Instances;

sealed class InstanceSessionManager : IAsyncDisposable {
	private static readonly ILogger Logger = PhantomLogger.Create<InstanceSessionManager>();

	private readonly AgentInfo agentInfo;
	private readonly string basePath;

	private readonly InstanceServices instanceServices;
	private readonly Dictionary<Guid, Instance> instances = new ();

	private readonly CancellationTokenSource shutdownCancellationTokenSource = new ();
	private readonly CancellationToken shutdownCancellationToken;
	private readonly SemaphoreSlim semaphore = new (1, 1);

	private uint instanceLoggerSequenceId = 0;

	public InstanceSessionManager(AgentInfo agentInfo, AgentFolders agentFolders, JavaRuntimeRepository javaRuntimeRepository, TaskManager taskManager, BackupManager backupManager) {
		this.agentInfo = agentInfo;
		this.basePath = agentFolders.InstancesFolderPath;
		this.shutdownCancellationToken = shutdownCancellationTokenSource.Token;

		var minecraftServerExecutables = new MinecraftServerExecutables(agentFolders.ServerExecutableFolderPath);
		var launchServices = new LaunchServices(minecraftServerExecutables, javaRuntimeRepository);
		var portManager = new PortManager(agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);

		this.instanceServices = new InstanceServices(taskManager, portManager, backupManager, launchServices);
	}

	private async Task<InstanceActionResult<T>> AcquireSemaphoreAndRun<T>(Func<Task<InstanceActionResult<T>>> func) {
		try {
			await semaphore.WaitAsync(shutdownCancellationToken);
			try {
				return await func();
			} finally {
				semaphore.Release();
			}
		} catch (OperationCanceledException) {
			return InstanceActionResult.General<T>(InstanceActionGeneralResult.AgentShuttingDown);
		}
	}

	[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
	private Task<InstanceActionResult<T>> AcquireSemaphoreAndRunWithInstance<T>(Guid instanceGuid, Func<Instance, Task<T>> func) {
		return AcquireSemaphoreAndRun(async () => {
			if (instances.TryGetValue(instanceGuid, out var instance)) {
				return InstanceActionResult.Concrete(await func(instance));
			}
			else {
				return InstanceActionResult.General<T>(InstanceActionGeneralResult.InstanceDoesNotExist);
			}
		});
	}

	public async Task<InstanceActionResult<ConfigureInstanceResult>> Configure(InstanceConfiguration configuration, InstanceLaunchProperties launchProperties, bool launchNow, bool alwaysReportStatus) {
		return await AcquireSemaphoreAndRun(async () => {
			var instanceGuid = configuration.InstanceGuid;
			var instanceFolder = Path.Combine(basePath, instanceGuid.ToString());
			Directories.Create(instanceFolder, Chmod.URWX_GRX);

			var heapMegabytes = configuration.MemoryAllocation.InMegabytes;
			var jvmProperties = new JvmProperties(
				InitialHeapMegabytes: heapMegabytes / 2,
				MaximumHeapMegabytes: heapMegabytes
			);

			var properties = new InstanceProperties(
				instanceGuid,
				configuration.JavaRuntimeGuid,
				jvmProperties,
				configuration.JvmArguments,
				instanceFolder,
				configuration.MinecraftVersion,
				new ServerProperties(configuration.ServerPort, configuration.RconPort),
				launchProperties
			);

			IServerLauncher launcher = configuration.MinecraftServerKind switch {
				MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
				MinecraftServerKind.Fabric  => new FabricLauncher(properties),
				_                           => InvalidLauncher.Instance
			};

			if (instances.TryGetValue(instanceGuid, out var instance)) {
				await instance.Reconfigure(configuration, launcher, shutdownCancellationToken);
				Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);

				if (alwaysReportStatus) {
					instance.ReportLastStatus();
				}
			}
			else {
				instances[instanceGuid] = instance = new Instance(GetInstanceLoggerName(instanceGuid), instanceServices, configuration, launcher);
				Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);

				instance.ReportLastStatus();
				instance.IsRunningChanged += OnInstanceIsRunningChanged;
			}

			if (launchNow) {
				await LaunchInternal(instance);
			}

			return InstanceActionResult.Concrete(ConfigureInstanceResult.Success);
		});
	}

	private string GetInstanceLoggerName(Guid guid) {
		var prefix = guid.ToString();
		return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref instanceLoggerSequenceId);
	}

	private ImmutableArray<Instance> GetRunningInstancesInternal() {
		return instances.Values.Where(static instance => instance.IsRunning).ToImmutableArray();
	}

	private void OnInstanceIsRunningChanged(object? sender, EventArgs e) {
		instanceServices.TaskManager.Run("Handle instance running state changed event", RefreshAgentStatus);
	}

	public async Task RefreshAgentStatus() {
		try {
			await semaphore.WaitAsync(shutdownCancellationToken);
			try {
				var runningInstances = GetRunningInstancesInternal();
				var runningInstanceCount = runningInstances.Length;
				var runningInstanceMemory = runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation);
				await ServerMessaging.Send(new ReportAgentStatusMessage(runningInstanceCount, runningInstanceMemory));
			} finally {
				semaphore.Release();
			}
		} catch (OperationCanceledException) {
			// ignore
		}
	}

	public Task<InstanceActionResult<LaunchInstanceResult>> Launch(Guid instanceGuid) {
		return AcquireSemaphoreAndRunWithInstance(instanceGuid, LaunchInternal);
	}

	private async Task<LaunchInstanceResult> LaunchInternal(Instance instance) {
		var runningInstances = GetRunningInstancesInternal();
		if (runningInstances.Length + 1 > agentInfo.MaxInstances) {
			return LaunchInstanceResult.InstanceLimitExceeded;
		}

		var availableMemory = agentInfo.MaxMemory - runningInstances.Aggregate(RamAllocationUnits.Zero, static (total, instance) => total + instance.Configuration.MemoryAllocation);
		if (availableMemory < instance.Configuration.MemoryAllocation) {
			return LaunchInstanceResult.MemoryLimitExceeded;
		}

		return await instance.Launch(shutdownCancellationToken);
	}

	public Task<InstanceActionResult<StopInstanceResult>> Stop(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
		return AcquireSemaphoreAndRunWithInstance(instanceGuid, instance => instance.Stop(stopStrategy, shutdownCancellationToken));
	}

	public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
		return AcquireSemaphoreAndRunWithInstance(instanceGuid, async instance => await instance.SendCommand(command, shutdownCancellationToken) ? SendCommandToInstanceResult.Success : SendCommandToInstanceResult.UnknownError);
	}

	public async ValueTask DisposeAsync() {
		Logger.Information("Stopping all instances...");
		
		shutdownCancellationTokenSource.Cancel();

		await semaphore.WaitAsync(CancellationToken.None);
		await Task.WhenAll(instances.Values.Select(static instance => instance.DisposeAsync().AsTask()));
		instances.Clear();
		
		shutdownCancellationTokenSource.Dispose();
		semaphore.Dispose();
	}
}