using System.IO.Compression;
using Microsoft.Extensions.Logging;
using SteUp.Data.LocalDb;
using SteUp.Shared.Core.Dto;
using SteUp.Shared.Core.Entities;
using SteUp.Shared.Core.Helpers;
using SteUp.Shared.Core.Interface.System;

namespace SteUp.Maui.Core.Services;

public class FileManager(
    IDbPathProvider dbPathProvider,
    ILogger<FileManager> logger) : IFileManager
{
    private static string AttachedRoot =>
        Path.Combine(FileSystem.CacheDirectory, "attached");

    private async Task<AttachedDto> ConvertToDto(FileInfo file, AttachedDto.TypeAttached type, bool isFromToUpload)
    {
        var (origUrl, thumbUrl) = await SaveAndCreateThumbAsync(
            await File.ReadAllBytesAsync(file.FullName),
            file.Name
        );

        return new AttachedDto
        {
            Name = file.Name,
            Path = file.FullName,
            TempPath = origUrl,
            ThumbPath = thumbUrl,
            Type = type,
            SavedOnAppData = true,
            ToUpload = isFromToUpload
        };
    }

    private const string ToUploadFolderName = "toUpload";

    private string GetInspectionBaseDir(Ispezione ispezione)
    {
        var baseDir = FileSystem.AppDataDirectory;
        return Path.Combine(baseDir, "attached", $"inspection_{GetInspectionKey(ispezione)}");
    }

    private string GetInspectionToUploadDir(Ispezione ispezione)
        => Path.Combine(GetInspectionBaseDir(ispezione), ToUploadFolderName);
    
    public string GetFileToUploadDir(Ispezione ispezione, string fileName) => 
        Path.Combine(GetInspectionToUploadDir(ispezione), fileName);

    private string GetInspectionFinalDir(Ispezione ispezione)
        => GetInspectionBaseDir(ispezione);

    /// <summary>
    /// Ritorna i file dell'ispezione filtrati per nome.
    /// Per default include sia "final" sia "toUpload" (utile per UI).
    /// </summary>
    public async Task<List<AttachedDto>?> GetInspectionFiles(
        Ispezione ispezione,
        List<string> fileNameFilter,
        bool includeToUpload,
        CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(ispezione);
        ArgumentNullException.ThrowIfNull(fileNameFilter);

        var baseDir = GetInspectionBaseDir(ispezione);
        if (!Directory.Exists(baseDir)) return null;

        var result = new List<AttachedDto>();

        var finalDir = GetInspectionFinalDir(ispezione);
        if (Directory.Exists(finalDir))
        {
            var finalFiles = new DirectoryInfo(finalDir)
                .GetFiles("*", SearchOption.TopDirectoryOnly);

            foreach (var file in finalFiles)
            {
                if (file.Directory?.Name == ToUploadFolderName)
                    continue;

                if (!fileNameFilter.Contains(file.FullName))
                    continue;

                ct.ThrowIfCancellationRequested();

                result.Add(await ConvertToDto(
                    file,
                    AttachedDto.TypeAttached.Image,
                    isFromToUpload: false));
            }
        }

        if (!includeToUpload) return result;
        
        var toUploadDir = GetInspectionToUploadDir(ispezione);
        if (!Directory.Exists(toUploadDir)) return result;

        var toUploadFiles = new DirectoryInfo(toUploadDir)
            .GetFiles("*", SearchOption.TopDirectoryOnly);

        foreach (var file in toUploadFiles)
        {
            if (!fileNameFilter.Contains(file.Name))
                continue;

            ct.ThrowIfCancellationRequested();

            result.Add(await ConvertToDto(
                file,
                AttachedDto.TypeAttached.Image,
                isFromToUpload: true));
        }

        return result;
    }

    /// <summary>
    /// Salva SEMPRE in /toUpload.
    /// </summary>
    public async Task<string?> SaveInspectionFile(
        Ispezione ispezione,
        byte[] file,
        string fileName,
        CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(ispezione);
        ArgumentNullException.ThrowIfNull(file);
        ArgumentException.ThrowIfNullOrWhiteSpace(fileName);

        var toUploadDir = GetInspectionToUploadDir(ispezione);
        Directory.CreateDirectory(toUploadDir);

        var filePath = Path.Combine(toUploadDir, fileName);
        await File.WriteAllBytesAsync(filePath, file, ct);

        return filePath;
    }

    public Task<string?> MoveInspectionFileFromToUploadToFinal(
        Ispezione ispezione,
        string fileName,
        bool overwrite,
        CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(ispezione);
        ArgumentException.ThrowIfNullOrWhiteSpace(fileName);

        ct.ThrowIfCancellationRequested();

        var toUploadDir = GetInspectionToUploadDir(ispezione);
        var finalDir = GetInspectionFinalDir(ispezione);

        if (!Directory.Exists(toUploadDir)) return Task.FromResult<string?>(null);

        var sourcePath = Path.Combine(toUploadDir, fileName);
        if (!File.Exists(sourcePath)) return Task.FromResult<string?>(null);

        Directory.CreateDirectory(finalDir);

        var destPath = Path.Combine(finalDir, fileName);

        if (File.Exists(destPath))
        {
            if (!overwrite) return Task.FromResult<string?>(null);
            File.Delete(destPath);
        }

        File.Move(sourcePath, destPath);

        // Pulizia: se /toUpload resta vuota la elimino
        CleanupDirectoriesIfEmpty(ispezione);

        return Task.FromResult<string?>(destPath);
    }

    /// <summary>
    /// Rimuove un file cercandolo prima in /toUpload e poi in final (o viceversa).
    /// Default: prova a cancellare ovunque.
    /// </summary>
    public bool RemoveInspectionFile(
        Ispezione ispezione,
        string fileName,
        bool removeAlsoFromFinal,
        bool removeAlsoFromToUpload)
    {
        ArgumentNullException.ThrowIfNull(ispezione);
        if (string.IsNullOrWhiteSpace(fileName)) return false;

        var removed = false;

        if (removeAlsoFromToUpload)
        {
            var toUploadPath = Path.Combine(GetInspectionToUploadDir(ispezione), fileName);
            if (File.Exists(toUploadPath))
            {
                File.Delete(toUploadPath);
                removed = true;
            }
        }

        if (removeAlsoFromFinal)
        {
            var finalPath = Path.Combine(GetInspectionFinalDir(ispezione), fileName);
            if (File.Exists(finalPath))
            {
                File.Delete(finalPath);
                removed = true;
            }
        }

        if (removed)
            CleanupDirectoriesIfEmpty(ispezione);

        return removed;
    }
    
    public bool RemoveInspection(Ispezione ispezione, bool removeAlsoFromFinal, bool removeAlsoFromToUpload)
    {
        ArgumentNullException.ThrowIfNull(ispezione);

        var removedAnything = false;

        if (removeAlsoFromToUpload)
            TryDeleteDirectory(GetInspectionToUploadDir(ispezione));

        if (removeAlsoFromFinal)
            TryDeleteDirectory(GetInspectionFinalDir(ispezione));
        
        try
        {
            CleanupDirectoriesIfEmpty(ispezione);
        }
        catch(Exception e)
        {
            logger.LogError(e, e.Message);
            SentrySdk.CaptureException(e);
        }

        return removedAnything;

        void TryDeleteDirectory(string? dir)
        {
            if (string.IsNullOrWhiteSpace(dir)) return;

            try
            {
                if (!Directory.Exists(dir)) return;
                
                Directory.Delete(dir, recursive: true);
                removedAnything = true;
            }
            catch(Exception e)
            {
                logger.LogError(e, e.Message);
                SentrySdk.CaptureException(e);
            }
        }
    }

    private void CleanupDirectoriesIfEmpty(Ispezione ispezione)
    {
        var baseDir = GetInspectionBaseDir(ispezione);
        var toUploadDir = GetInspectionToUploadDir(ispezione);

        // 1) se /toUpload esiste e vuota => delete
        if (Directory.Exists(toUploadDir) && !Directory.EnumerateFileSystemEntries(toUploadDir).Any())
            Directory.Delete(toUploadDir);

        // 2) se base dir vuota (attenzione: dopo delete toUpload) => delete
        if (Directory.Exists(baseDir) && !Directory.EnumerateFileSystemEntries(baseDir).Any())
            Directory.Delete(baseDir);
    }

    public async Task<string> SaveToTempStorage(Stream file, string fileName, CancellationToken ct = default)
    {
        ArgumentNullException.ThrowIfNull(file);

        if (file.CanSeek)
            file.Position = 0;

        fileName = Path.GetFileName(fileName);

        var dir = FileSystem.CacheDirectory;
        var filePath = Path.Combine(dir, fileName);

        await using var fileStream = File.Create(filePath);
        await file.CopyToAsync(fileStream, ct);

        return filePath;
    }

    public Task CleanTempStorageAsync(CancellationToken ct = default)
    {
        return Task.Run(() =>
        {
            if (Directory.Exists(AttachedRoot))
                Directory.Delete(AttachedRoot, true);
        }, ct);
    }

    public Task OpenFile(string fileName, string filePath)
    {
#if IOS
        throw new NotImplementedException();
#else
        return Launcher.OpenAsync(new OpenFileRequest
        {
            Title = "Apri file",
            File = new ReadOnlyFile(filePath)
        });
#endif
    }

    public async Task<(string originalUrl, string thumbUrl)> SaveAndCreateThumbAsync(
        byte[] bytes, string fileName, CancellationToken ct = default)
    {
        Directory.CreateDirectory(AttachedRoot);

        var id = Guid.NewGuid().ToString("N");
        var safeName = SanitizeFileName(fileName);

        var originalFile = $"{id}_{safeName}";
        var thumbFile = $"{id}_thumb.jpg";

        var originalPath = Path.Combine(AttachedRoot, originalFile);
        await File.WriteAllBytesAsync(originalPath, bytes, ct);

        var thumbPath = Path.Combine(AttachedRoot, thumbFile);
        await ImageThumb.CreateThumbnailAsync(originalPath, thumbPath, maxSide: 320, quality: 70, ct);

        return ($"https://localfiles/attached/{originalFile}",
            $"https://localfiles/attached/{thumbFile}");
    }

    private static string SanitizeFileName(string fileName)
    {
        var name = Path.GetFileName(fileName);
        return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
    }

    private static string GetInspectionKey(Ispezione ispezione) =>
        $"{ispezione.CodMdep}_{ispezione.Data:ddMMyyyy}_{ispezione.Rilevatore.ToLower()}";
    
    public List<SendEmailDto.AttachmentsDto> GetFileForExport()
    {
        var attachments = new List<SendEmailDto.AttachmentsDto>();

        // 1) log file singolo (se ti serve ancora)
        var logFile = RetrieveLogFile();
        if (!logFile.IsNullOrEmpty())
        {
            attachments.Add(new SendEmailDto.AttachmentsDto
            {
                FileName = $"logs_{DateTime.Today:yyyyMMdd}.zip",
                FileContent = CreateZipBytes(logFile!)
            });
        }

        // 2) database zip
        var dbZip = CreateDatabaseZipAttachment();
        if (dbZip != null) attachments.Add(dbZip);
        
        // 3) Img zip
        var attachedInfo = new DirectoryInfo(Path.Combine(FileSystem.AppDataDirectory, "attached"));
        if (!attachedInfo.Exists) return attachments;
        
        var attachedFiles = attachedInfo
            .EnumerateFiles("*", SearchOption.AllDirectories)
            .ToList();

        if (attachedFiles.Count > 0)
        {
            attachments.Add(new SendEmailDto.AttachmentsDto
            {
                FileName = $"immagini_allegate_{DateTime.Today:yyyyMMdd}.zip",
                FileContent = CreateZipBytes(attachedInfo.FullName, attachedFiles)
            });
        }

        return attachments;
    }
    
    private static List<FileInfo>? RetrieveLogFile()
    {
        var appDataPath = FileSystem.AppDataDirectory;
        var targetDirectory = Path.Combine(appDataPath, "logs");

        var directory = new DirectoryInfo(targetDirectory);

        List<FileInfo>? files = null;

        if (directory.Exists)
            files = directory.GetFiles().ToList();

        return files;
    }
    
    private SendEmailDto.AttachmentsDto? CreateDatabaseZipAttachment()
    {
        var files = new[]
        {
            new FileInfo(dbPathProvider.GetDbPath())
        };

        // Filtra solo quelli esistenti
        var existingFiles = files.Where(f => f.Exists).ToList();

        if (existingFiles.Count == 0)
            return null;

        using var memoryStream = new MemoryStream();

        using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
        {
            foreach (var file in existingFiles)
            {
                var entry = archive.CreateEntry(file.Name, CompressionLevel.Optimal);

                using var entryStream = entry.Open();
                using var fileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

                fileStream.CopyTo(entryStream);
            }
        }

        return new SendEmailDto.AttachmentsDto
        {
            FileName = $"database_{DateTime.Now:yyyyMMdd_HHmm}.zip",
            FileContent = memoryStream.ToArray()
        };
    }
    
    private static byte[] CreateZipBytes(IEnumerable<FileInfo> files)
    {
        using var ms = new MemoryStream();
        using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
        {
            foreach (var file in files)
            {
                if (!file.Exists)
                    continue;

                // Nome dentro lo zip (evita path e collisioni minime)
                var entryName = file.Name;

                var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);

                using var entryStream = entry.Open();
                using var fileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

                fileStream.CopyTo(entryStream);
            }
        }

        return ms.ToArray();
    }
    
    private static byte[] CreateZipBytes(string rootDir, IEnumerable<FileInfo> files)
    {
        using var ms = new MemoryStream();

        using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
        {
            foreach (var file in files)
            {
                if (!file.Exists)
                    continue;

                // Path relativo rispetto a rootDir -> mantiene le directory nello zip
                var relativePath = Path.GetRelativePath(rootDir, file.FullName);

                // Zip usa "/" come separatore: normalizziamo per compatibilità
                var entryName = relativePath.Replace('\\', '/');

                var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);

                using var entryStream = entry.Open();
                using var fileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

                fileStream.CopyTo(entryStream);
            }
        }

        return ms.ToArray();
    }
}