using System.Collections.Concurrent;
using System.Net;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Drives.Item.Items.Item.CreateUploadSession;
using Microsoft.Graph.Models;

namespace Fast.Shared.Logic.FileService;

public class SharePointFileService : IFileService
{
    private readonly FileServiceConfig _config;
    private readonly IShardingStrategy _shardingStrategy;

    private readonly Lazy<GraphServiceClient> _graphClient;
    private readonly Lazy<Task<string>> _driveId;

    // Caching for folder IDs and existence checks
    private readonly ConcurrentDictionary<string, (string folderId, DateTime expiry)> _folderIdCache = new();
    private readonly SemaphoreSlim _rateLimitSemaphore;

    // Simple circuit breaker state
    private int _consecutiveFailures;
    private DateTime? _circuitOpenUntil;
    private readonly Lock _circuitLock = new();
    private static readonly string[] GraphUrls = ["https://graph.microsoft.com/.default"];
    private static readonly char[] PathSeparators = ['/', '\\'];

    public SharePointFileService(
        FileServiceConfig config,
        IShardingStrategy? shardingStrategy = null)
    {
        _config = config ?? throw new ArgumentNullException(nameof(config));
        _shardingStrategy = shardingStrategy ?? new DefaultShardingStrategy();

        _rateLimitSemaphore = new SemaphoreSlim(_config.Performance.MaxDegreeOfParallelism);

        _graphClient = new Lazy<GraphServiceClient>(() =>
        {
            var credential = GetGraphCredential();
            return new GraphServiceClient(credential, GraphUrls);
        });

        _driveId = new Lazy<Task<string>>(async () =>
        {
            var site = await _graphClient.Value.Sites[_config.SharePoint.SiteName].GetAsync();
            var siteId = site?.Id;
            var siteDrives = await _graphClient.Value.Sites[siteId].Drives.GetAsync();
            var drive = siteDrives?.Value?.FirstOrDefault(d =>
                string.Equals(d?.Name, _config.SharePoint.DriveName, StringComparison.OrdinalIgnoreCase));
            return drive?.Id ?? throw new FileServiceException("SharePoint drive not found", FileServiceBackend.SharePoint);
        });
    }

    private ClientSecretCredential GetGraphCredential()
    {
        // Try config first, then fall back to database
        var tenantId = _config.SharePoint.TenantId;
        var clientId = _config.SharePoint.ClientId;
        var clientSecret = _config.SharePoint.ClientSecret;

        if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
        {
            var db = ProgramShared.CreateContext();
            var settings = db.AppSettings
                .Where(s => s.Name == "Graph-TenantID" || s.Name == "Graph-ClientID" || s.Name == "Graph-ClientSecret")
                .ToDictionary(s => s.Name, s => s.Value);

            tenantId = settings.GetValueOrDefault("Graph-TenantID") ?? throw new InvalidOperationException("Missing Graph-TenantID");
            clientId = settings.GetValueOrDefault("Graph-ClientID") ?? throw new InvalidOperationException("Missing Graph-ClientID");
            clientSecret = settings.GetValueOrDefault("Graph-ClientSecret") ?? throw new InvalidOperationException("Missing Graph-ClientSecret");
        }

        return new ClientSecretCredential(tenantId, clientId, clientSecret);
    }

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

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

    private static string ResolveSharePointFolderPath(string folderName, string? subFolder = null)
    {
        var basePath = CombineEnvironmentPath(folderName);
        if (!string.IsNullOrEmpty(subFolder))
            return ShardingExtensions.CombineSharePointPaths(basePath, subFolder);
        return basePath;
    }

