using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Text;
using MudBlazor;
using RobotNet.Script.Shares;
using RobotNet.Shares;
using RobotNet.Clients;
using RobotNet.WebApp.Scripts.Monaco;
using RobotNet.WebApp.Scripts.Monaco.Languages;
using System.Net.Http.Json;
using System.Text;
using System.Timers;

namespace RobotNet.WebApp.Scripts.Models;

public class ScriptWorkspace(IHttpClientFactory httpFactory) : IDisposable
{
    private static readonly CSharpParseOptions cSharpParse = CSharpParseOptions.Default.WithKind(SourceCodeKind.Script).WithLanguageVersion(LanguageVersion.Latest);
    private readonly AdhocWorkspace adhocWorkspace = new();
    private readonly ProjectId EditorProjectId = ProjectId.CreateNewId();

    public readonly List<ScriptFolderModel> Folders = [];
    public readonly List<ScriptFileModel> Files = [];
    public ScriptFolderModel? SelectedFolder { get; private set; }
    public ScriptFileModel? SelectedFile { get; private set; }
    public ScriptFileModel? CurrentFile { get; private set; }

    public ProcessorState ProcessorState { get; set; }
    public bool IsReadOnly => ProcessorState != ProcessorState.Ready && ProcessorState != ProcessorState.Idle && ProcessorState != ProcessorState.BuildError;

    public event Func<ScriptFileModel?, Task>? OnFileChanged;
    public event Func<IEnumerable<Diagnostic>, Task>? OnDiagnoticChanged;
    public event Action? RootChanged;

    private readonly HttpClient http = httpFactory.CreateClient("ScriptManagerAPI");

    private readonly System.Timers.Timer DiagnosticTimer = new()
    {
        AutoReset = false,
        Interval = 1000,
    };

