using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using RobotNet.Script;
using RobotNet.Script.Shares;
using RobotNet.ScriptManager.Data;
using RobotNet.ScriptManager.Helpers;
using RobotNet.ScriptManager.Models;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace RobotNet.ScriptManager.Services;

public class ScriptMissionManager(ScriptGlobalsManager globalsManager, IServiceScopeFactory scopeFactory)
{
    public ProcessorState State { get; private set; } = ProcessorState.Idle;
    private readonly Dictionary<string, ScriptMissionData> MissionDatas = [];
    private readonly Dictionary<string, ScriptRunner<IAsyncEnumerable<MissionState>>> Runners = [];
    private readonly ConcurrentDictionary<Guid, ScriptMission> Missions = [];
    private readonly ConcurrentQueue<ScriptMission> idleMissions = [];
    private readonly ConcurrentQueue<ScriptMission> runningMissions = [];

    public void Reset()
    {
        if (State != ProcessorState.Idle && State != ProcessorState.Ready)
        {
            throw new InvalidOperationException("Cannot reset missions while the processor is running.");
        }
        MissionDatas.Clear();
        Runners.Clear();

        foreach (var mission in Missions.Values)
        {
            mission.Dispose();
        }
        Missions.Clear();

        foreach (var mission in idleMissions)
        {
            mission.Dispose();
        }
        idleMissions.Clear();

        foreach (var mission in runningMissions)
        {
            mission.Dispose();
        }
        runningMissions.Clear();

        GC.Collect();

        State = ProcessorState.Idle;
    }

    public void LoadMissions(IEnumerable<ScriptMissionData> missionDatas)
    {
        if (State != ProcessorState.Idle && State != ProcessorState.Ready)
        {
            throw new InvalidOperationException("Cannot load missions while the processor is running.");
        }

        MissionDatas.Clear();
        Runners.Clear();
        runningMissions.Clear();
        idleMissions.Clear();
        runningMissions.Clear();

        foreach (var mission in missionDatas)
        {
            MissionDatas.Add(mission.Name, mission);
            var script = CSharpScript.Create<IAsyncEnumerable<MissionState>>(mission.Script, ScriptConfiguration.ScriptOptions, globalsType: typeof(ScriptMissionGlobals));
            Runners.Add(mission.Name, script.CreateDelegate());
        }
        State = ProcessorState.Ready;
    }

    public IEnumerable<ScriptMissionDto> GetMissionDatas() =>
        [.. MissionDatas.Values.Select(m => new ScriptMissionDto(m.Name, m.Parameters.Select(p => new ScriptMissionParameterDto(p.Name, p.Type.FullName ?? p.Type.Name, p.DefaultValue?.ToString())), m.Code))];

    public bool ContainsMissionName(string name) => Runners.ContainsKey(name);

    public void Create(Guid id, string name, IDictionary<string, string> parameterStrings)
    {
        if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found.");

        var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token);
        var parameters = new ConcurrentDictionary<string, object?>();
        bool hasCancellationToken = false;

        foreach (var param in missionData.Parameters)
        {
            if (param.Type == typeof(CancellationToken))
            {
                if (hasCancellationToken)
                {
                    throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined.");
                }
                hasCancellationToken = true;
                parameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission
                continue;
            }

            if (!parameterStrings.TryGetValue(param.Name, out string? valueStr)) throw new ArgumentException($"Parameter '{param.Name}' not found in provided parameters.");

            if (CSharpSyntaxHelper.ResolveValueFromString(valueStr, param.Type, out var value) && value != null)
            {
                parameters.TryAdd(param.Name, value);
            }
            else
            {
                throw new ArgumentException($"Invalid value for parameter '{param.Name}': {valueStr}");
            }
        }

