// 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 Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using System.Runtime.CompilerServices;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Uri = System.Uri;
using Duplicati.Library.Utility.Options;
using Duplicati.Library.Utility;
using Azure.Core.Pipeline;

namespace Duplicati.Library.Backend.AzureBlob
{
    /// <summary>
    /// Azure blob storage facade.
    /// </summary>
    public class AzureBlobWrapper
    {
        private readonly BlobContainerClient _container;
        private readonly TimeoutOptionsHelper.Timeouts _timeouts;
        private readonly IReadOnlySet<AccessTier> _archiveClasses;
        private readonly AccessTier? _accessTier;

        /// <summary>
        /// Gets an array of DNS names associated with the blob container.
        /// </summary>
        /// <returns>An array of DNS hostnames for the primary and secondary URIs of the container.</returns>
        public string[] DnsNames
        {
            get
            {
                var lst = new List<string>();
                if (_container != null && _container.Uri != null) lst.Add(_container.Uri.Host);
                return lst.ToArray();
            }
        }

        /// <summary>
        /// Initializes a new instance of the AzureBlobWrapper class.
        /// </summary>
        /// <param name="accountName">The Azure storage account name.</param>
        /// <param name="accessKey">The access key for the storage account.</param>
        /// <param name="sasToken">The Shared Access Signature (SAS) token for authentication.</param>
        /// <param name="containerName">The name of the blob container.</param>
        /// <param name="accessTier">The access tier assigned to blobs on upload.</param>
        /// <param name="archiveClasses">The storage classes that are considered archive classes.</param>
        /// <param name="timeouts">The timeout options.</param>
        /// <param name="maxRetries">The maximum number of retries for Azure operations.</param>
        public AzureBlobWrapper(string accountName, string? accessKey, string? sasToken, string containerName, AccessTier? accessTier, IReadOnlySet<AccessTier> archiveClasses, TimeoutOptionsHelper.Timeouts timeouts, int maxRetries)
        {
            BlobServiceClient blobServiceClient;
            var maxTicks = timeouts.ReadWriteTimeout.Ticks - TimeSpan.FromSeconds(1).Ticks;

            // If not infinite, then share the timeout across retries
            var delay = maxTicks <= 0
                ? TimeSpan.FromSeconds(1)
                : TimeSpan.FromTicks(maxTicks / Math.Max(1, maxRetries));

            var maxDelay = maxTicks <= 0
                ? TimeSpan.FromSeconds(10)
                : TimeSpan.FromTicks(maxTicks);

            var blobServiceOptions = new BlobClientOptions()
            {
                Retry =
                {
                    MaxRetries = maxRetries,
                    Mode = Azure.Core.RetryMode.Exponential,
                    Delay = delay,
                    MaxDelay = maxDelay
                },
                Transport = new HttpClientTransport(new HttpClient { Timeout = Timeout.InfiniteTimeSpan })
            };

            if (sasToken != null)
            {
                var sasUri = new Uri($"https://{accountName}.blob.core.windows.net/?{sasToken}");
                blobServiceClient = new BlobServiceClient(sasUri, blobServiceOptions);
            }
            else
            {
                var connectionString = $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accessKey};EndpointSuffix=core.windows.net";
                blobServiceClient = new BlobServiceClient(connectionString, blobServiceOptions);
            }

            _accessTier = accessTier;
            _archiveClasses = archiveClasses;
            _container = blobServiceClient.GetBlobContainerClient(containerName);
            _timeouts = timeouts;
        }

