// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
// 
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using Duplicati.Library.Utility.Options;
using System.Runtime.CompilerServices;

namespace Duplicati.Library.Backend
{
    // ReSharper disable once UnusedMember.Global
    // This class is instantiated dynamically in the BackendLoader.
    public class Dropbox : IBackend, IStreamingBackend, IRenameEnabledBackend, IFolderEnabledBackend
    {
        private static readonly string TOKEN_URL = AuthIdOptionsHelper.GetOAuthLoginUrl("dropbox", null);
        private readonly string m_path;
        private readonly DropboxHelper dbx;

        // ReSharper disable once UnusedMember.Global
        // This constructor is needed by the BackendLoader.
        public Dropbox()
        {
            m_path = null!;
            dbx = null!;
        }

        // ReSharper disable once UnusedMember.Global
        // This constructor is needed by the BackendLoader.
        public Dropbox(string url, Dictionary<string, string?> options)
        {
            var uri = new Utility.Uri(url);

            m_path = Utility.Uri.UrlDecode(uri.HostAndPath);
            if (m_path.Length != 0 && !m_path.StartsWith("/", StringComparison.Ordinal))
                m_path = "/" + m_path;

            if (m_path.EndsWith("/", StringComparison.Ordinal))
                m_path = m_path.Substring(0, m_path.Length - 1);

            var authId = AuthIdOptionsHelper.Parse(options);
            authId.RequireCredentials(TOKEN_URL);

            dbx = new DropboxHelper(authId, TimeoutOptionsHelper.Parse(options));
        }

        /// <inheritdoc/>
        public void Dispose()
        {
            // do nothing
        }

        /// <inheritdoc/>
        public string DisplayName => Strings.Dropbox.DisplayName;

        /// <inheritdoc/>
        public string ProtocolKey => "dropbox";

        /// <inheritdoc/>
        public bool SupportsStreaming => true;

        private IFileEntry ParseEntry(MetaData md)
        {
            var name = md.name;
            if (!md.IsFile && !string.IsNullOrEmpty(name) && !name.EndsWith("/"))
                name += "/";

            var ife = new FileEntry(name);
            if (md.IsFile)
            {
                ife.IsFolder = false;
                ife.Size = (long)md.size;
            }
            else
            {
                ife.IsFolder = true;
            }

            try
            {
                if (!string.IsNullOrWhiteSpace(md.server_modified))
                    ife.LastModification = ife.LastAccess = DateTime.Parse(md.server_modified).ToUniversalTime();
            }
            catch { }

            return ife;
        }

        private void ThrowFolderMissingException(DropboxException de)
        {
            if (de.errorJSON?["error"]?[".tag"]?.ToString() == "path" && de.errorJSON?["error"]?["path"]?[".tag"]?.ToString() == "not_found")
                throw new FolderMissingException();
        }

        private async Task<T> HandleListExceptions<T>(Func<Task<T>> func)
        {
            try
            {
                return await func().ConfigureAwait(false);
            }
            catch (DropboxException de)
            {
                ThrowFolderMissingException(de);
                throw;
            }
        }

        public IAsyncEnumerable<IFileEntry> ListAsync(CancellationToken cancelToken)
            => ListAsync(null, cancelToken);


        private string GetAbsolutePath(string? path)
        {
            if (string.IsNullOrWhiteSpace(path))
                return m_path;

            var p = path.Replace(Path.DirectorySeparatorChar, '/').Trim('/');
            if (string.IsNullOrWhiteSpace(m_path) || m_path == "/")
                p = "/" + p;
            else
                p = Util.AppendDirSeparator(m_path, "/") + p;

            if (p == "/")
                p = "";

            return p;
        }

        public async IAsyncEnumerable<IFileEntry> ListAsync(string? path, [EnumeratorCancellation] CancellationToken cancelToken)
        {
            var fullPath = GetAbsolutePath(path);
            var lfr = await HandleListExceptions(() => dbx.ListFiles(fullPath, cancelToken)).ConfigureAwait(false);

            foreach (var md in lfr.entries ?? [])
                yield return ParseEntry(md);

            while (lfr.has_more)
            {
                lfr = await HandleListExceptions(() => dbx.ListFilesContinue(lfr.cursor!, cancelToken)).ConfigureAwait(false);
                foreach (var md in lfr.entries ?? [])
                    yield return ParseEntry(md);
            }
        }

        public async Task<IFileEntry?> GetEntryAsync(string path, CancellationToken cancellationToken)
        {
            var fullPath = GetAbsolutePath(path);
            if (string.IsNullOrEmpty(fullPath))
                return new FileEntry(string.Empty) { IsFolder = true };

            try
            {
                var md = await dbx.GetMetadataAsync(fullPath, cancellationToken).ConfigureAwait(false);
                if (md == null)
                    return null;
                return ParseEntry(md);
            }
            catch (Exception ex) when (ex is FolderMissingException || ex is DropboxException)
            {
                return null;
            }
        }

        public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
        {
            using (var fs = File.OpenRead(filename))
                await PutAsync(remotename, fs, cancelToken).ConfigureAwait(false);
        }

        public async Task GetAsync(string remotename, string filename, CancellationToken cancelToken)
        {
            using (var fs = File.Create(filename))
                await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false);
        }

        public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
        {
            try
            {
                var path = $"{m_path}/{remotename}";
                await dbx.DeleteAsync(path, cancelToken).ConfigureAwait(false);
            }
            catch (DropboxException de)
            {
                ThrowFolderMissingException(de);
                throw;
            }
        }

        public IList<ICommandLineArgument> SupportedCommands =>
        [
            .. AuthIdOptionsHelper.GetOptions(TOKEN_URL),
            .. TimeoutOptionsHelper.GetOptions(),
        ];

        public string Description { get { return Strings.Dropbox.Description; } }

        public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(WebApi.Dropbox.Hosts());

        public Task TestAsync(CancellationToken cancelToken)
            => this.TestReadWritePermissionsAsync(cancelToken);

        public async Task CreateFolderAsync(CancellationToken cancelToken)
        {
            try
            {
                await dbx.CreateFolderAsync(m_path, cancelToken).ConfigureAwait(false);
            }
            catch (DropboxException de)
            {

                if (de.errorJSON?["error"]?[".tag"]?.ToString() == "path" && de.errorJSON["error"]?["path"]?[".tag"]?.ToString() == "conflict")
                    throw new FolderAreadyExistedException();
                throw;
            }
        }

        public async Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken)
        {
            try
            {
                string path = $"{m_path}/{remotename}";
                await dbx.UploadFileAsync(path, stream, cancelToken);
            }
            catch (DropboxException de)
            {
                ThrowFolderMissingException(de);
                throw;
            }
        }

        public async Task GetAsync(string remotename, Stream stream, CancellationToken cancelToken)
        {
            try
            {
                string path = string.Format("{0}/{1}", m_path, remotename);
                await dbx.DownloadFileAsync(path, stream, cancelToken).ConfigureAwait(false);
            }
            catch (DropboxException de)
            {
                ThrowFolderMissingException(de);
                throw;
            }
        }

        public async Task RenameAsync(string oldname, string newname, CancellationToken cancellationToken)
        {
            try
            {
                string oldPath = $"{m_path}/{oldname}";
                string newPath = $"{m_path}/{newname}";
                await dbx.MoveAsync(oldPath, newPath, cancellationToken).ConfigureAwait(false);
            }
            catch (DropboxException de)
            {
                ThrowFolderMissingException(de);
                throw;
            }
        }
    }
}
