using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.AIJobs;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.Media;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using AIStudio.Tools.Security;

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;

using DialogOptions = AIStudio.Dialogs.DialogOptions;
using EnterpriseEnvironment = AIStudio.Tools.EnterpriseEnvironment;

namespace AIStudio.Layout;

public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILang, IDisposable
{
    [Inject]
    private SettingsManager SettingsManager { get; init; } = null!;
    
    [Inject]
    private MessageBus MessageBus { get; init; } = null!;
    
    [Inject]
    private IDialogService DialogService { get; init; } = null!;
    
    [Inject]
    private RustService RustService { get; init; } = null!;

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

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

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

    [Inject]
    private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
    
    [Inject]
    private ISnackbar Snackbar { get; init; } = null!;
    
    [Inject]
    private NavigationManager NavigationManager { get; init; } = null!;
    
    [Inject]
    private ILogger<MainLayout> Logger { get; init; } = null!;
    
    [Inject]
    private MudTheme ColorTheme { get; init; } = null!;
    
    private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
    
    private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
    
    private const int NAVBAR_COLLAPSED_WIDTH_INT = 4;
    private const int NAVBAR_EXPANDED_WIDTH_INT = 10;
    private static readonly string NAVBAR_COLLAPSED_WIDTH = $"{NAVBAR_COLLAPSED_WIDTH_INT}em";
    private static readonly string NAVBAR_EXPANDED_WIDTH = $"{NAVBAR_EXPANDED_WIDTH_INT}em";
    
    private bool navBarOpen;
    private bool performingUpdate;
    private UpdateResponse? currentUpdateResponse;
    private MudThemeProvider themeProvider = null!;
    private bool useDarkMode;
    private bool startupCompleted;
    private bool settingsWriteProtectionWarningShown;
    private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1);
    private readonly SemaphoreSlim promptInjectionDialogSemaphore = new(1, 1);

    private IReadOnlyCollection<NavBarItem> navItems = [];
    
    #region Overrides of ComponentBase

    protected override async Task OnInitializedAsync()
    {
        this.NavigationManager.RegisterLocationChangingHandler(this.OnLocationChanging);
        this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
        
        //
        // We use the Tauri API (Rust) to get the data and config directories
        // for this app.
        //
        var dataDir = await this.RustService.GetDataDirectory();
        var configDir = await this.RustService.GetConfigDirectory();
        
        this.Logger.LogInformation($"The data directory is: '{dataDir}'");
        this.Logger.LogInformation($"The config directory is: '{configDir}'");
        
        // Store the directories in the settings manager:
        SettingsManager.ConfigDirectory = configDir;
        SettingsManager.DataDirectory = dataDir;
        Directory.CreateDirectory(SettingsManager.DataDirectory);
        
        //
        // Read the user language from Rust:
        //
        var userLanguage = await this.RustService.ReadUserLanguage();
        var userName = await this.RustService.ReadUserName();
        this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language.");
        this.Logger.LogInformation($"The OS says '{userName}' is the username.");
        
        // Ensure that all settings are loaded:
        await this.SettingsManager.LoadSettings();
        
        // Register this component with the message bus:
        this.MessageBus.RegisterComponent(this);
        this.MessageBus.ApplyFilters(this, [],
        [
            Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
            Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.SHOW_PROMPT_INJECTION_ALERT, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED,
            Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED,
            Event.CHAT_GENERATION_CHANGED, Event.ASSISTANT_SESSION_CHANGED, Event.ASSISTANT_SESSION_FINISHED,
        ]);
        
        // Set the snackbar for the update service:
        UpdateService.MarkBlazorReady();
        TemporaryChatService.Initialize();
        
        // Should the navigation bar be open by default?
        if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
            this.navBarOpen = true;

        // Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133:
        MudGlobal.TooltipDefaults.Duration = TimeSpan.Zero;
        
        // Send a message to start the plugin system:
        await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_PLUGIN_SYSTEM);
        
        await this.themeProvider.WatchSystemDarkModeAsync(this.SystemeThemeChanged);
        await this.UpdateThemeConfiguration();
        this.LoadNavItems();

        await base.OnInitializedAsync();
    }

    #endregion

    private void ShowSettingsWriteProtectionWarning()
    {
        if(!this.SettingsManager.SettingsWriteBlocked || this.settingsWriteProtectionWarningShown)
            return;

        this.settingsWriteProtectionWarningShown = true;
        var reason = this.SettingsManager.SettingsWriteBlockReason;
        var message = reason switch
        {
            SettingsWriteBlockReason.VERSION_NEWER_THAN_APP => T("Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."),
            SettingsWriteBlockReason.VERSION_MISSING => T("Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
            SettingsWriteBlockReason.VERSION_UNKNOWN => T("AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
            SettingsWriteBlockReason.FILE_UNREADABLE => T("AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."),
            SettingsWriteBlockReason.CURRENT_VERSION_INVALID => T("AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."),
            _ => T("AI Studio cannot safely save settings in this session. Please check for updates or contact support."),
        };
        message = $"{message} {T("Reason")}: {reason}";

        this.Snackbar.Add(message, Severity.Warning, config =>
        {
            config.Icon = Icons.Material.Filled.WarningAmber;
            config.IconSize = Size.Large;
            config.VisibleStateDuration = 32_000;
            config.HideTransitionDuration = 600;
            config.Action = T("Check for updates");
            config.ActionVariant = Variant.Filled;
            config.OnClick = async _ =>
            {
                await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
            };
        });
    }

    #region Implementation of ILang

    /// <inheritdoc />
    public string T(string fallbackEN) => this.GetText(this.Lang, fallbackEN);

    /// <inheritdoc />
    public string T(string fallbackEN, string? typeNamespace, string? typeName) => this.GetText(this.Lang, fallbackEN, typeNamespace, typeName);

    #endregion

    #region Implementation of IMessageBusReceiver

    public string ComponentName => nameof(MainLayout);
    
    public async Task ProcessMessage<TMessage>(ComponentBase? sendingComponent, Event triggeredEvent, TMessage? data)
    {
        await this.InvokeAsync(async () =>
        {
            switch (triggeredEvent)
            {
                case Event.INSTALL_UPDATE:
                    if (!this.UpdatePolicy.AllowsInstallations)
                        break;
                    this.performingUpdate = true;
                    this.StateHasChanged();
                    break;
                
                case Event.UPDATE_AVAILABLE:
                    if (data is UpdateResponse updateResponse)
                    {
                        this.currentUpdateResponse = updateResponse;
                        var message = string.Format(T("An update to version {0} is available."), updateResponse.NewVersion);
                        this.Snackbar.Add(message, Severity.Info, config =>
                        {
                            config.Icon = Icons.Material.Filled.Update;
                            config.IconSize = Size.Large;
                            config.HideTransitionDuration = 600;
                            config.VisibleStateDuration = 32_000;
                            config.OnClick = async _ =>
                            {
                                await this.ShowUpdateDialog();
                            };
                            config.Action = T("Show details");
                            config.ActionVariant = Variant.Filled;
                        });
                    }

                    break;

                case Event.CONFIGURATION_CHANGED:
                    if (this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
                        this.navBarOpen = true;
                    else
                        this.navBarOpen = false;

                    await this.UpdateThemeConfiguration();
                    this.LoadNavItems();
                    this.StateHasChanged();
                    if (this.startupCompleted)
                        _ = this.EnsureMandatoryInfosAcceptedAsync();
                    break;

                case Event.COLOR_THEME_CHANGED:
                    this.StateHasChanged();
                    break;

                case Event.AI_JOB_CHANGED:
                case Event.AI_JOB_FINISHED:
                case Event.CHAT_GENERATION_CHANGED:
                case Event.ASSISTANT_SESSION_CHANGED:
                case Event.ASSISTANT_SESSION_FINISHED:
                    this.LoadNavItems();
                    this.StateHasChanged();
                    break;

                case Event.SHOW_SUCCESS:
                    if (data is DataSuccessMessage success)
                        success.Show(this.Snackbar);

                    break;
                
                case Event.SHOW_PROMPT_INJECTION_ALERT:
                    if (data is PromptInjectionAlertMessage promptInjectionAlert)
                        await this.ShowPromptInjectionAlertAsync(promptInjectionAlert);

                    break;

                case Event.SHOW_ERROR:
                    if (data is DataErrorMessage error)
                        error.Show(this.Snackbar);

                    break;

                case Event.SHOW_WARNING:
                    if (data is DataWarningMessage warning)
                        warning.Show(this.Snackbar);

                    break;

                case Event.SHOW_INFO:
                    if (data is DataInfoMessage info)
                        info.Show(this.Snackbar);

                    break;

                case Event.STARTUP_PLUGIN_SYSTEM:
                    _ = Task.Run(async () =>
                    {
                        // Set up the plugin system:
                        if (PluginFactory.Setup())
                        {
                            // Ensure that all internal plugins are present:
                            await PluginFactory.EnsureInternalPlugins();

                            //
                            // Check if there is an enterprise configuration plugin to download:
                            //
                            var enterpriseEnvironments = this.MessageBus
                                .CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT)
                                .Where(env => env != default)
                                .ToList();
                            
                            var failedDeferredConfigIds = new HashSet<Guid>();
                            foreach (var env in enterpriseEnvironments)
                            {
                                var wasDownloadSuccessful = await PluginFactory.TryDownloadingConfigPluginAsync(env.ConfigurationId, env.ConfigurationServerUrl);
                                if (!wasDownloadSuccessful)
                                {
                                    failedDeferredConfigIds.Add(env.ConfigurationId);
                                    this.Logger.LogWarning("Failed to download deferred enterprise configuration '{ConfigId}' during startup. Keeping managed plugins unchanged.", env.ConfigurationId);
                                }
                            }

                            if (EnterpriseEnvironmentService.HasValidEnterpriseSnapshot)
                            {
                                var activeConfigIds = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS
                                    .Select(env => env.ConfigurationId)
                                    .ToHashSet();
                                
                                PluginFactory.RemoveUnreferencedManagedConfigurationPlugins(activeConfigIds);
                                if (failedDeferredConfigIds.Count > 0)
                                    this.Logger.LogWarning("Deferred startup updates failed for {FailedCount} enterprise configuration(s). Those configurations were kept unchanged.", failedDeferredConfigIds.Count);
                            }

                            // Initialize the enterprise encryption service for decrypting API keys:
                            await PluginFactory.InitializeEnterpriseEncryption(this.RustService);

                            // Load (but not start) all plugins without waiting for them:
                            #if DEBUG
                            var pluginLoadingTimeout = new CancellationTokenSource();
                            #else
                            var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                            #endif
                            await PluginFactory.LoadAll(pluginLoadingTimeout.Token);

                            // Set up hot reloading for plugins:
                            PluginFactory.SetUpHotReloading();
                            await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_COMPLETED);
                        }
                    });
                    break;

                case Event.PLUGINS_RELOADED:
                    this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
                    I18N.Init(this.Lang);
                    this.ShowSettingsWriteProtectionWarning();
                    this.LoadNavItems();

                    await this.InvokeAsync(this.StateHasChanged);
                    if (this.startupCompleted)
                        _ = this.EnsureMandatoryInfosAcceptedAsync();
                    break;

                case Event.STARTUP_COMPLETED:
                    this.startupCompleted = true;
                    _ = this.EnsureMandatoryInfosAcceptedAsync();
                    break;
            }
        });
    }
    
    private async Task ShowPromptInjectionAlertAsync(PromptInjectionAlertMessage alert)
    {
        await this.promptInjectionDialogSemaphore.WaitAsync();
        try
        {
            var dialogParameters = new DialogParameters<PromptInjectionAlertDialog>
            {
                { x => x.Result, alert.Result },
            };

            var dialogReference = await this.DialogService.ShowAsync<PromptInjectionAlertDialog>(
                T("Prompt Injection Detected"),
                dialogParameters,
                DialogOptions.BLOCKING_FULLSCREEN);

            await dialogReference.Result;
        }
        finally
        {
            this.promptInjectionDialogSemaphore.Release();
        }
    }

    public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
    {
        return Task.FromResult<TResult?>(default);
    }

    #endregion

    private void LoadNavItems()
    {
        this.navItems = new List<NavBarItem>(this.GetNavItems());
    }

    /// <summary>Refreshes navigation activity colors when a media import changes state.</summary>
    private void OnMediaImportStateChanged(MediaImportOwner owner)
    {
        _ = this.InvokeAsync(() =>
        {
            this.LoadNavItems();
            this.StateHasChanged();
        });
    }
    
    private IEnumerable<NavBarItem> GetNavItems()
    {
        var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
        var activityIndicatorLightColor = this.ColorTheme.GetActivityIndicatorLightColor();
        var activityIndicatorDarkColor = this.ColorTheme.GetActivityIndicatorDarkColor();
        var defaultLightColor = palette.DarkLighten;
        var defaultDarkColor = palette.GrayLight;
        var mediaSnapshots = this.MediaTranscriptionService.GetSnapshots();
        var hasActiveChatMedia = mediaSnapshots.Any(snapshot => snapshot is { IsBusy: true, Owner.Kind: MediaImportOwnerKind.CHAT });
        var hasActiveAssistantMedia = mediaSnapshots.Any(snapshot => snapshot is { IsBusy: true, Owner.Kind: MediaImportOwnerKind.ASSISTANT or MediaImportOwnerKind.VISUAL_BRIEFING });
        var hasActiveChatWork = this.AIJobService.HasActiveJobs || hasActiveChatMedia;
        var hasActiveAssistantWork = this.AssistantSessionService.HasActiveSessions || hasActiveAssistantMedia;
        var chatLightColor = hasActiveChatWork ? activityIndicatorLightColor : defaultLightColor;
        var chatDarkColor = hasActiveChatWork ? activityIndicatorDarkColor : defaultDarkColor;
        var assistantsLightColor = hasActiveAssistantWork ? activityIndicatorLightColor : defaultLightColor;
        var assistantsDarkColor = hasActiveAssistantWork ? activityIndicatorDarkColor : defaultDarkColor;
        
        yield return new(T("Home"), Icons.Material.Filled.Home, defaultLightColor, defaultDarkColor, Routes.HOME, true);
        yield return new(T("Chat"), Icons.Material.Filled.Chat, chatLightColor, chatDarkColor, Routes.CHAT, false);
        yield return new(T("Assistants"), Icons.Material.Filled.Apps, assistantsLightColor, assistantsDarkColor, Routes.ASSISTANTS, false);

        if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))
            yield return new(T("Writer"), Icons.Material.Filled.Create, defaultLightColor, defaultDarkColor, Routes.WRITER, false);

        yield return new(T("Plugins"), Icons.Material.TwoTone.Extension, defaultLightColor, defaultDarkColor, Routes.PLUGINS, false);
        yield return new(T("Supporters"), Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false);
        yield return new(T("Information"), Icons.Material.Filled.Info, defaultLightColor, defaultDarkColor, Routes.ABOUT, false);
        yield return new(T("Settings"), Icons.Material.Filled.Settings, defaultLightColor, defaultDarkColor, Routes.SETTINGS, false);
    }

    private async Task ShowUpdateDialog()
    {
        if (!this.UpdatePolicy.AllowsInstallations)
            return;

        if(this.currentUpdateResponse is null)
            return;
        
        //
        // Replace the fir line with `# Changelog`:
        //
        var changelog = this.currentUpdateResponse.Value.Changelog;
        if (!string.IsNullOrWhiteSpace(changelog))
        {
            var lines = changelog.Split('\n');
            if (lines.Length > 0)
                lines[0] = "# Changelog";
            
            changelog = string.Join('\n', lines);
        }
        
        var updatedResponse = this.currentUpdateResponse.Value with { Changelog = changelog };
        var dialogParameters = new DialogParameters<UpdateDialog>
        {
            { x => x.UpdateResponse, updatedResponse }
        };

        var dialogReference = await this.DialogService.ShowAsync<UpdateDialog>(T("Update"), dialogParameters, DialogOptions.FULLSCREEN_NO_HEADER);
        var dialogResult = await dialogReference.Result;
        if (dialogResult is null || dialogResult.Canceled)
            return;

        if (!this.UpdatePolicy.AllowsInstallations)
            return;
        
        this.performingUpdate = true;
        this.StateHasChanged();
        await this.RustService.InstallUpdate();
    }
    
    private async ValueTask OnLocationChanging(LocationChangingContext context)
    {
        if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
        {
            var dialogParameters = new DialogParameters<ConfirmDialog>
            {
                { x => x.Message, T("Are you sure you want to leave the chat page? All unsaved changes will be lost.") },
            };
        
            var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Leave Chat Page"), dialogParameters, DialogOptions.FULLSCREEN);
            var dialogResult = await dialogReference.Result;
            if (dialogResult is null || dialogResult.Canceled)
            {
                context.PreventNavigation();
                return;
            }

            // User accepted to leave the chat page, reset the chat state:
            await MessageBus.INSTANCE.SendMessage<bool>(this, Event.RESET_CHAT_STATE);
        }
    }
    
    private async Task SystemeThemeChanged(bool isDark)
    {
        this.Logger.LogInformation($"The system theme changed to {(isDark ? "dark" : "light")}.");
        await this.UpdateThemeConfiguration();
    }

    private async Task UpdateThemeConfiguration()
    {
        if (this.SettingsManager.ConfigurationData.App.PreferredTheme is Themes.SYSTEM)
            this.useDarkMode = await this.themeProvider.GetSystemDarkModeAsync();
        else
            this.useDarkMode = this.SettingsManager.ConfigurationData.App.PreferredTheme == Themes.DARK;

        this.SettingsManager.IsDarkMode = this.useDarkMode;
        await this.MessageBus.SendMessage<bool>(this, Event.COLOR_THEME_CHANGED);
        this.StateHasChanged();
    }

    private async Task EnsureMandatoryInfosAcceptedAsync()
    {
        if (!await this.mandatoryInfoDialogSemaphore.WaitAsync(0))
            return;

        try
        {
            while (true)
            {
                var pendingInfos = this.GetPendingMandatoryInfos().ToList();
                if (pendingInfos.Count == 0)
                    return;

                foreach (var info in pendingInfos)
                {
                    var wasAccepted = await this.ShowMandatoryInfoDialog(info);
                    if (!wasAccepted)
                    {
                        await this.RustService.ExitApplication();
                        return;
                    }

                    await this.StoreMandatoryInfoAcceptance(info);
                }
            }
        }
        finally
        {
            this.mandatoryInfoDialogSemaphore.Release();
        }
    }

    private IEnumerable<DataMandatoryInfo> GetPendingMandatoryInfos()
    {
        return PluginFactory.GetMandatoryInfos()
            .Where(info =>
            {
                var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id);
                return acceptance is null || !string.Equals(acceptance.AcceptedHash, info.AcceptanceHash, StringComparison.Ordinal);
            });
    }

    private async Task<bool> ShowMandatoryInfoDialog(DataMandatoryInfo info)
    {
        var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id);
        var dialogParameters = new DialogParameters<MandatoryInfoDialog>
        {
            { x => x.Info, info },
            { x => x.Acceptance, acceptance },
        };

        var dialogReference = await this.DialogService.ShowAsync<MandatoryInfoDialog>(info.Title, dialogParameters, DialogOptions.BLOCKING_FULLSCREEN);
        var dialogResult = await dialogReference.Result;
        return dialogResult is { Canceled: false, Data: true };
    }

    private async Task StoreMandatoryInfoAcceptance(DataMandatoryInfo info)
    {
        var acceptances = this.SettingsManager.ConfigurationData.MandatoryInformation.Acceptances;
        var acceptance = new DataMandatoryInfoAcceptance
        {
            InfoId = info.Id,
            AcceptedVersion = info.VersionText,
            AcceptedHash = info.AcceptanceHash,
            AcceptedAtUtc = DateTimeOffset.UtcNow,
            EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId,
        };

        var existingIndex = acceptances.FindIndex(item => item.InfoId == info.Id);
        if (existingIndex >= 0)
            acceptances[existingIndex] = acceptance;
        else
            acceptances.Add(acceptance);

        await this.SettingsManager.StoreSettings();
    }
    
    #region Implementation of IDisposable

    public void Dispose()
    {
        this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
        this.MessageBus.Unregister(this);
        this.mandatoryInfoDialogSemaphore.Dispose();
    }

    #endregion
}