@page "/user/browse"
@page "/user/browse/{DatabaseStr}/{CollectionStr}"
@page "/user/browse/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}"
@using Rms.Risk.Mango.Language
@using Rms.Risk.Mango.Pivot.Core.MongoDb
@implements IDisposable

@attribute [Authorize]

@inject NavigationManager     NavigationManager
@inject IUserSession           UserSession
@inject IJSRuntime            JsRuntime
@inject IAuthorizationService Auth

@*
 *                                dbMango
 *
 * Copyright 2025 Deutsche Bank AG
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *@

<style>

    .panel{
        height: calc(100vh - 150px);
    }

    .panel .form-control {
        height: 100%;
    }

    .panel .list-body {
        height: calc(100vh - 150px - 34px);
        max-height: calc(100vh - 150px - 34px);
        overflow-y: auto;
    }

    .res-field {
        width: calc(100% - 10px);
        height: calc(100vh - 212px);
        overflow: auto;
        margin-left: 5px;
        background-color: #1d2a3b;
    }

    .meta {
        color: grey;
    }

    pre {
        width: 100%;
        height: 100%;
        font-size: 10pt;
        background-color: #1d2a3b;
        color: #FFFFFF;
    }
</style>

<h3 class="mt-3">Browse: @SelectedCollection</h3>

<AuthorizedOnly Policy="ReadAccess" Resource="@UserSession.Database">

    <EditForm EditContext="EditContext">

        <div class="form-row">
            <FormItemText     Enabled="@IsReady" Class="mr-2" Name="Timeout (sec)"  @bind-Value="Timeout"   InputType="number"   Icon="icon-clock-sm"/>
            <FormItemText     Enabled="@IsReady" Class="mr-2" Name="Max fetch size" @bind-Value="FetchSize" InputType="number"   Icon="icon-download-selected-sm" />
            <FormItemSelect   Enabled="@IsReady" Class="mr-2" Name="Rows"           @bind-Value="Rows"      Values="@_rowValues" Icon="icon-number-outline"  />
            <FormItemCheckBox Enabled="@IsReady" Class="mr-2" Name="Show as Json"   @bind-Value="ShowAsJson"                     Icon="icon-tos-sm" />
            <div class="flex-stack-vertical">
                <div class="flex-stack-horizontal">
                    <div class="form-group">
                        <FormButton Enabled="@IsReady" Name="Find" Icon="icon-search-sm" OnClick="Run" IsPrimary="true" />
                    </div>
                </div>
                <div>See <a href="/doc/afh?tab=Expressions" target="_blank">documentation</a>...</div>
            </div>
        </div>

    </EditForm>

    <SplitPanel Orientation="horizontal" InitialSplit="0.2">
        <First>
            <div class="panel">
                <FormItemList Enabled="@IsReady" Class="mr-2" Name="Collection" @bind-Value="SelectedCollection" Values="@Collections" IsSelectable="IsSelectable">
                    <OptionTemplate>
                        <div class="d-flex">
                            <div class="flex-grow-1 m-1 @GetCollectionClass(context)">@context</div>
                            @if (Stats.TryGetValue(context, out var stat))
                            {
                                if (stat.Sharded)
                                {
                                    <div class="m-1">Sharded</div>
                                }
                                <div class="m-1 ">@NumbersUtils.ToHumanReadable(stat.Size)</div>
                            }
                        </div>
                    </OptionTemplate>
                </FormItemList>
            </div>
        </First>
        <Second>
            <div class="panel ml-2">
                @* <FormItemText @bind-Value="Text" Name="Filter (AFH syntax)" Class="w-100" /> *@
                <FormTextAutocomplete IsMultiline="true" @bind-Value="Text" Name="Filter (AFH syntax)" Class="w-100" Values="@History" @bind-ShowDropdown="@ShowDropdown" />

                @if ( !IsReady )
                {
                    <ProgressSpinner/>
                }
                else if (ShowAsJson)
                {
                    <FormJson Class="res-field row" Value="@Result"/>
                }
                else
                {
                    <SimplePivotComponent PivotName="find"
                                          Class="overflow-auto"
                                          Rows="@Rows"
                                          CollectionName="find"
                                          PivotData="@ResultPivot"
                                          HandleCellClick="@OnCellClick"
                    />
                }
            </div>
        </Second>
    </SplitPanel>


</AuthorizedOnly>

