using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApi.Common;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.Collections.Generic;

namespace DtoImporterService
{
	public class BookImporter : ItemsImporterBase
	{
		protected override IValidator Validator => new BookValidator();

		public Dictionary<string, Book> Cache { get; private set; } = new();

		private ContributorImporter contributorImporter { get; }
		private SeriesImporter seriesImporter { get; }
		private CategoryImporter categoryImporter { get; }

		public BookImporter(LibationContext context) : base(context)
		{
			contributorImporter = new ContributorImporter(DbContext);
			seriesImporter = new SeriesImporter(DbContext);
			categoryImporter = new CategoryImporter(DbContext);
		}

		protected override int DoImport(IEnumerable<ImportItem> importItems)
		{
			// pre-req.s
			contributorImporter.Import(importItems);
			seriesImporter.Import(importItems);
			categoryImporter.Import(importItems);

			// load db existing => hash table
			loadLocal_books(importItems);

			// upsert
			var qtyNew = upsertBooks(importItems);
			return qtyNew;
		}

		private void loadLocal_books(IEnumerable<ImportItem> importItems)
		{
			// get distinct
			var productIds = importItems
				.Select(i => i.DtoItem.ProductId)
				.Distinct()
				.ToList();

			Cache = DbContext.Books
				.GetBooks(b => productIds.Contains(b.AudibleProductId))
				.ToDictionarySafe(b => b.AudibleProductId);
		}

		private int upsertBooks(IEnumerable<ImportItem> importItems)
		{
			var qtyNew = 0;

			foreach (var item in importItems)
			{
				if (!Cache.TryGetValue(item.DtoItem.ProductId, out var book))
				{
					book = createNewBook(item);
					qtyNew++;
				}

				updateBook(item, book);
			}

			return qtyNew;
		}

		private Book createNewBook(ImportItem importItem)
		{
			var item = importItem.DtoItem;

			var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product;

			// absence of authors is very rare, but possible
			if (!item.Authors?.Any() ?? true)
				item.Authors = new[] { new Person { Name = "", Asin = null } };

			// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
			var authors = item
				.Authors
				.Select(a => contributorImporter.Cache[a.Name])
				.ToList();

			var narrators
				= item.Narrators is null || !item.Narrators.Any()
				// if no narrators listed, author is the narrator
				? authors
				// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
				: item
					.Narrators
					.Select(n => contributorImporter.Cache[n.Name])
					.ToList();

			// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
			// absence of categories is also possible

			// CATEGORY HACK: only use the 1st 2 categories
			// (real impl: var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? "";)
			var lastCategory
				= item.Categories.Length == 0 ? ""
				: item.Categories.Length == 1 ? item.Categories[0].CategoryId
				// 2+
				: item.Categories[1].CategoryId;

			var category = categoryImporter.Cache[lastCategory];

			Book book;
			try
			{
				book = DbContext.Books.Add(new Book(
					new AudibleProductId(item.ProductId),
					item.TitleWithSubtitle,
					item.Description,
					item.LengthInMinutes,
					contentType,
					authors,
					narrators,
					category,
					importItem.LocaleName)
					).Entity;
				Cache.Add(book.AudibleProductId, book);
			}
			catch (Exception ex)
			{
				Serilog.Log.Logger.Error(ex, "Error adding book. {@DebugInfo}", new {
					item.ProductId,
					item.TitleWithSubtitle,
					item.Description,
					item.LengthInMinutes,
					contentType,
					QtyAuthors = authors?.Count,
					QtyNarrators = narrators?.Count,
					Category = category?.Name,
					importItem.LocaleName
				});
				throw;
			}

			var publisherName = item.Publisher;
			if (!string.IsNullOrWhiteSpace(publisherName))
			{
				var publisher = contributorImporter.Cache[publisherName];
				book.ReplacePublisher(publisher);
			}

			book.UpdateBookDetails(item.IsAbridged, item.DatePublished);

			if (item.PdfUrl is not null)
				book.AddSupplementDownloadUrl(item.PdfUrl.ToString());

			return book;
		}

		private void updateBook(ImportItem importItem, Book book)
		{
			var item = importItem.DtoItem;

			// set/update book-specific info which may have changed
			if (item.PictureId is not null)
				book.PictureId = item.PictureId;
			
			if (item.PictureLarge is not null)
				book.PictureLarge = item.PictureLarge;

			book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);

			// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
			book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);

			// update series even for existing books. these are occasionally updated
			// these will upsert over library-scraped series, but will not leave orphans
			if (item.Series is not null)
			{
				foreach (var seriesEntry in item.Series)
				{
					var series = seriesImporter.Cache[seriesEntry.SeriesId];
					book.UpsertSeries(series, seriesEntry.Sequence);
				}
			}
		}
	}
}
