using System.Collections.Concurrent;

namespace Fast.Shared.Logic.FileService;

public class LocalFileService(
    FileServiceConfig config,
    IShardingStrategy? shardingStrategy = null) : IFileService
{
    private readonly FileServiceConfig _config = config ?? throw new ArgumentNullException(nameof(config));
    private readonly IShardingStrategy _shardingStrategy = shardingStrategy ?? new DefaultShardingStrategy();

    public string GetShardFolderName(string fileName) => _shardingStrategy.GetShardFolderName(fileName);

    public string ResolvePath(string folderName, string fileName, bool useSharding = true)
    {
        var basePath = ResolveFolderPath(folderName);
        if (useSharding)
        {
            var shardFolder = _shardingStrategy.GetShardFolderName(fileName);
            if (!string.IsNullOrEmpty(shardFolder))
                return Path.Combine(basePath, shardFolder, fileName);
        }
        return Path.Combine(basePath, fileName);
    }

    /// Resolves the actual filesystem path for a logical folder name.
    private string ResolveFolderPath(string folderName, string? subFolder = null)
    {
        string basePath;
        if (_config.LocalFolderMappings.TryGetValue(folderName, out var mappedPath))
        {
            basePath = mappedPath;
        }
        else
        {
            // Fall back to content root + folder name
            basePath = Path.Combine(_config.ContentRootPath, folderName);
        }

        if (!string.IsNullOrEmpty(subFolder))
        {
            basePath = Path.Combine(basePath, subFolder);
        }

        return basePath;
    }

    public async Task<bool> FileExistsAsync(string folderName, string fileName, bool useSharding = true)
    {
        return await Task.FromResult(FileExists(folderName, fileName, useSharding));
    }

    public bool FileExists(string folderName, string fileName, bool useSharding = true)
    {
        try
        {
            var path = ResolvePath(folderName, fileName, useSharding);
            return File.Exists(path);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error checking file existence: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task<byte[]> ReadAllBytesAsync(string folderName, string fileName, bool useSharding = true)
    {
        var path = ResolvePath(folderName, fileName, useSharding);
        try
        {
            if (!File.Exists(path))
                throw new FileServiceFileNotFoundException(folderName, fileName, FileServiceBackend.Local);

            return await File.ReadAllBytesAsync(path);
        }
        catch (FileServiceException)
        {
            throw;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error reading file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public byte[] ReadAllBytes(string folderName, string fileName, bool useSharding = true)
    {
        var path = ResolvePath(folderName, fileName, useSharding);
        try
        {
            if (!File.Exists(path))
                throw new FileServiceFileNotFoundException(folderName, fileName, FileServiceBackend.Local);

            return File.ReadAllBytes(path);
        }
        catch (FileServiceException)
        {
            throw;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error reading file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task<string> WriteAllBytesAsync(string folderName, string fileName, byte[] content, bool useSharding = true)
    {
        var basePath = ResolveFolderPath(folderName);

        try
        {
            // Ensure base directory exists
            Directory.CreateDirectory(basePath);

            string fullPath;
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.CreateShardedSubdirectory(basePath, fileName);
                fullPath = string.IsNullOrEmpty(shardFolder)
                    ? Path.Combine(basePath, fileName)
                    : Path.Combine(basePath, shardFolder, fileName);
            }
            else
                fullPath = Path.Combine(basePath, fileName);

            await File.WriteAllBytesAsync(fullPath, content);
            return fileName;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error writing file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public string WriteAllBytes(string folderName, string fileName, byte[] content, bool useSharding = true)
    {
        var basePath = ResolveFolderPath(folderName);

        try
        {
            Directory.CreateDirectory(basePath);

            string fullPath;
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.CreateShardedSubdirectory(basePath, fileName);
                fullPath = string.IsNullOrEmpty(shardFolder)
                    ? Path.Combine(basePath, fileName)
                    : Path.Combine(basePath, shardFolder, fileName);
            }
            else
                fullPath = Path.Combine(basePath, fileName);

            File.WriteAllBytes(fullPath, content);
            return fileName;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error writing file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task<bool> DeleteFileAsync(string folderName, string fileName, bool useSharding = true)
    {
        return await Task.FromResult(DeleteFile(folderName, fileName, useSharding));
    }

    public bool DeleteFile(string folderName, string fileName, bool useSharding = true)
    {
        var path = ResolvePath(folderName, fileName, useSharding);
        try
        {
            if (!File.Exists(path))
                return false;

            File.Delete(path);
            return true;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error deleting file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task CopyFileAsync(string sourceFolderName, string sourceFileName, string destFolderName, string destFileName, bool useSharding = true, bool overwrite = false)
    {
        try
        {
            var sourcePath = ResolvePath(sourceFolderName, sourceFileName, useSharding);
            var destBasePath = ResolveFolderPath(destFolderName);

            Directory.CreateDirectory(destBasePath);

            string destPath;
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.CreateShardedSubdirectory(destBasePath, destFileName);
                destPath = string.IsNullOrEmpty(shardFolder)
                    ? Path.Combine(destBasePath, destFileName)
                    : Path.Combine(destBasePath, shardFolder, destFileName);
            }
            else
                destPath = Path.Combine(destBasePath, destFileName);

            if (!File.Exists(sourcePath))
                throw new FileServiceFileNotFoundException(sourceFolderName, sourceFileName, FileServiceBackend.Local);

            if (!overwrite && File.Exists(destPath))
                throw new FileServiceFileExistsException(destFolderName, destFileName, FileServiceBackend.Local);

            await Task.Run(() => File.Copy(sourcePath, destPath, overwrite));
        }
        catch (FileServiceException)
        {
            throw;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(destFolderName, destFileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error copying file from {sourceFileName} to {destFileName}", destFolderName, destFileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task MoveFileAsync(string sourceFolderName, string sourceFileName, string destFolderName, string destFileName, bool useSharding = true, bool overwrite = false)
    {
        try
        {
            var sourcePath = ResolvePath(sourceFolderName, sourceFileName, useSharding);
            var destBasePath = ResolveFolderPath(destFolderName);

            Directory.CreateDirectory(destBasePath);

            string destPath;
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.CreateShardedSubdirectory(destBasePath, destFileName);
                destPath = string.IsNullOrEmpty(shardFolder)
                    ? Path.Combine(destBasePath, destFileName)
                    : Path.Combine(destBasePath, shardFolder, destFileName);
            }
            else
                destPath = Path.Combine(destBasePath, destFileName);

            if (!File.Exists(sourcePath))
                throw new FileServiceFileNotFoundException(sourceFolderName, sourceFileName, FileServiceBackend.Local);

            if (!overwrite && File.Exists(destPath))
                throw new FileServiceFileExistsException(destFolderName, destFileName, FileServiceBackend.Local);

            await Task.Run(() => File.Move(sourcePath, destPath, overwrite));
        }
        catch (FileServiceException)
        {
            throw;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(destFolderName, destFileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error moving file from {sourceFileName} to {destFileName}", destFolderName, destFileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task<Stream> OpenReadAsync(string folderName, string fileName, bool useSharding = true)
    {
        var path = ResolvePath(folderName, fileName, useSharding);
        try
        {
            if (!File.Exists(path))
                throw new FileServiceFileNotFoundException(folderName, fileName, FileServiceBackend.Local);

            return await Task.FromResult<Stream>(File.OpenRead(path));
        }
        catch (FileServiceException)
        {
            throw;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error opening file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task<string> WriteStreamAsync(string folderName, string fileName, Stream content, bool useSharding = true)
    {
        var basePath = ResolveFolderPath(folderName);

        try
        {
            Directory.CreateDirectory(basePath);

            string fullPath;
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.CreateShardedSubdirectory(basePath, fileName);
                fullPath = string.IsNullOrEmpty(shardFolder)
                    ? Path.Combine(basePath, fileName)
                    : Path.Combine(basePath, shardFolder, fileName);
            }
            else
                fullPath = Path.Combine(basePath, fileName);

            await using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None);
            await content.CopyToAsync(fileStream);
            return fileName;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, fileName, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error writing file: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }

    public async Task<string?> UploadFileAsync(string folderName, string fileName, IFormFile file, bool useSharding = true)
    {
        if (file == null || file.Length == 0)
            return null;

        try
        {
            var basePath = ResolveFolderPath(folderName);
            Directory.CreateDirectory(basePath);

            string fullPath;
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.CreateShardedSubdirectory(basePath, fileName);
                fullPath = string.IsNullOrEmpty(shardFolder)
                    ? Path.Combine(basePath, fileName)
                    : Path.Combine(basePath, shardFolder, fileName);
            }
            else
                fullPath = Path.Combine(basePath, fileName);

            await using var fileStream = new FileStream(fullPath, FileMode.Create);
            await file.CopyToAsync(fileStream);
            return fileName;
        }
        catch (Exception)
        {
            return null;
        }
    }

    public async Task<FileDownloadResult> DownloadFileAsync(string folderName, string fileName, bool useSharding = true)
    {
        try
        {
            var stream = await OpenReadAsync(folderName, fileName, useSharding);
            var path = ResolvePath(folderName, fileName, useSharding);
            var fileInfo = new FileInfo(path);

            return new FileDownloadResult
            {
                Stream = stream,
                ContentType = "application/octet-stream",
                FileName = fileName,
                Size = fileInfo.Length,
                LastModified = fileInfo.LastWriteTimeUtc,
                Success = true
            };
        }
        catch (FileServiceFileNotFoundException)
        {
            return new FileDownloadResult
            {
                Stream = Stream.Null,
                FileName = fileName,
                Success = false,
                ErrorMessage = $"File not found: {fileName}"
            };
        }
        catch (Exception ex)
        {
            return new FileDownloadResult
            {
                Stream = new MemoryStream(GetExceptionBytes(ex)),
                ContentType = "text/plain",
                FileName = "error.txt",
                Success = false,
                ErrorMessage = ex.Message
            };
        }
    }

    private static byte[] GetExceptionBytes(Exception ex)
    {
        var message = $"Error: {ex.Message}\r\n{ex.StackTrace}";
        return System.Text.Encoding.UTF8.GetBytes(message);
    }

    public async Task<bool> DirectoryExistsAsync(string folderName, string? subFolder = null)
    {
        return await Task.FromResult(DirectoryExists(folderName, subFolder));
    }

    public bool DirectoryExists(string folderName, string? subFolder = null)
    {
        var path = ResolveFolderPath(folderName, subFolder);
        return Directory.Exists(path);
    }

    public async Task<bool> CreateDirectoryAsync(string folderName, string? subFolder = null)
    {
        return await Task.FromResult(CreateDirectory(folderName, subFolder));
    }

    public bool CreateDirectory(string folderName, string? subFolder = null)
    {
        try
        {
            var path = ResolveFolderPath(folderName, subFolder);
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);
            return true;
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error creating directory: {folderName}", folderName, null, ex, FileServiceBackend.Local);
        }
    }

    public async Task<bool> DeleteDirectoryAsync(string folderName, string? subFolder = null, bool recursive = false)
    {
        return await Task.FromResult(DeleteDirectory(folderName, subFolder, recursive));
    }

    public bool DeleteDirectory(string folderName, string? subFolder = null, bool recursive = false)
    {
        try
        {
            var path = ResolveFolderPath(folderName, subFolder);
            if (!Directory.Exists(path))
                return false;

            if (!recursive)
            {
                // Check if directory is empty
                if (Directory.EnumerateFileSystemEntries(path).Any())
                    throw new FileServiceException($"Directory is not empty: {folderName}", folderName, null, FileServiceBackend.Local);
            }

            Directory.Delete(path, recursive);
            return true;
        }
        catch (FileServiceException)
        {
            throw;
        }
        catch (UnauthorizedAccessException ex)
        {
            throw new FileServiceAccessDeniedException(folderName, null, ex, FileServiceBackend.Local);
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error deleting directory: {folderName}", folderName, null, ex, FileServiceBackend.Local);
        }
    }

    public async Task<IReadOnlyList<string>> GetFilesAsync(string folderName, string? subFolder = null, string searchPattern = "*", bool recursive = false)
    {
        return await Task.FromResult(GetFilesInternal(folderName, subFolder, searchPattern, recursive));
    }

    private string[] GetFilesInternal(string folderName, string? subFolder, string searchPattern, bool recursive)
    {
        try
        {
            var path = ResolveFolderPath(folderName, subFolder);
            if (!Directory.Exists(path))
                return Array.Empty<string>();

            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
            return Directory.GetFiles(path, searchPattern, searchOption)
                .Select(Path.GetFileName)
                .Where(f => f != null)
                .Select(f => f!)
                .ToArray();
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error listing files in: {folderName}", folderName, null, ex, FileServiceBackend.Local);
        }
    }

    public async Task<IReadOnlyList<FileItemInfo>> GetFilesWithInfoAsync(string folderName, string? subFolder = null, string searchPattern = "*", bool recursive = false)
    {
        return await Task.FromResult(GetFilesWithInfoInternal(folderName, subFolder, searchPattern, recursive));
    }

    private FileItemInfo[] GetFilesWithInfoInternal(string folderName, string? subFolder, string searchPattern, bool recursive)
    {
        try
        {
            var path = ResolveFolderPath(folderName, subFolder);
            if (!Directory.Exists(path))
                return Array.Empty<FileItemInfo>();

            var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
            var directoryInfo = new DirectoryInfo(path);

            return directoryInfo.GetFiles(searchPattern, searchOption)
                .Select(fi => new FileItemInfo
                {
                    Name = fi.Name,
                    FullPath = fi.FullName,
                    Size = fi.Length,
                    CreatedDate = fi.CreationTimeUtc,
                    LastModifiedDate = fi.LastWriteTimeUtc
                })
                .ToArray();
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error listing files with info in: {folderName}", folderName, null, ex, FileServiceBackend.Local);
        }
    }

    public async Task<IReadOnlyList<string>> GetDirectoriesAsync(string folderName, string? subFolder = null)
    {
        try
        {
            var path = ResolveFolderPath(folderName, subFolder);
            if (!Directory.Exists(path))
                return Array.Empty<string>();

            return await Task.FromResult(
                Directory.GetDirectories(path)
                    .Select(Path.GetFileName)
                    .Where(d => d != null)
                    .Select(d => d!)
                    .ToArray());
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error listing directories in: {folderName}", folderName, null, ex, FileServiceBackend.Local);
        }
    }

    public async Task<IReadOnlyList<BatchOperationResult>> WriteAllFilesBatchedAsync(
        IEnumerable<(string folderName, string fileName, byte[] content)> files,
        bool useSharding = true,
        int maxDegreeOfParallelism = 4)
    {
        var results = new ConcurrentBag<BatchOperationResult>();
        var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);

        var tasks = files.Select(async file =>
        {
            await semaphore.WaitAsync();
            try
            {
                await WriteAllBytesAsync(file.folderName, file.fileName, file.content, useSharding);
                results.Add(new BatchOperationResult
                {
                    FolderName = file.folderName,
                    FileName = file.fileName,
                    Success = true
                });
            }
            catch (Exception ex)
            {
                results.Add(new BatchOperationResult
                {
                    FolderName = file.folderName,
                    FileName = file.fileName,
                    Success = false,
                    ErrorMessage = ex.Message,
                    Exception = ex
                });
            }
            finally
            {
                semaphore.Release();
            }
        });

        await Task.WhenAll(tasks);
        return results.ToArray();
    }

    public async Task<IReadOnlyList<BatchOperationResult>> DeleteFilesBatchedAsync(
        IEnumerable<(string folderName, string fileName)> files,
        bool useSharding = true,
        int maxDegreeOfParallelism = 4)
    {
        var results = new ConcurrentBag<BatchOperationResult>();
        var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);

        var tasks = files.Select(async file =>
        {
            await semaphore.WaitAsync();
            try
            {
                var deleted = await DeleteFileAsync(file.folderName, file.fileName, useSharding);
                results.Add(new BatchOperationResult
                {
                    FolderName = file.folderName,
                    FileName = file.fileName,
                    Success = deleted
                });
            }
            catch (Exception ex)
            {
                results.Add(new BatchOperationResult
                {
                    FolderName = file.folderName,
                    FileName = file.fileName,
                    Success = false,
                    ErrorMessage = ex.Message,
                    Exception = ex
                });
            }
            finally
            {
                semaphore.Release();
            }
        });

        await Task.WhenAll(tasks);
        return results.ToArray();
    }

    public async Task<FileItemInfo?> GetFileInfoAsync(string folderName, string fileName, bool useSharding = true)
    {
        try
        {
            var path = ResolvePath(folderName, fileName, useSharding);
            if (!File.Exists(path))
                return null;

            var fileInfo = new FileInfo(path);
            return await Task.FromResult(new FileItemInfo
            {
                Name = fileInfo.Name,
                FullPath = fileInfo.FullName,
                Size = fileInfo.Length,
                CreatedDate = fileInfo.CreationTimeUtc,
                LastModifiedDate = fileInfo.LastWriteTimeUtc
            });
        }
        catch (Exception ex)
        {
            throw new FileServiceException($"Error getting file info: {fileName}", folderName, fileName, ex, FileServiceBackend.Local);
        }
    }
}
