using Azure.Identity;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Identity.Client;

namespace Fast.Logic;

public static partial class Util
{
    public class SharePoint
    {
        private static readonly string siteName = "superiornatgas.sharepoint.com:/sites/T-Rex";
        private readonly MyDbContext db;
        public readonly GraphServiceClient graphClient;

        public class FileItemResult
        {
            public string DriveId { get; set; } = string.Empty;
            public DriveItem FileItem { get; set; } = default!;
        }
        public SharePoint()
        {
            db = ProgramShared.CreateContext();
            var graphCredential = GetGraphCredential();
            graphClient = new GraphServiceClient(graphCredential, new[] { "https://graph.microsoft.com/.default" });
        }

        private ClientSecretCredential GetGraphCredential()
        {
            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);

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

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

        //Method to wipe all folders and files from documents drive, used only for testing.
        // public async Task DeleteAllAsync()
        // {
        //     // get the "Documents" drive for your site
        //     var site = await graphClient.Sites[siteName].GetAsync();
        //     if (site == null) return;

        //     var drives = await graphClient.Sites[site.Id].Drives.GetAsync();
        //     var documentsDrive = drives?.Value?
        //         .FirstOrDefault(d => string.Equals(d?.Name, "Documents", StringComparison.OrdinalIgnoreCase));

        //     if (documentsDrive?.Id == null) return;
        //     var driveId = documentsDrive.Id;

        //     // expand children of the drive root
        //     var driveRoot = await graphClient.Drives[driveId]
        //         .Root
        //         .GetAsync(cfg => cfg.QueryParameters.Expand = new[] { "children" });

        //     if (driveRoot?.Children == null) return;

        //     // delete everything directly in the root (folders auto‑delete their contents)
        //     foreach (var item in driveRoot.Children)
        //     {
        //         await graphClient.Drives[driveId].Items[item.Id].DeleteAsync();
        //     }
        // }

        private async Task<string?> GetSiteIdAsync()
        {
            var site = await graphClient.Sites[siteName].GetAsync();
            return site?.Id;
        }

        public async Task<Drive?> GetDriveByNameAsync(string driveName)
        {
            var siteId = await GetSiteIdAsync();
            if (siteId == null)
                return null;

            var siteDrives = await graphClient.Sites[siteId].Drives.GetAsync();
            var sharePointDrive = siteDrives?.Value?.FirstOrDefault(d => string.Equals(d?.Name, driveName, StringComparison.OrdinalIgnoreCase));

            return sharePointDrive;
        }

        public async Task<List<DriveItem>> ListFolderContentsAsync(string driveId, string folderName)
        {
            var items = new List<DriveItem>();
            var page = await graphClient.Drives[driveId].Items["root"].Children.GetAsync();
            if (page?.Value != null)
                items.AddRange(page.Value);
            return items;
        }

        public async Task<List<FileItemResult>> GetNestedFilesForFolderAsync(string driveId, string folderId)
        {
            var results = new List<FileItemResult>();
            var folderItems = await graphClient.Drives[driveId].Items[folderId].Children.GetAsync();
            if (folderItems?.Value == null)
                return results;

            foreach (var item in folderItems.Value)
            {
                if (item.Folder != null)
                {
                    var subResults = await GetNestedFilesForFolderAsync(driveId, item.Id!);
                    results.AddRange(subResults);
                }
                else if (item.File != null)
                {
                    results.Add(new FileItemResult { DriveId = driveId, FileItem = item });
                }
            }

            return results;
        }

        private async Task<FileItemResult?> GetFileByNameAsync(string driveFolder, string fileNameOnDisk)
        {
            var drive = await GetDriveByNameAsync("documents");
            if (drive?.Id == null)
                return null;

            var shardFolderName = File.CreateShardedSubdirectory("", fileNameOnDisk, false);
            if (string.IsNullOrEmpty(shardFolderName))
                return null;

            var pathPieces = new List<string>();
            if (!string.IsNullOrWhiteSpace(driveFolder))
                pathPieces.Add(driveFolder);

            pathPieces.Add(shardFolderName);
            pathPieces.Add(fileNameOnDisk);
            var filePath = string.Join("/", pathPieces);
            var fileItem = await graphClient.Drives[drive.Id].Items["root"].ItemWithPath(filePath).GetAsync();
            if (fileItem == null)
                return null;

            return new FileItemResult { DriveId = drive.Id, FileItem = fileItem };
        }

        public async Task<string?> CreateFolder(string driveId, string primaryFolderId, string folderName)
        {
            try
            {
                var primaryFolderChildren = await graphClient.Drives[driveId].Items[primaryFolderId].Children.GetAsync();
                var existingFolderMatch = primaryFolderChildren?.Value?.FirstOrDefault(i =>
                    string.Equals(i?.Name, folderName, StringComparison.OrdinalIgnoreCase) && i?.Folder != null);

                if (existingFolderMatch != null)
                    return existingFolderMatch.Id;


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

                var createdFolder = await graphClient.Drives[driveId].Items[primaryFolderId].Children.PostAsync(newFolder);
                return createdFolder?.Id;
            }
            catch (Exception)
            {
                // Swallow exception
                return null;
            }
        }

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