    private async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action, string operationName)
    {
        // Check circuit breaker
        lock (_circuitLock)
        {
            if (_circuitOpenUntil.HasValue && DateTime.UtcNow < _circuitOpenUntil.Value)
            {
                throw new FileServiceCircuitOpenException(
                    $"Circuit breaker is open for SharePoint operations",
                    _circuitOpenUntil,
                    FileServiceBackend.SharePoint);
            }
        }

        Exception? lastException = null;
        for (int attempt = 0; attempt <= _config.Performance.MaxRetryAttempts; attempt++)
        {
            try
            {
                var result = await action();

                // Reset circuit breaker on success
                lock (_circuitLock)
                {
                    _consecutiveFailures = 0;
                }

                return result;
            }
            catch (Exception ex) when (IsTransientException(ex))
            {
                lastException = ex;

                // Update circuit breaker
                lock (_circuitLock)
                {
                    _consecutiveFailures++;
                    if (_consecutiveFailures >= _config.Performance.CircuitBreakerFailureThreshold)
                    {
                        _circuitOpenUntil = DateTime.UtcNow.AddSeconds(_config.Performance.CircuitBreakerResetTimeoutSeconds);
                    }
                }

                if (attempt < _config.Performance.MaxRetryAttempts)
                {
                    var delay = GetRetryDelay(attempt + 1);
                    await Task.Delay(delay);
                }
            }
        }

        throw lastException ?? new FileServiceException($"Operation {operationName} failed after retries", FileServiceBackend.SharePoint);
    }

    private async Task ExecuteWithRetryAsync(Func<Task> action, string operationName)
    {
        await ExecuteWithRetryAsync(async () => { await action(); return true; }, operationName);
    }

    private TimeSpan GetRetryDelay(int retryAttempt)
    {
        var delay = Math.Min(
            _config.Performance.InitialRetryDelayMs * Math.Pow(2, retryAttempt - 1),
            _config.Performance.MaxRetryDelayMs);
        // Add jitter
        var jitter = Random.Shared.NextDouble() * 0.2 * delay;
        return TimeSpan.FromMilliseconds(delay + jitter);
    }

    private static bool IsTransientException(Exception ex)
    {
        if (ex is Microsoft.Graph.Models.ODataErrors.ODataError oDataError)
        {
            var statusCode = oDataError.ResponseStatusCode;
            return statusCode == (int)HttpStatusCode.TooManyRequests || statusCode >= (int)HttpStatusCode.InternalServerError;
        }
        return ex is HttpRequestException || ex is TaskCanceledException;
    }

    private async Task<string?> GetOrCreateFolderIdAsync(string folderPath)
    {
        // Check cache first
        if (_folderIdCache.TryGetValue(folderPath, out var cached) && cached.expiry > DateTime.UtcNow)
            return cached.folderId;

        var driveId = await _driveId.Value;
        var folderId = await CreateFoldersByPathAsync(driveId, folderPath);

        if (folderId != null)
        {
            var expiry = DateTime.UtcNow.AddSeconds(_config.Performance.CacheTtlSeconds);
            _folderIdCache[folderPath] = (folderId, expiry);
        }

        return folderId;
    }

    private async Task<string?> CreateFoldersByPathAsync(string driveId, string? path)
    {
        if (string.IsNullOrWhiteSpace(path))
            return "root";

        var folderNames = path.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries);
        string parentFolderId = "root";

        foreach (var folderName in folderNames)
        {
            var allChildren = await GetAllChildrenAsync(driveId, parentFolderId);

            var existingFolder = allChildren.FirstOrDefault(i => string.Equals(i?.Name, folderName, StringComparison.OrdinalIgnoreCase) && i?.Folder != null);

            if (existingFolder != null)
            {
                parentFolderId = existingFolder.Id!;
                continue;
            }

            // Create the folder
            var newFolder = new DriveItem
            {
                Name = folderName,
                Folder = new Folder(),
                AdditionalData = new Dictionary<string, object> { { "@microsoft.graph.conflictBehavior", "fail" } }
            };

            try
            {
                var createdFolder = await _graphClient.Value.Drives[driveId].Items[parentFolderId].Children.PostAsync(newFolder);
                if (createdFolder?.Id == null)
                    return null;
                parentFolderId = createdFolder.Id;
            }
            catch (Exception)
            {
                // Folder may already exist due to race condition, try to get it
                var allChildren2 = await GetAllChildrenAsync(driveId, parentFolderId);

                var existing = allChildren2.FirstOrDefault(i => string.Equals(i?.Name, folderName, StringComparison.OrdinalIgnoreCase) && i?.Folder != null);
                if (existing?.Id == null)
                    return null;
                parentFolderId = existing.Id;
            }
        }

        return parentFolderId;
    }

    public async Task<bool> FileExistsAsync(string folderName, string fileName, bool useSharding = true)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveItem = await GetDriveItemAsync(folderName, fileName, useSharding);
            return driveItem != null;
        }, "FileExists");
    }

    public bool FileExists(string folderName, string fileName, bool useSharding = true)
    {
        return FileExistsAsync(folderName, fileName, useSharding).GetAwaiter().GetResult();
    }

    private async Task<DriveItem?> GetDriveItemAsync(string folderName, string fileName, bool useSharding)
    {
        var driveId = await _driveId.Value;
        var filePath = ResolvePath(folderName, fileName, useSharding);

        try
        {
            return await _graphClient.Value.Drives[driveId].Items["root"].ItemWithPath(filePath).GetAsync();
        }
        catch (Exception ex)
        {
            throw new FileServiceFileNotFoundException(folderName, fileName, FileServiceBackend.SharePoint, ex.Message);
        }
    }

    public async Task<byte[]> ReadAllBytesAsync(string folderName, string fileName, bool useSharding = true)
    {
        var stream = await OpenReadAsync(folderName, fileName, useSharding);
        using var ms = new MemoryStream();
        await stream.CopyToAsync(ms);
        return ms.ToArray();
    }

    public byte[] ReadAllBytes(string folderName, string fileName, bool useSharding = true)
    {
        return ReadAllBytesAsync(folderName, fileName, useSharding).GetAwaiter().GetResult();
    }

    public async Task<string> WriteAllBytesAsync(string folderName, string fileName, byte[] content, bool useSharding = true)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            using var stream = new MemoryStream(content);
            return await WriteStreamInternalAsync(folderName, fileName, stream, useSharding);
        }, "WriteAllBytes");
    }

    public string WriteAllBytes(string folderName, string fileName, byte[] content, bool useSharding = true)
    {
        return WriteAllBytesAsync(folderName, fileName, content, useSharding).GetAwaiter().GetResult();
    }

    public async Task<bool> DeleteFileAsync(string folderName, string fileName, bool useSharding = true)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveItem = await GetDriveItemAsync(folderName, fileName, useSharding);
            if (driveItem?.Id == null)
                return false;

            var driveId = await _driveId.Value;
            await _graphClient.Value.Drives[driveId].Items[driveItem.Id].DeleteAsync();
            return true;
        }, "DeleteFile");
    }

    public bool DeleteFile(string folderName, string fileName, bool useSharding = true)
    {
        return DeleteFileAsync(folderName, fileName, useSharding).GetAwaiter().GetResult();
    }

    public async Task CopyFileAsync(string sourceFolderName, string sourceFileName, string destFolderName, string destFileName, bool useSharding = true, bool overwrite = false)
    {
        await ExecuteWithRetryAsync(async () =>
        {
            var sourceItem = await GetDriveItemAsync(sourceFolderName, sourceFileName, useSharding);
            if (sourceItem?.Id == null)
                throw new FileServiceFileNotFoundException(sourceFolderName, sourceFileName, FileServiceBackend.SharePoint);

            var driveId = await _driveId.Value;
            var destFolderPath = ResolveSharePointFolderPath(destFolderName);
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.GetShardFolderName(destFileName);
                if (!string.IsNullOrEmpty(shardFolder))
                    destFolderPath = ShardingExtensions.CombineSharePointPaths(destFolderPath, shardFolder);
            }

            var destFolderId = await GetOrCreateFolderIdAsync(destFolderPath)
                ?? throw new FileServiceException($"Failed to create destination folder: {destFolderPath}", destFolderName, destFileName, FileServiceBackend.SharePoint);

            var copyRequest = new Microsoft.Graph.Drives.Item.Items.Item.Copy.CopyPostRequestBody
            {
                ParentReference = new ItemReference { DriveId = driveId, Id = destFolderId },
                Name = destFileName
            };

            await _graphClient.Value.Drives[driveId].Items[sourceItem.Id].Copy.PostAsync(copyRequest);
        }, "CopyFile");
    }

    public async Task MoveFileAsync(string sourceFolderName, string sourceFileName, string destFolderName, string destFileName, bool useSharding = true, bool overwrite = false)
    {
        await ExecuteWithRetryAsync(async () =>
        {
            var sourceItem = await GetDriveItemAsync(sourceFolderName, sourceFileName, useSharding);
            if (sourceItem?.Id == null)
                throw new FileServiceFileNotFoundException(sourceFolderName, sourceFileName, FileServiceBackend.SharePoint);

            var driveId = await _driveId.Value;
            var destFolderPath = ResolveSharePointFolderPath(destFolderName);
            if (useSharding)
            {
                var shardFolder = _shardingStrategy.GetShardFolderName(destFileName);
                if (!string.IsNullOrEmpty(shardFolder))
                    destFolderPath = ShardingExtensions.CombineSharePointPaths(destFolderPath, shardFolder);
            }

            var destFolderId = await GetOrCreateFolderIdAsync(destFolderPath)
                ?? throw new FileServiceException($"Failed to create destination folder: {destFolderPath}", destFolderName, destFileName, FileServiceBackend.SharePoint);

            var moveRequest = new DriveItem
            {
                ParentReference = new ItemReference { DriveId = driveId, Id = destFolderId },
                Name = destFileName
            };

            await _graphClient.Value.Drives[driveId].Items[sourceItem.Id].PatchAsync(moveRequest);
        }, "MoveFile");
    }

    public async Task<Stream> OpenReadAsync(string folderName, string fileName, bool useSharding = true)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveItem = await GetDriveItemAsync(folderName, fileName, useSharding);
            if (driveItem?.Id == null)
                throw new FileServiceFileNotFoundException(folderName, fileName, FileServiceBackend.SharePoint);

            var driveId = await _driveId.Value;
            var stream = await _graphClient.Value.Drives[driveId].Items[driveItem.Id].Content.GetAsync();
            return stream ?? throw new FileServiceException($"Failed to download file: {fileName}", folderName, fileName, FileServiceBackend.SharePoint);
        }, "OpenRead");
    }

    public async Task<string> WriteStreamAsync(string folderName, string fileName, Stream content, bool useSharding = true)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            return await WriteStreamInternalAsync(folderName, fileName, content, useSharding);
        }, "WriteStream");
    }

    private async Task<string> WriteStreamInternalAsync(string folderName, string fileName, Stream content, bool useSharding)
    {
        var driveId = await _driveId.Value;
        var folderPath = ResolveSharePointFolderPath(folderName);

        if (useSharding)
        {
            var shardFolder = _shardingStrategy.GetShardFolderName(fileName);
            if (!string.IsNullOrEmpty(shardFolder))
                folderPath = ShardingExtensions.CombineSharePointPaths(folderPath, shardFolder);
        }

        var folderId = await GetOrCreateFolderIdAsync(folderPath)
            ?? throw new FileServiceException($"Failed to create folder: {folderPath}", folderName, fileName, FileServiceBackend.SharePoint);

        // Check file size to determine upload method
        long contentLength;
        if (content.CanSeek)
        {
            contentLength = content.Length;
        }
        else
        {
            // Need to buffer to memory to check size
            var ms = new MemoryStream();
            await content.CopyToAsync(ms);
            ms.Position = 0;
            content = ms;
            contentLength = ms.Length;
        }

        if (contentLength <= _config.SharePoint.MaxSimpleUploadSize)
        {
            return await UploadSmallFileAsync(driveId, folderId, fileName, content);
        }
        else
        {
            return await UploadLargeFileAsync(driveId, folderId, fileName, content, contentLength);
        }
    }

    /// <summary>
    /// Attempts to delete an existing file. Retries on transient errors.
    /// Silently ignores file not found (404) and other non-transient errors.
    /// </summary>
    private async Task TryDeleteFileAsync(string driveId, string folderId, string fileName)
    {
        try
        {
            await ExecuteWithRetryAsync(async () =>
            {
                try
                {
                    var existingItem = await _graphClient.Value.Drives[driveId].Items[folderId].ItemWithPath(fileName).GetAsync();
                    if (existingItem?.Id != null)
                    {
                        await _graphClient.Value.Drives[driveId].Items[existingItem.Id].DeleteAsync();
                    }
                }
                catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == (int)HttpStatusCode.NotFound)
                {
                    // File doesn't exist - nothing to delete, which is fine
                }
            }, "TryDeleteFile");
        }
        catch
        {
            // Silently ignore errors after retries exhausted - file may be locked, permissions issue, etc.
        }
    }

    private async Task<string> UploadSmallFileAsync(string driveId, string folderId, string fileName, Stream content)
    {
        // Try to delete existing file first, because small uploads fail if the file already exists
        await TryDeleteFileAsync(driveId, folderId, fileName);

        // Reset stream position if needed
        if (content.CanSeek)
            content.Position = 0;

        // Simple upload for small files
        var uploadItem = await _graphClient.Value.Drives[driveId].Items[folderId].ItemWithPath(fileName).Content.PutAsync(content);
        if (uploadItem?.Id != null)
        {
            await CheckInIfNeeded(driveId, uploadItem.Id);
        }
        return fileName;
    }

    private async Task<string> UploadLargeFileAsync(string driveId, string folderId, string fileName, Stream content, long contentLength)
    {
        var uploadSessionRequest = new CreateUploadSessionPostRequestBody
        {
            Item = new DriveItemUploadableProperties
            {
                // Large file upload sessions with "replace" handle existing files properly
                AdditionalData = new Dictionary<string, object> { { "@microsoft.graph.conflictBehavior", "replace" } }
            }
        };

        var uploadSession = await _graphClient.Value.Drives[driveId].Items[folderId].ItemWithPath(fileName)
            .CreateUploadSession.PostAsync(uploadSessionRequest);

        if (uploadSession?.UploadUrl == null)
            throw new FileServiceUploadException("Failed to create upload session", "", fileName, contentLength, FileServiceBackend.SharePoint);

        // Upload in chunks
        var chunkSize = _config.SharePoint.UploadChunkSize;
        var buffer = new byte[chunkSize];
        long position = 0;

        using var httpClient = new HttpClient();

        while (position < contentLength)
        {
            var bytesToRead = (int)Math.Min(chunkSize, contentLength - position);
            var bytesRead = await content.ReadAsync(buffer.AsMemory(0, bytesToRead));
            var endPosition = position + bytesRead - 1;

            using var chunkStream = new MemoryStream(buffer, 0, bytesRead);
            using var request = new HttpRequestMessage(HttpMethod.Put, uploadSession.UploadUrl);
            request.Content = new StreamContent(chunkStream);
            request.Content.Headers.ContentRange = new System.Net.Http.Headers.ContentRangeHeaderValue(position, endPosition, contentLength);
            request.Content.Headers.ContentLength = bytesRead;

            var response = await httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode();

            position += bytesRead;
        }

        return fileName;
    }

    private async Task CheckInIfNeeded(string driveId, string itemId)
    {
        string[] CheckInSelection = ["id", "name", "publication"];

        try
        {
            var item = await _graphClient.Value.Drives[driveId].Items[itemId].GetAsync(rc =>
            {
                rc.QueryParameters.Select = CheckInSelection;
            });

            if (string.Equals(item?.Publication?.Level, "checkout", StringComparison.OrdinalIgnoreCase))
                await _graphClient.Value.Drives[driveId].Items[itemId].Checkin.PostAsync(new());
        }
        catch
        {
            // Ignore check-in errors
        }
    }

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

        try
        {
            await using var stream = new MemoryStream();
            await file.CopyToAsync(stream);
            stream.Position = 0;
            return await WriteStreamAsync(folderName, fileName, stream, useSharding);
        }
        catch
        {
            return null;
        }
    }

    public async Task<FileDownloadResult> DownloadFileAsync(string folderName, string fileName, bool useSharding = true)
    {
        try
        {
            var driveItem = await GetDriveItemAsync(folderName, fileName, useSharding);
            if (driveItem?.Id == null)
            {
                return new FileDownloadResult
                {
                    Stream = Stream.Null,
                    FileName = fileName,
                    Success = false,
                    ErrorMessage = $"File not found: {fileName}"
                };
            }

            var driveId = await _driveId.Value;
            var stream = await _graphClient.Value.Drives[driveId].Items[driveItem.Id].Content.GetAsync();

            return new FileDownloadResult
            {
                Stream = stream ?? Stream.Null,
                ContentType = "application/octet-stream",
                FileName = fileName,
                Size = driveItem.Size,
                LastModified = driveItem.LastModifiedDateTime?.UtcDateTime,
                Success = stream != null
            };
        }
        catch (Exception ex)
        {
            return new FileDownloadResult
            {
                Stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes($"Error: {ex.Message}")),
                ContentType = "text/plain",
                FileName = "error.txt",
                Success = false,
                ErrorMessage = ex.Message
            };
        }
    }

    public async Task<bool> DirectoryExistsAsync(string folderName, string? subFolder = null)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveId = await _driveId.Value;
            var folderPath = ResolveSharePointFolderPath(folderName, subFolder);

            try
            {
                var folder = await _graphClient.Value.Drives[driveId].Items["root"].ItemWithPath(folderPath).GetAsync();
                return folder?.Folder != null;
            }
            catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == (int)HttpStatusCode.NotFound)
            {
                return false;
            }
        }, "DirectoryExists");
    }

    public bool DirectoryExists(string folderName, string? subFolder = null)
    {
        return DirectoryExistsAsync(folderName, subFolder).GetAwaiter().GetResult();
    }

    public async Task<bool> CreateDirectoryAsync(string folderName, string? subFolder = null)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveId = await _driveId.Value;
            var folderPath = ResolveSharePointFolderPath(folderName, subFolder);
            var folderId = await GetOrCreateFolderIdAsync(folderPath);
            return folderId != null;
        }, "CreateDirectory");
    }

    public bool CreateDirectory(string folderName, string? subFolder = null)
    {
        return CreateDirectoryAsync(folderName, subFolder).GetAwaiter().GetResult();
    }

    public async Task<bool> DeleteDirectoryAsync(string folderName, string? subFolder = null, bool recursive = false)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveId = await _driveId.Value;
            var folderPath = ResolveSharePointFolderPath(folderName, subFolder);

            try
            {
                var folder = await _graphClient.Value.Drives[driveId].Items["root"].ItemWithPath(folderPath).GetAsync();
                if (folder?.Id == null || folder.Folder == null)
                    return false;

                if (!recursive)
                {
                    // Check if directory is empty
                    var children = await _graphClient.Value.Drives[driveId].Items[folder.Id].Children.GetAsync();
                    if (children?.Value?.Count > 0)
                        throw new FileServiceException($"Directory is not empty: {folderName}", folderName, null, FileServiceBackend.SharePoint);
                }

                await _graphClient.Value.Drives[driveId].Items[folder.Id].DeleteAsync();

                // Remove from cache
                _folderIdCache.TryRemove(folderPath, out _);

                return true;
            }
            catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == (int)HttpStatusCode.NotFound)
            {
                return false;
            }
        }, "DeleteDirectory");
    }

    public bool DeleteDirectory(string folderName, string? subFolder = null, bool recursive = false)
    {
        return DeleteDirectoryAsync(folderName, subFolder, recursive).GetAwaiter().GetResult();
    }

    public async Task<IReadOnlyList<string>> GetFilesAsync(string folderName, string? subFolder = null, string searchPattern = "*", bool recursive = false)
    {
        var filesWithInfo = await GetFilesWithInfoAsync(folderName, subFolder, searchPattern, recursive);
        return filesWithInfo.Select(f => f.Name).ToArray();
    }

    public async Task<IReadOnlyList<FileItemInfo>> GetFilesWithInfoAsync(string folderName, string? subFolder = null, string searchPattern = "*", bool recursive = false)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveId = await _driveId.Value;
            var folderPath = ResolveSharePointFolderPath(folderName, subFolder);

            try
            {
                var folder = await _graphClient.Value.Drives[driveId].Items["root"].ItemWithPath(folderPath).GetAsync();
                if (folder?.Id == null)
                    return Array.Empty<FileItemInfo>();

                if (recursive)
                {
                    return await GetNestedFilesAsync(driveId, folder.Id, searchPattern);
                }
                else
                {
                    var allChildren = await GetAllChildrenAsync(driveId, folder.Id);
                    return allChildren
                        .Where(item => item.File != null && !string.IsNullOrEmpty(item.Name))
                        .Where(item => MatchesPattern(item.Name!, searchPattern))
                        .Select(item => new FileItemInfo
                        {
                            Name = item.Name!,
                            FullPath = item.WebUrl ?? "",
                            Size = item.Size ?? 0,
                            CreatedDate = item.CreatedDateTime?.UtcDateTime,
                            LastModifiedDate = item.LastModifiedDateTime?.UtcDateTime,
                            WebUrl = item.WebUrl,
                            Id = item.Id
                        })
                        .ToArray();
                }
            }
            catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == (int)HttpStatusCode.NotFound)
            {
                return Array.Empty<FileItemInfo>();
            }
        }, "GetFilesWithInfo");
    }

    private async Task<FileItemInfo[]> GetNestedFilesAsync(string driveId, string folderId, string searchPattern)
    {
        var results = new List<FileItemInfo>();

        var allChildren = await GetAllChildrenAsync(driveId, folderId);
        foreach (var item in allChildren)
        {
            if (item.Folder != null && item.Id != null)
            {
                var subResults = await GetNestedFilesAsync(driveId, item.Id, searchPattern);
                results.AddRange(subResults);
            }
            else if (item.File != null && !string.IsNullOrEmpty(item.Name) && MatchesPattern(item.Name, searchPattern))
            {
                results.Add(new FileItemInfo
                {
                    Name = item.Name,
                    FullPath = item.WebUrl ?? "",
                    Size = item.Size ?? 0,
                    CreatedDate = item.CreatedDateTime?.UtcDateTime,
                    LastModifiedDate = item.LastModifiedDateTime?.UtcDateTime,
                    WebUrl = item.WebUrl,
                    Id = item.Id
                });
            }
        }

        return results.ToArray();
    }

    private static bool MatchesPattern(string fileName, string pattern)
    {
        if (pattern == "*" || pattern == "*.*")
            return true;

        // Simple pattern matching for common cases
        if (pattern.StartsWith("*."))
        {
            var extension = pattern[1..];
            return fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase);
        }

        return fileName.Contains(pattern.Replace("*", ""), StringComparison.OrdinalIgnoreCase);
    }

    public async Task<IReadOnlyList<string>> GetDirectoriesAsync(string folderName, string? subFolder = null)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveId = await _driveId.Value;
            var folderPath = ResolveSharePointFolderPath(folderName, subFolder);

            try
            {
                var folder = await _graphClient.Value.Drives[driveId].Items["root"].ItemWithPath(folderPath).GetAsync();
                if (folder?.Id == null)
                    return Array.Empty<string>();

                var allChildren = await GetAllChildrenAsync(driveId, folder.Id);
                return allChildren
                    .Where(item => item.Folder != null && !string.IsNullOrEmpty(item.Name))
                    .Select(item => item.Name!)
                    .ToArray() ?? Array.Empty<string>();
            }
            catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == (int)HttpStatusCode.NotFound)
            {
                return Array.Empty<string>();
            }
        }, "GetDirectories");
    }

    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(Math.Min(maxDegreeOfParallelism, _config.Performance.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(Math.Min(maxDegreeOfParallelism, _config.Performance.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)
    {
        return await ExecuteWithRetryAsync(async () =>
        {
            var driveItem = await GetDriveItemAsync(folderName, fileName, useSharding);
            if (driveItem == null)
                return null;

            return new FileItemInfo
            {
                Name = driveItem.Name ?? fileName,
                FullPath = driveItem.WebUrl ?? "",
                Size = driveItem.Size ?? 0,
                CreatedDate = driveItem.CreatedDateTime?.UtcDateTime,
                LastModifiedDate = driveItem.LastModifiedDateTime?.UtcDateTime,
                WebUrl = driveItem.WebUrl,
                Id = driveItem.Id
            };
        }, "GetFileInfo");
    }

    /// Combines the environment root folder with a relative path for SharePoint.
    public static string CombineEnvironmentPath(string? relativePath)
    {
        string envFolder = string.Empty;
        if (Main.IsDevEnvOrDb)
            envFolder = "Dev";
        else if (Main.IsTestDb)
            envFolder = "Test";

        if (string.IsNullOrEmpty(envFolder))
            return relativePath ?? string.Empty;
        if (string.IsNullOrEmpty(relativePath))
            return envFolder;
        return ShardingExtensions.CombineSharePointPaths(envFolder, relativePath);
    }

    private async Task<List<DriveItem>> GetAllChildrenAsync(string driveId, string folderId)
    {
        var allChildren = new List<DriveItem>();

        // Get the first page of children
        var response = await _graphClient.Value.Drives[driveId].Items[folderId].Children.GetAsync();
        if (response?.Value != null)
        {
            // Use PageIterator to collect all items across pages
            var pageIterator = PageIterator<DriveItem, DriveItemCollectionResponse>.CreatePageIterator(
                _graphClient.Value,
                response,
                (item) =>
                {
                    allChildren.Add(item);
                    return true;
                }
            );
            await pageIterator.IterateAsync();
        }

        return allChildren;
    }
}
