using System.Linq.Expressions;

using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;

using Lua;

namespace AIStudio.Tools.PluginSystem;

/// <summary>
/// Represents metadata for a configuration object from a configuration plugin. These are
/// complex objects such as configured LLM providers, chat templates, etc.
/// </summary>
public sealed record PluginConfigurationObject
{
    private static RustService RustService => Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
    private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
    private static ThreadSafeRandom Rng => Program.SERVICE_PROVIDER.GetRequiredService<ThreadSafeRandom>();
    private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<PluginConfigurationObject>();
    
    /// <summary>
    /// The id of the configuration plugin to which this configuration object belongs.
    /// </summary>
    public required Guid ConfigPluginId { get; init; } = Guid.NewGuid();
    
    /// <summary>
    /// The id of the configuration object, e.g., the id of a chat template.
    /// </summary>
    public required Guid Id { get; init; } = Guid.NewGuid();

    /// <summary>
    /// The type of the configuration object.
    /// </summary>
    public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE;

    /// <summary>
    /// Parses Lua table entries into configuration objects of the specified type, populating the
    /// provided list with results.
    /// </summary>
    /// <typeparam name="TClass">The type of configuration object to parse, which must
    /// inherit from <see cref="ConfigurationBaseObject"/>.</typeparam>
    /// <param name="configObjectType">The type of configuration object to process, as specified
    /// in <see cref="PluginConfigurationObjectType"/>.</param>
    /// <param name="configObjectSelection">An expression to retrieve existing configuration objects from
    /// the main configuration data.</param>
    /// <param name="nextConfigObjectNumSelection">An expression to retrieve the next available configuration
    /// object number from the main configuration data.</param>
    /// <param name="mainTable">The Lua table containing entries to parse into configuration objects.</param>
    /// <param name="configPluginId">The unique identifier of the plugin associated with the configuration
    /// objects being parsed.</param>
    /// <param name="configObjects">The list to populate with the parsed configuration objects.
    /// This parameter is passed by reference.</param>
    /// <param name="dryRun">Specifies whether to perform the operation as a dry run, where changes
    /// are not persisted.</param>
    /// <param name="pluginPath">An optional parameter specifying the file path of the plugin, used for relative paths in the Lua table.</param>
    /// <returns>Returns true if parsing succeeds and configuration objects are added
    /// to the list; otherwise, false.</returns>
    public static bool TryParse<TClass>(
        PluginConfigurationObjectType configObjectType,
        Expression<Func<Data, List<TClass>>> configObjectSelection,
        Expression<Func<Data, uint>> nextConfigObjectNumSelection,
        LuaTable mainTable,
        Guid configPluginId,
        ref List<PluginConfigurationObject> configObjects,
        bool dryRun,
        string pluginPath = ""
    ) where TClass : ConfigurationBaseObject
    {
        var luaTableName = configObjectType switch
        {
            PluginConfigurationObjectType.LLM_PROVIDER => "LLM_PROVIDERS",
            PluginConfigurationObjectType.CHAT_TEMPLATE => "CHAT_TEMPLATES",
            PluginConfigurationObjectType.DATA_SOURCE => "DATA_SOURCES",
            PluginConfigurationObjectType.EMBEDDING_PROVIDER => "EMBEDDING_PROVIDERS",
            PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => "TRANSCRIPTION_PROVIDERS",
            PluginConfigurationObjectType.PROFILE => "PROFILES",
            PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => "DOCUMENT_ANALYSIS_POLICIES",

            _ => null,
        };

        if (luaTableName is null)
        {
            LOG.LogError("The configuration object type '{ConfigObjectType}' is not supported yet (config plugin id: {ConfigPluginId}).", configObjectType, configPluginId);
            return false;
        }

        if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
        {
            LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, configPluginId);
            return false;
        }

