using System;
using System.Reflection;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using FastMember;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using System.Linq;

namespace Knoks.Framework.DataAccess
{
    public class ProcExecutor : IProcExecutor
    {
        private readonly Dictionary<string, string> _dbConnections;
        private readonly Dictionary<string, Dictionary<string, List<ParamMetaData>>> _metaData;
        private readonly ILogger<ProcExecutor> _logger;

        public ProcExecutor(IDictionary<string, string> dbConnections, ILogger<ProcExecutor> logger)
        {
            _dbConnections = new Dictionary<string, string>(dbConnections, StringComparer.OrdinalIgnoreCase);
            _metaData = new Dictionary<string, Dictionary<string, List<ParamMetaData>>>(StringComparer.OrdinalIgnoreCase);
            _logger = logger;
            foreach (var conn in _dbConnections.Keys)
            {
                _metaData[conn] = DbMetaData.LoadParamMetaDataSet(_dbConnections[conn]);
            }
        }

        public async Task<DataResult> Go(string procName, params object[] procParams)
        {
            return await Go(DatabaseNames.Operate, procName, procParams);
        }

        public async Task<DataResult> Go(string databaseName, string procName, params object[] procParams)
        {
            if (string.IsNullOrWhiteSpace(databaseName))
                throw new ArgumentException("Undefined database name.", "databaseName");

            if (string.IsNullOrWhiteSpace(procName))
                throw new ArgumentException("Undefined stored procedure name.", "procName");

            var tmpProcParams = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

            try
            {
                foreach (var prm in procParams)
                {
                    if (prm != null)
                    {
                        if (prm is IEnumerable<KeyValuePair<string, object>>)
                        {
                            foreach (var param in prm as IEnumerable<KeyValuePair<string, object>>)
                                AddParam(tmpProcParams, param);
                        }
                        else if (prm is KeyValuePair<string, object>)
                        {
                            AddParam(tmpProcParams, (KeyValuePair<string, object>)prm);
                        }
                        else
                        {
                            if (prm is string)
                                throw new ArgumentException("Stored procedure parameters cannot be represented with string type.", nameof(procParams));

                            var type = prm.GetType();
                            var typeInfo = type.GetTypeInfo();

                            if (typeInfo.IsDefined(typeOfIgnoreProcParamAttribute))
                                throw new ArgumentException("The type is marked with ignore attribute.", "procParams");

                            if (!typeInfo.IsClass)
                                throw new ArgumentException("Stored procedure parameters should be represented with class type.", nameof(procParams));

                            if (typeInfo.IsArray)
                                throw new ArgumentException("Stored procedure parameters cannot be represented with non IEnumerable<KeyValuePair<string, object>> array.", nameof(procParams));

                            var accessor = TypeAccessor.Create(type);
                            foreach (var member in accessor.GetMembers().Where(m => !m.IsDefined(typeOfIgnoreProcParamAttribute)))
                                AddParam(tmpProcParams, member.Name, accessor[prm, member.Name]);
                        }
                    }
                }
            }
            catch(Exception ex)
            {
                _logger.LogError(ex.ToString());
                throw ex;
            }

            return await Execute(databaseName, procName, tmpProcParams);
        }

        private static readonly Type typeOfIgnoreProcParamAttribute = typeof(ProcParamIgnoreAttribute);
        private static void AddParam(Dictionary<string, object> dic, KeyValuePair<string, object> kv)
        {
            if (!kv.Value.GetType().GetTypeInfo().IsDefined(typeOfIgnoreProcParamAttribute))
                AddParam(dic, kv.Key, kv.Value);
        }

        private static void AddParam(Dictionary<string, object> dic, string key, object value)
        {
                dic.Add("@" + key, value);
        }

        private async Task<DataResult> Execute(string databaseName, string procName, Dictionary<string, object> procParams)
        {
            try
            {
                using (var con = new SqlConnection(_dbConnections[databaseName]))
                using (var cmd = new SqlCommand(procName, con) { CommandType = CommandType.StoredProcedure })
                {
                    if (procParams.Count > 0 && !_metaData[databaseName].ContainsKey(procName))
                    {
                        throw new InvalidOperationException($"The procedure '{procName}' does not have the following parameter(s): [{string.Join(", ", procParams.Keys)}].");
                    }
                    else if (procParams.Count > 0)
                    {
                        foreach (var md in _metaData[databaseName][procName])
                        {
                            if (procParams.TryGetValue(md.ParamName, out object value))
                            {
                                procParams.Remove(md.ParamName);
                                cmd.Parameters.Add(md.GetSqlParameter(value));
                            }
                            else if (md.Direction == ParameterDirection.InputOutput || md.Direction == ParameterDirection.Output)
                            {
                                cmd.Parameters.Add(md.GetSqlParameter(value));
                            }
                        }
                    }

                    if (procParams.Count > 0)
                        throw new InvalidOperationException($"The procedure '{procName}' does not have the following parameter(s): [{string.Join(", ", procParams.Keys)}].");

                    var returnValue = new SqlParameter() { Direction = ParameterDirection.ReturnValue };
                    cmd.Parameters.Add(returnValue);

                    con.Open();

                    using (var reader = await cmd.ExecuteReaderAsync())
                    {
                        var result = new DataResult();

                        short tableId = 0;
                        do
                        {
                            result.Тables.Add(tableId++, new Table(reader));
                        }
                        while (reader.NextResult());

                        foreach (SqlParameter param in cmd.Parameters)
                        {
                            if (param.Direction == ParameterDirection.Output || param.Direction == ParameterDirection.InputOutput)
                            {
                                result.OutputParams.Add(param.ParameterName.Substring(1), param.Value == DBNull.Value ? null : param.Value);
                            }
                        }

                        if (returnValue.Value != null)
                            result.ReturnValue = Convert.ToInt32(returnValue.Value);

                        return result;
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.ToString());
                throw ex;
            }
        }
    }
}
