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

using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Logging;
using Duplicati.Library.Utility;
using Duplicati.Library.Utility.Options;
using Duplicati.Proprietary.Office365.SourceItems;

namespace Duplicati.Proprietary.Office365;

public partial class RestoreProvider : IRestoreDestinationProviderModule
{
    /// <summary>
    /// Log tag for RestoreProvider
    /// </summary>
    private static readonly string LOGTAG = Log.LogTagFromType<RestoreProvider>();

    /// <summary>
    /// API helper for accessing Office365 APIs
    /// </summary>
    private readonly APIHelper _apiHelper;
    /// <summary>
    /// Path to restore to
    /// </summary>
    private readonly string _restorePath;
    /// <summary>
    /// Whether the overwrite option has been set
    /// </summary>
    private readonly bool _hasSetOverwriteOption;
    /// <summary>
    /// Source provider for accessing currently existing items on the destination tenant
    /// </summary>
    internal SourceProvider SourceProvider { get; init; }

    /// <summary>
    /// Properties related to the restore target
    /// </summary>
    /// <param name="Path">The path to restore to</param>
    /// <param name="Entry">The source provider entry to restore to</param>
    /// <param name="Metadata">The metadata associated with the restore target</param>
    /// <param name="Type">The type of the restore target</param>
    internal record RestoreTargetData(
        string Path,
        ISourceProviderEntry Entry,
        Dictionary<string, string?> Metadata,
        SourceItemType Type);

    /// <summary>
    /// The restore target data
    /// </summary>
    internal RestoreTargetData? RestoreTarget { get; private set; }

    /// <summary>
    /// Temporary files created during the restore process
    /// </summary>
    private readonly ConcurrentDictionary<string, TempFile> _temporaryFiles = new();
    /// <summary>
    /// Metadata recorded during the restore process
    /// </summary>
    private readonly ConcurrentDictionary<string, Dictionary<string, string?>> _metadata = new();
    /// <summary>
    /// Map of restored email folder paths to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredEmailFolderMap = new();

    /// <summary>
    /// Map of restored contact folder paths to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredContactFolderMap = new();

    /// <summary>
    /// Map of restored drive paths to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredDriveMap = new();

    /// <summary>
    /// Map of restored drive folder paths to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredDriveFolderMap = new();

    /// <summary>
    /// Map of restored planner buckets to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredPlannerBucketMap = new();

    /// <summary>
    /// Map of restored notebooks to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredNotebookMap = new();

    /// <summary>
    /// Map of restored section groups to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredSectionGroupMap = new();

    /// <summary>
    /// Map of restored sections to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredSectionMap = new();

    /// <summary>
    /// Map of restored task lists to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredTaskListMap = new();

    /// <summary>
    /// Map of restored tasks to their corresponding IDs
    /// </summary>
    private readonly ConcurrentDictionary<string, string> _restoredTaskMap = new();

    /// <summary>
    /// Whether to overwrite existing items
    /// </summary>
    internal readonly bool _ignoreExisting;

    /// <summary>
    /// Default constructor for the restore provider.
    /// Only used for loading metadata about the provider.
    /// </summary>
    public RestoreProvider()
    {
        _apiHelper = null!;
        _restorePath = null!;
        SourceProvider = null!;
        _hasSetOverwriteOption = false;
    }

    /// <summary>
    /// Constructs the RestoreProvider with the given URL and options.
    /// </summary>
    /// <param name="url">The destination URL for the restore operation</param>
    /// <param name="options">The options for the restore operation</param>
    public RestoreProvider(string url, Dictionary<string, string?> options)
    {
        var uri = new Library.Utility.Uri(url);
        _restorePath = uri.HostAndPath;

        var parsedOptions = OptionsHelper.ParseAndValidateOptions(url, options);
        _apiHelper = APIHelper.Create(
            tenantId: parsedOptions.TenantId,
            authOptions: parsedOptions.AuthOptions,
            graphBaseUrl: parsedOptions.GraphBaseUrl,
            certificatePath: parsedOptions.CertificatePath,
            certificatePassword: parsedOptions.CertificatePassword,
            timeouts: TimeoutOptionsHelper.Parse(options),
            scope: parsedOptions.Scope
        );

        _ignoreExisting = Utility.ParseBoolOption(options, OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION);
        _hasSetOverwriteOption = Utility.ParseBoolOption(options, "overwrite");

        var sourceOpts = new Dictionary<string, string?>(options);
        sourceOpts["store-metadata-content-in-database"] = "true";

        SourceProvider = new SourceProvider("office365://", "", sourceOpts)
        {
            // Make sure we can reach all items during restore
            UsedForRestoreOperation = true
        };
    }

    /// <inheritdoc />
    public string Key => OptionsHelper.ModuleKey;

    /// <inheritdoc />
    public string DisplayName => Strings.ProviderDisplayName;

    /// <inheritdoc />
    public string Description => Strings.ProviderDescription;

    /// <inheritdoc />
    public IList<ICommandLineArgument> SupportedCommands => OptionsHelper.SupportedCommands.Concat([new CommandLineArgument(OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.OfficeIgnoreExistingOptionShort, Strings.OfficeIgnoreExistingOptionLong)]).ToList();

    /// <inheritdoc />
    public string TargetDestination => _restorePath;

    /// <inheritdoc />
    public Task<bool> CreateFolderIfNotExists(string path, CancellationToken cancel)
    {
        // TODO: Do we need to do anything here?
        return Task.FromResult(false);
    }

    /// <inheritdoc />
    public async Task Initialize(CancellationToken cancel)
    {
        if (!_hasSetOverwriteOption)
            throw new UserInformationException(Strings.RestoreTargetMissingOverwriteOption("overwrite", OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION), "OverwriteOptionNotSet");

        await _apiHelper.AcquireAccessTokenAsync(true, cancel);
        await SourceProvider.Initialize(cancel);
        var entry = await SourceProvider.GetEntry(_restorePath, isFolder: true, cancel);
        if (entry == null)
            throw new UserInformationException($"Restore target path not found: {_restorePath}", "RestoreTargetNotFound");
        var metadata = await entry.GetMinorMetadata(cancel);
        var type = metadata.TryGetValue("o365:Type", out var typeStr)
            && Enum.TryParse<SourceItemType>(typeStr, out var sourceItemType)
            ? sourceItemType
            : throw new UserInformationException($"Invalid restore target type in metadata: {typeStr}", "InvalidRestoreTargetType");

        RestoreTarget = new RestoreTargetData(_restorePath, entry, metadata, type);
    }

    /// <inheritdoc />
    public async Task Test(CancellationToken cancellationToken)
    {
        await _apiHelper.AcquireAccessTokenAsync(false, cancellationToken);

        // If we have no path, just check that we can connect
        if (string.IsNullOrWhiteSpace(_restorePath))
            return;

        // Otherwise, check that the path exists and is a valid target
        await Initialize(cancellationToken);
    }


    /// <summary>
    /// Normalizes the given path by applying the RemovedRestorePrefix if it has been removed
    /// </summary>
    /// <param name="path">The path to normalize</param>
    /// <returns>The normalized path</returns>
    private string NormalizePath(string path)
        => path;

    /// <summary>
    /// Replaces the extension of a path with a new extension
    /// </summary>
    /// <param name="path">The path to modify</param>
    /// <param name="oldExtension">The extension to replace</param>
    /// <param name="newExtension">The new extension</param>
    /// <returns>The modified path</returns>
    private static string ReplaceExtension(string path, string oldExtension, string newExtension)
    {
        if (path.EndsWith(oldExtension, StringComparison.OrdinalIgnoreCase))
            return path.Substring(0, path.Length - oldExtension.Length) + newExtension;

        return path;
    }