        var localSettingsManager = SettingsManagerAccess;
        var storedObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData);
        var numberObjects = luaTable.ArrayLength;
        ThreadSafeRandom? random = null;
        for (var i = 1; i <= numberObjects; i++)
        {
            var luaObjectTableValue = luaTable[i];
            if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
            {
                LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, i, configPluginId);
                continue;
            }

            var (wasParsingSuccessful, configObject) = configObjectType switch
            {
                PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
                PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject),
                PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject),
                PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
                PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject),
                PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => (DataDocumentAnalysisPolicy.TryProcessConfiguration(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject is DataDocumentAnalysisPolicy, configurationObject),

                _ => (false, NoConfigurationObject.INSTANCE)
            };

            if (wasParsingSuccessful)
            {
                // Store it in the config object list:
                configObjects.Add(new()
                {
                    ConfigPluginId = configPluginId,
                    Id = Guid.Parse(configObject.Id),
                    Type = configObjectType,
                });

                if (dryRun)
                    continue;

                var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
                
                // Case: The object already exists, we update it:
                if (objectIndex > -1)
                {
                    var existingObject = storedObjects[objectIndex];
                    configObject = configObject with { Num = existingObject.Num };
                    storedObjects[objectIndex] = (TClass)configObject;
                }
                
                // Case: The object does not exist, we have to add it
                else
                {
                    if (nextConfigObjectNumSelection.TryIncrement(localSettingsManager.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum })
                    {
                        // Case: Increment the next number was successful
                        configObject = configObject with { Num = nextNum };
                        storedObjects.Add((TClass)configObject);
                    }
                    else
                    {
                        // Case: The next number could not be incremented, we use a random number
                        random ??= Rng;
                        configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
                        storedObjects.Add((TClass)configObject);
                        LOG.LogWarning("The next number for the configuration object '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
                    }
                }
            }
            else
                LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid configuration object (type={ConfigObjectType}, config plugin id: {ConfigPluginId}).", luaTableName, i, configObjectType, configPluginId);
        }
        
        return true;
    }

    /// <summary>
    /// Parses configured data sources from a configuration plugin.
    /// </summary>
    /// <param name="mainTable">The Lua table containing entries to parse into data sources.</param>
    /// <param name="configPluginId">The unique identifier of the plugin associated with the data sources.</param>
    /// <param name="configObjects">The list to populate with the parsed configuration objects.</param>
    /// <param name="dryRun">Specifies whether to perform the operation as a dry run.</param>
    /// <returns>True if the table was present and processed; otherwise false.</returns>
    public static bool TryParseDataSources(
        LuaTable mainTable,
        Guid configPluginId,
        ref List<PluginConfigurationObject> configObjects,
        bool dryRun)
    {
        const string LUA_TABLE_NAME = "DATA_SOURCES";
        if (!mainTable.TryGetValue(LUA_TABLE_NAME, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
        {
            LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, configPluginId);
            return false;
        }

        var localSettingsManager = SettingsManagerAccess;
        var storedObjects = localSettingsManager.ConfigurationData.DataSources;
        var numberObjects = luaTable.ArrayLength;
        ThreadSafeRandom? random = null;
        for (var i = 1; i <= numberObjects; i++)
        {
            var luaObjectTableValue = luaTable[i];
            if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
            {
                LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
                continue;
            }

            if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, out var configObject))
            {
                LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid data source (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
                continue;
            }

            configObjects.Add(new()
            {
                ConfigPluginId = configPluginId,
                Id = Guid.Parse(configObject.Id),
                Type = PluginConfigurationObjectType.DATA_SOURCE,
            });

            if (dryRun)
                continue;

            var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
            if (objectIndex > -1)
            {
                var existingObject = storedObjects[objectIndex];
                configObject = configObject with { Num = existingObject.Num };
                storedObjects[objectIndex] = configObject;
            }
            else
            {
                if (IncrementDataSourceNum(localSettingsManager.ConfigurationData) is { Success: true, UpdatedValue: var nextNum })
                {
                    configObject = configObject with { Num = nextNum };
                    storedObjects.Add(configObject);
                }
                else
                {
                    random ??= Rng;
                    configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
                    storedObjects.Add(configObject);
                    LOG.LogWarning("The next number for the data source '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
                }
            }
        }

        return true;

        static IncrementResult<uint> IncrementDataSourceNum(Data data)
        {
            return ((Expression<Func<Data, uint>>)(x => x.NextDataSourceNum)).TryIncrement(data, IncrementType.POST);
        }
    }

    /// <summary>
    /// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
    /// </summary>
    /// <typeparam name="TClass">The type of configuration object to clean up.</typeparam>
    /// <param name="configObjectType">The type of configuration object to process.</param>
    /// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
    /// <param name="availablePlugins">A list of currently available plugins.</param>
    /// <param name="deployedConfigPluginIds">
    /// The IDs of all configuration plugins which are deployed on this machine, including those which
    /// could not be loaded. Objects of a deployed plugin are never removed, because the plugin was not
    /// removed either.
    /// </param>
    /// <param name="configObjectList">A list of all existing configuration objects.</param>
    /// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
    /// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
    /// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
    public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
        PluginConfigurationObjectType configObjectType,
        Expression<Func<Data, List<TClass>>> configObjectSelection,
        IList<IAvailablePlugin> availablePlugins,
        IReadOnlySet<Guid> deployedConfigPluginIds,
        IList<PluginConfigurationObject> configObjectList,
        SecretStoreType? secretStoreType = null,
        bool deleteSecret = false) where TClass : IConfigurationObject
    {
        var localSettingsManager = SettingsManagerAccess;
        var configuredObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData);
        var leftOverObjects = new List<TClass>();
        foreach (var configuredObject in configuredObjects)
        {
            // Only process objects that are based on enterprise configuration plugins (aka configuration plugins),
            // as only those can be left over after a plugin was removed:
            if(!configuredObject.IsEnterpriseConfiguration)
                continue;

            // From what plugin is this configuration object coming from?
            var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId;
            if(configObjectSourcePluginId == Guid.Empty)
                continue;

            //
            // Is the source plugin deployed, but could not be loaded? Then we must not touch any of
            // its objects. The plugin was not removed, it is broken: it might be invalid Lua code,
            // a missing `plugin.lua`, or an incomplete download. Removing the objects would delete
            // the organization's providers and data sources, including their secrets, although the
            // organization still manages this AI Studio instance:
            //
            if(deployedConfigPluginIds.Contains(configObjectSourcePluginId) && availablePlugins.All(plugin => plugin.Id != configObjectSourcePluginId))
                continue;

            // Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
            // over and should be removed:
            var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
            if(templateSourcePlugin is null)
            {
                LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing this object from the settings.");
                leftOverObjects.Add(configuredObject);
            }
            
            // Is the configuration object still present in the configuration plugin? If not, it is also left over and should be removed:
            if(!configObjectList.Any(configObject =>
                   configObject.Type == configObjectType &&
                   configObject.ConfigPluginId == configObjectSourcePluginId &&
                   configObject.Id.ToString() == configuredObject.Id))
            {
                LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the object from the settings.");
                leftOverObjects.Add(configuredObject);
            }
        }
        
        // Remove collected items after enumeration to avoid modifying the collection during iteration:
        var wasConfigurationChanged = leftOverObjects.Count > 0;
        foreach (var item in leftOverObjects.Distinct())
        {
            configuredObjects.Remove(item);
        
            // Delete the API key from the OS keyring if the removed object has one:
            if(deleteSecret && item is ISecretId regularSecretId)
            {
                var deleteResult = await RustService.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE);
                if (deleteResult.Success)
                    LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring.");
                else
                    LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}");
            }
            else if(secretStoreType is not null && item is ISecretId secretId)
            {
                var deleteResult = await RustService.DeleteAPIKey(secretId, secretStoreType.Value);
                if (deleteResult.Success)
                    LOG.LogInformation($"Successfully deleted API key for removed enterprise provider '{item.Name}' from the OS keyring.");
                else
                    LOG.LogWarning($"Failed to delete API key for removed enterprise provider '{item.Name}' from the OS keyring: {deleteResult.Issue}");
            }
        }

        return wasConfigurationChanged;
    }
}