using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using LiteDB;
using async_enumerable_dotnet;
using System.Runtime.CompilerServices;

namespace Dacryptero.DB
{
    partial class DBSingleController
    {
        public Task<DBValue<Data.Person>?> GetPersonAsync(ObjectId id,
            CancellationToken cancellatonToken
        )
        {
            return GetPersonAsync(id, false, cancellatonToken);
        }

        public async Task<DBValue<Data.Person>?> GetPersonAsync(ObjectId id, bool flat,
            CancellationToken cancellatonToken
        )
        {
            await mutex.WaitAsync(cancellatonToken).ConfigureAwait(false);
            try
            {
                if (database is null)
                    return null;
                Data.Person? person = database.Person.FindById(id);
                if (person is null)
                    return null;

                cancellatonToken.ThrowIfCancellationRequested();
                LoadPersonAttribute(person.Attributes, database.Attribute,
                    database.AttributeEntry, flat
                );

                cancellatonToken.ThrowIfCancellationRequested();
                if (confidential is null)
                    return new DBValue<Data.Person>(this, person);
                Data.ConfPersonData? conf = confidential.PersonData.FindById(id);

                cancellatonToken.ThrowIfCancellationRequested();
                person.ConfData = conf;
                LoadPersonAttribute(conf.Attributes, confidential.Attribute,
                    confidential.AttributeEntry, flat
                );
                return new DBValue<Data.Person>(this, person);
            }
            finally
            {
                mutex.Release();
            }
        }

        /// <summary>
        /// Update the basic information about the <see cref="Data.Person" /> including their
        /// <see cref="Data.ConfPersonData" /> if available. This will not update any attached
        /// <see cref="Data.Attribute" /> or <see cref="Data.AttributeEntry" />!
        /// </summary>
        /// <param name="person">the person to update</param>
        /// <param name="cancellationToken">the cancellation token</param>
        /// <returns>true if succeeded, null if failed</returns>
        public async Task<DBValue<bool>?> UpdatePersonAsync(Data.Person person, CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                if (person.ConfData is not null && confidential is not null)
                {
                    var success = confidential.PersonData.Update(person.ConfData);
                    if (!success)
                        return null;
                }
                cancellationToken.ThrowIfCancellationRequested();
                if (database is not null)
                {
                    var success = database.Person.Update(person);
                    if (success)
                        return new DBValue<bool>(this, true);
                }
                return null;
            }
            finally
            {
                mutex.Release();
            }
        }

        public IAsyncEnumerable<DBValue<Data.Person>> EnumeratePersonsAsync(
            Search.SearchIndex index,
            CancellationToken cancellationToken
        )
        {
            int count = 0;
            if (database is null)
                return AsyncEnumerable.Empty<DBValue<Data.Person>>();
            return new LockFilterEnumerable<Data.Person, DBValue<Data.Person>>(
                mutex,
                _ => new ThreadSafeEnumerable<Data.Person>(
                    () => database.Person.Query()
                        .Include(x => x.Attributes)
                        .Where(x => !x.Deleted)
                        .Where(index)
                        .ToEnumerable()
                ),
                (person, content) =>
                {
                    Data.ConfPersonData? conf = confidential?.PersonData.FindById(person.Id);
                    person.ConfData = conf;
                    LoadPersonAttribute(person.Attributes, database.Attribute, database.AttributeEntry);
                    if (conf is not null)
                        LoadPersonAttribute(person.ConfData!.Attributes, confidential!.Attribute,
                            confidential.AttributeEntry
                        );
                    count++;
                    if ((count % 500) == 0)
                        Serilog.Log.Debug("Enumerate Persons {count:#,000}...", count);
                    content.Content = new DBValue<Data.Person>(this, person);
                    return ValueTask.FromResult(true);
                },
                () =>
                {
                    Serilog.Log.Debug("Enumerate Persons stopped");
                    return ValueTask.CompletedTask;
                }
            );
        }

        private void LoadPersonAttribute(List<Data.Attribute> list,
            ILiteCollection<Data.Attribute> attributes,
            ILiteCollection<Data.AttributeEntry> attributeEntries,
            bool flat = true
        )
        {
            for (int i = 0; i < list.Count; ++i)
            {
                var attr = attributes.FindById(list[i].Id);
                if (attr is null)
                    continue;
                list[i] = attr;
                var recent = attributeEntries.FindById(attr.RecentEntry.Id);
                if (recent is null)
                    continue;
                attr.RecentEntry = recent;
                if (flat)
                    continue;
                for (int j = 0; j < attr.Entries.Count; ++j)
                {
                    var entry = attributeEntries.FindById(attr.Entries[j].Id);
                    if (entry is not null)
                        attr.Entries[j] = entry;
                }
            }
        }

        /// <summary>
        /// Add the person including their confidential data to the database. This call will fail if
        /// the database or confidential database is connected. If the
        /// <see cref="Data.Person.ConfData" /> field of the <paramref name="person" /> is null this
        /// call will also fail.
        /// </summary>
        /// <param name="person">The person with the confidential information.</param>
        /// <param name="cancellationToken"></param>
        /// <returns>true if the insertion succeeded</returns>
        public async Task<bool> AddPersonAsync(Data.Person person,
            CancellationToken cancellationToken
        )
        {
            if (database is null || confidential is null || person.ConfData is null)
                return false;
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                database.Person.Insert(person);
                person.Attributes.ForEach(x => x.PersonId = person.Id);
                person.ConfData.Attributes.ForEach(x => x.PersonId = person.Id);
                database.AttributeEntry.InsertBulk(person.Attributes.SelectMany(x => x.Entries));
                database.Attribute.InsertBulk(person.Attributes);
                confidential.AttributeEntry.InsertBulk(person.ConfData.Attributes.SelectMany(x => x.Entries));
                confidential.Attribute.InsertBulk(person.ConfData.Attributes);
                database.Person.Update(person);
                person.ConfData.Id = person.Id;
                confidential.PersonData.Insert(person.ConfData);
                return true;
            }
            finally
            {
                mutex.Release();
            }
        }

        public async Task<bool> UpdateAttributeAsync(Data.Person person, Data.Attribute attribute,
            bool confidential, CancellationToken cancellationToken
        )
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                var col = confidential ? this.confidential?.Attribute : database?.Attribute;
                var colEntry = confidential ? this.confidential?.AttributeEntry : database?.AttributeEntry;
                if (col is null || colEntry is null)
                    return false;
                colEntry.Upsert(attribute.RecentEntry);
                col.Upsert(attribute);
                if (person.ConfData is not null && this.confidential is not null)
                    this.confidential.PersonData.Update(person.ConfData);
                database!.Person.Update(person);
                return true;
            }
            finally
            {
                mutex.Release();
            }
        }

        public async IAsyncEnumerable<DBValue<Data.Person>> EnumerateRawPeopleFromInterviewAsync(
            ObjectId interview,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                if (confidential is null || database is null)
                    yield break;
                Data.Interview? interviewData = confidential.Interview.FindById(interview);
                if (interviewData is null)
                    yield break;
                foreach (var id in interviewData.Person)
                {
                    var person = database.Person.Query()
                        .Where(x => x.Id == id)
                        .FirstOrDefault();
                    if (person is null)
                        continue;
                    yield return new DBValue<Data.Person>(this, person);
                }
            }
            finally
            {
                mutex.Release();
            }
        }
    }
}