using System.Collections.Concurrent;
using System.Net;
using System.Text;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using InnovEnergy.Lib.S3Utils.DataTypes;
using InnovEnergy.Lib.Utils;
using S3Bucket = InnovEnergy.Lib.S3Utils.DataTypes.S3Bucket;
using S3Region = InnovEnergy.Lib.S3Utils.DataTypes.S3Region;

namespace InnovEnergy.Lib.S3Utils;

public static class S3
{
    private static readonly ConcurrentDictionary<S3Region, AmazonS3Client> S3ClientCache = new();

    // QOL method
    public static S3Bucket Bucket(this S3Region region, String name) => new
    (
        Name: name,
        Region: region
    );

    // QOL method
    public static S3Url Path(this S3Bucket bucket, String path) => new
    (
        Bucket: bucket,
        Path: path
    );

    public static IAsyncEnumerable<S3Url> ListObjects(this S3Bucket bucket) => ListObjects(bucket, null);

    public static IAsyncEnumerable<S3Url> ListObjects(this S3Bucket bucket, String? pathPrefix)
    {
        return bucket
              .Region
              .GetS3Client()
              .Paginators
              .ListObjectsV2(new() { BucketName = bucket.Name, Prefix = pathPrefix })
              .S3Objects
              .Select(o => new S3Url(o.Key, bucket));
    }
    
    public static Task<ListBucketsResponse> ListAllBuckets(this S3Region region)
    {
        return region
              .GetS3Client()
              .ListBucketsAsync();
    }

    public static Task<Boolean> PutObject(this S3Url path, String data, Encoding encoding) => path.PutObject(encoding.GetBytes(data));
    public static Task<Boolean> PutObject(this S3Url path, String data) => path.PutObject(data, Encoding.UTF8);
    public static Task<Boolean> PutObject(this S3Url path, Byte[] data) => path.PutObject(new MemoryStream(data));

    public static async Task<Boolean> PutObject(this S3Url path, Stream data)
    {
        var request = new PutObjectRequest
        {
            BucketName  = path.Bucket.Name,
            Key         = path.Path,
            InputStream = data
        };

        var response = await path
                            .Bucket
                            .Region
                            .GetS3Client()
                            .PutObjectAsync(request);

        return response.HttpStatusCode == HttpStatusCode.OK;
    }

    public static Task<String> GetObjectAsString(this S3Url path) => GetObjectAsString(path, Encoding.UTF8);

    public static async Task<String> GetObjectAsString(this S3Url path, Encoding encoding)
    {
        await using var stream = await GetObjectAsStream(path);
        using var reader = new StreamReader(stream, encoding);
        return await reader.ReadToEndAsync();
    }

    public static async Task<Stream> GetObjectAsStream(this S3Url path)
    {
        var request = new GetObjectRequest
        {
            BucketName = path.Bucket.Name,
            Key = path.Path
        };

        var response = await path
                            .Bucket
                            .Region
                            .GetS3Client()
                            .GetObjectAsync(request);

        return response.ResponseStream;
    }
    
    // public static async Task<Boolean> CheckRoleExists(this S3Region region, String roleId)
    // {
    //     
    //     
    //     var response = await region
    //         .GetIamClient()
    //         .GetRoleAsync(new GetRoleRequest(){RoleName = roleId});
    //     return response.HttpStatusCode != HttpStatusCode.NotFound;
    // }

    public static async Task<IReadOnlyList<Byte>> GetObject(this S3Url url)
    {
        // beautiful await using stream soup...

        await using var stream = await url.GetObjectAsStream();
        using var memoryStream = new MemoryStream();
        await stream.CopyToAsync(memoryStream);
        return memoryStream.ToArray();
    }

    public static IAsyncEnumerable<String> GetObjectLineByLine(this S3Url url) => GetObjectLineByLine(url, Encoding.UTF8);

    public static async IAsyncEnumerable<String> GetObjectLineByLine(this S3Url url, Encoding encoding)
    {
        await using var stream = await url.GetObjectAsStream();

        using var reader = new StreamReader(stream, encoding);

        while (true)
        {
            var line = await reader.ReadLineAsync();

            if (line is not null)
                yield return line;
            else
                yield break;
        }
    }
    
    public static async Task<S3Bucket?> PutBucket(this S3Region region, string name)
    {
        var request = new PutBucketRequest { BucketName = name };
        var response = await region.GetS3Client().PutBucketAsync(request);

        if (response.HttpStatusCode == HttpStatusCode.OK)
        {
            // Define CORS configuration rules
            var corsConfiguration = new CORSConfiguration
            {
                Rules = new List<CORSRule>
                {
                    new CORSRule
                    {
                        AllowedHeaders = new List<string> { "*" },
                        AllowedMethods = new List<string> { "GET", "HEAD" },
                        AllowedOrigins = new List<string> { "*" },
                        ExposeHeaders = new List<string>() // Empty list as per your settings
                    }
                }
            };

            // Create a PutCORSConfigurationRequest
            var putCorsRequest = new PutCORSConfigurationRequest
            {
                BucketName = name,
                Configuration = corsConfiguration
            };

            // Set the CORS configuration for the bucket
            var corsResponse = await region.GetS3Client().PutCORSConfigurationAsync(putCorsRequest);

            if (corsResponse.HttpStatusCode == HttpStatusCode.OK)
            {
                return region.Bucket(name);
            }
            else
            {
                Console.WriteLine("Failed to set CORS configuration.");
                return null;
            }
        }
        else
        {
            Console.WriteLine("Failed to create bucket.");
            return null;
        }
    }
    

    public static async Task<Boolean> PutCors(this S3Bucket bucket, CORSConfiguration corsConfiguration)
    {
        var request = new PutCORSConfigurationRequest
        {
            BucketName    = bucket.Name,
            Configuration = corsConfiguration
        };

        var response = await bucket
                            .GetS3Client()
                            .PutCORSConfigurationAsync(request);

        return response.HttpStatusCode == HttpStatusCode.OK;
    }

    public static async Task<Boolean> DeleteBucket(this S3Region region, String bucketName)
    {
        var request = new DeleteBucketRequest { BucketName = bucketName };
        
        var response = await region
                            .GetS3Client()
                            .DeleteBucketAsync(request);

        return response.HttpStatusCode == HttpStatusCode.NoContent;
    }

    private static AmazonS3Client GetS3Client(this S3Url    url   ) => url.Bucket.GetS3Client();
    private static AmazonS3Client GetS3Client(this S3Bucket bucket) => bucket.Region.GetS3Client();
    private static AmazonS3Client GetS3Client(this S3Region region)
    {
        return S3ClientCache.GetOrAdd(region, CreateS3Client); // Memoize
    }

    private static AmazonS3Client CreateS3Client(S3Region region) => new
    (
        credentials: new BasicAWSCredentials(region.Credentials.Key, region.Credentials.Secret),
        clientConfig: new()
        {
            ServiceURL = region.Name.EnsureStartsWith("https://"),
            ForcePathStyle = true,
        }
    );

}