        /// <summary>
        /// Creates a new blob container asynchronously and sets its access permissions to private.
        /// </summary>
        /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public async Task AddContainerAsync(CancellationToken cancellationToken)
        {
            await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, async ct =>
                // Even though PublicAccessType.None is by default, we set it explicitly to highlight it.
                await _container.CreateAsync(PublicAccessType.None, cancellationToken: ct).ConfigureAwait(false)
            ).ConfigureAwait(false);
        }

        /// <summary>
        /// Downloads a blob to a stream asynchronously.
        /// </summary>
        /// <param name="keyName">The name of the blob to download.</param>
        /// <param name="target">The stream to download the blob to.</param>
        /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
        /// <returns>A task that represents the asynchronous download operation.</returns>
        public async Task GetFileStreamAsync(string keyName, Stream target, CancellationToken cancellationToken)
        {
            var blobClient = _container.GetBlobClient(keyName);
            using var timeoutStream = target.ObserveWriteTimeout(_timeouts.ReadWriteTimeout, false);
            await blobClient.DownloadToAsync(target, cancellationToken).ConfigureAwait(false);
        }

        /// <summary>
        /// Uploads a stream to a blob asynchronously.
        /// </summary>
        /// <param name="keyName">The name to give the uploaded blob.</param>
        /// <param name="source">The stream containing the data to upload.</param>
        /// <param name="cancelToken">A token to monitor for cancellation requests.</param>
        /// <returns>A task that represents the asynchronous upload operation.</returns>
        public async Task AddFileStream(string keyName, Stream source, CancellationToken cancelToken)
        {
            var blobClient = _container.GetBlobClient(keyName);
            using var timeoutStream = source.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false);
            var options = new BlobUploadOptions()
            {
                Conditions = null, // Overwrite any existing blob
                AccessTier = _accessTier
            };

            await blobClient.UploadAsync(timeoutStream, options, cancelToken).ConfigureAwait(false);
        }

        /// <summary>
        /// Deletes a blob if it exists asynchronously.
        /// </summary>
        /// <param name="keyName">The name of the blob to delete.</param>
        /// <param name="cancelToken">A token to monitor for cancellation requests.</param>
        public async Task DeleteObjectAsync(string keyName, CancellationToken cancelToken)
        {
            var blobClient = _container.GetBlobClient(keyName);
            await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancelToken, async ct =>
                await blobClient.DeleteIfExistsAsync(cancellationToken: ct).ConfigureAwait(false)
            ).ConfigureAwait(false);
        }

        /// <summary>
        /// Copies a blob to a new name asynchronously.
        /// </summary>
        /// <param name="sourceKeyName">The name of the source blob.</param>
        /// <param name="destinationKeyName">The name of the destination blob.</param>
        /// <param name="cancelToken">A token to monitor for cancellation requests.</param>
        /// <returns>A task that represents the asynchronous copy operation.</returns>
        public async Task CopyFileAsync(string sourceKeyName, string destinationKeyName, CancellationToken cancelToken)
        {
            var sourceBlobClient = _container.GetBlobClient(sourceKeyName);
            var destinationBlobClient = _container.GetBlobClient(destinationKeyName);

            // Start the copy operation
            var operation = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancelToken, async ct =>
                await destinationBlobClient.StartCopyFromUriAsync(sourceBlobClient.Uri, cancellationToken: ct).ConfigureAwait(false)
            ).ConfigureAwait(false);

            // Wait for the copy to complete
            await operation.WaitForCompletionAsync(cancelToken).ConfigureAwait(false);
        }

        /// <summary>
        /// List container files.
        /// </summary>
        /// <param name="cancelToken">A token to monitor for cancellation requests.</param>
        /// <returns></returns>
        /// <exception cref="FolderMissingException">Thrown when the container is not found</exception>
        public virtual async IAsyncEnumerable<IFileEntry> ListContainerEntriesAsync([EnumeratorCancellation] CancellationToken cancelToken)
        {
            await using var blobEnumerator = _container.GetBlobsAsync().GetAsyncEnumerator(cancelToken);

            while (true)
            {
                bool hasNext;

                try
                {
                    hasNext = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, cancelToken, async ct
                        => await blobEnumerator.MoveNextAsync().ConfigureAwait(false)
                    ).ConfigureAwait(false);
                }
                catch (Azure.RequestFailedException ex) when (ex.Status == 404)
                {
                    throw new FolderMissingException(ex);
                }

                if (!hasNext) break;

                cancelToken.ThrowIfCancellationRequested();

                if (blobEnumerator.Current is { } blob)
                {
                    var blobName = Uri.UnescapeDataString(blob.Name.Replace("+", "%2B"));
                    var lastModified = blob.Properties.LastModified?.UtcDateTime ?? DateTime.UtcNow;
                    var isArchive = blob.Properties.AccessTier.HasValue && _archiveClasses.Contains(blob.Properties.AccessTier.Value);

                    yield return new FileEntry(
                        blobName,
                        blob.Properties.ContentLength ?? 0,
                        lastModified,
                        lastModified
                    )
                    {
                        IsArchived = isArchive,
                        IsFolder = false,
                    };
                }
            }
        }

        public async Task<DateTime?> GetObjectLockUntilAsync(string keyName, CancellationToken cancellationToken)
        {
            var blobClient = _container.GetBlobClient(keyName);

            try
            {
                var properties = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, ct
                    => blobClient.GetPropertiesAsync(cancellationToken: ct)
                ).ConfigureAwait(false);

                return properties.Value.ImmutabilityPolicy?.ExpiresOn?.UtcDateTime;
            }
            catch (RequestFailedException e) when (e.Status == 404
                                                    || e.ErrorCode == BlobErrorCode.BlobNotFound
                                                    || e.ErrorCode == BlobErrorCode.ResourceNotFound)
            {
                throw new FileMissingException(e.Message, e);
            }
            catch (RequestFailedException e) when (e.ErrorCode == BlobErrorCode.ContainerNotFound
                                                    || e.ErrorCode == BlobErrorCode.ContainerBeingDeleted
                                                    || e.ErrorCode == BlobErrorCode.ContainerDisabled)
            {
                throw new FolderMissingException(e.Message, e);
            }
        }

        public async Task SetObjectLockUntilAsync(string keyName, DateTime lockUntilUtc, BlobImmutabilityPolicyMode policyMode, CancellationToken cancellationToken)
        {
            var blobClient = _container.GetBlobClient(keyName);
            var policy = new BlobImmutabilityPolicy
            {
                ExpiresOn = new DateTimeOffset(lockUntilUtc.ToUniversalTime(), TimeSpan.Zero),
                PolicyMode = policyMode
            };

            try
            {
                var result = await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, async ct
                    => await blobClient.SetImmutabilityPolicyAsync(policy, cancellationToken: ct).ConfigureAwait(false)
                ).ConfigureAwait(false);

                if (result.Value.ExpiresOn == null)
                    throw new Exception("Failed to set object lock, call succeeded but no immutability policy returned");
            }
            catch (RequestFailedException e) when (e.Status == 404
                                                    || e.ErrorCode == BlobErrorCode.BlobNotFound
                                                    || e.ErrorCode == BlobErrorCode.ResourceNotFound)
            {
                throw new FileMissingException(e.Message, e);
            }
            catch (RequestFailedException e) when (e.ErrorCode == BlobErrorCode.ContainerNotFound
                                                    || e.ErrorCode == BlobErrorCode.ContainerBeingDeleted
                                                    || e.ErrorCode == BlobErrorCode.ContainerDisabled)
            {
                throw new FolderMissingException(e.Message, e);
            }
        }
    }
}
