using BukiVedi.Shared.Entities;
using BukiVedi.Shared.Models;
using DOM.DSL.Services;
using MongoDB.Driver;
using Sleopok.Engine;
using System.Text;
using ZeroLevel;
using ZeroLevel.Services.Semantic;

namespace BukiVedi.Shared.Services
{
    public interface ILibrary
    {
        Task<IEnumerable<BookEntity>> SearchBooksByAuthor(string author_id);
        Task<IEnumerable<BookEntity>> SearchBooks(string title);
        Task<IEnumerable<BookEntity>> SearchByTagBooks(string accountId, string tagId);
        Task<IEnumerable<BookEntity>> SearchTaggedBooks(string accountId, string tag = null!);
        Task<IEnumerable<BookEntity>> SearchFavoritesBooks(string accountId);
        Task<IEnumerable<BookEntity>> SearchBlockedBooks(string accountId);
        Task<IEnumerable<BookEntity>> SearchFavoriteAuthorsBooks(string accountId);
        Task<IEnumerable<BookEntity>> SearchToReadBooks(string accountId);
        Task<Book> DownloadToStream(Stream stream, string id);
    }

    public class Library
        : ILibrary
    {
        private readonly string _libraryPath;
        private readonly ZipLibraryReader _library;
        private readonly Dictionary<string, Author> Authors;
        private readonly Dictionary<string, Genre> Genres;
        private readonly SleoEngine<BookDocument> _engine;
        private readonly static ILexProvider _provider = LexProviderFactory.CreateProvider(new LemmaLexer());

        public Library()
        {
            _libraryPath = AppSettings.Settings.LibraryPath;
            if (Directory.Exists(_libraryPath) == false)
            {
                throw new Exception($"Library path '{_libraryPath ?? string.Empty}' not found");
            }

            _library = new ZipLibraryReader(_libraryPath);
            _engine = new SleoEngine<BookDocument>(AppSettings.Settings.Sleopok.Path, b => b.Id);

            var authors_count = (int)Tables.Authors.Count(Filters.Authors.Empty).Result;
            var genres_count = (int)Tables.Genres.Count(Filters.Genres.Empty).Result;

            Authors = new Dictionary<string, Author>(authors_count);
            Genres = new Dictionary<string, Genre>(genres_count);

            foreach (var a in Tables.Authors.GetAll().Result)
            {
                Authors[a.Id] = a;
            }
            foreach (var g in Tables.Genres.GetAll().Result)
            {
                Genres[g.Id] = g;
            }

            if (_engine.HasData() == false)
            {
                Reindex().Wait();
            }
        }

        private async Task<IEnumerable<BookEntity>> PrepareBooksByFilter(FilterDefinition<Book> bookFilter)
        {
            var result = await Tables.Books.Get(bookFilter);
            if (result != null && result.Any())
            {
                return result.Select(b => new BookEntity
                {
                    Authors = GetAuthors(b),
                    Title = b.Title,
                    Year = b.Year,
                    Description = b.Description,
                    Format = b.Format,
                    Genre = new Genre(),
                    Id = b.Id,
                });
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchByTagBooks(string accountId, string tagId)
        {
            var tag = await Tables.UserTag.GetById(tagId);
            var books = (await Tables.UserTag.Get(Filters.Tags.ByName(tag.Name, accountId)))?.Select(t => t.BookId)?.ToHashSet()?.ToHashSet()!;
            if (books != null && books.Any())
            {
                return await PrepareBooksByFilter(Filters.Books.ByIds(books));
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchFavoritesBooks(string accountId)
        {
            
            IEnumerable<string> bookIds = (await Tables.FavoriteBooks.Get(Filters.FavoriteBooks.ByUser(accountId)))?.Select(t => t.BookId)?.ToHashSet()!;
            if (bookIds != null && bookIds.Any())
            {
                var filter = Builders<Book>.Filter.In(b => b.Id, bookIds);
                return await PrepareBooksByFilter(filter);
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchToReadBooks(string accountId)
        {
            IEnumerable<string> bookIds = (await Tables.ReadQueue.Get(Filters.BooksToRead.ByUser(accountId)))?.Select(t => t.BookId)?.ToHashSet()!;
            if (bookIds != null && bookIds.Any())
            {
                return await PrepareBooksByFilter(Filters.Books.ByIds(bookIds));
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchBlockedBooks(string accountId)
        {
            IEnumerable<string> bookIds = (await Tables.DisgustingBooks.Get(Filters.DisgustingBooks.ByUser(accountId)))?.Select(t => t.BookId)?.ToHashSet()!;
            if (bookIds != null && bookIds.Any())
            {
                return await PrepareBooksByFilter(Filters.Books.ByIds(bookIds));
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchFavoriteAuthorsBooks(string accountId)
        {
            var authorIds = (await Tables.FavoriteAuthors.Get(Filters.FavoriteAuthors.ByUser(accountId)))?.Select(t => t.AuthorId)?.ToHashSet()!;
            if (authorIds != null && authorIds.Any())
            {
                return await PrepareBooksByFilter(Filters.Books.ByAuthors(authorIds));
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchBooksByAuthor(string author_id)
        {
            if (string.IsNullOrWhiteSpace(author_id) == false)
            {
                return await PrepareBooksByFilter(Filters.Books.ByAuthor(author_id));
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task<IEnumerable<BookEntity>> SearchTaggedBooks(string accountId, string tag = null)
        {
            IEnumerable<string> bookIds;
            if (string.IsNullOrWhiteSpace(tag))
            { 
                bookIds = (await Tables.UserTag.Get(Filters.Tags.ByUser(accountId)))?.Select(t => t.BookId)?.ToHashSet()!;
            }
            else
            {
                bookIds = (await Tables.UserTag.Get(Filters.Tags.ByName(tag, accountId)))?.Select(t => t.BookId)?.ToHashSet()!;
            }
            if (bookIds != null && bookIds.Any())
            {
                return await PrepareBooksByFilter(Filters.Books.ByIds(bookIds));
            }
            return Enumerable.Empty<BookEntity>();
        }

        public async Task ReadAllBooks(Func<BookEntity, bool> bookHandler)
        {
            foreach (var b in (await Tables.Books.GetAll()))
            {
                var authors = new List<Author>(b.AuthorIds?.Count ?? 0);
                Genre genre;

                if (b.AuthorIds?.Count > 0)
                {
                    foreach (var aid in b.AuthorIds)
                    {
                        if (Authors.TryGetValue(aid, out var a))
                        {
                            authors.Add(a);
                        }
                    }
                }

                if (string.IsNullOrWhiteSpace(b.GenreId) || false == Genres.TryGetValue(b.GenreId, out genre))
                {
                    genre = new Genre();
                }

                if (false == bookHandler.Invoke(
                    new BookEntity
                    {
                        Authors = authors.ToArray(),
                        Title = b.Title,
                        Year = b.Year,
                        Description = b.Description,
                        Format = b.Format,
                        Genre = genre,
                        Id = b.Id,
                    })) return;
            }
        }

        public async Task DownloadToStream(Stream stream, BookEntity book)
        {
            var b = await Tables.Books.GetById(book.Id);
            if (b != null)
            {
                await _library.Download(Path.Combine(_libraryPath, b.ArchivePath), b.ArchiveIndex, stream);
            }
        }

        public async Task<Book> DownloadToStream(Stream stream, string id)
        {
            var b = await Tables.Books.GetById(id);
            if (b != null)
            {
                await _library.Download(Path.Combine(_libraryPath, b.ArchivePath), b.ArchiveIndex, stream);
                return b;
            }
            return null;
        }

        public async Task<IEnumerable<BookEntity>> SearchBooks(string query)
        {
            var reader = _engine.CreateReader();

            var lemmas = _provider.ExtractLexTokens(query);
            var tokens = lemmas.Select(t => t.Token).ToArray();
            var words = lemmas.Select(t => t.Word.ToLowerInvariant()).ToArray();

            Log.Debug($"Search by query '{query}' (tokens string: {string.Join(' ', tokens)}. words string: '{string.Join(' ', words)}')");

            var docs = (await reader.Search(words, true))?.Where(d => d.Value > 0.99f)?.Take(1200);
            // поиск по точному совпадению
            if ((docs?.Any() ?? false) == false)
            {
                Log.Debug($"Not found books by query {query}. Exact match");
                docs = (await reader.Search(tokens, false))?.OrderByDescending(d => d.Value)?.Take(1200);
            }

            if (docs?.Any() ?? false)
            {
                var books = (await Tables.Books.Get(Filters.Books.ByIds(docs.Select(d => d.Key)))).ToDictionary(b => b.Id, b => b);
                var result = new BookEntity[books.Count];

                Log.Debug($"Found {books.Count} books by query {query}");

                int index = 0;
                foreach (var kv in docs.OrderByDescending(d => d.Value))
                {
                    var b = books[kv.Key];
                    if (b != null)
                    {
                        result[index] = new BookEntity
                        {
                            Authors = GetAuthors(b),
                            Title = b.Title,
                            Year = b.Year,
                            Description = b.Description,
                            Format = b.Format,
                            Genre = new Genre(),
                            Id = b.Id,
                        };
                        index++;
                    }
                }
                if (result.Length != index)
                {
                    Array.Resize(ref result, index);
                }
                return result;
            }
            else
            {
                Log.Debug($"Not found books by query {query}. Not exact match");
            }
            return Enumerable.Empty<BookEntity>();
        }

        private Author[] GetAuthors(Book book)
        {
            return book.AuthorIds.Where(a => Authors.ContainsKey(a)).Select(a => Authors[a]).ToArray();
        }

        private static string[] PrepareAuthorsLine(string[] authors)
        {
            if (authors?.Length > 0)
            {
                var list = new List<string>();
                var sb = new StringBuilder();
                foreach (var a in authors)
                {
                    TStringReader reader = new TStringReader(a);
                    reader.SkipSpaces();
                    reader.SkipBreaks();
                    while (reader.EOF == false)
                    {
                        var w = reader.ReadWord();
                        if (w.Length > 0)
                        {
                            sb.Append(w.CapitalizeFirstLetter());
                            reader.Move(w.Length);
                            if (reader.EOF == false && char.IsWhiteSpace(reader.Current))
                            {
                                sb.Append(' ');
                            }
                        }
                        else
                        {
                            switch (reader.Current)
                            {
                                case '-':
                                case '—':
                                case '–':
                                case '−':
                                    sb.Append('-');
                                    break;
                                case '.':
                                    if (sb.Length > 1 && sb[sb.Length - 1] != ' ')
                                        sb.Append(' ');
                                    break;
                            }
                            reader.Move();
                        }
                    }
                    if (sb.Length > 0)
                    {
                        list.Add(sb.ToString().Trim());
                    }
                    sb.Clear();
                }
                return list.ToArray();
            }
            return null!;
        }

        /// <summary>
        /// Индексация книг в библиотеке для поиска по названию, автору, жанру
        /// </summary>
        /// <returns></returns>
        public async Task Reindex()
        {
            Log.Debug("Reindex library started");
            var authors = new Dictionary<string, string>();
            var genres = new Dictionary<string, string>();
            foreach (var author in (await Tables.Authors.GetAll())) authors.Add(author.Id, author.Name);
            foreach (var genre in (await Tables.Genres.GetAll())) genres.Add(genre.Id, genre.Code);
            var batch = new List<BookDocument>(512);
            using (var indexer = _engine.CreateBuilder())
            {
                foreach (var book in (await Tables.Books.GetAll()))
                {
                    var authorLine = (book.AuthorIds?.Count > 0) ? string.Join(' ', book.AuthorIds.Select(a => authors[a])) : string.Empty;
                    var genre = string.IsNullOrWhiteSpace(book.GenreId) ? string.Empty : genres[book.GenreId];
                    var indexItem = new BookDocument
                    {
                        Id = book.Id,
                        Author = string.Join(' ', WordTokenizer.Tokenize(authorLine)?.Where(w => w.Length > 1) ?? Enumerable.Empty<string>()),
                        Genre = string.Join(' ', WordTokenizer.Tokenize(genre)?.Where(w => w.Length > 1) ?? Enumerable.Empty<string>()),
                        Title = string.Join(' ', WordTokenizer.Tokenize(book.Title) ?? Enumerable.Empty<string>()),
                        TitleLemmas = string.Join(' ', _provider.ExtractLexTokens(book.Title)?.Select(t => t.Token)?.ToArray() ?? Enumerable.Empty<string>())
                    };
                    batch.Add(indexItem);
                    if (batch.Count == 512)
                    {
                        await indexer.Write(batch);
                        batch.Clear();
                    }
                }
                if (batch.Count > 0)
                {
                    await indexer.Write(batch);
                    batch.Clear();
                }
            }
            Log.Debug("Reindex library completed");
        }

        /// <summary>
        /// Сканирование книг в архивах, заполнение БД метаданными
        /// </summary>
        public async Task Rescan()
        {
            var authors = new Dictionary<string, string>();
            var genres = new Dictionary<string, string>();

            foreach (var author in (await Tables.Authors.GetAll())) authors.Add(author.Name, author.Id);
            foreach (var genre in (await Tables.Genres.GetAll())) genres.Add(genre.Code, genre.Id);

            foreach (var zip_book in _library.ReadBooks())
            {
                var archive_path = Path.GetFileName(zip_book.ArchivePath);

                if (await Tables.Books.Exists(Filters.Books.ByArchiveLocation(archive_path, zip_book.ArchiveIndex))) continue;

                var author_ids = new HashSet<string>();
                var authors_list = PrepareAuthorsLine(zip_book.Authors);
                if (authors_list?.Length > 0)
                {
                    foreach (var author in authors_list)
                    {
                        if (authors.ContainsKey(author) == false)
                        {
                            var a = await Tables.Authors.Write(new Author { Name = author });
                            author_ids.Add(a.Id);
                            authors.Add(author, a.Id);
                            Console.WriteLine($"[{a.Name}]");
                        }
                        else
                        {
                            author_ids.Add(authors[author]);
                        }
                    }
                }

                var genre_id = string.Empty;
                var genre = zip_book.Genres?.Length > 0 ? zip_book.Genres[0] : string.Empty;
                if (string.IsNullOrEmpty(genre) == false)
                {
                    if (genres.ContainsKey(genre) == false)
                    {
                        var g = await Tables.Genres.Write(new Genre { Code = genre });
                        genre_id = g.Id;
                        genres.Add(genre, genre_id);
                    }
                }

                var book = new Book
                {
                    ArchiveIndex = zip_book.ArchiveIndex,
                    ArchivePath = archive_path,
                    AuthorIds = author_ids.ToList(),
                    Year = zip_book.Year,
                    Description = zip_book.Description,
                    Title = zip_book.Title,
                    GenreId = genre_id,
                    Format = zip_book.Format.ToString()
                };
                await Tables.Books.Write(book);
            }
        }
    }
}
