using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using SqlKata;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI;
using Wino.Services.Extensions;

namespace Wino.Services
{
    public class AccountService : BaseDatabaseService, IAccountService
    {
        public IAuthenticator ExternalAuthenticationAuthenticator { get; set; }

        private readonly ISignatureService _signatureService;
        private readonly IPreferencesService _preferencesService;

        private readonly ILogger _logger = Log.ForContext<AccountService>();

        public AccountService(IDatabaseService databaseService,
                              ISignatureService signatureService,
                              IPreferencesService preferencesService) : base(databaseService)
        {
            _signatureService = signatureService;
            _preferencesService = preferencesService;
        }


        public async Task ClearAccountAttentionAsync(Guid accountId)
        {
            var account = await GetAccountAsync(accountId);

            Guard.IsNotNull(account);

            account.AttentionReason = AccountAttentionReason.None;

            await UpdateAccountAsync(account);
        }

        public async Task UpdateMergedInboxAsync(Guid mergedInboxId, IEnumerable<Guid> linkedAccountIds)
        {
            // First, remove all accounts from merged inbox.
            await Connection.ExecuteAsync("UPDATE MailAccount SET MergedInboxId = NULL WHERE MergedInboxId = ?", mergedInboxId);

            // Then, add new accounts to merged inbox.
            var query = new Query("MailAccount")
                .WhereIn("Id", linkedAccountIds)
                .AsUpdate(new
                {
                    MergedInboxId = mergedInboxId
                });

            await Connection.ExecuteAsync(query.GetRawQuery());

            WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
        }

        public async Task UnlinkMergedInboxAsync(Guid mergedInboxId)
        {
            var mergedInbox = await Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId).ConfigureAwait(false);

            if (mergedInbox == null)
            {
                _logger.Warning("Could not find merged inbox with id {MergedInboxId}", mergedInboxId);

                return;
            }

            var query = new Query("MailAccount")
                .Where("MergedInboxId", mergedInboxId)
                .AsUpdate(new
                {
                    MergedInboxId = (Guid?)null
                });

            await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
            await Connection.DeleteAsync(mergedInbox).ConfigureAwait(false);

            // Change the startup entity id if it was the merged inbox.
            // Take the first account as startup account.

            if (_preferencesService.StartupEntityId == mergedInboxId)
            {
                var firstAccount = await Connection.Table<MailAccount>().FirstOrDefaultAsync();

                if (firstAccount != null)
                {
                    _preferencesService.StartupEntityId = firstAccount.Id;
                }
                else
                {
                    _preferencesService.StartupEntityId = null;
                }
            }

            WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
        }

        public async Task CreateMergeAccountsAsync(MergedInbox mergedInbox, IEnumerable<MailAccount> accountsToMerge)
        {
            if (mergedInbox == null) return;

            // 0. Give the merged inbox a new Guid.
            mergedInbox.Id = Guid.NewGuid();

            var accountFolderDictionary = new Dictionary<MailAccount, List<MailItemFolder>>();

            // 1. Make all folders in the accounts unsticky. We will stick them based on common special folder types.
            foreach (var account in accountsToMerge)
            {
                var accountFolderList = new List<MailItemFolder>();

                var folders = await Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == account.Id).ToListAsync();

                foreach (var folder in folders)
                {
                    accountFolderList.Add(folder);
                    folder.IsSticky = false;

                    await Connection.UpdateAsync(folder);
                }

                accountFolderDictionary.Add(account, accountFolderList);
            }

            // 2. Find the common special folders and stick them.
            // Only following types will be considered as common special folder.
            SpecialFolderType[] commonSpecialTypes =
            [
                SpecialFolderType.Inbox,
                SpecialFolderType.Sent,
                SpecialFolderType.Draft,
                SpecialFolderType.Archive,
                SpecialFolderType.Junk,
                SpecialFolderType.Deleted
            ];