@code {
    [CascadingParameter] public IModalService Modal  { get; set; } = null!;

    [Parameter] public string? DatabaseStr           { get; set; }
    [Parameter] public string? DatabaseInstanceStr   { get; set; }
    [Parameter] public string? CollectionStr         { get; set; }



    [Parameter]
    [SupplyParameterFromQuery(Name = "q")]
    public string Text { get; set; } = "";

    private                 List<string>                                  Collections { get; set; } = [];
    private                 ConcurrentDictionary<string, CollectionStats> Stats       { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    private static readonly List<int>                                     _rowValues                = Enumerable.Range(0, 8).Select(x => x * 5 + 25).ToList();
    private                 Dictionary<string, List<string>>              AllHistory  { get; set; } = [];

    private int Rows
    {
        get;
        set 
        {
            if (field == value)
                return;
            field = value;
            InvokeAsync(StateHasChanged);
        }
    } = 35;

    private List<string> History
    {
        get 
        {
            var key = $"{Database}|{DatabaseInstance}|{SelectedCollection}";
            if ( AllHistory.TryGetValue(key, out var hist) )
            {
                hist.Sort(StringComparer.OrdinalIgnoreCase);
                return hist;
            }
            hist = [];
            AllHistory[key] = hist;
            return hist;
        }
    }

    private string SelectedCollection
    {
        get => UserSession.Collection;
        set
        {
            if (UserSession.Collection == value)
                return;
            UserSession.Collection = value;
            ShowDropdown = false;
            Text = "";
            SyncUrl();
            InvokeAsync(StateHasChanged);
        }
    }

    private string Database
    {
        get => UserSession.Database;
        set
        {
            if (UserSession.Database == value)
                return;
            UserSession.Database = value;
            SyncUrl();
            InvokeAsync(StateHasChanged);
        }
    }

    private string DatabaseInstance
    {
        get => UserSession.DatabaseInstance;
        set
        {
            if (UserSession.DatabaseInstance == value)
                return;
            UserSession.DatabaseInstance = value;
            SyncUrl();
        }
    }

    private string Timeout      { get; set; } = "20";
    private string FetchSize    { get; set; } = "100";
    private bool   IsReady      { get; set; }
    private bool   ShowAsJson   { get; set; }
    private bool   ShowDropdown { get; set; }

    private string              Result      { get; set; } = "{}";
    private List<BsonDocument>  ResultBson  { get; set; } = [];
    public  ArrayBasedPivotData ResultPivot { get; set; } = new([]);

    private EditContext?   _editContext;
    private EditContext    EditContext => _editContext!;

    private readonly CancellationTokenSource _globalCancellation = new();

    private static string GetCollectionClass(string collectionName) =>
        collectionName.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase)
            ? "meta"
            : "";

    protected override void OnInitialized()
    {
        _editContext = new (this);
    }

    public void Dispose()
    {
        _globalCancellation.Cancel();
        _globalCancellation.Dispose();
    }



    private class BrowseState
    {
        public string                           Text       { get; set; } = "";
        public int                              Rows       { get; set; } = 25;
        public Dictionary<string, List<string>> History    { get; set; } = [];
        public string?                          Collection { get; set; }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
            return;

        if (string.IsNullOrWhiteSpace(DatabaseStr))
            DatabaseStr = Database;
        else
            Database = DatabaseStr;

        if (string.IsNullOrWhiteSpace(DatabaseInstanceStr))
            DatabaseInstanceStr = DatabaseInstance;
        else
            DatabaseInstance = DatabaseInstanceStr;

        var cts   = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
        var state = await GetState(cts.Token);

        if ( state != null )
        {
            if ( state.History.Any() )
                AllHistory = state.History;
            Rows = state.Rows;
            if ( string.IsNullOrWhiteSpace(Text) && !string.IsNullOrWhiteSpace(state.Text))
                Text = state.Text;
            if (string.IsNullOrWhiteSpace(CollectionStr) && !string.IsNullOrWhiteSpace(state.Collection))
                CollectionStr = state.Collection;
        }

        if (string.IsNullOrWhiteSpace(CollectionStr))
            CollectionStr = SelectedCollection;
        else
            SelectedCollection = CollectionStr;
        SyncUrl();


        if (Text is "null" or null)
            Text = "";

        _ = Task.Run(async () =>
        {
            try
            {
                var admin = UserSession.MongoDbAdmin;

                var cts2 = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
                var grouped = (await admin.ListCollections(cts2.Token))
                    .GroupBy(x =>
                        x.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase)
                            ? " Meta"
                            : x.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase)
                                ? " Cache"
                                : " Data"
                    );

                Collections.Clear();
                foreach( var group in grouped)
                {
                    Collections.Add(group.Key);
                    Collections.AddRange(group.OrderBy(x => x).Select(x => x));
                }

                if (!Collections.Contains(SelectedCollection))
                    SelectedCollection = Collections.FirstOrDefault( x => !x.StartsWith(" ")) ?? "";

                IsReady = true;
                await InvokeAsync(StateHasChanged);

                _ = Task.Run(LoadCollStats, _globalCancellation.Token);
            }
            catch (Exception e)
            {
                await Display(e);
            }
        }, _globalCancellation.Token);
    }

    private async Task LoadCollStats()
    {
        var admin = UserSession.MongoDbAdmin;
        var collections = Collections.ToList(); // make a copy to avoid modifying the original list during iteration

        foreach (var name in collections)
        {
            try
            {
                var cts = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);

                var doc = await admin.CollStats(name, cts.Token);
                doc.Details = null; // Remove details to avoid large output

                Stats[name] = doc;
                await InvokeAsync(StateHasChanged);
            }
            catch (Exception)
            {
                // ignore
            }
        }
    }

    private async Task<BrowseState?> GetState(CancellationToken token)  
    {  
        try  
        {  
            var json = await JsRuntime.LoadFromLocalStorage("Browse", token);

            if (string.IsNullOrWhiteSpace(json))  
                return null;  

            return JsonUtils.FromJson<BrowseState>(json);  
        }  
        catch (Exception)  
        {  
            return null;  
        }  
    }  

    enum ActionType
    {
        Find
    }

    private async Task Run(ActionType actionType)
    {
        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout)));

        try
        {
            IsReady = false;
            ShowDropdown = false;
            await InvokeAsync(StateHasChanged);

            await SaveState(cts.Token);

            if (int.TryParse(FetchSize, out var limit) == false)
                limit = 0;

            var json = "";
            if (!string.IsNullOrWhiteSpace(Text))
            {
                var afh = $$"""FROM "{{SelectedCollection}}" PIPELINE { WHERE {{Text}} }""";
                try
                {
                    var ast = LanguageParser.ParseScriptToAST(afh);
                    json = ast.AsJson()!.ToJsonString(new() { WriteIndented = true });
                }
                catch (Exception e)
                {
                    await Display(e);
                    return;
                }
            }

            var res = actionType switch
            {
                ActionType.Find => await RunFind(json, limit, cts.Token),
                _ => throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null)
            };

            ResultPivot = res.Item1;
            ResultBson = res.Item2;

            if (!History.Contains(Text))
            {
                History.Insert(0, Text);
                await SaveState(cts.Token);
            }

            SyncUrl();

            await Display(ResultBson);
        }
        catch (Exception e)
        {
            await Display(e);
        }
        finally
        {
            IsReady = true;
            await InvokeAsync(StateHasChanged);
        }
    }

    private async Task SaveState(CancellationToken token)
    {
        try
        {
            foreach( var key in AllHistory.Keys.ToList())
            {
                var hist = AllHistory[key];
                AllHistory[key] = hist.Distinct(StringComparer.OrdinalIgnoreCase).Take(100).ToList();
            }

            var state = new BrowseState
            {
                Text       = Text,
                Collection = SelectedCollection,
                Rows       = Rows,
                History    = AllHistory
            };

            var json = JsonUtils.ToJson(state);
            await JsRuntime.SaveToLocalStorage("Browse", json, token);
        }
        catch (Exception)
        {
            // Ignore errors during saving state
            // This is not critical, and we can continue without saving
        }
    }
    private Task Run() => Run(ActionType.Find);

    private Task Display(Exception e)
    {
        ShowAsJson = true;
        Result     = e.ToString();
        return InvokeAsync(StateHasChanged);
    }

    private Task Display(IEnumerable<BsonDocument> res)
    {
        Result = res.ToJson(new() { Indent = true });
        return InvokeAsync(StateHasChanged);
    }

    private async Task<(ArrayBasedPivotData, List<BsonDocument>)> RunFind(string text, int limit, CancellationToken token)
    {
        var service = UserSession.MongoDb;
        var results =
            string.IsNullOrWhiteSpace(text) || text.Trim() == "{}" 
                ? service.FindAsync("{}", limit: limit <= 0 ? -1 : limit, token: token)
                : service.AggregateAsyncRaw(text, limit <= 0 ? -1 : limit, token: token);


        var fieldMap = new ConcurrentDictionary<string, FieldMapping>
        {
            ["find"] = new(false)
        };

        var (pd, list) = await MongoDbDataSource.FetchPivotData(
            "find", 
            "find", 
            fieldMap,
            results, 
            null, 
            true,
            int.Parse(FetchSize),
            true,
            token);

        if (list.Count == 0)
            list.Add(BsonDocument.Parse(@$"{{""Result"": ""No data fetched from {SelectedCollection}""}}"));

        return (pd, list);
    }

    private void SyncUrl()
    {
        var url = NavigationManager.BaseUri + $"user/browse/{Database}";
        if (!string.IsNullOrWhiteSpace(DatabaseInstance))
            url += $"/{DatabaseInstance}";
        url += $"/{SelectedCollection}";
        if (!string.IsNullOrWhiteSpace(Text))
            url += $"?q={Uri.EscapeDataString(Text)}";
        JsRuntime.InvokeAsync<string>("DashboardUtils.ChangeUrl", url);
    }

    private async Task<bool> OnCellClick(DynamicObject row, string columnName)
    {
        try
        {
            var id = TableControl.GetDynamicMember(row, "_id");
            if (id == null)
                return true;

            var bson = ResultBson.FirstOrDefault(x => x["_id"].ToString() == id.ToString());
            if (bson == null)
                return true;

            var oldId = bson["_id"];

            var enableWrite = await Auth.AuthorizeAsync(
                UserSession.User.GetUser(), 
                UserSession.Database, 
                [new WriteAccessRequirement()]);

            var res = await Find.ShowBsonDialog(Modal, id.ToString()!, bson, enableWrite.Succeeded);
            if (res == null)
                return true;

            if (enableWrite.Succeeded)
            {
                if (res.Value.ShouldUpdate)
                {
                    bson = BsonDocument.Parse(res.Value.Json);
                    await OnUpdate(oldId, bson);
                }
                else
                    await OnDelete(oldId);
            }
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", e);
        }
        return true;
    }

    private async Task OnDelete(BsonValue id)
    {
        var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal);
        if (string.IsNullOrWhiteSpace(ticket))
            return;

        var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Delete {id}", "Are you sure to delete document?");
        if (r.Cancelled)
            return;

        var command = BsonDocument.Parse($@"{{
     ""delete"": ""{SelectedCollection}"",
     ""deletes"": [{{
        ""q"": {{  }},
        ""limit"": 1
     }}]
}}");

        var idDoc = new BsonDocument
            {
                ["_id"] = id
            };

        command["deletes"][0]["q"] = idDoc;

        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout)));
        Shell.UpdateComment(command, ticket, UserSession.User.GetEmail());

        var res      = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token);
        var deleted = res["n"].ToInt32();
        if (deleted != 1)
        {
            Result = 
                res.ToJson(new() { Indent = true }) +
                "\n------------------------------------------------------------------\n"+
                command.ToJson(new() { Indent = true });
            ShowAsJson = true;
            await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Failure");
            await InvokeAsync(StateHasChanged);
        }
        else
            await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Success");
    }

    private async Task OnUpdate(BsonValue id, BsonDocument newBson)
    {
        var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal);
        if (string.IsNullOrWhiteSpace(ticket))
            return;

        var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Update {id}", "Are you sure to update document?");
        if (r.Cancelled)
            return;

        var command = BsonDocument.Parse($@"{{
    update: ""{SelectedCollection}"",
    updates: [
       {{
         q: {{ }},
         u: {{ }},
         upsert: false,
         multi: false,
       }}
    ],
    ordered: false,
    bypassDocumentValidation: false
}}");

        var idDoc = new BsonDocument
        {
            ["_id"] = id
        };

        command["updates"][0]["q"] = idDoc;
        command["updates"][0]["u"] = newBson;

        Shell.UpdateComment(command, ticket, UserSession.User.GetEmail());

        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout)));
        var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token);

        var modified = res["nModified"].ToInt32();
        if (modified != 1)
        {
            Result = 
                res.ToJson(new() { Indent = true }) +
                "\n------------------------------------------------------------------\n"+
                command.ToJson(new() { Indent = true });
            ShowAsJson = true;
            await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Failure");
            await InvokeAsync(StateHasChanged);
        }
        else
            await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Success");
    }

    private bool IsSelectable(string arg) => !arg.StartsWith(" ");

}
