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

using System.Text.Json;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Logging;
using Duplicati.Proprietary.GoogleWorkspace.SourceItems;
using Google.Apis.HangoutsChat.v1.Data;
using Google.Apis.Upload;

namespace Duplicati.Proprietary.GoogleWorkspace;

partial class RestoreProvider
{
    private ChatRestoreHelper? _chatRestoreHelper = null;
    internal ChatRestoreHelper ChatRestore => _chatRestoreHelper ??= new ChatRestoreHelper(this);

    internal class ChatRestoreHelper(RestoreProvider Provider)
    {
        private string? _targetUserId = null;
        private bool _hasLoadedTargetInfo = false;

        public async Task<string?> GetUserIdAndChatTarget(CancellationToken cancel)
        {
            if (_hasLoadedTargetInfo)
                return _targetUserId;

            var target = Provider.RestoreTarget;
            if (target == null)
                throw new InvalidOperationException("Restore target is not set");

            // Only restore Chat messages if the target is a User or UserChat
            if (target.Type == SourceItemType.User)
            {
                _targetUserId = target.Metadata.GetValueOrDefault("gsuite:Id");
            }
            else if (target.Type == SourceItemType.UserChat)
            {
                _targetUserId = target.Path.TrimStart(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Skip(1).FirstOrDefault();
            }
            else if (target.Type == SourceItemType.ChatSpace)
            {
                _targetUserId = target.Metadata.GetValueOrDefault("gsuite:UserId");
            }
            else
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreChatInvalidTargetType", null, $"Restore target type {target.Type} is not valid for restoring chat messages. Chat messages can only be restored to a User, UserChat, or ChatSpace target.");
            }

            _hasLoadedTargetInfo = true;

            if (string.IsNullOrWhiteSpace(_targetUserId))
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreChatMissingUserId", null, $"Missing target userId for restoring chat messages.");
                return null;
            }

            return _targetUserId;
        }

        public async Task<string?> CreateSpace(string userId, Space space, CancellationToken cancel)
        {
            var chatService = Provider._apiHelper.GetChatCreateSpaceService(userId);

            if (string.IsNullOrWhiteSpace(space.DisplayName))
                space.DisplayName = "Restored Space";

            // Check for duplicates by display name if available
            if (!Provider._ignoreExisting)
            {
                try
                {
                    var existingSpaces = await chatService.Spaces.List().ExecuteAsync(cancel);
                    var duplicate = existingSpaces.Spaces?.FirstOrDefault(s =>
                        s.DisplayName?.Equals(space.DisplayName, StringComparison.OrdinalIgnoreCase) == true);

                    if (duplicate != null)
                    {
                        Log.WriteInformationMessage(LOGTAG, "CreateSpaceSkipDuplicate", $"Space with name '{space.DisplayName}' already exists, skipping.");
                        return duplicate.Name;
                    }
                }
                catch
                {
                    // List might fail, continue with creation
                }
            }

            // Store the original space history state to set after creation
            var originalHistoryState = space.SpaceHistoryState;

            // Clean up properties that shouldn't be sent on creation
            space.Name = null;
            space.CreateTimeDateTimeOffset = null;
            space.SpaceHistoryState = null;
            space.Type = null;
            space.SpaceType = "SPACE";
            space.SingleUserBotDm = null;

            var createdSpace = await chatService.Spaces.Create(space).ExecuteAsync(cancel);

            // If there was a history state, update the space to set it
            if (!string.IsNullOrEmpty(originalHistoryState) && !string.IsNullOrEmpty(createdSpace.Name))
            {
                try
                {
                    var updateSpace = new Space
                    {
                        SpaceHistoryState = originalHistoryState
                    };
                    var updateRequest = chatService.Spaces.Patch(updateSpace, createdSpace.Name);
                    updateRequest.UpdateMask = "spaceHistoryState";
                    await updateRequest.ExecuteAsync(cancel);
                }
                catch (Exception ex)
                {
                    Log.WriteWarningMessage(LOGTAG, "UpdateSpaceHistoryStateFailed", ex, $"Failed to update space history state for {createdSpace.Name}");
                }
            }

            return createdSpace.Name;
        }

