using System.Collections.Concurrent;
using System.Threading.Channels;
using Ardalis.GuardClauses;
using AsyncReadProcess;
using SharpIDE.Application.Features.Debugging;
using SharpIDE.Application.Features.Evaluation;
using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;

namespace SharpIDE.Application.Features.Run;

public class RunService
{
	private readonly ConcurrentDictionary<SharpIdeProjectModel, SemaphoreSlim> _projectLocks = [];
	public ConcurrentDictionary<SharpIdeFile, List<Breakpoint>> Breakpoints { get; } = [];
	private Debugger? _debugger; // TODO: Support multiple debuggers for multiple running projects
	public async Task RunProject(SharpIdeProjectModel project, bool isDebug = false)
	{
		Guard.Against.Null(project, nameof(project));
		Guard.Against.NullOrWhiteSpace(project.FilePath, nameof(project.FilePath), "Project file path cannot be null or empty.");
		await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);

		var semaphoreSlim = _projectLocks.GetOrAdd(project, new SemaphoreSlim(1, 1));
		var waitResult = await semaphoreSlim.WaitAsync(0).ConfigureAwait(false);
		if (waitResult is false) throw new InvalidOperationException($"Project {project.Name} is already running.");
		if (project.RunningCancellationTokenSource is not null) throw new InvalidOperationException($"Project {project.Name} is already running with a cancellation token source.");

		project.RunningCancellationTokenSource = new CancellationTokenSource();
		var launchProfiles = await LaunchSettingsParser.GetLaunchSettingsProfiles(project);
		var launchProfile = launchProfiles.FirstOrDefault();
		try
		{
			var processStartInfo = new ProcessStartInfo2
			{
				FileName = "dotnet",
				WorkingDirectory = Path.GetDirectoryName(project.FilePath),
				//Arguments = $"run --project \"{project.FilePath}\" --no-restore",
				Arguments = GetRunArguments(project),
				RedirectStandardOutput = true,
				RedirectStandardError = true,
				EnvironmentVariables = []
			};
			processStartInfo.EnvironmentVariables["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = "1";
			// processStartInfo.EnvironmentVariables["TERM"] = "xterm"; // may be necessary on linux/macOS
			if (launchProfile is not null)
			{
				foreach (var envVar in launchProfile.EnvironmentVariables)
				{
					processStartInfo.EnvironmentVariables[envVar.Key] = envVar.Value;
				}
				if (launchProfile.ApplicationUrl != null) processStartInfo.EnvironmentVariables["ASPNETCORE_URLS"] = launchProfile.ApplicationUrl;
			}
			if (isDebug)
			{
				processStartInfo.EnvironmentVariables["DOTNET_DefaultDiagnosticPortSuspend"] = "1";
			}

			var process = new Process2
			{
				StartInfo = processStartInfo
			};

			process.Start();

			project.RunningOutputChannel = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions
			{
				SingleReader = true,
				SingleWriter = false,
			});
			var logsDrained = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
			_ = Task.Run(async () =>
			{
				await foreach(var log in process.CombinedOutputChannel.Reader.ReadAllAsync().ConfigureAwait(false))
				{
					//var logString = System.Text.Encoding.UTF8.GetString(log, 0, log.Length);
					//Console.Write(logString);
					await project.RunningOutputChannel.Writer.WriteAsync(log).ConfigureAwait(false);
				}
				project.RunningOutputChannel.Writer.Complete();
				logsDrained.TrySetResult();
			});

			if (isDebug)
			{
				// Attach debugger (which internally uses a DiagnosticClient to resume startup)
				var debugger = new Debugger { Project = project, ProcessId = process.ProcessId };
				_debugger = debugger;
				await debugger.Attach(project.RunningCancellationTokenSource.Token, Breakpoints.ToDictionary()).ConfigureAwait(false);
			}

			project.Running = true;
			project.OpenInRunPanel = true;
			if (isDebug)
			{
				GlobalEvents.Instance.ProjectStartedDebugging.InvokeParallelFireAndForget(project);
			}
			else
			{
				GlobalEvents.Instance.ProjectsRunningChanged.InvokeParallelFireAndForget();
				GlobalEvents.Instance.StartedRunningProject.InvokeParallelFireAndForget();
				GlobalEvents.Instance.ProjectStartedRunning.InvokeParallelFireAndForget(project);
			}
			project.InvokeProjectStartedRunning();
			await process.WaitForExitAsync().WaitAsync(project.RunningCancellationTokenSource.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
			if (project.RunningCancellationTokenSource.IsCancellationRequested)
			{
				process.End();
				await process.WaitForExitAsync().ConfigureAwait(false);
			}

			await logsDrained.Task.ConfigureAwait(false);
			project.RunningCancellationTokenSource.Dispose();
			project.RunningCancellationTokenSource = null;
			project.Running = false;
			if (isDebug)
			{
				GlobalEvents.Instance.ProjectStoppedDebugging.InvokeParallelFireAndForget(project);
			}
			else
			{
				GlobalEvents.Instance.ProjectsRunningChanged.InvokeParallelFireAndForget();
				GlobalEvents.Instance.ProjectStoppedRunning.InvokeParallelFireAndForget(project);
			}

			project.InvokeProjectStoppedRunning();

			Console.WriteLine("Project finished running");
		}
		finally
		{
			semaphoreSlim.Release();
		}
	}

	public async Task CancelRunningProject(SharpIdeProjectModel project)
	{
		Guard.Against.Null(project, nameof(project));
		if (project.Running is false) throw new InvalidOperationException($"Project {project.Name} is not running.");
		if (project.RunningCancellationTokenSource is null) throw new InvalidOperationException($"Project {project.Name} does not have a running cancellation token source.");

		await project.RunningCancellationTokenSource.CancelAsync().ConfigureAwait(false);
	}

	public async Task SendDebuggerStepOver(int threadId)
	{
		await _debugger!.StepOver(threadId);
	}

	public async Task<ThreadsStackTraceModel> GetInfoAtStopPoint()
	{
		return await _debugger!.GetInfoAtStopPoint();
	}

	private string GetRunArguments(SharpIdeProjectModel project)
	{
		var dllFullPath = ProjectEvaluation.GetOutputDllFullPath(project);
		if (project.IsBlazorProject)
		{
			var blazorDevServerVersion = project.BlazorDevServerVersion;
			// TODO: Naive implementation which doesn't handle a relocated NuGet package cache
			var blazorDevServerDllPath = Path.Combine(
				Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
				".nuget",
				"packages",
				"microsoft.aspnetcore.components.webassembly.devserver",
				blazorDevServerVersion,
				"tools",
				"blazor-devserver.dll");
			var blazorDevServerFile = new FileInfo(blazorDevServerDllPath);
			if (blazorDevServerFile.Exists is false) throw new FileNotFoundException($"Blazor dev server not found at expected path: {blazorDevServerDllPath}");
			// C:/Users/Matthew/.nuget/packages/microsoft.aspnetcore.components.webassembly.devserver/9.0.7/tools/blazor-devserver.dll --applicationpath C:\Users\Matthew\Documents\Git\BlazorCodeBreaker\artifacts\bin\WebUi\debug\WebUi.dll
			return $" \"{blazorDevServerFile.FullName}\" --applicationpath  \"{dllFullPath}\"";
		}
		return $"\"{dllFullPath}\"";
	}
}
