using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using iiie.Logs.DataAccess;
using iiie.Logs.DBO;
using System.Linq.Dynamic;

namespace iiie.WebApiUtils.BusinessManager
{
    /// <summary>
    /// Helper for SQL Server data access
    /// </summary>
    public abstract class SqlServerManager<TDbObject, TDbo, TEntities, TThis>
        where TDbObject : class
        where TEntities : DbContext, new()
        where TThis : SqlServerManager<TDbObject, TDbo, TEntities, TThis>,  new()
    {
        private class DbObjectId
        {
            public long id { get; set; }
        }

        /// <summary>
        /// Return the table to be used
        /// </summary>
        /// <param name="db">The database</param>
        /// <returns>The table instance</returns>
        public abstract DbSet<TDbObject> GetTable(TEntities db);

        public abstract TDbo DbToDbo(TDbObject obj);

        public abstract TDbObject DboToDb(TDbo obj);

        /// <summary>
        /// Return the database table to use
        /// </summary>
        /// <param name="db">The database instance</param>
        /// <returns>The table</returns>
        public static DbSet<TDbObject> GetTableStatic(TEntities db)
        {
            var type = new TThis();
            return type.GetTable(db);
        }

        /// <summary>
        /// Helper to convert DB object to DBO
        /// </summary>
        /// <param name="res">The result to convert</param>
        /// <returns>The DBO object</returns>
        public static OpResult<TDbo> DbToDboStatic(OpResult<TDbObject> res)
        {
            if (!res)
                return OpResult<TDbo>.Error(res);
            var obj = new TThis();
            return OpResult<TDbo>.Ok(obj.DbToDbo(res.Data));
        }
        
        /// <summary>
        /// Helper to convert DBO object to DB
        /// </summary>
        /// <param name="res">The result to convert</param>
        /// <returns>The DB object</returns>
        public static OpResult<TDbObject> DboToDbStatic(OpResult<TDbo> res)
        {
            if (!res)
                return OpResult<TDbObject>.Error(res);
            var obj = new TThis();
            return OpResult<TDbObject>.Ok(obj.DboToDb(res.Data));
        }

        /// <summary>
        /// Helper to convert DB object to DBO
        /// </summary>
        /// <param name="res">The result to convert</param>
        /// <returns>The DBO object</returns>
        public static TDbo DbToDboStatic(TDbObject res)
        {
            var obj = new TThis();
            return obj.DbToDbo(res);
        }
        
        /// <summary>
        /// Helper to convert DBO object to DB
        /// </summary>
        /// <param name="res">The result to convert</param>
        /// <returns>The DB object</returns>
        public static TDbObject DboToDbStatic(TDbo res)
        {
            var obj = new TThis();
            return obj.DboToDb(res);
        }

        /// <summary>
        /// Execute SQL quries in try catch
        /// </summary>
        /// <param name="func">The queries to be performed</param>
        /// <returns>The result or an error</returns>
        public static OpResult<U> Execute<U>(Func<TEntities, DbSet<TDbObject>, OpResult<U>> func)
        {
            try
            {
                using (var db = new TEntities())
                {
                    var table = GetTableStatic(db);
                    return func(db, table);
                }
            }
            catch (Exception e)
            {
                return Logger.Error<U>(ResultStatus.DBError, e);
            }
        }

        /// <summary>
        /// Get a single DB object matching the predicate
        /// </summary>
        /// <param name="predicate">The predicate</param>
        /// <returns>The object that match, or null</returns>
        public static OpResult<TDbObject> GetSingle(Expression<Func<TDbObject, bool>> predicate)
        {
            return Execute((db, table) =>
            {
                var o = table.FirstOrDefault(predicate);
                if (o == null)
                {
                    return OpResult<TDbObject>.Error(ResultStatus.NotFound, typeof(TThis).Name + ": Value not found", "");
                }
                return OpResult<TDbObject>.Ok(o);

            });
        }

        /// <summary>
        /// Get a single DBO matching the predicate
        /// </summary>
        /// <param name="predicate">The predicate</param>
        /// <returns>The object that match, or null</returns>
        public static OpResult<TDbo> GetSingleDbo(Expression<Func<TDbObject, bool>> predicate)
        {
            return DbToDboStatic(GetSingle(predicate));
        }

        /// <summary>
        /// Get all DB object matching the predicate
        /// </summary>
        /// <param name="predicate">The predicate</param>
        /// <returns>All matching objects</returns>
        public static OpResult<IEnumerable<TDbObject>> GetMultiple(Expression<Func<TDbObject, bool>> predicate)
        {
            return Execute((db, table) => OpResult<IEnumerable<TDbObject>>.Ok(table.Where(predicate).ToList()));
        }

        /// <summary>
        /// Get all DBO matching the predicate
        /// </summary>
        /// <typeparam name="TDbo">The DBO type</typeparam>
        /// <param name="predicate">The predicate</param>
        /// <returns>All matching objects</returns>
        public static OpResult<IEnumerable<TDbo>> GetMultipleDbo(Expression<Func<TDbObject, bool>> predicate)
        {
            var res = GetMultiple(predicate);
            if (!res)
                return OpResult<IEnumerable<TDbo>>.Error(res);
            var dbo = res.Data.Select(DbToDboStatic);
            return OpResult<IEnumerable<TDbo>>.Ok(dbo);
        }

        /// <summary>
        /// Add an entry in the database
        /// </summary>
        /// <param name="obj">The object to be added</param>
        /// <returns>Always true or an OpResult error</returns>
        public static OpResult<bool> Add(TDbObject obj)
        {
            return Execute((db, table) =>
            {
                table.Add(obj);
                db.SaveChanges();
                return OpResult<bool>.Ok(true);
            });
        }

        /// <summary>
        /// Add an entry in the database
        /// </summary>
        /// <param name="obj"></param>
        /// <returns>Always true or an OpResult error</returns>
        public static OpResult<bool> AddDbo(TDbo obj)
        {
            return Add(DboToDbStatic(obj));
        }

        /// <summary>
        /// Get an object by its primary key(s)
        /// </summary>
        /// <param name="pairs">The key names and values</param>
        /// <returns>The object</returns>
        public static OpResult<TDbObject> GetByPrimary(params KeyValuePair<string, object>[] pairs)
        {
            var param = Expression.Parameter(typeof(TDbObject), "x");
            Expression totalExp = null;
            foreach (var pair in pairs)
            {
                var equalExp = Expression.Equal(Expression.Property(param, pair.Key), Expression.Constant(pair.Value));
                totalExp = totalExp == null ? equalExp : Expression.And(equalExp, totalExp);
            }
            var lambda = Expression.Lambda<Func<TDbObject, bool>>(totalExp, param);
            return GetSingle(lambda);
        }

        /// <summary>
        /// Get an object by its id
        /// </summary>
        /// <param name="id">The id of the object</param>
        /// <returns>The object</returns>
        public static OpResult<TDbObject> GetById(long id)
        {
            return GetByPrimary(new KeyValuePair<string, object>("id", id));
        }

        /// <summary>
        /// Get an object by its id
        /// </summary>
        /// <param name="id">The id of the object</param>
        /// <returns>The object</returns>
        public static OpResult<TDbo> GetByIdDbo(long id)
        {
            return DbToDboStatic(GetById(id));
        }
    }
}