    private bool IsInitialized = false;
    public async Task InitializeAsync()
    {
        if (IsInitialized) return;

        IsInitialized = true;
        
        var collectionsDllStream = await http.GetStreamAsync($"dlls/System.Collections.dll");
        var runtimeDllStream = await http.GetStreamAsync($"dlls/System.Runtime.dll");
        var expressionsDllStream = await http.GetStreamAsync($"dlls/System.Linq.Expressions.dll");
        var robotNetDllStream = await http.GetStreamAsync($"dlls/RobotNet.Script.dll");
        var robotNetDocBuf = await http.GetByteArrayAsync($"dlls/RobotNet.Script.xml");
        var robotNetExpressionsDllStream = await http.GetStreamAsync($"dlls/RobotNet.Script.Expressions.dll");
        var robotNetExpressionsDocBuf = await http.GetByteArrayAsync($"dlls/RobotNet.Script.Expressions.xml");

        var usingNamespaces = await http.GetFromJsonAsync<IEnumerable<string>>("api/Script/UsingNamespaces") ?? [];
        var preCode = await http.GetFromJsonAsync<string>("api/Script/PreCode") ?? "";

        var projectInfo = ProjectInfo.Create(EditorProjectId, VersionStamp.Create(), "EditorProject", "EditorAssembly", LanguageNames.CSharp)
                                    .WithMetadataReferences([
                                        MetadataReference.CreateFromStream(collectionsDllStream, MetadataReferenceProperties.Assembly),
                                        MetadataReference.CreateFromStream(runtimeDllStream, MetadataReferenceProperties.Assembly),
                                        MetadataReference.CreateFromStream(expressionsDllStream, MetadataReferenceProperties.Assembly),
                                        MetadataReference.CreateFromStream(robotNetDllStream, MetadataReferenceProperties.Assembly, documentation: XmlDocumentationProvider.CreateFromBytes(robotNetDocBuf)),
                                        MetadataReference.CreateFromStream(robotNetExpressionsDllStream, MetadataReferenceProperties.Assembly, documentation: XmlDocumentationProvider.CreateFromBytes(robotNetExpressionsDocBuf)),
                                    ])
                                    .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, usings: usingNamespaces))
                                    .WithParseOptions(cSharpParse);

        Solution updatedSolution = adhocWorkspace.CurrentSolution.AddProject(projectInfo);

        updatedSolution = updatedSolution.AddDocument(DocumentId.CreateNewId(EditorProjectId), "PreScript.cs", SourceText.From(preCode), ["/"], "/PreScript.cs");

        if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Khởi tạo project thất bại");

        Folders.Clear();
        Files.Clear();

        var rootFolder = await http.GetFromJsonAsync<ScriptFolder>("api/Script") ?? new ScriptFolder("Script", [], []);

        Files.AddRange([.. rootFolder.Files.Select(file => CreateFileModel(file, 0, null))]);
        Files.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));

        Folders.AddRange([.. rootFolder.Folders.Select(folder => CreateFolderModel(folder, 0, null))]);
        Folders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));

        var editorProject = adhocWorkspace.CurrentSolution.GetProject(EditorProjectId);
        if (editorProject == null) return;

        var compilation = await editorProject.GetCompilationAsync();
        var diagnostics = compilation?.GetDiagnostics() ?? [];

        foreach (var file in Files)
        {
            await GetDiagnosticsToModel(editorProject, diagnostics, file);
        }

        foreach (var folder in Folders)
        {
            await GetDiagnosticsToModel(editorProject, diagnostics, folder);
        }

        DiagnosticTimer.Elapsed += DiagnosticTimer_Elapsed;

        RootChanged?.Invoke();
    }

    private async void DiagnosticTimer_Elapsed(object? sender, ElapsedEventArgs e)
    {
        var editorProject = adhocWorkspace.CurrentSolution.GetProject(EditorProjectId);
        if (editorProject == null) return;

        var compilation = await editorProject.GetCompilationAsync();
        var diagnostics = compilation?.GetDiagnostics() ?? [];

        foreach (var file in Files)
        {
            await GetDiagnosticsToModel(editorProject, diagnostics, file);
        }

        foreach (var folder in Folders)
        {
            await GetDiagnosticsToModel(editorProject, diagnostics, folder);
        }

        if (OnDiagnoticChanged is not null && CurrentFile is not null)
        {
            await OnDiagnoticChanged.Invoke(CurrentFile.Diagnostics);
        }
    }

    private static async Task GetDiagnosticsToModel(Project project, IEnumerable<Diagnostic> diagnostics, ScriptFileModel file)
    {
        var document = project.Solution.GetDocument(file.Id);
        if (document == null) return;

        var syntaxTree = await document.GetSyntaxTreeAsync();
        if (syntaxTree == null) return;

        file.SetDiagnostics(diagnostics.Where(d => d.Location.IsInSource && d.Location.SourceTree == syntaxTree));
    }

    private static async Task GetDiagnosticsToModel(Project project, IEnumerable<Diagnostic> diagnostics, ScriptFolderModel folders)
    {
        foreach (var file in folders.Files)
        {
            await GetDiagnosticsToModel(project, diagnostics, file);
        }
        foreach (var folder in folders.Folders)
        {
            await GetDiagnosticsToModel(project, diagnostics, folder);
        }
    }

    private ScriptFileModel CreateFileModel(ScriptFile file, int level, ScriptFolderModel? parrent)
    {
        Solution updatedSolution = adhocWorkspace.CurrentSolution;
        var newId = DocumentId.CreateNewId(EditorProjectId);

        updatedSolution = updatedSolution.AddDocument(newId, file.Name, SourceText.From(file.Code), parrent?.Path.Split("/"), Path.Combine(parrent?.Path ?? "", file.Name));

        if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Tạo document mới thất bại");

        var model = new ScriptFileModel(newId, file, level, parrent);

        return model;
    }

    private ScriptFolderModel CreateFolderModel(ScriptFolder folder, int level, ScriptFolderModel? parrent)
    {
        var model = new ScriptFolderModel(parrent, folder.Name, level);
        model.AddFiles([.. folder.Files.Select(file => CreateFileModel(file, level + 1, model))]);
        model.AddFolders([.. folder.Folders.Select(dir => CreateFolderModel(dir, level + 1, model))]);
        return model;
    }

    public bool SelectFolder(ScriptFolderModel? folder)
    {
        if (SelectedFolder != folder)
        {
            SelectedFolder = folder;
            SelectedFile = null;
            return true;
        }
        else
        {
            return false;
        }
    }

    public async Task SelectFile(ScriptFileModel? file)
    {
        CurrentFile = file;
        SelectedFile = CurrentFile;
        SelectedFolder = file?.Parrent;

        if (OnFileChanged is not null)
        {
            await OnFileChanged.Invoke(file);
        }
    }


    public async Task<MessageResult> CreateNewFile(string name)
    {
        var result = await http.PostFromJsonAsync<MessageResult>("api/Script/File", new CreateModel(SelectedFolder?.Path ?? "", name));
        if (result is null)
        {
            return new(false, "Không thể tạo file mới");
        }

        if (result.IsSuccess)
        {
            var file = new ScriptFile(name, "");
            if (SelectedFolder is null)
            {
                Files.Add(CreateFileModel(file, 0, null));
                Files.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
                RootChanged?.Invoke();
            }
            else
            {
                SelectedFolder.AddFiles(CreateFileModel(file, SelectedFolder.Level + 1, SelectedFolder));
            }
        }

        return result;
    }

    public async Task<MessageResult> CreateNewFolder(string name)
    {
        var result = await http.PostFromJsonAsync<MessageResult>("api/Script/Directory", new CreateModel(SelectedFolder?.Path ?? "", name));
        if (result is null)
        {
            return new(false, "Không thể tạo thư mục mới");
        }

        if (result.IsSuccess)
        {
            var folder = new ScriptFolder(name, [], []);
            if (SelectedFolder is null)
            {
                Folders.Add(CreateFolderModel(folder, 0, null));
                Folders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
                RootChanged?.Invoke();
            }
            else
            {
                SelectedFolder.AddFolders(CreateFolderModel(folder, SelectedFolder.Level + 1, SelectedFolder));
            }
        }

        return result;
    }

    public void WriteDocument(string text)
    {
        if (CurrentFile is null) return;

        Solution updatedSolution = adhocWorkspace.CurrentSolution;
        updatedSolution = updatedSolution.WithDocumentText(CurrentFile.Id, SourceText.From(text));
        if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Cập nhật project ban đầu thất bại");

        CurrentFile.ChangeCode(text);
        DiagnosticTimer.Stop();
        DiagnosticTimer.Start();
    }

    public async Task<MessageResult> SaveCurrentFileAsync()
    {
        if (CurrentFile is null || !CurrentFile.IsModified || CurrentFile.EditCode == CurrentFile.Code) return new(true, "");

        var result = await http.PatchFromJsonAsync<MessageResult>("api/Script/File", new UpdateModel(CurrentFile.Path, CurrentFile.EditCode));
        if (result is null)
        {
            return new(false, "Không thể update được code");
        }

        if (result.IsSuccess)
        {
            CurrentFile.Saved();
        }

        return result;
    }

    public async Task<IEnumerable<MessageResult>> SaveSelectAsync()
    {
        if (SelectedFolder is not null)
        {
            var results = new List<MessageResult>();
            foreach (var subFolder in SelectedFolder.Folders)
            {
                results.AddRange(await SaveAllAsync(subFolder));
            }
            results.AddRange(await SaveAllAsync(Files));
            return results;
        }
        else if (SelectedFile is not null)
        {
            return await SaveAllAsync([SelectedFile]);
        }
        else
        {
            return [];
        }
    }

    public async Task<IEnumerable<MessageResult>> SaveAllAsync()
    {
        var results = new List<MessageResult>();
        foreach (var subFolder in Folders)
        {
            results.AddRange(await SaveAllAsync(subFolder));
        }
        results.AddRange(await SaveAllAsync(Files));
        return results;
    }

    private async Task<IEnumerable<MessageResult>> SaveAllAsync(IEnumerable<ScriptFileModel> files)
    {
        var results = new List<MessageResult>();
        foreach (var file in files)
        {
            if (!file.IsModified || file.EditCode == file.Code) continue;
            var result = await http.PatchFromJsonAsync<MessageResult>("api/Script/File", new UpdateModel(file.Path, file.EditCode));
            if (result is null)
            {
                results.Add(new(false, $"Không thể update được code của {file.Name}"));
                continue;
            }
            if (result.IsSuccess)
            {
                file.Saved();
            }
            results.Add(result);
        }
        return results;
    }


    private async Task<IEnumerable<MessageResult>> SaveAllAsync(ScriptFolderModel folder)
    {
        var results = new List<MessageResult>();
        results.AddRange(await SaveAllAsync(folder.Files));
        foreach (var subFolder in folder.Folders)
        {
            results.AddRange(await SaveAllAsync(subFolder));
        }
        return results;
    }

    public async Task<MessageResult> DeleteFile(ScriptFileModel file)
    {
        var result = await http.DeleteFromJsonAsync<MessageResult>($"api/Script/File?path={file.Path}");
        if (result?.IsSuccess ?? false)
        {
            if (Files.Remove(file))
            {
                RootChanged?.Invoke();
            }
            else
            {
                file.Parrent?.RemoveFile(file);
            }
            if (SelectedFile == file)
            {
                await SelectFile(null);
            }

            Solution updatedSolution = adhocWorkspace.CurrentSolution;

            updatedSolution = updatedSolution.RemoveDocument(file.Id);

            if (!adhocWorkspace.TryApplyChanges(updatedSolution))
            {
                return new(false, "Xóa document trong workspace mới thất bại");
            }

            DiagnosticTimer.Stop();
            DiagnosticTimer.Start();
        }
        return result ?? new MessageResult(false, "Lỗi server response");
    }

    public async Task<MessageResult> DeleteFolder(ScriptFolderModel folder)
    {
        var result = await http.DeleteFromJsonAsync<MessageResult>($"api/Script/Directory?path={folder.Path}");
        if (result?.IsSuccess ?? false)
        {
            if (Folders.Remove(folder))
            {
                RootChanged?.Invoke();
            }
            else
            {
                folder.Parrent?.RemoveFolder(folder);
            }
            Solution updatedSolution = adhocWorkspace.CurrentSolution;
            await RemoveFolderFromWorkspace(updatedSolution, folder);
            if (!adhocWorkspace.TryApplyChanges(updatedSolution))
            {
                return new(false, "Xóa document trong workspace mới thất bại");
            }

            DiagnosticTimer.Stop();
            DiagnosticTimer.Start();

        }
        return result ?? new MessageResult(false, "Lỗi server response");
    }

    private async Task RemoveFolderFromWorkspace(Solution updatedSolution, ScriptFolderModel folder)
    {
        foreach (var dir in folder.Folders)
        {
            await RemoveFolderFromWorkspace(updatedSolution, dir);
        }
        foreach (var file in folder.Files)
        {
            if (SelectedFile == file)
            {
                _ = SelectFile(null);
            }
            updatedSolution = updatedSolution.RemoveDocument(file.Id);
        }
    }

    public async Task<MessageResult> Rename(ScriptFolderModel folder, string newName)
    {
        var result = await http.PatchFromJsonAsync<MessageResult>("api/Script/DirectoryName", new RenameModel(folder.Path, newName));
        if (result?.IsSuccess ?? false)
        {
            folder.Rename(newName);
        }

        return result ?? new MessageResult(false, "Lỗi server response");
    }

    public async Task<MessageResult> Rename(ScriptFileModel file, string newName)
    {
        var result = await http.PatchFromJsonAsync<MessageResult>("api/Script/FileName", new RenameModel(file.Path, newName));
        if (result?.IsSuccess ?? false)
        {
            file.Rename(newName);
        }

        return result ?? new MessageResult(false, "Lỗi server response");
    }

    public bool AnyChanges() => AnyChanges(Files) || Folders.Any(AnyChanges);
    private static bool AnyChanges(ScriptFolderModel folder) => AnyChanges(folder.Files) || folder.Folders.Any(AnyChanges);
    private static bool AnyChanges(IEnumerable<ScriptFileModel> files) => files.Any(file => file.IsModified);

    public async Task<IEnumerable<Diagnostic>> GetAllDiagnostics()
    {
        var editorProject = adhocWorkspace.CurrentSolution.GetProject(EditorProjectId);
        if (editorProject == null) return [];

        var compilation = await editorProject.GetCompilationAsync();

        return compilation?.GetDiagnostics() ?? [];
    }

    public async Task<IEnumerable<Monaco.Languages.CompletionItem>> GetCompletionCurrentFileAsync(int line, int column, int kind, char? triggerCharacter)
    {
        return CurrentFile is null ? [] : await adhocWorkspace.GetCompletionAsync(CurrentFile.Id, line, column, kind, triggerCharacter);
    }


    public async Task<string?> GetQuickInfoCurrentFile(int line, int column)
    {
        if (CurrentFile is null) return null;

        var document = adhocWorkspace.CurrentSolution.GetDocument(CurrentFile.Id);
        if (document is null) return null;

        var sourceText = await document.GetTextAsync();
        var position = sourceText.Lines.GetPosition(new LinePosition(line - 1, column - 1));

        var quickInfoService = QuickInfoService.GetService(document);
        if (quickInfoService is null) return string.Empty;

        var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position);
        if (quickInfo is null) return string.Empty;

        var finalTextBuilder = new StringBuilder();

        bool lastSectionHadLineBreak = true;
        var description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description);
        if (description is not null)
        {
            finalTextBuilder.AppendSection(description, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak);
        }

        var summary = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments);
        if (summary is not null)
        {
            finalTextBuilder.AppendSection(summary, MarkdownFormat.Default, ref lastSectionHadLineBreak);
        }

        foreach (var section in quickInfo.Sections)
        {
            switch (section.Kind)
            {
                case QuickInfoSectionKinds.Description:
                case QuickInfoSectionKinds.DocumentationComments:
                    continue;

                case QuickInfoSectionKinds.TypeParameters:
                    finalTextBuilder.AppendSection(section, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak);
                    break;

                case QuickInfoSectionKinds.AnonymousTypes:
                    // The first line is "Anonymous Types:"
                    // Then we want all anonymous types to be C# highlighted
                    finalTextBuilder.AppendSection(section, MarkdownFormat.FirstLineDefaultRestCSharp, ref lastSectionHadLineBreak);
                    break;

                case "NullabilityAnalysis":
                    // Italicize the nullable analysis for emphasis.
                    finalTextBuilder.AppendSection(section, MarkdownFormat.Italicize, ref lastSectionHadLineBreak);
                    break;

                default:
                    finalTextBuilder.AppendSection(section, MarkdownFormat.Default, ref lastSectionHadLineBreak);
                    break;
            }
        }

        return finalTextBuilder.ToString().Trim();
    }

    public async Task<SignatureHelpResult?> GetSignatureHelpCurrentFile(int line, int column)
    {
        if (CurrentFile is null) return null;

        var document = adhocWorkspace.CurrentSolution.GetDocument(CurrentFile.Id);
        if (document is null) return null;

        return await document.GetSignatureHelpAsync(line, column);
    }

    private bool isDisposed = false;
    public void Dispose()
    {
        if (isDisposed) throw new NotImplementedException();

        isDisposed = true;

        DiagnosticTimer.Elapsed -= DiagnosticTimer_Elapsed;
        DiagnosticTimer.Stop();

        adhocWorkspace.Dispose();
        DiagnosticTimer.Dispose();
        GC.SuppressFinalize(this);
    }
}
