using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Remotely.Server.Enums;
using Remotely.Server.Hubs;
using Remotely.Server.Models.Messages;
using Remotely.Server.Services;
using Remotely.Server.Services.Stores;
using Remotely.Shared;
using Remotely.Shared.Entities;
using Remotely.Shared.Enums;
using Remotely.Shared.Utilities;
using Remotely.Shared.ViewModels;
using System.Collections.Concurrent;
using System.Text.Json;

namespace Remotely.Server.Components.Devices;

public partial class DeviceCard : AuthComponentBase
{
    private readonly ConcurrentDictionary<string, double> _fileUploadProgressLookup = new();
    private ElementReference _card;
    private Version _currentVersion = new();
    private Theme _theme;
    private DeviceCardState _state;
    private DeviceGroup[] _deviceGroups = Array.Empty<DeviceGroup>();

    [Parameter]
    public Device Device { get; set; } = null!;

    [CascadingParameter]
    public DevicesFrame ParentFrame { get; init; } = null!;


    [Inject]
    public required ISelectedCardsStore SelectedCards { get; init; }

    [Inject]
    public required IThemeProvider ThemeProvider { get; init; }

    [Inject]
    public required ICircuitConnection CircuitConnection { get; init; }

    [Inject]
    public required IDataService DataService { get; init; }

    [Inject]
    public required IChatSessionStore ChatCache { get; init; }

    [Inject]
    public required ILogger<DeviceCard> Logger { get; init; }

    private bool IsExpanded => _state == DeviceCardState.Expanded;

    private bool IsOutdated =>
        Version.TryParse(Device.AgentVersion, out var result) &&
        result < _currentVersion;

    private bool IsSelected => SelectedCards.SelectedDevices.Contains(Device.ID);

    [Inject]
    private IJsInterop JsInterop { get; init; } = null!;

    [Inject]
    private IModalService ModalService { get; init; } = null!;

    [Inject]
    private IAgentHubSessionCache ServiceSessionCache { get; init; } = null!;

    [Inject]
    private IToastService ToastService { get; init; } = null!;

    [Inject]
    private IUpgradeService UpgradeService { get; init; } = null!;

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        EnsureUserSet();
        _theme = await ThemeProvider.GetEffectiveTheme();
        _currentVersion = UpgradeService.GetCurrentVersion();
        _deviceGroups = DataService.GetDeviceGroups(UserName);

        await Register<DeviceCardStateChangedMessage, string>(
            CircuitConnection.ConnectionId,
            HandleDeviceCardStateChanged);

