using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using LiteDB;
using async_enumerable_dotnet;
using Dacryptero.Search;
using Dacryptero.Search.AST;

namespace Dacryptero.DB
{
    public partial class DBSingleController : IDBDataQuery
    {
        private class MainData
        {
            public LiteDatabase Database { get; }

            public ILiteCollection<Data.File> File { get; }

            public ILiteCollection<Data.FileEntry> FileEntry { get; }

            public ILiteCollection<Data.Person> Person { get; }

            public ILiteCollection<Data.Attribute> Attribute { get; }

            public ILiteCollection<Data.AttributeEntry> AttributeEntry { get; }

            public ILiteCollection<Data.Relationship> Relationship { get; }

            public IndexCache Index { get; }

            public MainData(string directoryPath, DBConfig config, ReadOnlyMemory<byte> key)
            {
                Database = new LiteDatabase(new ConnectionString()
                {
                    Filename = Path.Combine(directoryPath, DatabaseFileName),
                    Collation = new Collation(
                        System.Globalization.CultureInfo.InvariantCulture.LCID, 
                        System.Globalization.CompareOptions.IgnoreCase
                    ),
                    Password = Convert.ToBase64String(key.Span),
                });
                Database.Mapper.EmptyStringToNull = false;
                new Migration.Migrator().Migrate(Database, config, false);
                AttributeEntry = Database.GetCollection<Data.AttributeEntry>("attribute_entry");
                Attribute = Database.GetCollection<Data.Attribute>("attribute");
                Person = Database.GetCollection<Data.Person>("person");
                FileEntry = Database.GetCollection<Data.FileEntry>("file_entry");
                File = Database.GetCollection<Data.File>("file");
                File.EnsureIndex(x => x.ParentId);
                File.EnsureIndex(x => x.Person);
                Relationship = Database.GetCollection<Data.Relationship>("relationship");
                var index = Database.GetCollection<Data.Index>("index");
                index.EnsureIndex(x => x.Token);
                Index = new IndexCache(index);
            }
        }

        private class ConfData
        {
            public LiteDatabase Database { get; }

            public ILiteCollection<Data.FileEntry> FileEntry { get; }

            public ILiteCollection<Data.File> File { get; }

            public ILiteCollection<Data.ConfPersonData> PersonData{ get; }

            public ILiteCollection<Data.Attribute> Attribute { get; }

            public ILiteCollection<Data.AttributeEntry> AttributeEntry { get; }

            public ILiteCollection<Data.Interview> Interview { get; }

            public IndexCache Index { get; }

            public ConfData(string directoryPath, DBConfig config,ReadOnlyMemory<byte> key)
            {
                Database = new LiteDatabase(new ConnectionString()
                {
                    Filename = Path.Combine(directoryPath, ConfidentialFileName),
                    Collation = new Collation(
                        System.Globalization.CultureInfo.InvariantCulture.LCID, 
                        System.Globalization.CompareOptions.IgnoreCase
                    ),
                    Password = Convert.ToBase64String(key.Span),
                });
                Database.Mapper.EmptyStringToNull = false;
                new Migration.Migrator().Migrate(Database, config, true);
                Interview = Database.GetCollection<Data.Interview>("interview");
                AttributeEntry = Database.GetCollection<Data.AttributeEntry>("attribute_entry");
                Attribute = Database.GetCollection<Data.Attribute>("attribute");
                PersonData = Database.GetCollection<Data.ConfPersonData>("conf_person_data");
                PersonData.EnsureIndex(x => x.Interview);
                File = Database.GetCollection<Data.File>("file");
                FileEntry = Database.GetCollection<Data.FileEntry>("file_entry");
                File.EnsureIndex(x => x.ParentId);
                File.EnsureIndex(x => x.Person);
                var index = Database.GetCollection<Data.Index>("index");
                index.EnsureIndex(x => x.Token);
                Index = new IndexCache(index);
            }
        }

        public static string DatabaseFileName = "data.db";

        public static string ConfidentialFileName = "confidential.db";

        public DBConfig Config { get; }

        public bool HasConfidential { get; private set; }

