// 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.PeopleService.v1.Data;

namespace Duplicati.Proprietary.GoogleWorkspace;

partial class RestoreProvider
{
    private ContactRestoreHelper? _contactRestoreHelper = null;
    internal ContactRestoreHelper ContactRestore => _contactRestoreHelper ??= new ContactRestoreHelper(this);

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

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

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

            if (target.Type == SourceItemType.User)
            {
                _targetUserId = target.Metadata.GetValueOrDefault("gsuite:Id");
            }
            else if (target.Type == SourceItemType.UserContacts)
            {
                _targetUserId = target.Path.TrimStart(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Skip(1).FirstOrDefault();
            }
            else
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreContactsInvalidTargetType", null, $"Restore target type {target.Type} is not valid for restoring contacts.");
            }

            _hasLoadedTargetInfo = true;

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

            return _targetUserId;
        }

        /// <summary>
        /// Cleans contact metadata and removes read-only fields that cannot be sent during creation.
        /// </summary>
        private static void CleanContactMetadata(Person contact)
        {
            // Remove top-level read-only properties
            contact.ResourceName = null;
            contact.ETag = null;
            contact.Metadata = null;
            contact.Photos = null; // Photos are read-only and must be uploaded separately

            // Remove source IDs from fields as they are not allowed during creation
            if (contact.Names != null)
            {
                foreach (var name in contact.Names)
                {
                    name.Metadata = null;
                }
            }
            if (contact.EmailAddresses != null)
            {
                foreach (var email in contact.EmailAddresses)
                {
                    email.Metadata = null;
                }
            }
            if (contact.PhoneNumbers != null)
            {
                foreach (var phone in contact.PhoneNumbers)
                {
                    phone.Metadata = null;
                }
            }
            if (contact.Addresses != null)
            {
                foreach (var address in contact.Addresses)
                {
                    address.Metadata = null;
                }
            }
            if (contact.Organizations != null)
            {
                foreach (var org in contact.Organizations)
                {
                    org.Metadata = null;
                }
            }
            if (contact.ImClients != null)
            {
                foreach (var im in contact.ImClients)
                {
                    im.Metadata = null;
                }
            }
            if (contact.Birthdays != null)
            {
                foreach (var birthday in contact.Birthdays)
                {
                    birthday.Metadata = null;
                }
            }
            if (contact.Biographies != null)
            {
                foreach (var bio in contact.Biographies)
                {
                    bio.Metadata = null;
                }
            }
            if (contact.Nicknames != null)
            {
                foreach (var nickname in contact.Nicknames)
                {
                    nickname.Metadata = null;
                }
            }
            if (contact.Occupations != null)
            {
                foreach (var occupation in contact.Occupations)
                {
                    occupation.Metadata = null;
                }
            }
            if (contact.Relations != null)
            {
                foreach (var relation in contact.Relations)
                {
                    relation.Metadata = null;
                }
            }
            if (contact.Locations != null)
            {
                foreach (var location in contact.Locations)
                {
                    location.Metadata = null;
                }
            }
            if (contact.Events != null)
            {
                foreach (var evt in contact.Events)
                {
                    evt.Metadata = null;
                }
            }
        }

        public async Task<string?> CreateContact(string userId, Person contact, CancellationToken cancel)
        {
            var peopleService = Provider._apiHelper.GetPeopleService(userId);

            // Check for duplicates by email if available
            if (!Provider._ignoreExisting && contact.EmailAddresses?.Count > 0)
            {
                var searchEmail = contact.EmailAddresses[0].Value;
                if (!string.IsNullOrWhiteSpace(searchEmail))
                {
                    try
                    {
                        var searchRequest = peopleService.People.SearchContacts();
                        searchRequest.Query = searchEmail;
                        var searchResponse = await searchRequest.ExecuteAsync(cancel);
                        var duplicate = searchResponse.Results?.FirstOrDefault(r =>
                            r.Person?.EmailAddresses?.Any(e => e.Value?.Equals(searchEmail, StringComparison.OrdinalIgnoreCase) == true) == true);

                        if (duplicate != null)
                        {
                            Log.WriteInformationMessage(LOGTAG, "CreateContactSkipDuplicate", $"Contact with email {searchEmail} already exists, skipping.");
                            return duplicate.Person?.ResourceName;
                        }
                    }
                    catch
                    {
                        // Search might fail, continue with creation
                    }
                }
            }

            // Clean up properties that shouldn't be sent on creation
            CleanContactMetadata(contact);

            var createdContact = await peopleService.People.CreateContact(contact).ExecuteAsync(cancel);
            return createdContact.ResourceName;
        }

        public async Task UploadContactPhoto(string userId, string contactResourceName, Stream photoStream, CancellationToken cancel)
        {
            var peopleService = Provider._apiHelper.GetPeopleService(userId);

            // Read the photo data into a byte array
            using var memoryStream = new MemoryStream();
            await photoStream.CopyToAsync(memoryStream, cancel);
            var photoBytes = memoryStream.ToArray();

            // Upload the photo using the updateContactPhoto endpoint
            var updateRequest = new Google.Apis.PeopleService.v1.Data.UpdateContactPhotoRequest
            {
                PhotoBytes = Convert.ToBase64String(photoBytes)
            };

            await peopleService.People.UpdateContactPhoto(updateRequest, contactResourceName).ExecuteAsync(cancel);
        }

        public async Task<string?> CreateContactGroup(string userId, ContactGroup group, CancellationToken cancel)
        {
            var peopleService = Provider._apiHelper.GetPeopleService(userId);

            // Check for duplicates by name
            if (!Provider._ignoreExisting)
            {
                var existingGroups = await peopleService.ContactGroups.List().ExecuteAsync(cancel);
                var duplicate = existingGroups.ContactGroups?.FirstOrDefault(g =>
                    g.Name?.Equals(group.Name, StringComparison.OrdinalIgnoreCase) == true);

                if (duplicate != null)
                {
                    Log.WriteInformationMessage(LOGTAG, "CreateContactGroupSkipDuplicate", $"Contact group {group.Name} already exists, skipping.");
                    return duplicate.ResourceName;
                }
            }

            // Clean up properties
            group.ResourceName = null;
            group.ETag = null;
            group.MemberCount = null;
            group.MemberResourceNames = null;

            var createdGroup = await peopleService.ContactGroups.Create(new CreateContactGroupRequest { ContactGroup = group }).ExecuteAsync(cancel);
            return createdGroup.ResourceName;
        }

        public async Task AddContactToGroup(string userId, string groupResourceName, string contactResourceName, CancellationToken cancel)
        {
            var peopleService = Provider._apiHelper.GetPeopleService(userId);

            var modifyRequest = new ModifyContactGroupMembersRequest
            {
                ResourceNamesToAdd = new List<string> { contactResourceName }
            };

            await peopleService.ContactGroups.Members.Modify(modifyRequest, groupResourceName).ExecuteAsync(cancel);
        }
    }

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

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

        var userId = await ContactRestore.GetUserIdAndContactsTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId))
            return;

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

            try
            {
                var originalPath = contact.Key;
                // Look for content.json in the contact folder
                var contentJsonPath = SystemIO.IO_OS.PathCombine(originalPath, "content.json");
                var contentEntry = _temporaryFiles.GetValueOrDefault(contentJsonPath);

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

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

                if (contactData == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreContactsInvalidContent", null, $"Invalid content for contact {originalPath}, skipping.");
                    continue;
                }

                var createdResourceName = await ContactRestore.CreateContact(userId, contactData, cancel);

                // Restore contact photos if any exist
                if (!string.IsNullOrEmpty(createdResourceName))
                {
                    await RestoreContactPhotos(userId, originalPath, createdResourceName, cancel);
                }

                // Clean up metadata and temporary files
                _metadata.TryRemove(originalPath, out _);
                _metadata.TryRemove(contentJsonPath, out _);
                _temporaryFiles.TryRemove(contentJsonPath, out var contentFile);
                contentFile?.Dispose();

                // Discard the contact.vcf file if it exists
                var vcfPath = SystemIO.IO_OS.PathCombine(originalPath, "contact.vcf");
                if (_temporaryFiles.TryRemove(vcfPath, out var vcfFile))
                {
                    vcfFile?.Dispose();
                    _metadata.TryRemove(vcfPath, out _);
                }
            }
            catch (Exception ex)
            {
                Log.WriteErrorMessage(LOGTAG, "RestoreContactsFailed", ex, $"Failed to restore contact {contact.Key}");
            }
        }
    }

    private async Task RestoreContactPhotos(string userId, string contactPath, string contactResourceName, CancellationToken cancel)
    {
        // Look for photo files in the contact folder (photo-0.jpg, photo-1.jpg, etc.)
        var photoIndex = 0;
        while (true)
        {
            var photoPath = SystemIO.IO_OS.PathCombine(contactPath, $"photo-{photoIndex}.jpg");
            if (!_temporaryFiles.TryGetValue(photoPath, out var photoEntry))
            {
                // No more photos found
                break;
            }

            try
            {
                using (var photoStream = SystemIO.IO_OS.FileOpenRead(photoEntry))
                {
                    await ContactRestore.UploadContactPhoto(userId, contactResourceName, photoStream, cancel);
                }

                Log.WriteInformationMessage(LOGTAG, "RestoreContactPhotoSuccess", $"Successfully restored photo {photoIndex} for contact {contactResourceName}");
            }
            catch (Exception ex)
            {
                Log.WriteWarningMessage(LOGTAG, "RestoreContactPhotoFailed", ex, $"Failed to restore photo {photoIndex} for contact {contactResourceName}");
            }
            finally
            {
                // Clean up the photo file
                if (_temporaryFiles.TryRemove(photoPath, out var photoFile))
                {
                    photoFile?.Dispose();
                }
                _metadata.TryRemove(photoPath, out _);
            }

            photoIndex++;
        }
    }

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

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

        var userId = await ContactRestore.GetUserIdAndContactsTarget(cancel);
        if (string.IsNullOrWhiteSpace(userId))
            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 group {originalPath}, skipping.");
                    continue;
                }

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

                if (groupData == null)
                {
                    Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupsInvalidContent", null, $"Invalid content for group {originalPath}, skipping.");
                    continue;
                }

                var newGroupResourceName = await ContactRestore.CreateContactGroup(userId, groupData, cancel);

                // Restore group members if available
                var membersPath = SystemIO.IO_OS.PathCombine(originalPath, "members.json");
                if (_temporaryFiles.TryRemove(membersPath, out var membersEntry))
                {
                    try
                    {
                        using var membersStream = SystemIO.IO_OS.FileOpenRead(membersEntry);
                        var members = await JsonSerializer.DeserializeAsync<List<string>>(membersStream, GoogleApiJsonDeserializer.Options, cancellationToken: cancel);

                        if (members != null && newGroupResourceName != null)
                        {
                            foreach (var memberResourceName in members)
                            {
                                try
                                {
                                    await ContactRestore.AddContactToGroup(userId, newGroupResourceName, memberResourceName, cancel);
                                }
                                catch (Exception ex)
                                {
                                    Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupMemberFailed", ex, $"Failed to add member {memberResourceName} to group {newGroupResourceName}");
                                }
                            }
                        }

                        _metadata.TryRemove(membersPath, out _);
                        membersEntry?.Dispose();
                    }
                    catch (Exception ex)
                    {
                        Log.WriteWarningMessage(LOGTAG, "RestoreContactGroupMembersFailed", ex, $"Failed to restore members for group {originalPath}");
                    }
                }

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