    /// <inheritdoc />
    public void Dispose()
    {
        _apiHelper?.Dispose();
        SourceProvider?.Dispose();

        foreach (var file in _temporaryFiles.Values)
            file.Dispose();
        _temporaryFiles.Clear();
    }

    /// <inheritdoc />
    public async Task<bool> FileExists(string path, CancellationToken cancel)
    {
        path = NormalizePath(path);

        if (_temporaryFiles.ContainsKey(path))
            return true;

        var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false);
        return entry != null;
    }

    /// <inheritdoc />
    public Task<Stream> OpenWrite(string path, CancellationToken cancel)
    {
        path = NormalizePath(path);

        var file = _temporaryFiles.GetOrAdd(path, _ => new TempFile());
        return Task.FromResult<Stream>(SystemIO.IO_OS.FileOpenWrite(file));
    }

    /// <inheritdoc />
    public async Task<Stream> OpenRead(string path, CancellationToken cancel)
    {
        path = NormalizePath(path);

        if (_temporaryFiles.ContainsKey(path))
            return SystemIO.IO_OS.FileOpenRead(_temporaryFiles[path]);

        var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false);
        if (entry != null)
            return await entry.OpenRead(cancel).ConfigureAwait(false);

        throw new FileNotFoundException($"File not found: {path}");
    }

    /// <inheritdoc />
    public async Task<Stream> OpenReadWrite(string path, CancellationToken cancel)
    {
        path = NormalizePath(path);

        if (_temporaryFiles.ContainsKey(path))
            return SystemIO.IO_OS.FileOpenReadWrite(_temporaryFiles[path]);

        var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false);
        if (entry == null)
        {
            _temporaryFiles.GetOrAdd(path, _ => new TempFile());
            return SystemIO.IO_OS.FileOpenReadWrite(_temporaryFiles[path]);
        }

        if (entry.IsFolder)
            throw new FileNotFoundException($"Path is a folder: {path}");

        var file = new TempFile();
        using (var fs = SystemIO.IO_OS.FileOpenWrite(file))
        using (var stream = await entry.OpenRead(cancel).ConfigureAwait(false))
            await stream.CopyToAsync(fs).ConfigureAwait(false);

        _temporaryFiles[path] = file;
        return SystemIO.IO_OS.FileOpenReadWrite(file);
    }

    /// <inheritdoc />
    public async Task<long> GetFileLength(string path, CancellationToken cancel)
    {
        path = NormalizePath(path);

        if (_temporaryFiles.ContainsKey(path))
            return SystemIO.IO_OS.FileLength(_temporaryFiles[path]);

        var entry = await SourceProvider.GetEntry(path, isFolder: false, cancel).ConfigureAwait(false);
        if (entry == null)
            throw new FileNotFoundException($"File not found: {path}");

        return entry.Size;
    }

    /// <inheritdoc />
    public Task<bool> HasReadOnlyAttribute(string path, CancellationToken cancel)
        => Task.FromResult(false);

    /// <inheritdoc />
    public Task ClearReadOnlyAttribute(string path, CancellationToken cancel)
        => Task.CompletedTask;

    /// <inheritdoc />
    public Task<bool> WriteMetadata(string path, Dictionary<string, string?> metadata, bool restoreSymlinkMetadata, bool restorePermissions, CancellationToken cancel)
    {
        path = NormalizePath(path);

        _metadata.AddOrUpdate(path, metadata, (_, _) => metadata);
        return Task.FromResult(true);
    }

    /// <inheritdoc />
    public Task DeleteFolder(string path, CancellationToken cancel)
        => Task.CompletedTask;

    /// <inheritdoc />
    public Task DeleteFile(string path, CancellationToken cancel)
        // Should only happen if attempting to restore a symlink, which is not supported
        => throw new NotImplementedException("File deletion is not supported in Office365 RestoreProvider");

    /// <inheritdoc />
    public IList<string> GetPriorityFiles() => Array.Empty<string>();

    /// <inheritdoc />
    public async Task Finalize(Action<double>? progressCallback, CancellationToken cancel)
    {
        var totalFiles = _metadata.Count;
        try
        {
            await RestoreUserEmailFolders(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreUserEmailFoldersFailed", ex, $"Failed to restore user email folders");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreUserEmails(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreUserEmailsFailed", ex, $"Failed to restore user emails");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreUserMailboxSettings(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreUserMailboxSettingsFailed", ex, $"Failed to restore user mailbox settings");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreUserMailboxRules(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreUserMailboxRulesFailed", ex, $"Failed to restore user mailbox rules");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreDrives(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreDrivesFailed", ex, $"Failed to restore drives");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreSiteDrives(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreSiteDrivesFailed", ex, $"Failed to restore site drives");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreSharePointLists(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreSharePointListsFailed", ex, $"Failed to restore SharePoint lists");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreSharePointListItems(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreSharePointListItemsFailed", ex, $"Failed to restore SharePoint list items");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreDriveFolders(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreDriveFoldersFailed", ex, $"Failed to restore drive folders");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreDriveFiles(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreDriveFilesFailed", ex, $"Failed to restore drive files");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreCalendarEvents(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreCalendarEventsFailed", ex, $"Failed to restore calendar events");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreContactFolders(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreContactFoldersFailed", ex, $"Failed to restore contact folders");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreContacts(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreContactsFailed", ex, $"Failed to restore contacts");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreContactGroups(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreContactGroupsFailed", ex, $"Failed to restore contact groups");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreGroupChannels(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreGroupChannelsFailed", ex, $"Failed to restore group channels");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreGroupSettings(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreGroupSettingsFailed", ex, $"Failed to restore group settings");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreGroupMembers(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreGroupMembersFailed", ex, $"Failed to restore group members");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreGroupOwners(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreGroupOwnersFailed", ex, $"Failed to restore group owners");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreTeamApps(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreTeamAppsFailed", ex, $"Failed to restore team apps");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreChannelTabs(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreChannelTabsFailed", ex, $"Failed to restore channel tabs");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreChannelMessages(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreChannelMessagesFailed", ex, $"Failed to restore channel messages");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreGroupConversations(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreGroupConversationsFailed", ex, $"Failed to restore group conversations");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreGroupCalendarEvents(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreGroupCalendarEventsFailed", ex, $"Failed to restore group calendar events");
        }

        try
        {
            await ChannelRestore.EndMigrationMode(cancel);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "EndMigrationModeFailed", ex, $"Failed to end migration mode for channels");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestorePlannerBuckets(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestorePlannerBucketsFailed", ex, $"Failed to restore planner buckets");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestorePlannerTasks(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestorePlannerTasksFailed", ex, $"Failed to restore planner tasks");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreNotebooks(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreNotebooksFailed", ex, $"Failed to restore notebooks");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreNotebookSectionGroups(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreNotebookSectionGroupsFailed", ex, $"Failed to restore notebook section groups");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreNotebookSections(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreNotebookSectionsFailed", ex, $"Failed to restore notebook sections");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreNotebookPages(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreNotebookPagesFailed", ex, $"Failed to restore notebook pages");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreTaskLists(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreTaskListsFailed", ex, $"Failed to restore task lists");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreTaskListTasks(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreTaskListTasksFailed", ex, $"Failed to restore task list tasks");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreTaskChecklistItems(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreTaskChecklistItemsFailed", ex, $"Failed to restore task checklist items");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreTaskLinkedResources(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreTaskLinkedResourcesFailed", ex, $"Failed to restore task linked resources");
        }

        progressCallback?.Invoke(_metadata.Count / (double)totalFiles);

        try
        {
            await RestoreUserProfile(cancel).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "FinalizeRestoreUserProfileFailed", ex, $"Failed to restore user profile");
        }

        RemoveMetaEntriesForNonRestoredItems();

        // We should have restored all items by now
        if (_metadata.Count > 0 || _temporaryFiles.Count > 0)
        {
            var displayItems = _metadata.Values
                .Select(x => (Type: x.GetValueOrDefault("o365:Type"), Name: x.GetValueOrDefault("o365:Name") ?? x.GetValueOrDefault("o365:DisplayName")))
                .Where(x => !string.IsNullOrWhiteSpace(x.Type) && !string.IsNullOrWhiteSpace(x.Name))
                .Take(10);

            foreach (var item in displayItems)
                Log.WriteWarningMessage(LOGTAG, "FinalizeIncompleteItem", null, $"Unrestored item - Type: {item.Type}, Name: {item.Name}");

            if (_metadata.Count > displayItems.Count())
                Log.WriteWarningMessage(LOGTAG, "FinalizeIncomplete", null, $"... and {_metadata.Count - displayItems.Count()} more unrestored items.");
        }

        var tempFiles = _temporaryFiles.Values.ToList();
        _temporaryFiles.Clear();
        foreach (var file in tempFiles)
            file.Dispose();
        _metadata.Clear();
    }

    /// <summary>
    /// Gets metadata entries by their source item type
    /// </summary>
    /// <param name="type">The source item type to filter by</param>
    /// <returns>The list of metadata entries matching the given type</returns>
    private List<KeyValuePair<string, Dictionary<string, string?>>> GetMetadataByType(SourceItemType type)
        => _metadata
            .Where(kv => kv.Value.TryGetValue("o365:Type", out var typeStr)
                && typeStr == type.ToString())
            .ToList();

    /// <summary>
    /// Removes metadata entries for items that were not restored and are not required to be processed
    /// </summary>
    private void RemoveMetaEntriesForNonRestoredItems()
    {
        var toRemove = new HashSet<string>();
        foreach (var kv in _metadata)
        {
            // Some intermediate directories may not have a type
            if (!kv.Value.ContainsKey("o365:Type"))
            {
                toRemove.Add(kv.Key);
                continue;
            }

            var key = kv.Value.GetValueOrDefault("o365:Type");
            if (!Enum.TryParse<SourceItemType>(key, out var type))
                continue;

            switch (type)
            {
                // Various meta-types that are not restored directly
                case SourceItemType.User:
                case SourceItemType.UserMailbox:
                case SourceItemType.UserContacts:
                case SourceItemType.Calendar:
                case SourceItemType.Drive:
                    toRemove.Add(kv.Key);
                    break;

                default:
                    break;
            }

            // If we are restoring TO a channel, we don't need to restore the channel itself
            if (type == SourceItemType.GroupChannel && RestoreTarget != null && RestoreTarget.Type == SourceItemType.GroupChannel)
                toRemove.Add(kv.Key);

            // If we are restoring TO a site, we don't need to restore the site itself
            if (type == SourceItemType.Site && RestoreTarget != null && RestoreTarget.Type == SourceItemType.Site)
                toRemove.Add(kv.Key);

            if (type == SourceItemType.Planner && RestoreTarget != null)
            {
                if (RestoreTarget.Type == SourceItemType.Group || RestoreTarget.Type == SourceItemType.Planner)
                    toRemove.Add(kv.Key);
                toRemove.Add(kv.Key);
            }
        }

        foreach (var key in toRemove)
        {
            _metadata.TryRemove(key, out _);
            _temporaryFiles.TryRemove(key, out var f);
            f?.Dispose();
        }
    }

    /// <summary>
    /// Restores user email folders and updates the folder ID map
    /// </summary>
    /// <param name="cancel">The cancellation token</param>
    /// <returns>An awaitable task</returns>
    private async Task RestoreUserEmailFolders(CancellationToken cancel)
    {
        if (RestoreTarget == null)
            throw new InvalidOperationException("Restore target entry is not set");

        // Find email entries in _metadata
        var emailFolders = GetMetadataByType(SourceItemType.UserMailboxFolder);
        if (emailFolders.Count == 0)
            return;

        (var userId, var mailboxId) = await EmailRestore.GetUserIdAndMailboxTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(mailboxId))
            return; // A warning has already been logged

        // Create folders
        // Sort folders by path length to ensure parents are created before children
        var sortedFolders = emailFolders.OrderBy(k => k.Key.Split(Path.DirectorySeparatorChar).Length).ToList();

        foreach (var folder in sortedFolders)
        {
            if (cancel.IsCancellationRequested)
                break;

            var originalPath = folder.Key;
            var metadata = folder.Value;
            var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? metadata.GetValueOrDefault("o365:Id");

            _metadata.TryRemove(originalPath, out _);

            if (string.IsNullOrWhiteSpace(displayName))
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreUserEmailFoldersMissingName", null, $"Missing display name for folder {originalPath}, skipping.");
                continue;
            }

            // Determine parent folder ID
            // We assume the path structure reflects the hierarchy
            // If parent path is not in map, we assume it's the root (mailboxId)
            var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
            var parentId = mailboxId;

            if (parentPath != null && _restoredEmailFolderMap.TryGetValue(parentPath, out var mappedParentId))
                parentId = mappedParentId;

            try
            {
                // Check if folder exists
                var existingFolder = await EmailApi.GetChildFolderAsync(userId, parentId, displayName, cancel);
                if (existingFolder != null)
                {
                    _restoredEmailFolderMap[originalPath] = existingFolder.Id;
                }
                else
                {
                    var newFolder = await EmailApi.CreateMailFolderAsync(userId, parentId, displayName, cancel);
                    _restoredEmailFolderMap[originalPath] = newFolder.Id;
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreUserEmailFoldersFailed", ex, $"Failed to restore folder {displayName} at {originalPath}");
            }
        }
    }

    /// <summary>
    /// Restores user emails
    /// </summary>
    /// <param name="cancel">>The cancellation token</param>
    /// <returns>An awaitable task</returns>
    private async Task RestoreUserEmails(CancellationToken cancel)
    {
        if (RestoreTarget == null)
            throw new InvalidOperationException("Restore target entry is not set");

        // Find email entries in _metadata
        var emailSources = GetMetadataByType(SourceItemType.UserMailboxEmail);
        if (emailSources.Count == 0)
            return;

        (var userId, var mailboxId) = await EmailRestore.GetUserIdAndMailboxTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(mailboxId))
            return; // A warning has already been logged

        // Restore emails
        foreach (var emailSource in emailSources)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var contentPath = SystemIO.IO_OS.PathCombine(emailSource.Key, "content.eml");
                var metadataPath = SystemIO.IO_OS.PathCombine(emailSource.Key, "metadata.json");

                var internetMessageId = emailSource.Value.GetValueOrDefault("o365:InternetMessageId");

                // Determine target folder
                var targetFolderId = mailboxId;
                var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(emailSource.Key.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
                if (parentPath != null && _restoredEmailFolderMap.TryGetValue(parentPath, out var mappedFolderId))
                {
                    targetFolderId = mappedFolderId;
                }

                if (!_ignoreExisting && !string.IsNullOrWhiteSpace(internetMessageId) && await EmailApi.EmailExistsInFolderByInternetMessageIdAsync(userId, targetFolderId, internetMessageId, cancel))
                {
                    Log.WriteInformationMessage(LOGTAG, "RestoreUserEmailsSkipExisting", $"Email with InternetMessageId {internetMessageId} already exists in target mailbox, skipping restore for {emailSource.Key}.");
                }
                else
                {
                    var contentEntry = _temporaryFiles.GetValueOrDefault(contentPath);
                    var metadataEntry = _temporaryFiles.GetValueOrDefault(metadataPath);

                    if (contentEntry == null)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreUserEmailsMissingContent", null, $"Missing email content for {emailSource.Key}, skipping.");
                        continue;
                    }

                    if (metadataEntry == null)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreUserEmailsMissingMetadata", null, $"Missing email metadata for {emailSource.Key}, skipping.");
                        continue;
                    }


                    using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                    using (var metadataStream = SystemIO.IO_OS.FileOpenRead(metadataEntry))
                        await EmailApi.RestoreEmailToFolderAsync(userId, targetFolderId, contentStream, metadataStream, cancel);
                }

                // Clean up when done
                _metadata.TryRemove(emailSource.Key, out _);
                _metadata.TryRemove(contentPath, out _);
                _metadata.TryRemove(metadataPath, out _);
                _temporaryFiles.TryRemove(contentPath, out var contentFile);
                _temporaryFiles.TryRemove(metadataPath, out var metadataFile);
                contentFile?.Dispose();
                metadataFile?.Dispose();
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreUserEmailsFailed", ex, $"Failed to restore email at {emailSource.Key}");
            }
        }
    }

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

        var driveSources = GetMetadataByType(SourceItemType.Drive);
        if (driveSources.Count == 0)
            return;

        if (RestoreTarget.Type == SourceItemType.Drive)
        {
            var targetDriveId = RestoreTarget.Metadata.GetValueOrDefault("o365:Id");
            if (!string.IsNullOrWhiteSpace(targetDriveId))
            {
                foreach (var driveSource in driveSources)
                {
                    _restoredDriveMap[driveSource.Key] = targetDriveId;
                }
            }
            return;
        }

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

        if (!string.IsNullOrWhiteSpace(targetUserId))
        {
            try
            {
                var primaryDrive = await SourceProvider.OneDriveApi.GetUserPrimaryDriveAsync(targetUserId, cancel);
                foreach (var driveSource in driveSources)
                {
                    _restoredDriveMap[driveSource.Key] = primaryDrive.Id;
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreDrivesFailed", ex, $"Failed to restore drives");
            }
        }
        else if (!string.IsNullOrWhiteSpace(targetSiteId))
        {
            try
            {
                var primaryDrive = await SourceProvider.SiteApi.GetSitePrimaryDriveAsync(targetSiteId, cancel);
                foreach (var driveSource in driveSources)
                {
                    _restoredDriveMap[driveSource.Key] = primaryDrive.Id;
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreDrivesFailed", ex, $"Failed to restore drives");
            }
        }
    }

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

        var folders = GetMetadataByType(SourceItemType.DriveFolder);
        if (folders.Count == 0)
            return;

        // Sort folders by path length to ensure parents are created before children
        var sortedFolders = folders.OrderBy(k => k.Key.Split(Path.DirectorySeparatorChar).Length).ToList();

        var restoreTarget = await DriveRestore.GetDefaultDriveAndFolder(cancel);
        if (!string.IsNullOrWhiteSpace(restoreTarget.FolderId))
        {
            var rootFolderPath = Util.AppendDirSeparator(Path.GetDirectoryName(sortedFolders.First().Key.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
            _restoredDriveFolderMap.AddOrUpdate(rootFolderPath, restoreTarget.FolderId, (k, v) => v);
        }

        foreach (var folder in sortedFolders)
        {
            if (cancel.IsCancellationRequested)
                break;

            var originalPath = folder.Key;
            var metadata = folder.Value;
            var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? metadata.GetValueOrDefault("o365:Id");

            _metadata.TryRemove(originalPath, out _);

            if (string.IsNullOrWhiteSpace(displayName))
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreDriveFoldersMissingName", null, $"Missing display name for folder {originalPath}, skipping.");
                continue;
            }

            // Find the drive ID
            var drivePath = _restoredDriveMap.Keys
                .Where(k => originalPath.StartsWith(Util.AppendDirSeparator(k)))
                .OrderByDescending(k => k.Length)
                .FirstOrDefault();

            string? driveId = null;
            string defaultRootId = "root";

            if (drivePath != null)
            {
                driveId = _restoredDriveMap[drivePath];
            }
            else
            {
                driveId = restoreTarget.DriveId;
                if (restoreTarget.FolderId != null) defaultRootId = restoreTarget.FolderId;
            }

            if (driveId == null)
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreDriveFoldersMissingDrive", null, $"Could not find restored drive for folder {originalPath}, skipping.");
                continue;
            }

            // Determine parent folder ID
            var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
            var parentId = defaultRootId;

            if (parentPath != null && (drivePath == null || parentPath.TrimEnd(Path.DirectorySeparatorChar) != drivePath.TrimEnd(Path.DirectorySeparatorChar)))
            {
                if (_restoredDriveFolderMap.TryGetValue(parentPath, out var mappedParentId))
                {
                    parentId = mappedParentId;
                }
                else
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreDriveFoldersMissingParent", null, $"Could not find parent folder for {originalPath}, skipping.");
                    continue;
                }
            }

            try
            {
                // Skip if folder already exists
                var restoredFolder = await DriveApi.GetDriveItemAsync(driveId, parentId, displayName, cancel);
                if (restoredFolder != null)
                {
                    _restoredDriveFolderMap[originalPath] = restoredFolder.Id;
                    continue;
                }

                var newFolder = await DriveApi.CreateDriveFolderAsync(driveId, parentId, displayName, cancel);
                _restoredDriveFolderMap[originalPath] = newFolder.Id;

                // Restore permissions
                var permissionsJson = metadata.GetValueOrDefault("o365:Permissions");
                if (!string.IsNullOrWhiteSpace(permissionsJson))
                {
                    try
                    {
                        await DriveApi.RestoreDriveItemPermissionsAsync(driveId, newFolder.Id, permissionsJson, cancel);
                    }
                    catch (Exception ex)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreDriveFolderPermissionsFailed", ex, $"Failed to restore permissions for folder {originalPath}");
                    }
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreDriveFoldersFailed", ex, $"Failed to restore folder {displayName} at {originalPath}");
            }
        }
    }

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

        var files = GetMetadataByType(SourceItemType.DriveFile);
        if (files.Count == 0)
            return;

        foreach (var file in files)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = file.Key;
                var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);

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

                // Find drive ID
                var drivePath = _restoredDriveMap.Keys
                    .Where(k => originalPath.StartsWith(Util.AppendDirSeparator(k)))
                    .OrderByDescending(k => k.Length)
                    .FirstOrDefault();

                string? driveId = null;
                string defaultRootId = "root";

                if (drivePath != null)
                {
                    driveId = _restoredDriveMap[drivePath];
                }
                else
                {
                    var defaults = await DriveRestore.GetDefaultDriveAndFolder(cancel);
                    driveId = defaults.DriveId;
                    if (defaults.FolderId != null) defaultRootId = defaults.FolderId;
                }

                if (driveId == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreDriveFilesMissingDrive", null, $"Could not find restored drive for file {originalPath}, skipping.");
                    continue;
                }

                // Determine parent folder ID
                var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
                var parentId = defaultRootId;

                if (parentPath != null && (drivePath == null || parentPath.TrimEnd(Path.DirectorySeparatorChar) != drivePath.TrimEnd(Path.DirectorySeparatorChar)))
                {
                    if (_restoredDriveFolderMap.TryGetValue(parentPath, out var mappedParentId))
                    {
                        parentId = mappedParentId;
                    }
                    else
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreDriveFilesMissingParent", null, $"Could not find parent folder for {originalPath}, skipping.");
                        continue;
                    }
                }

                var displayName = file.Value.GetValueOrDefault("o365:Name") ?? Path.GetFileName(originalPath);

                if (!_ignoreExisting)
                {
                    var existingItem = await DriveApi.GetDriveItemAsync(driveId, parentId, displayName, cancel);
                    if (existingItem != null)
                    {
                        // Check if it's the same file
                        // We can check size
                        var sizeStr = file.Value.GetValueOrDefault("o365:Size");
                        long? sourceSize = null;
                        if (long.TryParse(sizeStr, out var s)) sourceSize = s;
                        else
                        {
                            sourceSize = SystemIO.IO_OS.FileLength(contentEntry);
                        }

                        if (existingItem.Size == sourceSize)
                        {
                            // Check hash if available
                            string? sourceHash = null;
                            var sourceHashStr = file.Value.GetValueOrDefault("o365:Hashes");
                            if (sourceHashStr != null)
                            {
                                var sourceHashes = JsonSerializer.Deserialize<GraphDriveHashes>(sourceHashStr);
                                sourceHash = sourceHashes?.QuickXorHash ?? sourceHashes?.Sha1Hash;
                            }
                            var existingHash = existingItem.File?.Hashes?.QuickXorHash ?? existingItem.File?.Hashes?.Sha1Hash;

                            if (!string.IsNullOrWhiteSpace(sourceHash) && !string.IsNullOrWhiteSpace(existingHash))
                            {
                                if (string.Equals(sourceHash, existingHash, StringComparison.OrdinalIgnoreCase))
                                {
                                    Log.WriteVerboseMessage(LOGTAG, "RestoreDriveFilesSkipDuplicate", $"Skipping existing file {originalPath} (Name: {displayName}, Hash match)");
                                    _metadata.TryRemove(originalPath, out _);
                                    _temporaryFiles.TryRemove(originalPath, out var cFile);
                                    cFile?.Dispose();
                                    continue;
                                }
                            }
                            else
                            {
                                // Fallback to size only if hash is missing
                                Log.WriteVerboseMessage(LOGTAG, "RestoreDriveFilesSkipDuplicate", $"Skipping existing file {originalPath} (Name: {displayName}, Size match)");
                                _metadata.TryRemove(originalPath, out _);
                                _temporaryFiles.TryRemove(originalPath, out var cFile);
                                cFile?.Dispose();
                                continue;
                            }
                        }
                    }
                }

                GraphDriveItem restoredItem;
                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    restoredItem = await DriveApi.RestoreDriveItemToFolderAsync(driveId, parentId, displayName, contentStream, null, cancel);
                }

                // Restore metadata (timestamps)
                var createdStr = file.Value.GetValueOrDefault("o365:CreatedDateTime");
                var modifiedStr = file.Value.GetValueOrDefault("o365:LastModifiedDateTime");

                DateTimeOffset? created = null;
                DateTimeOffset? modified = null;

                if (DateTimeOffset.TryParse(createdStr, CultureInfo.InvariantCulture, out var c)) created = c;
                if (DateTimeOffset.TryParse(modifiedStr, CultureInfo.InvariantCulture, out var m)) modified = m;

                if (created != null || modified != null)
                {
                    try
                    {
                        await DriveApi.UpdateDriveItemFileSystemInfoAsync(driveId, restoredItem.Id, created, modified, cancel);
                    }
                    catch (Exception ex)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreDriveFileMetadataFailed", ex, $"Failed to restore metadata for file {file.Key}");
                    }
                }

                // Restore permissions
                var permissionsJson = file.Value.GetValueOrDefault("o365:Permissions");
                if (!string.IsNullOrWhiteSpace(permissionsJson))
                {
                    try
                    {
                        await DriveApi.RestoreDriveItemPermissionsAsync(driveId, restoredItem.Id, permissionsJson, cancel);
                    }
                    catch (Exception ex)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreDriveFilePermissionsFailed", ex, $"Failed to restore permissions for file {file.Key}");
                    }
                }

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

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

        var events = GetMetadataByType(SourceItemType.CalendarEvent);
        if (events.Count == 0)
            return;

        (var userId, var calendarId) = await CalendarRestore.GetUserIdAndCalendarTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(calendarId))
            return;

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

        var singles = new List<(string Path, GraphEvent Event)>();
        var masters = new List<(string Path, GraphEvent Event)>();
        var exceptions = new List<(string Path, GraphEvent Event)>();

        foreach (var eventItem in events)
        {
            if (cancel.IsCancellationRequested) break;

            var originalPath = eventItem.Key;
            GraphEvent? graphEvent = null;

            try
            {
                var contentPath = SystemIO.IO_OS.PathCombine(originalPath, "content.json");
                if (_temporaryFiles.TryRemove(contentPath, out var contentFile))
                {
                    using var tf = contentFile;
                    using var stream = SystemIO.IO_OS.FileOpenRead(tf);
                    graphEvent = await JsonSerializer.DeserializeAsync<GraphEvent>(stream, cancellationToken: cancel);
                }

                // Clean up content file metadata
                _metadata.TryRemove(contentPath, out var _);

                // Temporary patch for missing ICalUId
                _metadata.TryGetValue(originalPath, out var eventMeta);
                if (string.IsNullOrWhiteSpace(graphEvent?.ICalUId) && eventMeta != null && eventMeta.TryGetValue("o365:CalUId", out var iCalUId))
                    graphEvent!.ICalUId = iCalUId;

                if (graphEvent == null) continue;

                if (graphEvent.Type == "seriesMaster")
                    masters.Add((originalPath, graphEvent));
                else if (graphEvent.Type == "exception")
                    exceptions.Add((originalPath, graphEvent));
                else if (graphEvent.Type == "occurrence")
                    continue; // Skip occurrences
                else
                    singles.Add((originalPath, graphEvent));
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreCalendarEventAnalysisFailed", ex, $"Failed to analyze event {originalPath}");
            }
        }

        // Restore singles
        foreach (var item in singles)
            await RestoreSingleEvent(userId, calendarId, item.Path, item.Event, attachments, cancel);

        // Restore masters
        var masterMap = new Dictionary<string, string>(); // OldId -> NewId
        foreach (var item in masters)
        {
            var newId = await RestoreMasterEvent(userId, calendarId, item.Path, item.Event, attachments, cancel);
            if (newId != null && !string.IsNullOrWhiteSpace(item.Event.Id))
                masterMap[item.Event.Id] = newId;
        }

        // Restore exceptions
        foreach (var item in exceptions)
            await RestoreExceptionEvent(userId, calendarId, item.Path, item.Event, masterMap, attachments, cancel);
    }

    private async Task RestoreCalendarEventAttachments(string userId, string calendarId, string eventId, string eventPath, Dictionary<string, List<KeyValuePair<string, Dictionary<string, string?>>>> attachments, CancellationToken cancel)
    {
        var eventPathWithSep = Util.AppendDirSeparator(eventPath);
        if (!attachments.TryGetValue(eventPathWithSep, out var eventAttachments))
            return;

        foreach (var att in eventAttachments)
        {
            try
            {
                var attPath = att.Key;
                var metadata = att.Value;
                var name = metadata.GetValueOrDefault("o365:Name") ?? "attachment";
                var contentType = metadata.GetValueOrDefault("o365:ContentType") ?? "application/octet-stream";

                if (!_temporaryFiles.TryRemove(attPath, out var contentFile))
                    continue;

                using var tf = contentFile;
                using var stream = SystemIO.IO_OS.FileOpenRead(tf);
                await CalendarApi.AddAttachmentAsync(userId, calendarId, eventId, name, contentType, stream, cancel);

                _metadata.TryRemove(attPath, out _);
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreCalendarAttachmentFailed", ex, $"Failed to restore attachment for event {eventPath}");
            }
        }
    }

    private async Task RestoreSingleEvent(string userId, string calendarId, string path, GraphEvent eventItem, Dictionary<string, List<KeyValuePair<string, Dictionary<string, string?>>>> attachments, CancellationToken cancel)
    {
        try
        {
            if (!_ignoreExisting && !string.IsNullOrWhiteSpace(eventItem.ICalUId))
            {
                var existing = await CalendarApi.FindEventsAsync(userId, calendarId, eventItem, cancel);
                if (existing.Count > 0)
                {
                    Log.WriteInformationMessage(LOGTAG, "RestoreSingleEventSkipDuplicate", $"Skipping duplicate event {path} (Subject: {eventItem.Subject})");
                    _metadata.TryRemove(path, out _);
                    return;
                }
            }

            // Clean up properties that shouldn't be sent on creation
            eventItem.Id = null!;
            eventItem.CreatedDateTime = null;
            eventItem.LastModifiedDateTime = null;

            var created = await CalendarApi.CreateCalendarEventAsync(userId, calendarId, eventItem, cancel);
            await RestoreCalendarEventAttachments(userId, calendarId, created.Id, path, attachments, cancel);
            _metadata.TryRemove(path, out _);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "RestoreSingleEventFailed", ex, $"Failed to restore event {path}");
        }
    }

    private async Task<string?> RestoreMasterEvent(string userId, string calendarId, string path, GraphEvent eventItem, Dictionary<string, List<KeyValuePair<string, Dictionary<string, string?>>>> attachments, CancellationToken cancel)
    {
        try
        {
            if (!_ignoreExisting && !string.IsNullOrWhiteSpace(eventItem.ICalUId))
            {
                var existing = await CalendarApi.FindEventsAsync(userId, calendarId, eventItem, cancel);
                if (existing.Count > 0)
                {
                    Log.WriteInformationMessage(LOGTAG, "RestoreMasterEventSkipDuplicate", $"Skipping duplicate master event {path} (Subject: {eventItem.Subject})");
                    _metadata.TryRemove(path, out _);
                    // Return the ID of the existing event so exceptions can be attached to it?
                    // If we skip it, we should probably return the existing ID.
                    return existing[0].Id;
                }
            }

            eventItem.Id = null!;
            eventItem.CreatedDateTime = null;
            eventItem.LastModifiedDateTime = null;

            var created = await CalendarApi.CreateCalendarEventAsync(userId, calendarId, eventItem, cancel);
            await RestoreCalendarEventAttachments(userId, calendarId, created.Id, path, attachments, cancel);
            _metadata.TryRemove(path, out _);
            return created.Id;
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "RestoreMasterEventFailed", ex, $"Failed to restore master event {path}");
            return null;
        }
    }

    private async Task RestoreExceptionEvent(string userId, string calendarId, string path, GraphEvent eventItem, Dictionary<string, string> masterMap, Dictionary<string, List<KeyValuePair<string, Dictionary<string, string?>>>> attachments, CancellationToken cancel)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(eventItem.SeriesMasterId) || !masterMap.TryGetValue(eventItem.SeriesMasterId, out var newMasterId))
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreExceptionEventMissingMaster", null, $"Could not find restored master for exception event {path}, skipping.");
                return;
            }

            if (eventItem.OriginalStart == null)
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreExceptionEventMissingOriginalStart", null, $"Missing original start time for exception event {path}, skipping.");
                return;
            }

            // Find the occurrence
            // We search for instances around the original start time
            var searchStart = eventItem.OriginalStart.Value.AddMinutes(-1);
            var searchEnd = eventItem.OriginalStart.Value.AddMinutes(1);

            var instances = await CalendarApi.GetCalendarEventInstancesAsync(userId, newMasterId, searchStart, searchEnd, cancel);

            var instance = instances.FirstOrDefault(i =>
            {
                // Parse start time from instance
                if (i.Start is JsonElement startElem && startElem.ValueKind == JsonValueKind.Object)
                {
                    if (startElem.TryGetProperty("dateTime", out var dtProp) && dtProp.GetString() is string dtStr)
                    {
                        if (DateTimeOffset.TryParse(dtStr, out var dt))
                        {
                            return Math.Abs((dt - eventItem.OriginalStart.Value).TotalSeconds) < 5;
                        }
                    }
                }
                return false;
            });

            if (instance == null)
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreExceptionEventInstanceNotFound", null, $"Could not find occurrence instance for exception event {path}, skipping.");
                return;
            }

            // Update the instance
            eventItem.Id = null!;
            eventItem.SeriesMasterId = null;
            eventItem.Type = null;
            eventItem.CreatedDateTime = null;
            eventItem.LastModifiedDateTime = null;
            eventItem.OriginalStart = null;

            var updated = await CalendarApi.UpdateCalendarEventAsync(userId, instance.Id, eventItem, cancel);
            await RestoreCalendarEventAttachments(userId, calendarId, updated.Id, path, attachments, cancel);
            _metadata.TryRemove(path, out _);
        }
        catch (Exception ex)
        {
            Log.WriteErrorMessage(LOGTAG, "RestoreExceptionEventFailed", ex, $"Failed to restore exception event {path}");
        }
    }

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

        var folders = GetMetadataByType(SourceItemType.UserContactFolder);
        if (folders.Count == 0)
            return;

        (var userId, var defaultFolderId) = await ContactRestore.GetUserIdAndContactFolderTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(defaultFolderId))
            return;

        // Sort folders by path length to ensure parents are created before children
        var sortedFolders = folders.OrderBy(k => k.Key.Split(Path.DirectorySeparatorChar).Length).ToList();

        foreach (var folder in sortedFolders)
        {
            if (cancel.IsCancellationRequested)
                break;

            var originalPath = folder.Key;
            var metadata = folder.Value;
            var displayName = metadata.GetValueOrDefault("o365:Name") ?? metadata.GetValueOrDefault("o365:DisplayName") ?? metadata.GetValueOrDefault("o365:Id");

            _metadata.TryRemove(originalPath, out _);

            if (string.IsNullOrWhiteSpace(displayName))
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreContactFoldersMissingName", null, $"Missing display name for folder {originalPath}, skipping.");
                continue;
            }

            // Determine parent folder ID
            var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
            var parentId = defaultFolderId;

            if (parentPath != null && _restoredContactFolderMap.TryGetValue(parentPath, out var mappedParentId))
            {
                parentId = mappedParentId;
            }

            try
            {
                // Check if folder exists
                var existingFolder = await ContactApi.GetContactFolderByNameAsync(userId, displayName, parentId, cancel);
                if (existingFolder != null)
                {
                    _restoredContactFolderMap[originalPath] = existingFolder.Id;
                }
                else
                {
                    var newFolder = await ContactApi.CreateContactFolderAsync(userId, displayName, parentId, cancel);
                    _restoredContactFolderMap[originalPath] = newFolder.Id;
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreContactFoldersFailed", ex, $"Failed to restore folder {displayName} at {originalPath}");
            }
        }
    }

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

        var contacts = GetMetadataByType(SourceItemType.UserContact);
        if (contacts.Count == 0)
            return;

        (var userId, var defaultFolderId) = await ContactRestore.GetUserIdAndContactFolderTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(defaultFolderId))
            return; // A warning has already been logged

        foreach (var contact in contacts)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = contact.Key;
                var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);

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

                // Determine target folder
                var targetFolderId = defaultFolderId;
                var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
                if (parentPath != null && _restoredContactFolderMap.TryGetValue(parentPath, out var mappedFolderId))
                {
                    targetFolderId = mappedFolderId;
                }

                if (!_ignoreExisting)
                {
                    using (var checkStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                    {
                        var contactData = await JsonSerializer.DeserializeAsync<GraphContact>(checkStream, cancellationToken: cancel);
                        if (contactData != null)
                        {
                            string? email = null;
                            if (contactData.EmailAddresses != null && contactData.EmailAddresses.Count > 0)
                            {
                                email = contactData.EmailAddresses[0].Address;
                            }

                            var displayName = contactData.DisplayName ?? contact.Value.GetValueOrDefault("o365:DisplayName");

                            if (!string.IsNullOrWhiteSpace(email) || !string.IsNullOrWhiteSpace(displayName))
                            {
                                var existing = await ContactApi.FindContactsAsync(userId, targetFolderId, email, displayName, cancel);
                                if (existing.Count > 0)
                                {
                                    Log.WriteInformationMessage(LOGTAG, "RestoreContactsSkipDuplicate", $"Skipping duplicate contact {originalPath} (Name: {displayName}, Email: {email})");
                                    _metadata.TryRemove(originalPath, out _);
                                    _temporaryFiles.TryRemove(originalPath, out var cFile);
                                    cFile?.Dispose();
                                    continue;
                                }
                            }
                        }
                    }
                }

                GraphContact? restoredContact = null;
                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    restoredContact = await ContactApi.RestoreContactToFolderAsync(userId, targetFolderId, contentStream, cancel);
                }

                _metadata.TryRemove(originalPath, out _);
                _temporaryFiles.TryRemove(originalPath, out var contentFile);
                contentFile?.Dispose();

                // Restore photo if exists
                var photoPath = ReplaceExtension(originalPath, ".json", ".photo");
                if (originalPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase) && _temporaryFiles.TryGetValue(photoPath, out var photoEntry))
                {
                    try
                    {
                        using (var photoStream = SystemIO.IO_OS.FileOpenRead(photoEntry))
                        {
                            // May have stored empty photo
                            if (photoStream.Length > 0)
                                await ContactApi.RestoreContactPhotoAsync(userId, restoredContact.Id, targetFolderId, photoStream, cancel);
                        }
                        _temporaryFiles.TryRemove(photoPath, out var photoFile);
                        photoFile?.Dispose();
                    }
                    catch (Exception ex)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreContactPhotoFailed", ex, $"Failed to restore photo for contact {originalPath}");
                    }
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreContactsFailed", ex, $"Failed to restore contact {contact.Key}");
            }
        }
    }


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

        var groups = GetMetadataByType(SourceItemType.UserContactGroup);
        if (groups.Count == 0)
            return;

        (var userId, var defaultFolderId) = await ContactRestore.GetUserIdAndContactFolderTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(defaultFolderId))
            return;

        foreach (var group in groups)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = group.Key;
                var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);

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

                // Determine target folder
                var targetFolderId = defaultFolderId;
                var parentPath = Util.AppendDirSeparator(Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)) ?? "");
                if (parentPath != null && _restoredContactFolderMap.TryGetValue(parentPath, out var mappedFolderId))
                    targetFolderId = mappedFolderId;

                GraphContactGroup? restoredGroup = null;
                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    var groupData = await JsonSerializer.DeserializeAsync<GraphContactGroup>(contentStream, cancellationToken: cancel);
                    if (groupData == null)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupsInvalidContent", null, $"Invalid content for {originalPath}, skipping.");
                        continue;
                    }

                    var displayName = groupData.DisplayName ?? group.Value.GetValueOrDefault("o365:DisplayName") ?? "Restored Group";

                    if (!_ignoreExisting)
                    {
                        var existing = await ContactApi.FindContactGroupsAsync(userId, targetFolderId, displayName, cancel);
                        if (existing.Count > 0)
                        {
                            Log.WriteInformationMessage(LOGTAG, "RestoreContactGroupsSkipDuplicate", $"Skipping duplicate contact group {originalPath} (Name: {displayName})");
                            restoredGroup = existing[0];
                        }
                    }

                    if (restoredGroup == null)
                        restoredGroup = await ContactApi.CreateContactGroupAsync(userId, displayName, targetFolderId, cancel);
                }

                _metadata.TryRemove(originalPath, out _);
                _temporaryFiles.TryRemove(originalPath, out var contentFile);
                contentFile?.Dispose();

                // Restore members
                var membersPath = ReplaceExtension(originalPath, ".json", ".members.json");
                if (_temporaryFiles.TryGetValue(membersPath, out var membersEntry))
                {
                    try
                    {
                        using (var membersStream = SystemIO.IO_OS.FileOpenRead(membersEntry))
                        {
                            var members = await JsonSerializer.DeserializeAsync<List<GraphContact>>(membersStream, cancellationToken: cancel);
                            if (members != null)
                            {
                                foreach (var member in members)
                                {
                                    var contactEmail = member.EmailAddresses?.FirstOrDefault();
                                    if (contactEmail == null)
                                    {
                                        Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupMemberMissingEmail", null, $"Member {member.DisplayName} has no email address, skipping.");
                                        continue;
                                    }

                                    var contacts = await ContactApi.FindContactsAsync(userId, targetFolderId, contactEmail.Address, member.DisplayName, cancel);
                                    if (contacts.Count > 0)
                                    {
                                        try
                                        {
                                            var address = new GraphEmailAddress
                                            {
                                                Address = contactEmail.Address,
                                                Name = member.DisplayName
                                            };

                                            await ContactApi.AddContactGroupMemberAsync(userId, targetFolderId, restoredGroup.Id, address, cancel);
                                        }
                                        catch (Exception ex)
                                        {
                                            // Member might already exist
                                            Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupMemberFailed", ex, $"Failed to add member {member.DisplayName} to group {restoredGroup.DisplayName}");
                                        }
                                    }
                                    else
                                    {
                                        Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupMemberNotFound", null, $"Could not find contact for member {member.DisplayName} ({contactEmail}) in target folder.");
                                    }
                                }
                            }
                        }
                        _temporaryFiles.TryRemove(membersPath, out var membersFile);
                        membersFile?.Dispose();
                    }
                    catch (Exception ex)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupMembersFailed", ex, $"Failed to restore members for group {originalPath}");
                    }
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreContactGroupsFailed", ex, $"Failed to restore contact group {group.Key}");
            }
        }
    }

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

        var buckets = GetMetadataByType(SourceItemType.PlannerBucket);
        if (buckets.Count == 0)
            return;

        string? targetPlanId = await PlannerRestore.GetTargetPlanId(cancel);
        var existingBucketsByPlan = new Dictionary<string, List<GraphPlannerBucket>>();

        foreach (var bucket in buckets)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = bucket.Key;
                var metadata = bucket.Value;
                var name = metadata.GetValueOrDefault("o365:Name") ?? "Restored Bucket";
                var originalBucketId = metadata.GetValueOrDefault("o365:Id");

                // Determine Plan ID
                var planId = targetPlanId;
                if (string.IsNullOrWhiteSpace(planId))
                {
                    // Try to extract from path: .../PlanId/buckets/BucketId
                    var parent = Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)); // .../PlanId/buckets
                    if (parent != null)
                    {
                        var grandParent = Path.GetDirectoryName(parent); // .../PlanId
                        if (grandParent != null)
                        {
                            planId = Path.GetFileName(grandParent);
                        }
                    }
                }

                if (string.IsNullOrWhiteSpace(planId))
                {
                    Log.WriteWarningMessage(LOGTAG, "RestorePlannerBucketsMissingPlanId", null, $"Missing target plan ID for bucket {originalPath}, skipping.");
                    continue;
                }

                if (!_ignoreExisting)
                {
                    if (!existingBucketsByPlan.TryGetValue(planId, out var existingBuckets))
                    {
                        existingBuckets = await PlannerApi.GetPlannerBucketsAsync(planId, cancel);
                        existingBucketsByPlan[planId] = existingBuckets;
                    }

                    if (existingBuckets.Any(b => b.Name == name))
                    {
                        Log.WriteInformationMessage(LOGTAG, "RestorePlannerBucketsSkipDuplicate", $"Skipping duplicate bucket {originalPath} (Name: {name})");

                        // Map existing bucket ID so tasks can find it
                        var existingBucket = existingBuckets.First(b => b.Name == name);
                        _restoredPlannerBucketMap[originalPath] = existingBucket.Id;
                        if (!string.IsNullOrWhiteSpace(originalBucketId))
                            _restoredPlannerBucketMap[originalBucketId] = existingBucket.Id;
                        _metadata.TryRemove(originalPath, out _);
                        continue;
                    }
                }

                var newBucket = await PlannerApi.CreatePlannerBucketAsync(planId, name, null, cancel);
                _restoredPlannerBucketMap[originalPath] = newBucket.Id;
                if (!string.IsNullOrWhiteSpace(originalBucketId))
                    _restoredPlannerBucketMap[originalBucketId] = newBucket.Id;
                _metadata.TryRemove(originalPath, out _);

                if (!_ignoreExisting && existingBucketsByPlan.TryGetValue(planId, out var currentBuckets))
                {
                    currentBuckets.Add(newBucket);
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestorePlannerBucketsFailed", ex, $"Failed to restore bucket {bucket.Key}");
            }
        }
    }

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

        var tasks = GetMetadataByType(SourceItemType.UserPlannerTasks);
        if (tasks.Count == 0)
            return;

        string? targetPlanId = await PlannerRestore.GetTargetPlanId(cancel);
        var existingTasksByPlan = new Dictionary<string, List<GraphPlannerTask>>();

        foreach (var task in tasks)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = task.Key;
                var metadata = task.Value;
                var title = metadata.GetValueOrDefault("o365:Name") ?? "Restored Task";
                var originalBucketId = metadata.GetValueOrDefault("o365:BucketId");

                // Determine Plan ID
                var planId = targetPlanId;
                if (string.IsNullOrWhiteSpace(planId))
                {
                    // Try to extract from path: .../PlanId/tasks/TaskId
                    var parent = Path.GetDirectoryName(originalPath.TrimEnd(Path.DirectorySeparatorChar)); // .../PlanId/tasks
                    if (parent != null)
                    {
                        var grandParent = Path.GetDirectoryName(parent); // .../PlanId
                        if (grandParent != null)
                        {
                            planId = Path.GetFileName(grandParent);
                        }
                    }
                }

                if (string.IsNullOrWhiteSpace(planId))
                {
                    Log.WriteWarningMessage(LOGTAG, "RestorePlannerTasksMissingPlanId", null, $"Missing target plan ID for task {originalPath}, skipping.");
                    continue;
                }

                if (!_ignoreExisting)
                {
                    if (!existingTasksByPlan.TryGetValue(planId, out var existingTasks))
                    {
                        existingTasks = await PlannerApi.GetPlannerTasksAsync(planId, cancel);
                        existingTasksByPlan[planId] = existingTasks;
                    }

                    if (existingTasks.Any(t => t.Title == title))
                    {
                        Log.WriteInformationMessage(LOGTAG, "RestorePlannerTasksSkipDuplicate", $"Skipping duplicate task {originalPath} (Title: {title})");
                        _metadata.TryRemove(originalPath, out _);

                        // Clean up temp files
                        var dupTaskJsonPath = SystemIO.IO_OS.PathCombine(originalPath, "task.json");
                        _temporaryFiles.TryRemove(dupTaskJsonPath, out var tFile);
                        tFile?.Dispose();

                        var dupContentPath = SystemIO.IO_OS.PathCombine(originalPath, "content.json");
                        _temporaryFiles.TryRemove(dupContentPath, out var cFile);
                        cFile?.Dispose();

                        continue;
                    }
                }

                // Determine Bucket ID
                string? bucketId = null;
                if (!string.IsNullOrWhiteSpace(originalBucketId))
                    _restoredPlannerBucketMap.TryGetValue(originalBucketId, out bucketId);

                if (string.IsNullOrWhiteSpace(bucketId))
                    bucketId = await PlannerRestore.GetTargetBucketId(cancel);

                // Read task.json if available
                var taskJsonPath = SystemIO.IO_OS.PathCombine(originalPath, "task.json");
                var taskJsonEntry = _temporaryFiles.GetValueOrDefault(taskJsonPath);
                GraphPlannerTask? taskData = null;

                if (taskJsonEntry != null)
                {
                    using (var stream = SystemIO.IO_OS.FileOpenRead(taskJsonEntry))
                    {
                        taskData = await JsonSerializer.DeserializeAsync<GraphPlannerTask>(stream, cancellationToken: cancel);
                    }
                    _metadata.TryRemove(taskJsonPath, out _);
                    _temporaryFiles.TryRemove(taskJsonPath, out var taskJsonFile);
                    taskJsonFile?.Dispose();
                }

                if (title == "Conduct needs analysis")
                {
                    Console.WriteLine("Debug");
                }

                var newTask = await PlannerApi.CreatePlannerTaskAsync(
                    planId,
                    bucketId,
                    title,
                    taskData?.StartDateTime,
                    taskData?.DueDateTime,
                    taskData?.PercentComplete,
                    taskData?.Priority,
                    taskData?.Assignments,
                    taskData?.AppliedCategories,
                    cancel);

                if (!_ignoreExisting && existingTasksByPlan.TryGetValue(planId, out var currentTasks))
                {
                    currentTasks.Add(newTask);
                }

                // Restore details
                var contentPath = SystemIO.IO_OS.PathCombine(originalPath, "content.json");
                var contentEntry = _temporaryFiles.GetValueOrDefault(contentPath);

                if (contentEntry != null)
                {
                    using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                    {
                        await PlannerApi.UpdatePlannerTaskDetailsAsync(newTask.Id, contentStream, cancel);
                    }

                    _metadata.TryRemove(contentPath, out _);
                    _temporaryFiles.TryRemove(contentPath, out var contentFile);
                    contentFile?.Dispose();
                }

                _metadata.TryRemove(originalPath, out _);
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestorePlannerTasksFailed", ex, $"Failed to restore task {task.Key}");
            }
        }
    }

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

        var settings = GetMetadataByType(SourceItemType.UserMailboxSettings);
        if (settings.Count == 0)
            return;

        (var userId, _) = await EmailRestore.GetUserIdAndMailboxTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId))
            return;

        foreach (var setting in settings)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = setting.Key;
                var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);

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

                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    await EmailRestore.RestoreMailboxSettings(userId, contentStream, cancel);
                }

                _metadata.TryRemove(originalPath, out _);
                _temporaryFiles.TryRemove(originalPath, out var contentFile);
                contentFile?.Dispose();
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreUserMailboxSettingsFailed", ex, $"Failed to restore mailbox settings {setting.Key}");
            }
        }
    }

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

        var rules = GetMetadataByType(SourceItemType.UserMailboxRule);
        if (rules.Count == 0)
            return;

        (var userId, _) = await EmailRestore.GetUserIdAndMailboxTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId))
            return;

        foreach (var rule in rules)
        {
            if (cancel.IsCancellationRequested)
                break;

            try
            {
                var originalPath = rule.Key;
                var contentEntry = _temporaryFiles.GetValueOrDefault(originalPath);

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

                using (var contentStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
                {
                    await EmailRestore.RestoreMessageRule(userId, contentStream, cancel);
                }

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