        public async Task<string?> CreateMessage(string userId, string spaceName, Message message, CancellationToken cancel)
        {
            var chatService = Provider._apiHelper.GetChatService(userId);

            // Clean up properties that shouldn't be sent on creation
            message.Name = null;
            message.CreateTimeDateTimeOffset = null;
            message.LastUpdateTimeDateTimeOffset = null;
            message.AccessoryWidgets = null;

            // Note: Creating messages in Chat API has limitations
            // Messages can typically only be created by the user who is sending them
            // For restore purposes, we attempt to create the message
            try
            {
                var createdMessage = await chatService.Spaces.Messages.Create(message, spaceName).ExecuteAsync(cancel);
                return createdMessage.Name;
            }
            catch (Exception ex)
            {
                Log.WriteWarningMessage(LOGTAG, "CreateMessageFailed", ex, $"Failed to create message in space {spaceName}. Note: Chat API has limitations on message creation.");
                return null;
            }
        }

        public async Task<string?> CreateMessageWithAttachment(string userId, string spaceName, Message message, string attachmentFileName, Stream attachmentStream, string? contentType, CancellationToken cancel)
        {
            var chatService = Provider._apiHelper.GetChatService(userId);

            // Clean up properties that shouldn't be sent on creation
            message.Name = null;
            message.CreateTimeDateTimeOffset = null;
            message.LastUpdateTimeDateTimeOffset = null;

            try
            {
                // For Chat API, attachments need to be handled differently
                // We upload the attachment to Drive first, then reference it in the message
                // This is a workaround since the Chat API doesn't support direct binary uploads
                // in the same way as other Google APIs
                var driveService = Provider._apiHelper.GetDriveService(userId);

                // Upload attachment to Drive
                var fileMetadata = new Google.Apis.Drive.v3.Data.File
                {
                    Name = attachmentFileName
                };

                var uploadRequest = driveService.Files.Create(fileMetadata, attachmentStream, contentType ?? "application/octet-stream");
                var uploadResult = await uploadRequest.UploadAsync(cancel);

                if (uploadResult.Status == UploadStatus.Failed)
                {
                    Log.WriteWarningMessage(LOGTAG, "CreateMessageAttachmentUploadFailed", uploadResult.Exception, $"Failed to upload attachment to Drive for message in space {spaceName}.");
                    // Fall back to creating message without attachment
                    return await CreateMessage(userId, spaceName, message, cancel);
                }

                var fileId = uploadRequest.ResponseBody?.Id;
                if (string.IsNullOrEmpty(fileId))
                {
                    Log.WriteWarningMessage(LOGTAG, "CreateMessageAttachmentNoFileId", null, $"No file ID returned after uploading attachment for message in space {spaceName}.");
                    return await CreateMessage(userId, spaceName, message, cancel);
                }

                // Set up attachment in the message
                message.Attachment = new List<Attachment>
                {
                    new Attachment
                    {
                        DriveDataRef = new DriveDataRef
                        {
                            DriveFileId = fileId
                        },
                        ContentName = attachmentFileName,
                        ContentType = contentType
                    }
                };

                // Create the message with the Drive attachment reference
                var createdMessage = await chatService.Spaces.Messages.Create(message, spaceName).ExecuteAsync(cancel);
                return createdMessage.Name;
            }
            catch (Exception ex)
            {
                Log.WriteWarningMessage(LOGTAG, "CreateMessageWithAttachmentFailed", ex, $"Failed to create message with attachment in space {spaceName}. Note: Chat API has limitations on message creation.");
                return null;
            }
        }
    }

    private async Task RestoreChatMessages(CancellationToken cancel)
    {
        if (RestoreTarget == null)
            throw new InvalidOperationException("Restore target entry is not set");

        // Note: Google Chat API has significant limitations for restoring messages
        // Messages will appear as being created by the impersonated user, not the original sender
        // To fix this, the Import API would need to be used

        var spaces = GetMetadataByType(SourceItemType.ChatSpace);
        var messages = GetMetadataByType(SourceItemType.ChatMessage);

        if (spaces.Count == 0 && messages.Count == 0)
            return;

        var userId = await ChatRestore.GetUserIdAndChatTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId))
            return;

        // First, restore spaces and build a map of original space paths to created space names
        var spacePathToNameMap = new Dictionary<string, string>();
        foreach (var space in spaces)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = space.Key;
                var contentJsonPath = SystemIO.IO_OS.PathCombine(originalPath, "content.json");
                var contentEntry = _temporaryFiles.GetValueOrDefault(contentJsonPath);

                if (contentEntry == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreChatSpacesMissingContent", null, $"Missing content for space {originalPath}, skipping.");
                    continue;
                }

                Space? spaceData;
                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    spaceData = await JsonSerializer.DeserializeAsync<Space>(contentStream, GoogleApiJsonDeserializer.Options, cancellationToken: cancel);
                }

                if (spaceData == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreChatSpacesInvalidContent", null, $"Invalid content for space {originalPath}, skipping.");
                    continue;
                }

                var createdSpaceName = await ChatRestore.CreateSpace(userId, spaceData, cancel);
                if (!string.IsNullOrEmpty(createdSpaceName))
                {
                    spacePathToNameMap[originalPath] = createdSpaceName;
                }

                _metadata.TryRemove(originalPath, out _);
                _metadata.TryRemove(contentJsonPath, out _);
                _temporaryFiles.TryRemove(contentJsonPath, out var contentFile);
                contentFile?.Dispose();
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreChatSpacesFailed", ex, $"Failed to restore Chat space {space.Key}");
            }
        }

        // Group attachments by message path
        var attachments = GetMetadataByType(SourceItemType.ChatAttachment)
            .GroupBy(k => Util.AppendDirSeparator(Path.GetDirectoryName(k.Key.TrimEnd(Path.DirectorySeparatorChar)) ?? ""))
            .ToDictionary(g => g.Key, g => g.ToList());

        // Attempt to restore messages (will likely fail due to API limitations)
        foreach (var message in messages)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = message.Key;
                var messageJsonPath = SystemIO.IO_OS.PathCombine(originalPath, "message.json");
                var contentEntry = _temporaryFiles.GetValueOrDefault(messageJsonPath);

                if (contentEntry == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreChatMessagesMissingContent", null, $"Missing content for message {originalPath}, skipping.");
                    continue;
                }

                // Get space name from the restored spaces map
                // The message's parent path corresponds to the space path
                var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
                if (!spacePathToNameMap.TryGetValue(parentPath, out var spaceName) || string.IsNullOrWhiteSpace(spaceName))
                {
                    // Fallback to metadata if available
                    spaceName = message.Value.GetValueOrDefault("gsuite:SpaceName");
                }

                if (string.IsNullOrWhiteSpace(spaceName))
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreChatMessagesMissingSpace", null, $"Missing space name for message {originalPath}, skipping.");
                    continue;
                }

                Message? messageData;
                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    messageData = await JsonSerializer.DeserializeAsync<Message>(contentStream, GoogleApiJsonDeserializer.Options, cancellationToken: cancel);
                }

                if (messageData == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreChatMessagesInvalidContent", null, $"Invalid content for message {originalPath}, skipping.");
                    continue;
                }

                // Check for attachments
                var messagePathWithSep = Util.AppendDirSeparator(originalPath);
                if (attachments.TryGetValue(messagePathWithSep, out var messageAttachments) && messageAttachments.Count > 0)
                {
                    // For now, we only support one attachment per message via media upload
                    // Additional attachments would need to be handled separately
                    var att = messageAttachments.First();
                    var attPath = att.Key;
                    var attMetadata = att.Value;
                    var attFileName = attMetadata.GetValueOrDefault("gsuite:Name") ?? "attachment";
                    var attContentType = attMetadata.GetValueOrDefault("gsuite:ContentType") ?? "application/octet-stream";

                    if (_temporaryFiles.TryRemove(attPath, out var attContent))
                    {
                        try
                        {
                            using var attStream = SystemIO.IO_OS.FileOpenRead(attContent);
                            await ChatRestore.CreateMessageWithAttachment(userId, spaceName, messageData, attFileName, attStream, attContentType, cancel);

                            // Clean up all attachments for this message
                            foreach (var attachment in messageAttachments)
                            {
                                _metadata.TryRemove(attachment.Key, out _);
                                if (_temporaryFiles.TryRemove(attachment.Key, out var attFile))
                                {
                                    attFile?.Dispose();
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            Log.WriteWarningMessage(LOGTAG, "RestoreChatMessageAttachmentFailed", ex, $"Failed to restore attachment for message {originalPath}");
                            // Fall back to creating message without attachment
                            await ChatRestore.CreateMessage(userId, spaceName, messageData, cancel);
                        }
                    }
                    else
                    {
                        await ChatRestore.CreateMessage(userId, spaceName, messageData, cancel);
                    }
                }
                else
                {
                    await ChatRestore.CreateMessage(userId, spaceName, messageData, cancel);
                }

                _metadata.TryRemove(originalPath, out _);
                _metadata.TryRemove(messageJsonPath, out _);
                _temporaryFiles.TryRemove(messageJsonPath, out var contentFile);
                contentFile?.Dispose();
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreChatMessagesFailed", ex, $"Failed to restore Chat message {message.Key}");
            }
        }
    }
}
