using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace Kyoo
{
	/// 
	/// The database handle used for all local repositories.
	/// This is an abstract class. It is meant to be implemented by plugins. This allow the core to be database agnostic.
	///  
	/// 
	/// It should not be used directly, to access the database use a  
	public abstract class DatabaseContext : DbContext
	{
		/// 
		/// All libraries of Kyoo. See  
		public DbSet Libraries { get; set; }
		/// 
		/// All collections of Kyoo. See  
		public DbSet Collections { get; set; }
		/// 
		/// All shows of Kyoo. See  
		public DbSet Shows { get; set; }
		/// 
		/// All seasons of Kyoo. See  
		public DbSet Seasons { get; set; }
		/// 
		/// All episodes of Kyoo. See  
		public DbSet Episodes { get; set; }
		/// 
		/// All tracks of Kyoo. See  
		public DbSet Tracks { get; set; }
		/// 
		/// All genres of Kyoo. See  
		public DbSet Genres { get; set; }
		/// 
		/// All people of Kyoo. See  
		public DbSet People { get; set; }
		/// 
		/// All studios of Kyoo. See  
		public DbSet Studios { get; set; }
		/// 
		/// All providers of Kyoo. See  
		public DbSet Providers { get; set; }
		/// 
		/// All metadataIDs (ExternalIDs) of Kyoo. See  
		public DbSet MetadataIds { get; set; }
		/// 
		/// The list of registered users.
		///  
		public DbSet Users { get; set; }
		
		/// 
		/// All people's role. See  
		public DbSet PeopleRoles { get; set; }
		
		/// 
		/// Episodes with a watch percentage. See  
		public DbSet WatchedEpisodes { get; set; }
		/// 
		/// Get a generic link between two resource types.
		///  
		/// Types are order dependant. You can't inverse the order. Please always put the owner first. 
		/// The first resource type of the relation. It is the owner of the second 
		/// The second resource type of the relation. It is the contained resource. 
		/// All links between the two types. 
		public DbSet()
			where T1 : class, IResource
			where T2 : class, IResource
		{
			return Set
		/// The default constructor
		///  
		protected DatabaseContext() { }
		/// 
		/// Create a new  
		/// 
		/// Set basic configurations (like preventing query tracking)
		///  
		/// 
		/// Set database parameters to support every types of Kyoo.
		///  
		/// ()
				.Property(t => t.IsDefault)
				.ValueGeneratedNever();
			
			modelBuilder.Entity()
				.Property(t => t.IsForced)
				.ValueGeneratedNever();
			modelBuilder.Entity()
				.HasMany(x => x.Libraries)
				.WithMany(x => x.Providers)
				.UsingEntity.PrimaryKey));
			
			modelBuilder.Entity()
				.HasMany(x => x.Libraries)
				.WithMany(x => x.Collections)
				.UsingEntity.PrimaryKey));
			
			modelBuilder.Entity()
				.HasMany(x => x.Libraries)
				.WithMany(x => x.Shows)
				.UsingEntity.PrimaryKey));
			
			modelBuilder.Entity()
				.HasMany(x => x.Collections)
				.WithMany(x => x.Shows)
				.UsingEntity.PrimaryKey));
			modelBuilder.Entity()
				.HasMany(x => x.Shows)
				.WithMany(x => x.Genres)
				.UsingEntity.PrimaryKey));
			
			modelBuilder.Entity()
				.HasMany(x => x.Watched)
				.WithMany("users")
				.UsingEntity.PrimaryKey));
			modelBuilder.Entity()
				.HasOne(x => x.Show)
				.WithMany(x => x.ExternalIDs)
				.OnDelete(DeleteBehavior.Cascade);
			modelBuilder.Entity()
				.HasOne(x => x.Season)
				.WithMany(x => x.ExternalIDs)
				.OnDelete(DeleteBehavior.Cascade);
			modelBuilder.Entity()
				.HasOne(x => x.Episode)
				.WithMany(x => x.ExternalIDs)
				.OnDelete(DeleteBehavior.Cascade);
			modelBuilder.Entity()
				.HasOne(x => x.People)
				.WithMany(x => x.ExternalIDs)
				.OnDelete(DeleteBehavior.Cascade);
			modelBuilder.Entity()
				.HasOne(x => x.Provider)
				.WithMany(x => x.MetadataLinks)
				.OnDelete(DeleteBehavior.Cascade);
			modelBuilder.Entity()
				.HasKey(x => new {First = x.FirstID, Second = x.SecondID});
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity().Property(x => x.Slug).IsRequired();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => new {x.ShowID, x.SeasonNumber})
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => new {x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber})
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => new {x.EpisodeID, x.Type, x.Language, x.TrackIndex, x.IsForced})
				.IsUnique();
			modelBuilder.Entity()
				.HasIndex(x => x.Slug)
				.IsUnique();
		}
		/// 
		/// Return a new or an in cache temporary object wih the same ID as the one given
		///  
		/// The type of the resource 
		/// A resource that is now tracked by this context. 
		public T GetTemporaryObject(T model)
			where T : class, IResource
		{
			T tmp = Set().Local.FirstOrDefault(x => x.ID == model.ID);
			if (tmp != null)
				return tmp;
			Entry(model).State = EntityState.Unchanged;
			return model;
		}
		/// 
		/// Save changes that are applied to this context.
		///  
		/// A duplicated item has been found. 
		/// The number of state entries written to the database. 
		public override int SaveChanges()
		{
			try
			{
				return base.SaveChanges();
			}
			catch (DbUpdateException ex)
			{
				DiscardChanges();
				if (IsDuplicateException(ex))
					throw new DuplicatedItemException();
				throw;
			}
		}
		/// 
		/// Save changes that are applied to this context.
		///  
		/// A duplicated item has been found. 
		/// The number of state entries written to the database. 
		public override int SaveChanges(bool acceptAllChangesOnSuccess)
		{
			try
			{
				return base.SaveChanges(acceptAllChangesOnSuccess);
			}
			catch (DbUpdateException ex)
			{
				DiscardChanges();
				if (IsDuplicateException(ex))
					throw new DuplicatedItemException();
				throw;
			}
		}
		
		/// 
		/// Save changes that are applied to this context.
		///  
		/// A duplicated item has been found. 
		/// The number of state entries written to the database. 
		public int SaveChanges(string duplicateMessage)
		{
			try
			{
				return base.SaveChanges();
			}
			catch (DbUpdateException ex)
			{
				DiscardChanges();
				if (IsDuplicateException(ex))
					throw new DuplicatedItemException(duplicateMessage);
				throw;
			}
		}
		/// 
		/// Save changes that are applied to this context.
		///  
		/// A duplicated item has been found. 
		/// The number of state entries written to the database. 
		public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, 
			CancellationToken cancellationToken = new())
		{
			try
			{
				return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
			}
			catch (DbUpdateException ex)
			{
				DiscardChanges();
				if (IsDuplicateException(ex))
					throw new DuplicatedItemException();
				throw;
			}
		}
		/// 
		/// Save changes that are applied to this context.
		///  
		/// A duplicated item has been found. 
		/// The number of state entries written to the database. 
		public override async Task SaveChangesAsync(CancellationToken cancellationToken = new())
		{
			try
			{
				return await base.SaveChangesAsync(cancellationToken);
			}
			catch (DbUpdateException ex)
			{
				DiscardChanges();
				if (IsDuplicateException(ex))
					throw new DuplicatedItemException();
				throw;
			}
		}
		
		/// 
		/// Save changes that are applied to this context.
		///  
		/// A duplicated item has been found. 
		/// The number of state entries written to the database. 
		public async Task SaveChangesAsync(string duplicateMessage,
			CancellationToken cancellationToken = new())
		{
			try
			{
				return await base.SaveChangesAsync(cancellationToken);
			}
			catch (DbUpdateException ex)
			{
				DiscardChanges();
				if (IsDuplicateException(ex))
					throw new DuplicatedItemException(duplicateMessage);
				throw;
			}
		}
		/// 
		/// Save changes if no duplicates are found. If one is found, no change are saved but the current changes are no discarded.
		/// The current context will still hold those invalid changes.
		///  
		/// The number of state entries written to the database or -1 if a duplicate exist. 
		public async Task SaveIfNoDuplicates(CancellationToken cancellationToken = new())
		{
			try
			{
				return await SaveChangesAsync(cancellationToken);
			}
			catch (DuplicatedItemException)
			{
				return -1;
			}
		}
		/// 
		/// Save items or retry with a custom method if a duplicate is found.
		///  
		/// The type of the item to save 
		/// The number of state entries written to the database. 
		public Task SaveOrRetry(T obj, Func onFail, CancellationToken cancellationToken = new())
		{
			return SaveOrRetry(obj, onFail, 0, cancellationToken);
		}
		
		/// 
		/// Save items or retry with a custom method if a duplicate is found.
		///  
		/// The type of the item to save 
		/// The number of state entries written to the database. 
		private async Task SaveOrRetry(T obj,
			Func onFail,
			int recurse,
			CancellationToken cancellationToken = new())
		{
			try
			{
				await base.SaveChangesAsync(true, cancellationToken);
				return obj;
			}
			catch (DbUpdateException ex) when (IsDuplicateException(ex))
			{
				recurse++;
				return await SaveOrRetry(onFail(obj, recurse), onFail, recurse, cancellationToken);
			}
			catch (DbUpdateException)
			{
				DiscardChanges();
				throw;
			}
		}
		/// 
		/// Check if the exception is a duplicated exception.
		///  
		/// True if the exception is a duplicate exception. False otherwise 
		protected abstract bool IsDuplicateException(Exception ex);
		/// 
		/// Delete every changes that are on this context.
		///  
		private void DiscardChanges()
		{
			foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged
			                                                                 && x.State != EntityState.Detached))
			{
				entry.State = EntityState.Detached;
			}
		}
		/// 
		/// Perform a case insensitive like operation.
		///  
		/// The type of the item to query 
		/// An expression representing the like query. It can directly be passed to a where call. 
		public abstract Expression> Like(Expression> query, string format);
	}
}