// Copyright (c) 2026 Duplicati Inc. All rights reserved.

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Duplicati.Library.Logging;
using Duplicati.Proprietary.Office365.SourceItems;

namespace Duplicati.Proprietary.Office365;

partial class RestoreProvider
{
    private DriveRestoreHelper? _driveRestoreHelper = null;
    internal DriveRestoreHelper DriveRestore => _driveRestoreHelper ??= new DriveRestoreHelper(this);

    internal class DriveRestoreHelper(RestoreProvider Provider)
    {
        private string? _cachedDefaultDriveId;
        private string? _cachedDefaultFolderId;
        private bool _defaultDriveIdChecked;

        public async Task<(string? DriveId, string? FolderId)> GetDefaultDriveAndFolder(CancellationToken cancel)
        {
            if (_defaultDriveIdChecked)
                return (_cachedDefaultDriveId, _cachedDefaultFolderId);

            _defaultDriveIdChecked = true;

            var target = Provider.RestoreTarget;
            if (target == null)
                return (null, null);

            string? driveId = null;

            if (target.Type == SourceItemType.Drive)
            {
                driveId = target.Metadata.GetValueOrDefault("o365:Id");
            }
            else
            {
                string? targetUserId = null;
                string? targetSiteId = null;

                if (target.Type == SourceItemType.User)
                {
                    targetUserId = target.Metadata.GetValueOrDefault("o365:Id");
                }
                else if (target.Type == SourceItemType.Site)
                {
                    targetSiteId = target.Metadata.GetValueOrDefault("o365:Id");
                }

                if (!string.IsNullOrWhiteSpace(targetUserId))
                {
                    try
                    {
                        var primaryDrive = await Provider.SourceProvider.OneDriveApi.GetUserPrimaryDriveAsync(targetUserId, cancel);
                        driveId = primaryDrive.Id;
                    }
                    catch (Exception ex)
                    {
                        Log.WriteErrorMessage(LOGTAG, "GetDefaultDriveIdFailed", ex, $"Failed to get user primary drive");
                    }
                }
                else if (!string.IsNullOrWhiteSpace(targetSiteId))
                {
                    try
                    {
                        var primaryDrive = await Provider.SourceProvider.SiteApi.GetSitePrimaryDriveAsync(targetSiteId, cancel);
                        driveId = primaryDrive.Id;
                    }
                    catch (Exception ex)
                    {
                        Log.WriteErrorMessage(LOGTAG, "GetDefaultDriveIdFailed", ex, $"Failed to get site primary drive");
                    }
                }
            }

            if (driveId != null)
            {
                _cachedDefaultDriveId = driveId;

                // Create "Restored" folder
                try
                {
                    var restoredFolder = await Provider.DriveApi.GetDriveItemAsync(driveId, "root", "Restored", cancel);
                    if (restoredFolder == null)
                    {
                        restoredFolder = await Provider.DriveApi.CreateDriveFolderAsync(driveId, "root", "Restored", cancel);
                    }
                    _cachedDefaultFolderId = restoredFolder.Id;
                }
                catch (Exception ex)
                {
                    Log.WriteWarningMessage(LOGTAG, "CreateRestoredFolderFailed", ex, $"Failed to create 'Restored' folder in drive {driveId}");
                    // Fallback to root
                    _cachedDefaultFolderId = "root";
                }
            }

            return (_cachedDefaultDriveId, _cachedDefaultFolderId);
        }
    }

    internal DriveApiImpl DriveApi => new DriveApiImpl(_apiHelper);

    internal class DriveApiImpl(APIHelper provider)
    {
        public async Task<GraphDriveItem> CreateDriveFolderAsync(
            string driveId,
            string parentItemId,
            string name,
            CancellationToken ct)
        {
            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var parent = Uri.EscapeDataString(parentItemId);

            // POST /drives/{drive-id}/items/{parent-id}/children
            var url = $"{baseUrl}/v1.0/drives/{drive}/items/{parent}/children";

            var body = new
            {
                name = name,
                folder = new { },
                @microsoft_graph_conflictBehavior = "rename"
            };

            async Task<HttpRequestMessage> requestFactory(CancellationToken rct)
            {
                var req = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
                req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, rct).ConfigureAwait(false);
                req.Content = JsonContent.Create(body);
                return req;
            }

