using Duplicati.Library.Interface;
using Duplicati.Library.Main;
using Duplicati.Library.Utility;
using Duplicati.Server;
using Duplicati.Server.Database;
using Duplicati.WebserverCore.Abstractions;

namespace Duplicati.WebserverCore.Endpoints.Shared;

public record BackendTupleDisposeWrapper(IBackend Backend, IEnumerable<IGenericModule> Modules) : IDisposable
{
    public void Dispose()
    {
        Backend?.Dispose();
        foreach (var n in Modules)
            if (n is IDisposable disposable)
                disposable.Dispose();
    }
}

public record SourceProviderTupleDisposeWrapper(ISourceProvider SourceProvider, IEnumerable<IGenericModule> Modules) : IDisposable
{
    public void Dispose()
    {
        SourceProvider?.Dispose();
        foreach (var n in Modules)
            if (n is IDisposable disposable)
                disposable.Dispose();
    }
}

public record RestoreDestinationProviderTupleDisposeWrapper(IRestoreDestinationProvider RestoreDestinationProvider, IEnumerable<IGenericModule> Modules) : IDisposable
{
    public void Dispose()
    {
        RestoreDestinationProvider?.Dispose();
        foreach (var n in Modules)
            if (n is IDisposable disposable)
                disposable.Dispose();
    }
}

public class SharedRemoteOperation
{
    public static Dictionary<string, string?> ParseUrlOptions(Connection connection, Library.Utility.Uri uri)
    {
        var qp = uri.QueryParameters;

        var opts = Runner.GetCommonOptions(connection);
        foreach (var k in qp.Keys.Cast<string>())
            opts[k] = qp[k];

        return opts;
    }

    public static IEnumerable<IGenericModule> ConfigureModules(IDictionary<string, string?> opts)
    {
        var modules = Library.DynamicLoader.GenericLoader.Modules.OfType<IConnectionModule>()
            .Select(x => Library.DynamicLoader.GenericLoader.GetModule(x.Key))
            .WhereNotNull()
            .ToArray();

        foreach (var n in modules)
            n.Configure(opts);

        return modules;
    }

    /// <summary>
    /// This method will unmask the URL if needed, apply secrets from the secret provider, and merge in remote control storage API credentials if it's a Duplicati backend URL. 
    /// It returns the expanded URL and the options used for the expansion, which may be needed for later operations to ensure consistency.
    /// </summary>
    /// <param name="connection">The database connection</param>
    /// <param name="applicationSettings">The application settings</param>
    /// <param name="url">The URL to expand</param>
    /// <param name="backupId">The backup ID</param>
    /// <param name="sourcePrefix">The prefix, if this is a remote source</param>
    /// <param name="cancelToken">The cancellation token</param>
    /// <returns>The unmasked URL and the options after the expansion</returns>
    public static async Task<(string Url, Dictionary<string, string?> Options)> ExpandUrl(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken)
    {
        url = UnmaskUrl(connection, url, backupId, connectionStringId, sourcePrefix);
        var uri = new Library.Utility.Uri(url);
        var opts = ParseUrlOptions(connection, uri);

        var tmp = new[] { uri };
        await SecretProviderHelper.ApplySecretProviderAsync([], tmp, opts, Library.Utility.TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancelToken);
        url = tmp[0].ToString();

        if (uri.Scheme.Equals(Library.Backend.Duplicati.DuplicatiBackend.PROTOCOL, StringComparison.OrdinalIgnoreCase))
            url = Library.Backend.Duplicati.DuplicatiBackend.MergeArgsIntoUrl(
                url,
                connection.ApplicationSettings.RemoteControlStorageApiId,
                connection.ApplicationSettings.RemoteControlStorageApiKey,
                connection.ApplicationSettings.RemoteControlStorageEndpointUrl
            );

        return (url, opts);
    }

    public static async Task<BackendTupleDisposeWrapper> GetBackend(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, CancellationToken cancelToken)
    {
        (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, null, cancelToken);
        var modules = ConfigureModules(opts);
        var backend = Library.DynamicLoader.BackendLoader.GetBackend(url, opts);
        return new BackendTupleDisposeWrapper(backend, modules);
    }

    public static async Task<SourceProviderTupleDisposeWrapper> GetSourceProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken)
    {
        (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, sourcePrefix, cancelToken);
        var modules = ConfigureModules(opts);
        var sourceProvider = await Library.DynamicLoader.SourceProviderLoader.GetSourceProviderForTesting(url, "", opts, cancelToken);

        return new SourceProviderTupleDisposeWrapper(sourceProvider, modules);
    }

    public static async Task<RestoreDestinationProviderTupleDisposeWrapper> GetRestoreDestinationProviderForTesting(Connection connection, IApplicationSettings applicationSettings, string url, string? backupId, long connectionStringId, string? sourcePrefix, CancellationToken cancelToken)
    {
        (url, var opts) = await ExpandUrl(connection, applicationSettings, url, backupId, connectionStringId, sourcePrefix, cancelToken);
        var modules = ConfigureModules(opts);
        var restoreDestinationProvider = await Library.DynamicLoader.RestoreDestinationProviderLoader.GetRestoreDestinationProviderForTesting(url, opts, cancelToken);

        return new RestoreDestinationProviderTupleDisposeWrapper(restoreDestinationProvider, modules);
    }

    public static Exception GetInnerException<T>(Exception ex) where T : Exception
    {
        var original = ex;
        while (true)
        {
            if (ex == null)
                return original;

            if (ex is T)
                return (T)ex;

            if (ex.InnerException == null)
                throw new ArgumentNullException(nameof(ex.InnerException));

            ex = ex.InnerException;
        }
    }

    /// <summary>
    /// Unmask the URL if it is masked
    /// </summary>
    /// <param name="connection">The database connection</param>
    /// <param name="maskedurl">The masked URL</param>
    /// <param name="backupId">The backup ID</param>
    /// <param name="prefix">The prefix, if this is a remote source</param>
    /// <returns>The unmasked URL</returns>
    private static string UnmaskUrl(Connection connection, string maskedurl, string? backupId, long connectionStringId, string? prefix)
    {
        var backup = !string.IsNullOrWhiteSpace(backupId) ? connection.GetBackup(backupId) : null;
        var previousUrl = backup?.TargetURL;

        if (string.IsNullOrWhiteSpace(previousUrl) && connectionStringId > 0)
            previousUrl = connection.GetConnectionString(connectionStringId)?.BaseUrl;

        if (!string.IsNullOrWhiteSpace(prefix) && backup?.Sources != null)
        {
            previousUrl = backup.Sources?.FirstOrDefault(s => s.StartsWith(prefix + '|', StringComparison.Ordinal));
            if (!string.IsNullOrWhiteSpace(previousUrl))
                previousUrl = previousUrl.Split('|', 2).LastOrDefault();
        }

        var unmasked = string.IsNullOrWhiteSpace(previousUrl)
            ? maskedurl
            : QuerystringMasking.Unmask(maskedurl, previousUrl);

        if (Connection.UrlContainsPasswordPlaceholder(unmasked))
            throw new ArgumentException("Unmasked URL contains password placeholder");

        return unmasked;
    }
}