            var folderNames = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            string parentFolderId = "root";
            foreach (var folderName in folderNames)
            {
                var nextFolderId = await CreateFolder(driveId, parentFolderId, folderName);
                if (nextFolderId == null)
                    return null;

                parentFolderId = nextFolderId;
            }

            return parentFolderId;
        }

        public async Task<Stream?> DownloadFileAsync(string driveFolder, string fileNameOnDisk)
        {
            var fileItemResult = await GetFileByNameAsync(driveFolder, fileNameOnDisk);
            if (fileItemResult?.FileItem?.Id == null)
                return null;

            var fileStream = await graphClient.Drives[fileItemResult.DriveId].Items[fileItemResult.FileItem.Id].Content.GetAsync();
            return fileStream;
        }

        public async Task<bool> DeleteFileAsync(string driveFolder, string fileNameOnDisk)
        {
            var fileItemResult = await GetFileByNameAsync(driveFolder, fileNameOnDisk);
            if (fileItemResult == null)
                return false;

            await graphClient.Drives[fileItemResult.DriveId].Items[fileItemResult.FileItem.Id].DeleteAsync();
            return true;
        }

        public async Task<string?> UploadFileAsync(string fileNameOnDisk, string driveFolderPath, IFormFile file, bool shardFile = true)
        {
            var uploadDrive = await GetDriveByNameAsync("documents");
            if (uploadDrive?.Id == null)
                return null;

            var driveFolderId = await CreateFoldersByPath(uploadDrive.Id, driveFolderPath);
            if (driveFolderId == null)
                return null;

            string parentFolderId = driveFolderId;

            if (shardFile)
            {
                var shardFolderName = File.CreateShardedSubdirectory("", fileNameOnDisk, false);
                if (string.IsNullOrEmpty(shardFolderName))
                    return null;

                var shardFolderId = await CreateFolder(uploadDrive.Id, driveFolderId, shardFolderName);
                if (shardFolderId == null)
                    return null;

                parentFolderId = shardFolderId;
            }

            try
            {
                var existingItem = await graphClient.Drives[uploadDrive.Id]
                    .Items[parentFolderId]
                    .ItemWithPath(fileNameOnDisk)
                    .GetAsync();

                if (existingItem != null)
                {
                    try
                    {
                        // Try to check in first
                        await graphClient.Drives[uploadDrive.Id].Items[existingItem.Id].Checkin.PostAsync(new());
                    }
                    catch (Microsoft.Graph.Models.ODataErrors.ODataError ex)
                    {
                        // If the file is locked or checked out by someone else then delete it
                        if (ex.Error?.Code?.Contains("resourceLocked", StringComparison.OrdinalIgnoreCase) == true || ex.Error?.Code?.Contains("resourceCheckedOut", StringComparison.OrdinalIgnoreCase) == true)
                        {
                            await graphClient.Drives[uploadDrive.Id].Items[existingItem.Id].DeleteAsync();
                        }
                    }
                }
            }
            catch
            {
                // Swallow file not found to allow it to be created
            }

            var body = new Microsoft.Graph.Drives.Item.Items.Item.CreateUploadSession.CreateUploadSessionPostRequestBody
            {
                Item = new DriveItemUploadableProperties
                {
                    AdditionalData = new Dictionary<string, object>
                {
                    { "@microsoft.graph.conflictBehavior", "replace" }
                }

                }
            };

            await using var fileStream = new MemoryStream();
            await file.CopyToAsync(fileStream);
            fileStream.Position = 0;

            Microsoft.Graph.Models.UploadSession? uploadSession = null;
            try
            {
                uploadSession = await graphClient.Drives[uploadDrive.Id].Items[parentFolderId].ItemWithPath(fileNameOnDisk).CreateUploadSession.PostAsync(body);
            }
            catch (Microsoft.Graph.Models.ODataErrors.ODataError ex)
            {
                //File Upload sessions can hang for up to an hour in some cases, failures can occur on overwrites.
                if (ex.ResponseStatusCode == 409 && ex.Error?.Message?.Contains("currently being uploaded") == true)
                {
                    try
                    {
                        var existingItem = await graphClient.Drives[uploadDrive.Id].Items[parentFolderId].ItemWithPath(fileNameOnDisk).GetAsync();
                        if (existingItem != null)
                        {
                            await graphClient.Drives[uploadDrive.Id].Items[existingItem.Id].DeleteAsync();
                            uploadSession = await graphClient.Drives[uploadDrive.Id].Items[parentFolderId].ItemWithPath(fileNameOnDisk).CreateUploadSession.PostAsync(body);
                        }
                    }
                    catch
                    {
                        throw;
                    }
                }
                else
                {
                    throw;
                }
            }

            const int maxChunkSize = 10 * 1024 * 1024; // 10 MB
            var largeFileUploadTask =
                new LargeFileUploadTask<DriveItem>(uploadSession, fileStream, maxChunkSize);

            var uploadResult = await largeFileUploadTask.UploadAsync();

            if (uploadResult.UploadSucceeded && uploadResult.ItemResponse != null)
            {
                await graphClient.Drives[uploadDrive.Id].Items[uploadResult.ItemResponse.Id].Checkin.PostAsync(new());
                return fileNameOnDisk;
            }

            return null;
        }
    }
}