        private MainData? database;

        private ConfData? confidential;

        public bool IsOpen => database is not null;

        private readonly SemaphoreSlim mutex = new SemaphoreSlim(1, 1);

        public DBSingleController(DBConfig config)
        {
            Config = config;
            HasConfidential = File.Exists(Path.Combine(config.DirectoryPath, ConfidentialFileName));
        }

        public void Close()
        {
            if (database is not null)
            {
                database.Database.Dispose();
                database = null;
            }
            if (confidential is not null)
            {
                confidential.Database.Dispose();
                confidential = null;
            }
        }

        public bool Decrypt(ReadOnlyMemory<byte> key, bool forceConfidential = false)
        {
            try
            {
                if (!Directory.Exists(Config.DirectoryPath))
                {
                    Directory.CreateDirectory(Config.DirectoryPath);
                }
                if (database is null)
                    database = new MainData(Config.DirectoryPath, Config, key);
                if (confidential is null && (HasConfidential || forceConfidential))
                {
                    confidential = new ConfData(Config.DirectoryPath, Config, key);
                    HasConfidential = true;
                }
            }
            catch (LiteException e)
            {
                Serilog.Log.Error(e, "Cannot decrypt database at {path}", Config.DirectoryPath);
                return false;
            }
            catch (Exception e)
            {
                Serilog.Log.Fatal(e, "Unexpected exception");
                return false;
            }
            return true;
        }

        public IAsyncEnumerable<SearchResult> DoSearch(AstBase ast,
            Search.Sorting.SortConfig sortConfig,
            CancellationToken cancellationToken)
        {
            // no locks here because the Enumerate*Async funcs will hold these
            if (indexing)
            {
                Serilog.Log.Information("Search in db {name} skipped because of indexing", Config.Name);
                return AsyncEnumerable.Empty<SearchResult>();
            }

            return AsyncEnumerable.Concat(
                DoSearch(
                    ast.Clone(),
                    SearchResultType.File,
                    GetFileEnumeration,
                    sortConfig
                ),
                DoSearch(
                    ast.Clone(),
                    SearchResultType.Interview,
                    GetInterviewEnumeration,
                    sortConfig
                ),
                DoSearch(
                    ast.Clone(),
                    SearchResultType.Person,
                    GetPersonEnumeration,
                    sortConfig
                )
            );
        }

        private async Task<IAsyncEnumerable<Data.File>> GetFileEnumeration(AstBase ast,
            CancellationToken cancellationToken
        )
        {
            var index = await ast.GetSearchIndexAsync(this, Data.IndexSource.Files, cancellationToken)
                .ConfigureAwait(false);
            Serilog.Log.Debug("File Search Index: {index}", index);
            return EnumerateFilesAsync(index, cancellationToken).Map(x => x.Value);
        }

        private async Task<IAsyncEnumerable<Data.Interview>> GetInterviewEnumeration(AstBase ast,
            CancellationToken cancellationToken
        )
        {
            var index = await ast.GetSearchIndexAsync(this, Data.IndexSource.Interviews, cancellationToken)
                .ConfigureAwait(false);
            Serilog.Log.Debug("Interview Search Index: {index}", index);
            return EnumerateInterviewsAsync(index, cancellationToken).Map(x => x.Value);
        }

        private async Task<IAsyncEnumerable<Data.Person>> GetPersonEnumeration(AstBase ast,
            CancellationToken cancellationToken
        )
        {
            var index = await ast.GetSearchIndexAsync(this, Data.IndexSource.Persons, cancellationToken);
            Serilog.Log.Debug("Person Search Index: {index}", index);
            return EnumeratePersonsAsync(index, cancellationToken).Map(x => x.Value);
        }

        private IAsyncEnumerable<SearchResult> DoSearch<T>(AstBase ast,
            SearchResultType resultType,
            Func<AstBase, CancellationToken, Task<IAsyncEnumerable<T>>> entries,
            Search.Sorting.SortConfig sortConfig
        )
            where T : ISearchable
        {
            return new SearchHandler<T>(this, ast, resultType, entries, sortConfig);
        }
    }
}