            using var resp = await provider.SendWithRetryShortAsync(
                requestFactory,
                ct).ConfigureAwait(false);

            await APIHelper.EnsureOfficeApiSuccessAsync(resp, ct).ConfigureAwait(false);

            await using var respStream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
            return await JsonSerializer.DeserializeAsync<GraphDriveItem>(respStream, cancellationToken: ct).ConfigureAwait(false)
                ?? throw new InvalidOperationException("Graph did not return the created folder item.");
        }

        public async Task<GraphDriveItem> RestoreDriveItemToFolderAsync(
            string driveId,
            string targetFolderItemId,
            string fileName,
            Stream contentStream,
            string? contentType,
            CancellationToken cancellationToken)
        {
            // Use upload session for files larger than 4MB
            if (contentStream.Length > 4 * 1024 * 1024)
            {
                return await RestoreLargeDriveItemToFolderAsync(driveId, targetFolderItemId, fileName, contentStream, contentType, cancellationToken).ConfigureAwait(false);
            }

            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var folder = Uri.EscapeDataString(targetFolderItemId);
            var name = Uri.EscapeDataString(fileName);

            // PUT /drives/{drive-id}/items/{parent-id}:/{filename}:/content
            var url =
                $"{baseUrl}/v1.0/drives/{drive}/items/{folder}:/{name}:/content";

            async Task<HttpRequestMessage> requestFactory(CancellationToken ct)
            {
                var req = new HttpRequestMessage(HttpMethod.Put, new Uri(url));
                req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct).ConfigureAwait(false);

                contentStream.Position = 0;
                req.Content = new StreamContent(contentStream);
                req.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType ?? "application/octet-stream");