        Create(id, name, parameters, cts);
    }

    public void Create(Guid id, string name, object[] parameters)
    {
        if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found.");
        if (parameters.Length != missionData.Parameters.Count())
        {
            var count = missionData.Parameters.Count(p => p.Type == typeof(CancellationToken));
            if (count == 1)
            {
                if (parameters.Length != missionData.Parameters.Count() - count)
                {
                    throw new ArgumentException($"Mission '{name}' expects {missionData.Parameters.Count()} parameters, but received {parameters.Length} without CancellationToken.");
                }
            }
            else if (count != 0)
            {
                throw new ArgumentException($"Mission '{name}' just have one CancellationToken, but received {parameters.Length}.");
            }
        }

        var inputParameters = new ConcurrentDictionary<string, object?>();
        bool hasCancellationToken = false;

        var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token);
        int index = 0;
        foreach (var param in missionData.Parameters)
        {
            if (param.Type == typeof(CancellationToken))
            {
                if (hasCancellationToken)
                {
                    throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined.");
                }
                hasCancellationToken = true;
                inputParameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission
                continue;
            }
            inputParameters.TryAdd(param.Name, parameters[index]);
            index++;
        }
        Create(id, name, inputParameters, cts);
    }

    public void Create(Guid id, string name, ConcurrentDictionary<string, object?> parameters, CancellationTokenSource cts)
    {
        if (!Runners.TryGetValue(name, out var runner)) throw new ArgumentException($"Mission '{name}' not found.");

        var robotnet = globalsManager.GetRobotNetMission(id);
        var mission = new ScriptMission(id, name, runner, new ScriptMissionGlobals(globalsManager.Globals, robotnet, parameters), cts);
        Missions.TryAdd(id, mission);
        idleMissions.Enqueue(mission);
    }

    private CancellationTokenSource internalCts = new();
    private Thread? thread;
    public void Start(CancellationToken cancellationToken = default)
    {
        Stop(); // Ensure previous thread is stopped before starting a new one
        ResetMissionDb();
        internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        var token = internalCts.Token;

        thread = new Thread(() =>
        {
            State = ProcessorState.Running;
            while (!token.IsCancellationRequested || !runningMissions.IsEmpty)
            {
                var stopwatch = Stopwatch.StartNew();

                MiningIdleMissionHandle();
                MiningRunningMissionHandle();

                stopwatch.Stop();
                int elapsed = (int)stopwatch.ElapsedMilliseconds;
                int remaining = 1000 - elapsed;

                // If execution time exceeds ProcessTime, add another cycle
                if (elapsed > 900)
                {
                    remaining += 1000;
                }

                if (remaining > 0)
                {
                    try
                    {
                        Thread.Sleep(remaining);
                    }
                    catch (ThreadInterruptedException)
                    {
                        break;
                    }
                }
            }

            State = ProcessorState.Ready;
        })
        {
            IsBackground = true,
            Priority = ThreadPriority.Highest,
        };
        thread.Start();
    }

    public bool Pause(Guid id)
    {
        if (Missions.TryGetValue(id, out var mission))
        {
            return mission.Pause();
        }

        return false;
    }

    public bool Resume(Guid id)
    {
        if (Missions.TryGetValue(id, out var mission))
        {
            return mission.Resume();
        }
        return false;
    }

    public bool Cancel(Guid id, string reason)
    {
        if (Missions.TryGetValue(id, out var mission))
        {
            return mission.Cancel(reason);
        }

        return false; // Mission not found or not running
    }

    public void Stop()
    {
        if (!idleMissions.IsEmpty || !runningMissions.IsEmpty)
        {
            var listWaitHandles = new List<WaitHandle>();
            while (idleMissions.TryDequeue(out var mission))
            {
                mission.Cancel("Cancel by script mission manager is stoped");
                listWaitHandles.Add(mission.WaitHandle);
            }
            while (runningMissions.TryDequeue(out var mission))
            {
                mission.Cancel("Cancel by script mission manager is stoped");
                listWaitHandles.Add(mission.WaitHandle);
            }

            WaitHandle.WaitAll([.. listWaitHandles]);
        }

        if (!internalCts.IsCancellationRequested)
        {
            internalCts.Cancel();
        }

        if (thread != null && thread.IsAlive)
        {
            thread.Interrupt();
            thread.Join();
        }

        internalCts.Dispose();
        thread = null;
    }

    private void RemoveMission(ScriptMission mission)
    {
        mission.Dispose();
        Missions.TryRemove(mission.Id, out _);
    }

    public void ResetMissionDb()
    {
        using var scope = scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();

        var missions = dbContext.InstanceMissions.Where(m => m.Status == MissionStatus.Running
                                                            || m.Status == MissionStatus.Pausing
                                                            || m.Status == MissionStatus.Paused
                                                            || m.Status == MissionStatus.Canceling
                                                            || m.Status == MissionStatus.Resuming).ToList();
        foreach (var mission in missions)
        {
            mission.Log += $"{Environment.NewLine}{DateTime.UtcNow}: Mission Manager start, but instance mission has state {mission.Status}";
            mission.Status = MissionStatus.Error;
        }
        dbContext.SaveChanges();
    }

    private void MiningIdleMissionHandle()
    {
        using var scope = scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();

        int count = idleMissions.Count;
        bool hasChanges = false;

        for (int i = 0; i < count; i++)
        {
            if (!idleMissions.TryDequeue(out var mission)) break;

            var dbMission = dbContext.InstanceMissions.Find(mission.Id);
            if (dbMission == null)
            {
                RemoveMission(mission);
                continue; // Skip if mission not found in database
            }

            if (mission.Status == MissionStatus.Idle)
            {
                mission.Start();
                runningMissions.Enqueue(mission);
                dbMission.Status = mission.Status;
            }
            else
            {
                RemoveMission(mission);
                if (mission.Status == MissionStatus.Canceled)
                {
                    dbMission.Status = MissionStatus.Canceled;
                    dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
                }
                else
                {
                    dbMission.Status = MissionStatus.Error;
                    dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}{Environment.NewLine}{DateTime.UtcNow}: Mission is not in idle state. [{mission.Status}]";
                }
                hasChanges = true;
            }

        }
        if (hasChanges)
        {
            dbContext.SaveChanges();
        }
    }

    private void MiningRunningMissionHandle()
    {
        using var scope = scopeFactory.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<ScriptManagerDbContext>();

        bool hasChanges = false;
        int count = runningMissions.Count;
        for (int i = 0; i < count; i++)
        {
            if (!runningMissions.TryDequeue(out var mission)) break;

            var dbMission = dbContext.InstanceMissions.Find(mission.Id);
            if (dbMission == null)
            {
                RemoveMission(mission);
                continue; // Skip if mission not found in database
            }

            switch (mission.Status)
            {
                case MissionStatus.Running:
                case MissionStatus.Paused:
                case MissionStatus.Canceling:
                case MissionStatus.Resuming:
                case MissionStatus.Pausing:
                    if (dbMission.Status != mission.Status)
                    {
                        dbMission.Status = mission.Status;
                        hasChanges = true;
                    }
                    runningMissions.Enqueue(mission);
                    break;
                case MissionStatus.Completed:
                case MissionStatus.Canceled:
                case MissionStatus.Error:
                    dbMission.Status = mission.Status;
                    dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
                    dbMission.StopedAt = DateTime.UtcNow;
                    hasChanges = true;
                    RemoveMission(mission);
                    break; // Handle these statuses in their respective methods
                default:
                    dbMission.Status = MissionStatus.Error;
                    dbMission.Log += $"{Environment.NewLine} Wrong mission status on running: {mission.Status}";
                    dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}";
                    hasChanges = true;
                    RemoveMission(mission);
                    continue; // Skip unknown statuses
            }
        }
        if (hasChanges)
        {
            dbContext.SaveChanges();
        }
    }
}
