using Minio;
using Minio.DataModel.Args;
using Minio.Exceptions;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using Knot.Shared.Kernel.Security;

using Knot.Shared.Kernel.Storage;

namespace Knot.Modules.Storage.Infrastructure.Storage;

public class S3FileStorageService : IFileStorageService
{
    private readonly IMinioClient _minioClient;
    private readonly IEncryptionService _encryptionService;
    private readonly string _bucketName;

    public S3FileStorageService(IMinioClient minioClient, IEncryptionService encryptionService, string bucketName)
    {
        _minioClient = minioClient;
        _encryptionService = encryptionService;
        _bucketName = bucketName;
    }

    private async Task EnsureBucketExistsAsync()
    {
        try
        {
            var bktExistArgs = new BucketExistsArgs().WithBucket(_bucketName);
            bool found = await _minioClient.BucketExistsAsync(bktExistArgs).ConfigureAwait(false);
            if (!found)
            {
                var mkBktArgs = new MakeBucketArgs().WithBucket(_bucketName);
                await _minioClient.MakeBucketAsync(mkBktArgs).ConfigureAwait(false);
            }
        }
        catch (MinioException e)
        {
            Console.WriteLine($"[Bucket] Minio Error: {e.Message}");
        }
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType)
    {
        await EnsureBucketExistsAsync();

        var tempUnencryptedPath = Path.GetTempFileName();
        var tempEncryptedPath = Path.GetTempFileName();
        byte[] ivParams;

        try
        {
            // 1. Сохраняем входной поток (который может быть не seekable из-за HTTP/Kestrel)
            // во временный файл
            using (var unencryptedFs = new FileStream(tempUnencryptedPath, FileMode.Create, FileAccess.Write))
            {
                await fileStream.CopyToAsync(unencryptedFs);
            }

            // 2. Вычисляем SHA-256 для дедупликации, читая из локального временного файла
            string fileHash;
            using (var unencryptedFs = new FileStream(tempUnencryptedPath, FileMode.Open, FileAccess.Read))
            using (var sha256 = SHA256.Create())
            {
                var hashBytes = await sha256.ComputeHashAsync(unencryptedFs);
                fileHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
            }

            var ext = Path.GetExtension(fileName);
            if (string.IsNullOrEmpty(ext)) ext = ""; // For files without extension
            var fileId = $"{fileHash}{ext}";

            // 3. Шифруем файл "на лету" во второй временный файл
            using (var unencryptedFs = new FileStream(tempUnencryptedPath, FileMode.Open, FileAccess.Read))
            using (var encryptedFs = new FileStream(tempEncryptedPath, FileMode.Create, FileAccess.Write))
            using (var cryptoStream = _encryptionService.CreateEncryptionStream(encryptedFs, out ivParams))
            {
                await unencryptedFs.CopyToAsync(cryptoStream);
            }

            // 4. Загружаем зашифрованный файл в MinIO
            using (var fileToUpload = new FileStream(tempEncryptedPath, FileMode.Open, FileAccess.Read))
            {
                var actualContentType = contentType ?? "application/octet-stream";
                var metadata = new System.Collections.Generic.Dictionary<string, string>
                {
                    { "ContentType", actualContentType },
                    { "OriginalFileName", Uri.EscapeDataString(fileName ?? "unknown") },
                    { "IV", Convert.ToBase64String(ivParams) },
                    { "KeyVersion", "1" },
                    { "EncryptionAlgorithm", "AES-256-CBC" }
                };

                var putObjectArgs = new PutObjectArgs()
                    .WithBucket(_bucketName)
                    .WithObject(fileId)
                    .WithStreamData(fileToUpload)
                    .WithObjectSize(fileToUpload.Length)
                    .WithContentType(actualContentType)
                    .WithHeaders(metadata);

                await _minioClient.PutObjectAsync(putObjectArgs).ConfigureAwait(false);
            }

            return fileId;
        }
        finally
        {
            if (File.Exists(tempUnencryptedPath)) File.Delete(tempUnencryptedPath);
            if (File.Exists(tempEncryptedPath)) File.Delete(tempEncryptedPath);
        }
    }