                return req;
            }

            using var resp = await provider.SendWithRetryShortAsync(
                requestFactory,
                cancellationToken).ConfigureAwait(false);

            await APIHelper.EnsureOfficeApiSuccessAsync(resp, cancellationToken).ConfigureAwait(false);

            await using var respStream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
            return await JsonSerializer.DeserializeAsync<GraphDriveItem>(respStream, cancellationToken: cancellationToken).ConfigureAwait(false)
                ?? throw new InvalidOperationException("Graph did not return the restored item.");
        }

        private async Task<GraphDriveItem> RestoreLargeDriveItemToFolderAsync(
            string driveId,
            string targetFolderItemId,
            string fileName,
            Stream contentStream,
            string? contentType,
            CancellationToken cancellationToken)
        {
            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var folder = Uri.EscapeDataString(targetFolderItemId);
            var name = Uri.EscapeDataString(fileName);

            // 1. Create upload session
            // POST /drives/{drive-id}/items/{parent-id}:/{filename}:/createUploadSession
            var createSessionUrl = $"{baseUrl}/v1.0/drives/{drive}/items/{folder}:/{name}:/createUploadSession";

            var sessionBody = new
            {
                item = new
                {
                    @microsoft_graph_conflictBehavior = "rename",
                    name = name
                }
            };

            async Task<HttpRequestMessage> createSessionRequestFactory(CancellationToken ct)
            {
                var req = new HttpRequestMessage(HttpMethod.Post, new Uri(createSessionUrl));
                req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct).ConfigureAwait(false);
                req.Content = JsonContent.Create(sessionBody);
                return req;
            }

            using var sessionResp = await provider.SendWithRetryShortAsync(
                createSessionRequestFactory,
                cancellationToken).ConfigureAwait(false);

            await APIHelper.EnsureOfficeApiSuccessAsync(sessionResp, cancellationToken).ConfigureAwait(false);

            var uploadSession = await sessionResp.Content.ReadFromJsonAsync<GraphUploadSession>(cancellationToken: cancellationToken).ConfigureAwait(false)
                ?? throw new InvalidOperationException("Failed to create upload session.");

            // 2. Upload fragments
            var uploadUrl = uploadSession.uploadUrl;
            var totalSize = contentStream.Length;
            // 320 KiB is the minimum fragment size, we use a multiple of it (e.g. 5MB approx)
            // 320 * 1024 = 327680 bytes. 16 * 327680 = 5242880 bytes (5MB)
            var fragmentSize = 320 * 1024 * 16;
            var buffer = new byte[fragmentSize];
            long position = 0;

            contentStream.Position = 0;

            GraphDriveItem? resultItem = null;

            while (position < totalSize)
            {
                var bytesRead = await contentStream.ReadAsync(buffer, 0, fragmentSize, cancellationToken).ConfigureAwait(false);
                var isLast = position + bytesRead >= totalSize;

                async Task<HttpRequestMessage> uploadFragmentRequestFactory(CancellationToken ct)
                {
                    var req = new HttpRequestMessage(HttpMethod.Put, new Uri(uploadUrl));
                    // No auth header needed for upload session URL
                    req.Content = new ByteArrayContent(buffer, 0, bytesRead);
                    req.Content.Headers.ContentRange = new ContentRangeHeaderValue(position, position + bytesRead - 1, totalSize);
                    req.Content.Headers.ContentLength = bytesRead;

                    return req;
                }

                using var uploadResp = await provider.SendWithRetryShortAsync(
                    uploadFragmentRequestFactory,
                    cancellationToken).ConfigureAwait(false);

                if (!uploadResp.IsSuccessStatusCode)
                {
                    // If we get an error, we should probably check if the session is still valid or if we can retry.
                    // For now, we rely on SendWithRetryAsync for transient errors, but if it fails here, it's likely fatal or needs session recreation.
                    // We'll throw and let the caller handle it.
                    var error = await uploadResp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
                    throw new InvalidOperationException($"Failed to upload fragment: {uploadResp.StatusCode} {error}");
                }

                if (isLast)
                {
                    await using var respStream = await uploadResp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
                    resultItem = await JsonSerializer.DeserializeAsync<GraphDriveItem>(respStream, cancellationToken: cancellationToken).ConfigureAwait(false);
                }

                position += bytesRead;
            }

            return resultItem ?? throw new InvalidOperationException("Upload completed but no item returned.");
        }

        public async Task UpdateDriveItemFileSystemInfoAsync(
            string driveId,
            string itemId,
            DateTimeOffset? created,
            DateTimeOffset? modified,
            CancellationToken cancellationToken)
        {
            if (created == null && modified == null) return;

            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var item = Uri.EscapeDataString(itemId);

            var url = $"{baseUrl}/v1.0/drives/{drive}/items/{item}";

            var body = new
            {
                fileSystemInfo = new
                {
                    createdDateTime = created,
                    lastModifiedDateTime = modified
                }
            };

            async Task<HttpRequestMessage> requestFactory(CancellationToken ct)
            {
                var req = new HttpRequestMessage(HttpMethod.Patch, new Uri(url));
                req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct).ConfigureAwait(false);
                req.Content = JsonContent.Create(body);
                return req;
            }

            using var resp = await provider.SendWithRetryShortAsync(
                requestFactory, cancellationToken).ConfigureAwait(false);

            await APIHelper.EnsureOfficeApiSuccessAsync(resp, cancellationToken).ConfigureAwait(false);
        }

        private class GraphUploadSession
        {
            public string uploadUrl { get; set; } = "";
            public DateTimeOffset? expirationDateTime { get; set; }
            public string[]? nextExpectedRanges { get; set; }
        }

        public async Task RestoreDriveItemMetadataAsync(
            string driveId,
            string itemId,
            Stream metadataJsonStream,
            CancellationToken cancellationToken)
        {
            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var item = Uri.EscapeDataString(itemId);

            var url = $"{baseUrl}/v1.0/drives/{drive}/items/{item}";

            async Task<HttpRequestMessage> requestFactory(CancellationToken ct)
            {
                var req = new HttpRequestMessage(HttpMethod.Patch, new Uri(url));
                req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct).ConfigureAwait(false);

                metadataJsonStream.Position = 0;
                req.Content = new StreamContent(metadataJsonStream);
                req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                return req;
            }

            using var resp = await provider.SendWithRetryShortAsync(
                requestFactory, cancellationToken).ConfigureAwait(false);

            await APIHelper.EnsureOfficeApiSuccessAsync(resp, cancellationToken).ConfigureAwait(false);
        }

        public async Task<GraphDriveItem?> GetDriveItemAsync(
            string driveId,
            string parentId,
            string name,
            CancellationToken ct)
        {
            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var parent = Uri.EscapeDataString(parentId);
            var itemName = Uri.EscapeDataString(name);

            var url = $"{baseUrl}/v1.0/drives/{drive}/items/{parent}:/{itemName}";

            async Task<HttpRequestMessage> requestFactory(CancellationToken rct)
            {
                var req = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
                req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, rct).ConfigureAwait(false);
                return req;
            }

            using var resp = await provider.SendWithRetryShortAsync(
                requestFactory,
                ct).ConfigureAwait(false);

            if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
                return null;

            await APIHelper.EnsureOfficeApiSuccessAsync(resp, ct).ConfigureAwait(false);

            using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
            return await JsonSerializer.DeserializeAsync<GraphDriveItem>(stream, cancellationToken: ct).ConfigureAwait(false);
        }

        public async Task RestoreDriveItemPermissionsAsync(
            string driveId,
            string itemId,
            string permissionsJson,
            CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(permissionsJson)) return;

            List<GraphPermission>? permissions;
            try
            {
                permissions = JsonSerializer.Deserialize<List<GraphPermission>>(permissionsJson);
            }
            catch
            {
                return;
            }

            if (permissions == null || permissions.Count == 0) return;

            var baseUrl = provider.GraphBaseUrl.TrimEnd('/');
            var drive = Uri.EscapeDataString(driveId);
            var item = Uri.EscapeDataString(itemId);

            // POST /drives/{drive-id}/items/{item-id}/invite
            var url = $"{baseUrl}/v1.0/drives/{drive}/items/{item}/invite";

            foreach (var perm in permissions)
            {
                // We can only restore permissions that have roles and recipients (grantedTo or grantedToIdentities)
                // We skip links for now as they are complex to restore (might need to create new links)
                // We also skip if no roles are defined.

                if (perm.Roles == null || perm.Roles.Count == 0) continue;

                var recipients = new List<object>();

                if (perm.GrantedTo?.User?.Email != null)
                {
                    recipients.Add(new { email = perm.GrantedTo.User.Email });
                }

                if (perm.GrantedToIdentities != null)
                {
                    foreach (var identity in perm.GrantedToIdentities)
                    {
                        if (identity.User?.Email != null)
                        {
                            recipients.Add(new { email = identity.User.Email });
                        }
                    }
                }

                // Also check Invitation email
                if (perm.Invitation?.Email != null)
                {
                    recipients.Add(new { email = perm.Invitation.Email });
                }

                if (recipients.Count == 0) continue;

                var body = new
                {
                    requireSignIn = true, // Default to true for security
                    sendInvitation = false, // Don't spam users during restore
                    roles = perm.Roles,
                    recipients = recipients,
                    message = "Restored by Duplicati"
                };

                async Task<HttpRequestMessage> requestFactory(CancellationToken ct)
                {
                    var req = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
                    req.Headers.Authorization = await provider.GetAuthenticationHeaderAsync(false, ct).ConfigureAwait(false);
                    req.Content = JsonContent.Create(body);
                    return req;
                }

                try
                {
                    using var resp = await provider.SendWithRetryShortAsync(
                        requestFactory,
                        cancellationToken).ConfigureAwait(false);

                    // We don't throw on permission restore failure, just log/ignore
                    // await APIHelper.EnsureOfficeApiSuccessAsync(resp, cancellationToken).ConfigureAwait(false);
                }
                catch
                {
                    // Ignore permission restore errors
                }
            }
        }
    }
}
