namespace Hims.Infrastructure.Repositories.Dapper
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Threading.Tasks;
    using global::Dapper;
    using Shared.Dapper.Extensions;
    using Shared.Dapper.SqlGenerator;

    /// <summary>
    /// Base Repository
    /// </summary>
    /// <typeparam name="TEntity">
    /// The entity.
    /// </typeparam>
    public partial class DapperRepository<TEntity> where TEntity : class
    {
        /// <summary>
        /// The execute join query.
        /// </summary>
        /// <param name="sqlQuery">
        /// The sql query.
        /// </param>
        /// <param name="transaction">
        /// The transaction.
        /// </param>
        /// <param name="includes">
        /// The includes.
        /// </param>
        /// <typeparam name="TChild1">
        /// The t child 1
        /// </typeparam>
        /// <typeparam name="TChild2">
        /// The t child 2
        /// </typeparam>
        /// <typeparam name="TChild3">
        /// The t child 3
        /// </typeparam>
        /// <typeparam name="TChild4">
        /// The t child 4
        /// </typeparam>
        /// <typeparam name="TChild5">
        /// The t child 5
        /// </typeparam>
        /// <typeparam name="TChild6">
        /// The t child 6
        /// </typeparam>
        /// <returns>
        /// The <see cref="IEnumerable{T}"/>.
        /// </returns>
        /// <exception cref="NotSupportedException">
        /// The exception
        /// </exception>
        protected virtual IEnumerable<TEntity> ExecuteJoinQuery<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
            SqlQuery sqlQuery,
            IDbTransaction transaction,
            params Expression<Func<TEntity, object>>[] includes)
        {
            var type = typeof(TEntity);

            var childPropertyNames = includes.Select(ExpressionHelper.GetPropertyName).ToList();
            var childProperties = childPropertyNames.Select(p => type.GetProperty(p)).ToList();

            var keyProperties = this.SqlGenerator.KeySqlProperties.Select(q => q.PropertyInfo).ToArray();
            var childKeyProperties = new List<PropertyInfo>();
            var lookup = new Dictionary<object, TEntity>();
            const bool Buffered = true;

            var splitOn = string.Join(",", childKeyProperties.Select(q => q.Name));

            switch (includes.Length)
            {
                case 1:
                    this.Connection.Query<TEntity, TChild1, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1) => EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                            lookup,
                            keyProperties,
                            childKeyProperties,
                            childProperties,
                            childPropertyNames,
                            type,
                            entity,
                            child1),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;

                case 2:
                    this.Connection.Query<TEntity, TChild1, TChild2, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                                keyProperties,
                                childKeyProperties,
                                childProperties,
                                childPropertyNames,
                                type,
                                entity,
                                child1,
                                child2),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;

                case 3:
                    this.Connection.Query<TEntity, TChild1, TChild2, TChild3, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                                keyProperties,
                                childKeyProperties,
                                childProperties,
                                childPropertyNames,
                                type,
                                entity,
                                child1,
                                child2,
                                child3),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;

                case 4:
                    this.Connection.Query<TEntity, TChild1, TChild2, TChild3, TChild4, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3, child4) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                            keyProperties,
                            childKeyProperties,
                            childProperties,
                            childPropertyNames,
                            type,
                            entity,
                            child1,
                            child2,
                            child3,
                            child4),
                    sqlQuery.Param,
                    transaction,
                    Buffered,
                    splitOn);
                    break;

                case 5:
                    this.Connection.Query<TEntity, TChild1, TChild2, TChild3, TChild4, TChild5, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3, child4, child5) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                            keyProperties,
                            childKeyProperties,
                            childProperties,
                            childPropertyNames,
                            type,
                            entity,
                            child1,
                            child2,
                            child3,
                            child4,
                            child5),
                    sqlQuery.Param,
                    transaction,
                    Buffered,
                    splitOn);
                    break;

                case 6:
                    this.Connection.Query<TEntity, TChild1, TChild2, TChild3, TChild4, TChild5, TChild6, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3, child4, child5, child6) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                            keyProperties,
                            childKeyProperties,
                            childProperties,
                            childPropertyNames,
                            type,
                            entity,
                            child1,
                            child2,
                            child3,
                            child4,
                            child5,
                            child6),
                    sqlQuery.Param,
                    transaction,
                    Buffered,
                    splitOn);
                    break;

                default:
                    throw new NotSupportedException();
            }

            return lookup.Values;
        }

        /// <summary>
        /// The execute join query async.
        /// </summary>
        /// <param name="sqlQuery">
        /// The sql query.
        /// </param>
        /// <param name="transaction">
        /// The transaction.
        /// </param>
        /// <param name="includes">
        /// The includes.
        /// </param>
        /// <typeparam name="TChild1">
        /// The t child 1
        /// </typeparam>
        /// <typeparam name="TChild2">
        /// The t child 2
        /// </typeparam>
        /// <typeparam name="TChild3">
        /// The t child 3
        /// </typeparam>
        /// <typeparam name="TChild4">
        /// The t child 4
        /// </typeparam>
        /// <typeparam name="TChild5">
        /// The t child 5
        /// </typeparam>
        /// <typeparam name="TChild6">
        /// The t child 6
        /// </typeparam>
        /// <returns>
        /// The <see cref="IEnumerable{T}"/>.
        /// </returns>
        /// <exception cref="NotSupportedException">
        /// The exception
        /// </exception>
        protected virtual async Task<IEnumerable<TEntity>> ExecuteJoinQueryAsync<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
            SqlQuery sqlQuery,
            IDbTransaction transaction,
            params Expression<Func<TEntity, object>>[] includes)
        {
            var type = typeof(TEntity);

            var childPropertyNames = includes.Select(ExpressionHelper.GetPropertyName).ToList();
            var childProperties = childPropertyNames.Select(p => type.GetProperty(p)).ToList();

            if (!this.SqlGenerator.KeySqlProperties.Any())
            {
                throw new NotSupportedException("Join doesn't support without [Key] attribute");
            }

            var keyProperties = this.SqlGenerator.KeySqlProperties.Select(q => q.PropertyInfo).ToArray();
            var childKeyProperties = new List<PropertyInfo>();

            if (!childKeyProperties.Any())
            {
                throw new NotSupportedException("Join doesn't support without [Key] attribute");
            }

            var lookup = new Dictionary<object, TEntity>();
            const bool Buffered = true;

            var splitOn = string.Join(",", childKeyProperties.Select(q => q.Name));

            switch (includes.Length)
            {
                case 1:
                    await this.Connection.QueryAsync<TEntity, TChild1, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1) => EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                            lookup,
                            keyProperties,
                            childKeyProperties,
                            childProperties,
                            childPropertyNames,
                            type,
                            entity,
                            child1),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;
                case 2:
                    await this.Connection.QueryAsync<TEntity, TChild1, TChild2, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                                keyProperties,
                                childKeyProperties,
                                childProperties,
                                childPropertyNames,
                                type,
                                entity,
                                child1,
                                child2),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;
                case 3:
                    await this.Connection.QueryAsync<TEntity, TChild1, TChild2, TChild3, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                                keyProperties,
                                childKeyProperties,
                                childProperties,
                                childPropertyNames,
                                type,
                                entity,
                                child1,
                                child2,
                                child3),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;
                case 4:
                    await this.Connection.QueryAsync<TEntity, TChild1, TChild2, TChild3, TChild4, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3, child4) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                                keyProperties,
                                childKeyProperties,
                                childProperties,
                                childPropertyNames,
                                type,
                                entity,
                                child1,
                                child2,
                                child3,
                                child4),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;
                case 5:
                    await this.Connection.QueryAsync<TEntity, TChild1, TChild2, TChild3, TChild4, TChild5, TEntity>(
                        sqlQuery.GetSql(),
                        (entity, child1, child2, child3, child4, child5) =>
                            EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                lookup,
                                keyProperties,
                                childKeyProperties,
                                childProperties,
                                childPropertyNames,
                                type,
                                entity,
                                child1,
                                child2,
                                child3,
                                child4,
                                child5),
                        sqlQuery.Param,
                        transaction,
                        Buffered,
                        splitOn);
                    break;
                case 6:
                    await this.Connection
                        .QueryAsync<TEntity, TChild1, TChild2, TChild3, TChild4, TChild5, TChild6, TEntity>(
                            sqlQuery.GetSql(),
                            (entity, child1, child2, child3, child4, child5, child6) =>
                                EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
                                    lookup,
                                    keyProperties,
                                    childKeyProperties,
                                    childProperties,
                                    childPropertyNames,
                                    type,
                                    entity,
                                    child1,
                                    child2,
                                    child3,
                                    child4,
                                    child5,
                                    child6),
                            sqlQuery.Param,
                            transaction,
                            Buffered,
                            splitOn);
                    break;
                default:
                    throw new NotSupportedException();
            }

            return lookup.Values;
        }

        /// <summary>
        /// The entity join mapping.
        /// </summary>
        /// <param name="lookup">
        /// The lookup.
        /// </param>
        /// <param name="keyProperties">
        /// The key properties.
        /// </param>
        /// <param name="childKeyProperties">
        /// The child key properties.
        /// </param>
        /// <param name="childProperties">
        /// The child properties.
        /// </param>
        /// <param name="propertyNames">
        /// The property names.
        /// </param>
        /// <param name="entityType">
        /// The entity type.
        /// </param>
        /// <param name="entity">
        /// The entity.
        /// </param>
        /// <param name="entities">
        /// The entities.
        /// </param>
        /// <typeparam name="TChild1">
        /// The t child 1
        /// </typeparam>
        /// <typeparam name="TChild2">
        /// The t child 2
        /// </typeparam>
        /// <typeparam name="TChild3">
        /// The t child 3
        /// </typeparam>
        /// <typeparam name="TChild4">
        /// The t child 4
        /// </typeparam>
        /// <typeparam name="TChild5">
        /// The t child 5
        /// </typeparam>
        /// <typeparam name="TChild6">
        /// The t child 6
        /// </typeparam>
        /// <returns>
        /// The <see cref="TEntity"/>.
        /// The entity
        /// </returns>
        /// <exception cref="NotSupportedException">
        /// The exception
        /// </exception>
        private static TEntity EntityJoinMapping<TChild1, TChild2, TChild3, TChild4, TChild5, TChild6>(
            IDictionary<object, TEntity> lookup,
            PropertyInfo[] keyProperties,
            IList<PropertyInfo> childKeyProperties,
            IList<PropertyInfo> childProperties,
            IList<string> propertyNames,
            Type entityType,
            TEntity entity,
            params object[] entities)
        {
            var compositeKeyProperty = string.Join("|", keyProperties.Select(q => q.GetValue(entity)?.ToString()));

            if (!lookup.TryGetValue(compositeKeyProperty, out var target))
            {
                lookup.Add(compositeKeyProperty, target = entity);
            }

            for (var i = 0; i < entities.Length; i++)
            {
                var child = entities[i];
                var childProperty = childProperties[i];
                var propertyName = propertyNames[i];
                var childKeyProperty = childKeyProperties[i];

                if (childProperty != null && childProperty.PropertyType.IsGenericType())
                {
                    var list = (IList)childProperty.GetValue(target);
                    if (list == null)
                    {
                        list = i switch
                        {
                            0 => new List<TChild1>(),
                            1 => new List<TChild2>(),
                            2 => new List<TChild3>(),
                            3 => new List<TChild4>(),
                            4 => new List<TChild5>(),
                            5 => new List<TChild6>(),
                            _ => throw new NotSupportedException()
                        };

                        childProperty.SetValue(target, list);
                    }

                    if (child == null)
                    {
                        continue;
                    }

                    var childKey = childKeyProperty.GetValue(child);
                    var exist = (from object item in list select childKeyProperty.GetValue(item)).Contains(childKey);
                    if (!exist)
                    {
                        list.Add(child);
                    }
                }
                else
                {
                    entityType.GetProperty(propertyName)?.SetValue(target, child);
                }
            }

            return target;
        }

        /// <summary>
        /// The don't map.
        /// </summary>

        // ReSharper disable once IdentifierTypo
        // ReSharper disable once ClassNeverInstantiated.Local
        private class DontMap
        {
        }
    }
}