            foreach (var type in commonSpecialTypes)
            {
                var isCommonType = accountFolderDictionary
                    .Select(a => a.Value)
                    .Where(a => a.Any(a => a.SpecialFolderType == type))
                    .Count() == accountsToMerge.Count();

                if (isCommonType)
                {
                    foreach (var account in accountsToMerge)
                    {
                        var folder = accountFolderDictionary[account].FirstOrDefault(a => a.SpecialFolderType == type);

                        if (folder != null)
                        {
                            folder.IsSticky = true;

                            await Connection.UpdateAsync(folder);
                        }
                    }
                }
            }

            // 3. Insert merged inbox and assign accounts.
            await Connection.InsertAsync(mergedInbox);

            foreach (var account in accountsToMerge)
            {
                account.MergedInboxId = mergedInbox.Id;

                await Connection.UpdateAsync(account);
            }

            WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested());
        }

        public async Task RenameMergedAccountAsync(Guid mergedInboxId, string newName)
        {
            var query = new Query("MergedInbox")
                .Where("Id", mergedInboxId)
                .AsUpdate(new
                {
                    Name = newName
                });

            await Connection.ExecuteAsync(query.GetRawQuery());

            ReportUIChange(new MergedInboxRenamed(mergedInboxId, newName));
        }

        public async Task FixTokenIssuesAsync(Guid accountId)
        {
            var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);

            if (account == null) return;

            //var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType);

            //// This will re-generate token.
            //var token = await authenticator.GenerateTokenInformationAsync(account);

            // TODO: Rest?
            // Guard.IsNotNull(token);
        }

        private Task<MailAccountPreferences> GetAccountPreferencesAsync(Guid accountId)
            => Connection.Table<MailAccountPreferences>().FirstOrDefaultAsync(a => a.AccountId == accountId);

        public async Task<List<MailAccount>> GetAccountsAsync()
        {
            var accounts = await Connection.Table<MailAccount>().OrderBy(a => a.Order).ToListAsync();

            foreach (var account in accounts)
            {
                // Load IMAP server configuration.
                if (account.ProviderType == MailProviderType.IMAP4)
                    account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id);

                // Load MergedInbox information.
                if (account.MergedInboxId != null)
                    account.MergedInbox = await GetMergedInboxInformationAsync(account.MergedInboxId.Value);

                account.Preferences = await GetAccountPreferencesAsync(account.Id);
            }

            return accounts;
        }

        public async Task CreateRootAliasAsync(Guid accountId, string address)
        {
            var rootAlias = new MailAccountAlias()
            {
                AccountId = accountId,
                AliasAddress = address,
                IsPrimary = true,
                IsRootAlias = true,
                IsVerified = true,
                ReplyToAddress = address,
                Id = Guid.NewGuid()
            };

            await Connection.InsertAsync(rootAlias).ConfigureAwait(false);

            Log.Information("Created root alias for the account {AccountId}", accountId);
        }

        public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
        {
            var query = new Query(nameof(MailAccountAlias))
                .Where(nameof(MailAccountAlias.AccountId), accountId)
                .OrderByDesc(nameof(MailAccountAlias.IsRootAlias));

            return await Connection.QueryAsync<MailAccountAlias>(query.GetRawQuery()).ConfigureAwait(false);
        }

        private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
            => Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId);

        public async Task DeleteAccountAsync(MailAccount account)
        {
            // TODO: Delete mime messages and attachments.
            // TODO: Delete token cache by underlying provider.

            await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE Id IN(SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id from MailItemFolder WHERE MailAccountId == ?))", account.Id);

            await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
            await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
            await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);

            // Account belongs to a merged inbox.
            // In case of there'll be a single account in the merged inbox, remove the merged inbox as well.

            if (account.MergedInboxId != null)
            {
                var mergedInboxAccountCount = await Connection.Table<MailAccount>().Where(a => a.MergedInboxId == account.MergedInboxId.Value).CountAsync();

                // There will be only one account in the merged inbox. Remove the link for the other account as well.
                if (mergedInboxAccountCount == 2)
                {
                    var query = new Query("MailAccount")
                    .Where("MergedInboxId", account.MergedInboxId.Value)
                    .AsUpdate(new
                    {
                        MergedInboxId = (Guid?)null
                    });

                    await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
                }
            }

            if (account.ProviderType == MailProviderType.IMAP4)
                await Connection.Table<CustomServerInformation>().DeleteAsync(a => a.AccountId == account.Id);

            if (account.Preferences != null)
                await Connection.DeleteAsync(account.Preferences);

            await Connection.DeleteAsync(account);

            // Clear out or set up a new startup entity id.
            // Next account after the deleted one will be the startup account.

            if (_preferencesService.StartupEntityId == account.Id || _preferencesService.StartupEntityId == account.MergedInboxId)
            {
                var firstNonStartupAccount = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id != account.Id);

                if (firstNonStartupAccount != null)
                {
                    _preferencesService.StartupEntityId = firstNonStartupAccount.Id;
                }
                else
                {
                    _preferencesService.StartupEntityId = null;
                }
            }

            ReportUIChange(new AccountRemovedMessage(account));
        }

        public async Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation)
        {
            var account = await GetAccountAsync(accountId).ConfigureAwait(false);

            if (account != null)
            {
                account.SenderName = profileInformation.SenderName;
                account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;

                if (string.IsNullOrEmpty(account.Address))
                {
                    account.Address = profileInformation.AccountAddress;
                }
                // Forcefully add or update a contact data with the provided information.

                var accountContact = new AccountContact()
                {
                    Address = account.Address,
                    Name = account.SenderName,
                    Base64ContactPicture = account.Base64ProfilePictureData,
                    IsRootContact = true
                };

                await Connection.InsertOrReplaceAsync(accountContact).ConfigureAwait(false);

                await UpdateAccountAsync(account).ConfigureAwait(false);
            }
        }

        public async Task<MailAccount> GetAccountAsync(Guid accountId)
        {
            var account = await Connection.Table<MailAccount>().FirstOrDefaultAsync(a => a.Id == accountId);

            if (account == null)
            {
                _logger.Error("Could not find account with id {AccountId}", accountId);
            }
            else
            {
                if (account.ProviderType == MailProviderType.IMAP4)
                    account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id);

                account.Preferences = await GetAccountPreferencesAsync(account.Id);

                return account;
            }

            return null;
        }

        public Task<CustomServerInformation> GetAccountCustomServerInformationAsync(Guid accountId)
            => Connection.Table<CustomServerInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);

        public async Task UpdateAccountAsync(MailAccount account)
        {
            await Connection.UpdateAsync(account.Preferences).ConfigureAwait(false);
            await Connection.UpdateAsync(account).ConfigureAwait(false);

            ReportUIChange(new AccountUpdatedMessage(account));
        }

        public async Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
        {
            // Delete existing ones.
            await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);

            // Insert new ones.
            foreach (var alias in aliases)
            {
                await Connection.InsertAsync(alias).ConfigureAwait(false);
            }
        }

        public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
        {
            var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
            var rootAlias = localAliases.Find(a => a.IsRootAlias);

            foreach (var remoteAlias in remoteAccountAliases)
            {
                var existingAlias = localAliases.Find(a => a.AccountId == account.Id && a.AliasAddress == remoteAlias.AliasAddress);

                if (existingAlias == null)
                {
                    // Create new alias.
                    var newAlias = new MailAccountAlias()
                    {
                        AccountId = account.Id,
                        AliasAddress = remoteAlias.AliasAddress,
                        IsPrimary = remoteAlias.IsPrimary,
                        IsVerified = remoteAlias.IsVerified,
                        ReplyToAddress = remoteAlias.ReplyToAddress,
                        Id = Guid.NewGuid(),
                        IsRootAlias = remoteAlias.IsRootAlias,
                        AliasSenderName = remoteAlias.AliasSenderName
                    };

                    await Connection.InsertAsync(newAlias);
                    localAliases.Add(newAlias);
                }
                else
                {
                    // Update existing alias.
                    existingAlias.IsPrimary = remoteAlias.IsPrimary;
                    existingAlias.IsVerified = remoteAlias.IsVerified;
                    existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
                    existingAlias.AliasSenderName = remoteAlias.AliasSenderName;

                    await Connection.UpdateAsync(existingAlias);
                }
            }

            // Make sure there is only 1 root alias and 1 primary alias selected.

            bool shouldUpdatePrimary = localAliases.Count(a => a.IsPrimary) != 1;
            bool shouldUpdateRoot = localAliases.Count(a => a.IsRootAlias) != 1;

            if (shouldUpdatePrimary)
            {
                localAliases.ForEach(a => a.IsPrimary = false);

                var idealPrimaryAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();

                idealPrimaryAlias.IsPrimary = true;
                await Connection.UpdateAsync(idealPrimaryAlias).ConfigureAwait(false);
            }

            if (shouldUpdateRoot)
            {
                localAliases.ForEach(a => a.IsRootAlias = false);

                var idealRootAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();

                idealRootAlias.IsRootAlias = true;
                await Connection.UpdateAsync(idealRootAlias).ConfigureAwait(false);
            }
        }

        public async Task DeleteAccountAliasAsync(Guid aliasId)
        {
            // Create query to delete alias.

            var query = new Query("MailAccountAlias")
                .Where("Id", aliasId)
                .AsDelete();

            await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
        }

        public async Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation)
        {
            Guard.IsNotNull(account);

            var accountCount = await Connection.Table<MailAccount>().CountAsync();

            // If there are no accounts before this one, set it as startup account.
            if (accountCount == 0)
            {
                _preferencesService.StartupEntityId = account.Id;
            }
            else
            {
                // Set the order of the account.
                // This can be changed by the user later in manage accounts page.
                account.Order = accountCount;
            }

            await Connection.InsertAsync(account);

            var preferences = new MailAccountPreferences()
            {
                Id = Guid.NewGuid(),
                AccountId = account.Id,
                IsNotificationsEnabled = true,
                ShouldAppendMessagesToSentFolder = false
            };

            account.Preferences = preferences;

            // Outlook & Office 365 supports Focused inbox. Enabled by default.
            bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365;

            // TODO: This should come from account settings API.
            // Wino doesn't have MailboxSettings yet.
            if (isMicrosoftProvider)
                account.Preferences.IsFocusedInboxEnabled = true;

            // Setup default signature.
            var defaultSignature = await _signatureService.CreateDefaultSignatureAsync(account.Id);

            account.Preferences.SignatureIdForNewMessages = defaultSignature.Id;
            account.Preferences.SignatureIdForFollowingMessages = defaultSignature.Id;
            account.Preferences.IsSignatureEnabled = true;

            await Connection.InsertAsync(preferences);

            if (customServerInformation != null)
                await Connection.InsertAsync(customServerInformation);
        }

        public async Task<string> UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier)
        {
            var account = await GetAccountAsync(accountId);

            if (account == null)
            {
                _logger.Error("Could not find account with id {AccountId}", accountId);
                return string.Empty;
            }

            var currentIdentifier = account.SynchronizationDeltaIdentifier;

            bool shouldUpdateIdentifier = account.ProviderType == MailProviderType.Gmail ?
                    string.IsNullOrEmpty(currentIdentifier) ? true : !string.IsNullOrEmpty(currentIdentifier)
                    && ulong.TryParse(currentIdentifier, out ulong currentIdentifierValue)
                    && ulong.TryParse(newIdentifier, out ulong newIdentifierValue)
                    && newIdentifierValue > currentIdentifierValue : true;

            if (shouldUpdateIdentifier)
            {
                account.SynchronizationDeltaIdentifier = newIdentifier;

                await UpdateAccountAsync(account);
            }

            return account.SynchronizationDeltaIdentifier;
        }

        public async Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair)
        {
            foreach (var pair in accountIdOrderPair)
            {
                var account = await GetAccountAsync(pair.Key);

                if (account == null)
                {
                    _logger.Information("Could not find account with id {Key} for reordering. It may be a linked account.", pair.Key);
                    continue;
                }

                account.Order = pair.Value;

                await Connection.UpdateAsync(account);
            }

            Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair));
        }

        public async Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId)
        {
            var aliases = await GetAccountAliasesAsync(accountId);

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

            return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First();
        }

        public async Task<bool> IsAccountFocusedEnabledAsync(Guid accountId)
        {
            var account = await GetAccountAsync(accountId);
            return account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
        }
    }
}
