using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SqlKata;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Services.Extensions;

namespace Wino.Services.Threading
{
    public class ImapThreadingStrategy : BaseDatabaseService, IImapThreadingStrategy
    {
        private readonly IFolderService _folderService;

        public ImapThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService)
        {
            _folderService = folderService;
        }

        private Task<MailCopy> GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
        {
            if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult<MailCopy>(null);

            var query = new Query("MailCopy")
                .Distinct()
                .Take(1)
                .Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
                .Where("MailItemFolder.MailAccountId", accountId)
                .WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
                .Where("MailCopy.MessageId", replyItem.InReplyTo)
                .WhereNot("MailCopy.Id", replyItem.Id)
                .Select("MailCopy.*");

            return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
        }

        private Task<MailCopy> GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
        {
            if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult<MailCopy>(null);

            var query = new Query("MailCopy")
                .Distinct()
                .Take(1)
                .Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
                .WhereNot("MailCopy.Id", originalItem.Id)
                .Where("MailItemFolder.MailAccountId", accountId)
                .Where("MailCopy.InReplyTo", originalItem.MessageId)
                .WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
                .Select("MailCopy.*");

            var raq = query.GetRawQuery();

            return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
        }

        public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
        {
            var threads = new List<ThreadMailItem>();

            var account = items.First().AssignedAccount;
            var accountId = account.Id;

            // Child -> Parent approach.

            var mailLookupTable = new Dictionary<string, bool>();

            // Fill up the mail lookup table to prevent double thread creation.
            foreach (var mail in items)
                if (!mailLookupTable.ContainsKey(mail.Id))
                    mailLookupTable.Add(mail.Id, false);

            var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Sent);
            var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Draft);

            // Threading is not possible. Return items as it is.

            if (sentFolder == null || draftFolder == null) return new List<IMailItem>(items);

            foreach (var replyItem in items)
            {
                if (mailLookupTable[replyItem.Id])
                    continue;

                mailLookupTable[replyItem.Id] = true;

                var threadItem = new ThreadMailItem();

                threadItem.AddThreadItem(replyItem);

                var replyToChild = await GetReplyParentAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);

                // Build up 
                while (replyToChild != null)
                {
                    replyToChild.AssignedAccount = account;

                    if (replyToChild.FolderId == draftFolder.Id)
                        replyToChild.AssignedFolder = draftFolder;

                    if (replyToChild.FolderId == sentFolder.Id)
                        replyToChild.AssignedFolder = sentFolder;

                    if (replyToChild.FolderId == replyItem.AssignedFolder.Id)
                        replyToChild.AssignedFolder = replyItem.AssignedFolder;

                    threadItem.AddThreadItem(replyToChild);

                    if (mailLookupTable.ContainsKey(replyToChild.Id))
                        mailLookupTable[replyToChild.Id] = true;

                    replyToChild = await GetReplyParentAsync(replyToChild, accountId, replyToChild.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
                }

                // Build down
                var replyToParent = await GetInReplyToReplyAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);

                while (replyToParent != null)
                {
                    replyToParent.AssignedAccount = account;

                    if (replyToParent.FolderId == draftFolder.Id)
                        replyToParent.AssignedFolder = draftFolder;

                    if (replyToParent.FolderId == sentFolder.Id)
                        replyToParent.AssignedFolder = sentFolder;

                    if (replyToParent.FolderId == replyItem.AssignedFolder.Id)
                        replyToParent.AssignedFolder = replyItem.AssignedFolder;

                    threadItem.AddThreadItem(replyToParent);

                    if (mailLookupTable.ContainsKey(replyToParent.Id))
                        mailLookupTable[replyToParent.Id] = true;

                    replyToParent = await GetInReplyToReplyAsync(replyToParent, accountId, replyToParent.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
                }

                // It's a thread item.

                if (threadItem.ThreadItems.Count > 1 && !threads.Exists(a => a.Id == threadItem.Id))
                {
                    threads.Add(threadItem);
                }
                else
                {
                    // False alert. This is not a thread item.
                    mailLookupTable[replyItem.Id] = false;

                    // TODO: Here potentially check other algorithms for threading like References.
                }
            }

            // At this points all mails in the list belong to single items.
            // Merge with threads.
            // Last sorting will be done later on in MailService.

            // Remove single mails that are included in thread.
            items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]);

            var finalList = new List<IMailItem>(items);

            finalList.AddRange(threads);

            return finalList;
        }

        public bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
        {
            bool isChild = originalItem.InReplyTo != null && originalItem.InReplyTo == targetItem.MessageId;
            bool isParent = originalItem.MessageId != null && originalItem.MessageId == targetItem.InReplyTo;

            return isChild || isParent;
        }
    }
}