        await Register<DeviceStateChangedMessage, string>(
            CircuitConnection.ConnectionId,
            HandleDeviceStateChanged);
    }

    private async Task HandleDeviceCardStateChanged(object subscriber, DeviceCardStateChangedMessage message)
    {
        if (message.DeviceId == Device.ID)
        {
            if (message.State == _state)
            {
                return;
            }
            _state = message.State;
            await InvokeAsync(StateHasChanged);
        }
        else
        {
            if (_state == DeviceCardState.Normal)
            {
                return;
            }
            _state = DeviceCardState.Normal;
            await InvokeAsync(StateHasChanged);
            return;
        }
    }

    private async Task HandleDeviceStateChanged(object subscriber, DeviceStateChangedMessage message)
    {
        if (message.Device.ID != Device.ID)
        {
            return;
        }

        // TODO: It would be cool to decorate user-editable properties
        // with a "UserEditable" attribute, then use a source generator
        // to create/update a method that copies property values for
        // those that do not have the attribute.  We could do the same
        // with reflection, but this method is called too frequently,
        // and the performance hit would likely be significant.

        // If the card is expanded, only update the immutable UI
        // elements, so any changes to the form fields aren't lost.
        if (IsExpanded)
        {
            Device.CurrentUser = message.Device.CurrentUser;
            Device.Platform = message.Device.Platform;
            Device.TotalStorage = message.Device.TotalStorage;
            Device.UsedStorage = message.Device.UsedStorage;
            Device.Drives = message.Device.Drives;
            Device.CpuUtilization = message.Device.CpuUtilization;
            Device.TotalMemory = message.Device.TotalMemory;
            Device.UsedMemory = message.Device.UsedMemory;
            Device.AgentVersion = message.Device.AgentVersion;
            Device.LastOnline = message.Device.LastOnline;
            Device.PublicIP = message.Device.PublicIP;
            Device.MacAddresses = message.Device.MacAddresses;
        }
        else
        {
            Device = message.Device;
        }
        await InvokeAsync(StateHasChanged);
    }

    private void ContextMenuOpening(MouseEventArgs args)
    {
        if (_state == DeviceCardState.Normal)
        {
            JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank");
        }
    }

    private async Task ExpandCard(MouseEventArgs args)
    {
        if (_state == DeviceCardState.Expanded)
        {
            return;
        }

        await Messenger.Send(
            new DeviceCardStateChangedMessage(Device.ID, DeviceCardState.Expanded),
            CircuitConnection.ConnectionId);

        JsInterop.ScrollToElement(_card);

        await CircuitConnection.TriggerHeartbeat(Device.ID);
    }


    private string GetCardStateClass()
    {
        return $"{_state}".ToLower();
    }

    private string GetProgressMessage(string key)
    {
        if (_fileUploadProgressLookup.TryGetValue(key, out var value))
        {
            return $"{MathHelper.GetFormattedPercent(value)} - {key}";
        }

        return string.Empty;
    }

    private void HandleHeaderClick()
    {
        if (IsExpanded)
        {
            _state = DeviceCardState.Normal;
        }
    }
    private async Task HandleValidSubmit()
    {
        EnsureUserSet();
        if (!DataService.DoesUserHaveAccessToDevice(Device.ID, User))
        {
            ToastService.ShowToast("Unauthorized.", classString: "bg-warning");
            return;
        }
       
        await DataService.UpdateDevice(Device.ID,
              Device.Tags,
              Device.Alias,
              Device.DeviceGroupID,
              Device.Notes);

        ToastService.ShowToast("Device settings saved.");

        await CircuitConnection.TriggerHeartbeat(Device.ID);
    }

    private async Task OnFileInputChanged(InputFileChangeEventArgs args)
    {
        try
        {
            EnsureUserSet();

            ToastService.ShowToast("File upload started.");

            if (args.File.Size > AppConstants.MaxUploadFileSize)
            {
                var maxFileSize = AppConstants.MaxUploadFileSize / 1000 / 1000;
                ToastService.ShowToast2($"File size exceeds the maximum allowed size of {maxFileSize}MB.", ToastType.Warning);
                return;
            }
            
            var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress);
            var transferId = Guid.NewGuid().ToString();
            var result = await CircuitConnection.TransferFileFromBrowserToAgent(Device.ID, transferId, [fileId]);

            if (!result)
            {
                ToastService.ShowToast("Device not found.", classString: "bg-warning");
            }
            else
            {
                ToastService.ShowToast("File upload completed.");
            }
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "Error while uploading file to device.");
            ToastService.ShowToast2("Failed to upload file", ToastType.Error);
        }
        finally
        {
            if (args.File.Name is not null)
            {
                _ = _fileUploadProgressLookup.TryRemove(args.File.Name, out _);
                await InvokeAsync(StateHasChanged);
            }
        }
    }

    private void OnFileInputProgress(double percentComplete, string fileName)
    {
        if (_fileUploadProgressLookup.TryGetValue(fileName, out var existingValue) &&
            percentComplete < 1 &&
            percentComplete - existingValue < .05)
        {
            // Avoid too frequent of updates.
            return;
        }
        _fileUploadProgressLookup.AddOrUpdate(fileName, percentComplete, (k, v) => percentComplete);
        InvokeAsync(StateHasChanged);
    }

    private void OpenDeviceDetails()
    {
        JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank");
    }

    private void ShowAllDisks()
    {
        var disksString = JsonSerializer.Serialize(Device.Drives, JsonSerializerHelper.IndentedOptions);

        void modalBody(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
        {
            builder.AddMarkupContent(0, $"<div style='white-space: pre'>{disksString}</div>");
        }

        ModalService.ShowModal($"All Disks for {Device.DeviceName}", modalBody);
    }

    private void StartChat()
    {
        var session = ChatCache.GetOrAdd(Device.ID, key =>
        {
            return new ChatSession()
            {
                DeviceId = key,
                DeviceName = Device.DeviceName,
                IsExpanded = true
            };
        });

        session.IsExpanded = true;
        Messenger.Send(new ChatSessionsChangedMessage(), CircuitConnection.ConnectionId);
    }

    private async Task StartRemoteControl(bool viewOnly)
    {
        if (!ServiceSessionCache.TryGetConnectionId(Device.ID, out _))
        {
            ToastService.ShowToast("Device connection not found", classString: "bg-danger");
            return;
        }

        var result = await CircuitConnection.RemoteControl(Device.ID, viewOnly);
        if (!result.IsSuccess)
        {
            return;
        }

        var session = result.Value;

        if (!await session.WaitForSessionReady(TimeSpan.FromSeconds(20)))
        {
            ToastService.ShowToast("Session failed to start", classString: "bg-danger");
            return;
        }

        JsInterop.OpenWindow(
            $"/Viewer" +
                $"?mode=Unattended&sessionId={session.UnattendedSessionId}" +
                $"&accessKey={session.AccessKey}" +
                $"&viewonly={viewOnly}", 
            "_blank");
    }

    private void ToggleIsSelected(ChangeEventArgs args)
    {
        if (args.Value is not bool isSelected)
        {
            return;
        }

        if (isSelected)
        {
            SelectedCards.Add(Device.ID);
        }
        else
        {
            SelectedCards.Remove(Device.ID);
        }
    }

    private async Task UninstallAgent()
    {
        var result = await JsInterop.Confirm("Are you sure you want to uninstall this agent?  This is permanent!");
        if (result)
        {
            await CircuitConnection.UninstallAgents(new[] { Device.ID });
            _state = DeviceCardState.Normal;
            await ParentFrame.Refresh();
        }
    }

    private async Task WakeDevice()
    {
        var result = await CircuitConnection.WakeDevice(Device);
        if (result.IsSuccess)
        {
            ToastService.ShowToast2(
                $"Wake command sent to peer devices.", 
                ToastType.Success);
        }
        else
        {
            ToastService.ShowToast2(
                $"Wake command failed.  Reason: {result.Reason}", 
                ToastType.Error);
        }
    }
}