using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using FileAccessAgent.Application.Abstractions;
using FileAccessAgent.Application.Models;
using FileAccessAgent.Infrastructure.Configuration;
using Microsoft.Extensions.Options;

namespace FileAccessAgent.Infrastructure.Services;

public sealed class MinioFileStorage : IFileStorage
{
    private readonly MinioOptions _options;
    private readonly IAmazonS3 _s3;
    private readonly SemaphoreSlim _bucketLock = new(1, 1);
    private bool _bucketEnsured;

    public MinioFileStorage(IOptions<MinioOptions> options)
    {
        _options = options.Value;
        if (string.IsNullOrWhiteSpace(_options.Endpoint))
        {
            throw new InvalidOperationException("Minio:Endpoint is required.");
        }

        if (string.IsNullOrWhiteSpace(_options.AccessKey) || string.IsNullOrWhiteSpace(_options.SecretKey))
        {
            throw new InvalidOperationException("Minio:AccessKey and Minio:SecretKey are required.");
        }

        if (string.IsNullOrWhiteSpace(_options.BucketName))
        {
            throw new InvalidOperationException("Minio:BucketName is required.");
        }

        var config = new AmazonS3Config
        {
            ServiceURL = BuildServiceUrl(_options.Endpoint, _options.UseSsl),
            ForcePathStyle = true,
            SignatureVersion = "4"
        };

        if (!string.IsNullOrWhiteSpace(_options.Region))
        {
            config.RegionEndpoint = RegionEndpoint.GetBySystemName(_options.Region);
        }

        _s3 = new AmazonS3Client(new BasicAWSCredentials(_options.AccessKey, _options.SecretKey), config);
    }

    public async Task<FileObjectResult> UploadAsync(
        Guid tenantId,
        string objectKey,
        Stream content,
        long? contentLength,
        string? contentType,
        CancellationToken cancellationToken)
    {
        var normalizedKey = ObjectKeyHelper.Normalize(objectKey);
        await EnsureBucketAsync(cancellationToken);

        var request = new PutObjectRequest
        {
            BucketName = _options.BucketName,
            Key = normalizedKey,
            InputStream = content,
            AutoCloseStream = false,
            ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType
        };

        if (contentLength.HasValue && contentLength.Value >= 0)
        {
            request.Headers.ContentLength = contentLength.Value;
        }
        else if (content.CanSeek)
        {
            request.Headers.ContentLength = content.Length - content.Position;
        }
        else
        {
            throw new InvalidOperationException("Content-Length is required for MinIO upload.");
        }

        var put = await _s3.PutObjectAsync(request, cancellationToken);
        var metadata = await GetMetadataAsync(tenantId, normalizedKey, cancellationToken);
        if (metadata is null)
        {
            throw new InvalidOperationException("Object was uploaded but metadata was not retrievable.");
        }

        return metadata with { ETag = put.ETag };
    }

    public async Task<FileObjectResult?> GetMetadataAsync(Guid tenantId, string objectKey, CancellationToken cancellationToken)
    {
        var normalizedKey = ObjectKeyHelper.Normalize(objectKey);
        await EnsureBucketAsync(cancellationToken);

        try
        {
            var response = await _s3.GetObjectMetadataAsync(new GetObjectMetadataRequest
            {
                BucketName = _options.BucketName,
                Key = normalizedKey
            }, cancellationToken);

            return new FileObjectResult(
                tenantId,
                normalizedKey,
                response.Headers.ContentLength,
                string.IsNullOrWhiteSpace(response.Headers.ContentType) ? "application/octet-stream" : response.Headers.ContentType,
                response.LastModified.ToUniversalTime(),
                ETag: response.ETag);
        }
        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound || ex.ErrorCode == "NoSuchKey")
        {
            return null;
        }
    }

    public async Task<FileDownloadResult?> OpenReadAsync(Guid tenantId, string objectKey, CancellationToken cancellationToken)
    {
        var normalizedKey = ObjectKeyHelper.Normalize(objectKey);
        await EnsureBucketAsync(cancellationToken);

        try
        {
            var response = await _s3.GetObjectAsync(new GetObjectRequest
            {
                BucketName = _options.BucketName,
                Key = normalizedKey
            }, cancellationToken);

            return new FileDownloadResult(
                new ResponseStream(response),
                string.IsNullOrWhiteSpace(response.Headers.ContentType) ? "application/octet-stream" : response.Headers.ContentType,
                response.Headers.ContentLength,
                response.LastModified.ToUniversalTime(),
                response.ETag);
        }
        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound || ex.ErrorCode == "NoSuchKey")
        {
            return null;
        }
    }

    public async Task<bool> DeleteAsync(Guid tenantId, string objectKey, CancellationToken cancellationToken)
    {
        var normalizedKey = ObjectKeyHelper.Normalize(objectKey);
        await EnsureBucketAsync(cancellationToken);

        var metadata = await GetMetadataAsync(tenantId, normalizedKey, cancellationToken);
        if (metadata is null)
        {
            return false;
        }

        await _s3.DeleteObjectAsync(new DeleteObjectRequest
        {
            BucketName = _options.BucketName,
            Key = normalizedKey
        }, cancellationToken);

        return true;
    }

    private async Task EnsureBucketAsync(CancellationToken cancellationToken)
    {
        if (_bucketEnsured)
        {
            return;
        }

        await _bucketLock.WaitAsync(cancellationToken);
        try
        {
            if (_bucketEnsured)
            {
                return;
            }

            var exists = await BucketExistsAsync(cancellationToken);
            if (!exists)
            {
                await _s3.PutBucketAsync(new PutBucketRequest { BucketName = _options.BucketName }, cancellationToken);
            }

            _bucketEnsured = true;
        }
        finally
        {
            _bucketLock.Release();
        }
    }

    private static string BuildServiceUrl(string endpoint, bool useSsl)
    {
        if (endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
            || endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
        {
            return endpoint.TrimEnd('/');
        }

        return $"{(useSsl ? "https" : "http")}://{endpoint.TrimEnd('/')}";
    }

    private async Task<bool> BucketExistsAsync(CancellationToken cancellationToken)
    {
        try
        {
            await _s3.GetBucketLocationAsync(new GetBucketLocationRequest
            {
                BucketName = _options.BucketName
            }, cancellationToken);

            return true;
        }
        catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound || ex.ErrorCode == "NoSuchBucket")
        {
            return false;
        }
    }

    private sealed class ResponseStream : Stream
    {
        private readonly GetObjectResponse _response;
        private readonly Stream _inner;

        public ResponseStream(GetObjectResponse response)
        {
            _response = response;
            _inner = response.ResponseStream;
        }

        public override bool CanRead => _inner.CanRead;
        public override bool CanSeek => _inner.CanSeek;
        public override bool CanWrite => _inner.CanWrite;
        public override long Length => _inner.Length;
        public override long Position { get => _inner.Position; set => _inner.Position = value; }
        public override void Flush() => _inner.Flush();
        public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
        public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
        public override void SetLength(long value) => _inner.SetLength(value);
        public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => _inner.ReadAsync(buffer, cancellationToken);
        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.WriteAsync(buffer, offset, count, cancellationToken);

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _response.Dispose();
            }

            base.Dispose(disposing);
        }

        public override async ValueTask DisposeAsync()
        {
            _response.Dispose();
            await base.DisposeAsync();
        }
    }
}