    public async Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId)
    {
        // Для скачивания, мы транслируем из Minio во временный файл, расшифровываем и возвращаем.
        var tempFilePath = Path.GetTempFileName();
        string contentType = "application/octet-stream";
        string fileName = fileId;
        string ivBase64 = string.Empty;

        var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId);
        var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false);

        var metaData = new Dictionary<string, string>(stat.MetaData, StringComparer.OrdinalIgnoreCase);

        // Function to reliably get metadata by key with or without x-amz-meta-
        string? GetMeta(string key)
        {
            if (metaData.TryGetValue(key, out var val)) return val;
            if (metaData.TryGetValue($"X-Amz-Meta-{key}", out var val2)) return val2;
            return null;
        }

        if (GetMeta("Contenttype") is string ct)
        {
            contentType = ct;
        }

        if (GetMeta("Originalfilename") is string ofn)
        {
            try 
            {
                fileName = Uri.UnescapeDataString(ofn);
            }
            catch 
            {
                fileName = ofn; // fallback to unescaped if it was somehow valid
            }
        }

        if (GetMeta("Iv") is string ivStr)
        {
            ivBase64 = ivStr;
        }

        // Загружаем во временный файл (так как CryptoStream требует правильного чтения/записи)
        var getObjArgs = new GetObjectArgs()
            .WithBucket(_bucketName)
            .WithObject(fileId)
            .WithCallbackStream((stream) =>
            {
                using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write);
                stream.CopyTo(fs);
            });

        await _minioClient.GetObjectAsync(getObjArgs).ConfigureAwait(false);

        // Создаем поток дешифрования над байтами временного файла, оборачиваем в MemoryStream, чтобы он автоматически закрывался
        // В реальном масштабируемом продакшене лучше возвращать FileStream напрямую, обернутый в crypto stream.
        var msResult = new MemoryStream();
        using (var fsRead = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read))
        {
            var iv = string.IsNullOrEmpty(ivBase64) ? new byte[16] : Convert.FromBase64String(ivBase64);
            using var cryptoStream = _encryptionService.CreateDecryptionStream(fsRead, iv);
            await cryptoStream.CopyToAsync(msResult);
        }

        File.Delete(tempFilePath);
        msResult.Position = 0;

        return (msResult, contentType, fileName);
    }

    public async Task DeleteFileAsync(string fileId)
    {
        try
        {
            var removeArgs = new RemoveObjectArgs()
                .WithBucket(_bucketName)
                .WithObject(fileId);
            await _minioClient.RemoveObjectAsync(removeArgs).ConfigureAwait(false);
        }
        catch (MinioException e)
        {
            Console.WriteLine($"[Bucket] Error deleting file {fileId}: {e.Message}");
        }
    }

    public async Task<IEnumerable<(string FileId, long Size)>> ListFilesAsync()
    {
        var result = new List<(string FileId, long Size)>();
        try
        {
            await EnsureBucketExistsAsync();
            var listArgs = new ListObjectsArgs().WithBucket(_bucketName).WithRecursive(true);

            await foreach (var item in _minioClient.ListObjectsEnumAsync(listArgs).ConfigureAwait(false))
            {
                if (item != null)
                {
                    result.Add((item.Key, (long)item.Size));
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[Bucket] Error listing files: {ex.Message}");
        }
        return result;
    }

    public async Task<long> GetFileSizeAsync(string fileId, System.Threading.CancellationToken ct = default)
    {
        try
        {
            var statArgs = new StatObjectArgs().WithBucket(_bucketName).WithObject(fileId);
            var stat = await _minioClient.StatObjectAsync(statArgs).ConfigureAwait(false);
            return stat.Size;
        }
        catch
        {
            return 0;
        }
    }

    public async Task<System.Collections.Generic.Dictionary<string, long>> GetFileSizesAsync(System.Collections.Generic.IEnumerable<string> fileIds, System.Threading.CancellationToken ct = default)
    {
        var result = new System.Collections.Generic.Dictionary<string, long>();
        foreach (var fileId in fileIds)
        {
            result[fileId] = await GetFileSizeAsync(fileId, ct);
        }
        return result;
    }
}
