using Npgsql;
using Npgsql.NameTranslation;
using System.Data;
using System.Text;

namespace Kit.Helpers.Postgres;

public class SqlHelper : ISqlHelper
{
    private SqlHelper() { }

    public INpgsqlNameTranslator Translator { get; } = new NpgsqlSnakeCaseNameTranslator();

    public static SqlHelper Instance { get; } = new SqlHelper();

    private IEnumerable<KeyValuePair<string, object>>? UpdateParamNames(IEnumerable<KeyValuePair<string, object>>? @params)
    {
        var result = new List<KeyValuePair<string, object>>();
        if (@params.IsNullOrEmpty()) return result;

        @params.ForEach(_param =>
        {
            string key = "_" + Translator.TranslateMemberName(_param.Key.Remove(" ")!.Trim('@', '_'));
            result.Add(new KeyValuePair<string, object>(key, _param.Value));
        });
        return result;
    }

    private string GenerateParamsForQuery(IEnumerable<KeyValuePair<string, object>>? @params)
    {
        var sb = new StringBuilder();
        if (@params.IsNullOrEmpty() == false)
        {
            for (int i = 0; i < @params.Count(); i++)
            {
                string key = @params.ElementAt(i).Key;

                sb.Append(key).Append(" => :").Append(key);

                if (i < @params.Count() - 1)
                {
                    sb.Append(", ");
                }
            }
        }
        return sb.ToString();
    }

    private string TranslateName(string name, bool withStrictSyntax)
    {
        if (withStrictSyntax)
        {
            return name;
        }

        return name.Remove("[")!.Remove("]")!.Split('.').Select(x => Translator.TranslateMemberName(x)).Join(".");
    }

    private NpgsqlCommand AppendTypes(NpgsqlCommand command, RequestParams requestParams)
    {
        requestParams.CustomTypes?.ForEach(x =>
        {
            command.Connection?.TypeMapper.MapComposite(x.Type, TranslateName(x.Name, requestParams.WithStrictSyntax), Translator);
        });
        return command;
    }

    private NpgsqlCommand CreateCommand(RequestParams requestParams)
    {
        string sql = TranslateName(requestParams.CommandText, requestParams.WithStrictSyntax);

        bool hasParameters = requestParams.Parameters != null && requestParams.Parameters.Count() > 0;

        var parameters = UpdateParamNames(requestParams.Parameters);


        if (requestParams.ConnectionString.Value.IsNullOrEmpty()) throw new ArgumentNullException("connectionString");
        if (sql.IsNullOrEmpty()) throw new ArgumentNullException("sql");

        var connection = new NpgsqlConnection(requestParams.ConnectionString.Value);

        NpgsqlCommand command = connection.CreateCommand();
        command.CommandTimeout = connection.ConnectionTimeout;
        if (requestParams.IsStoredProcedure)
        {
            sql = $"select * from {sql}({GenerateParamsForQuery(parameters)});";
        }

        command.CommandType = System.Data.CommandType.Text;
        command.CommandText = sql;

        if (parameters != null && parameters.Any())
        {
            foreach (var keyValuePair in parameters)
            {
                string key = keyValuePair.Key.Remove("@").Remove(" ").ToLower();
                if (keyValuePair.Value is Guid)
                {
                    command.Parameters.AddWithValue(key.ToLower(), keyValuePair.Value ?? Guid.Empty);
                }
                else
                {
                    command.Parameters.AddWithValue(key.ToLower(), keyValuePair.Value ?? DBNull.Value);
                }
            }
        }

        return command;
    }

    public void ExecuteNonQuery(RequestParams requestParams)
    {
        NpgsqlCommand command = CreateCommand(requestParams);
        using (command.Connection)
        {
            command.Connection.Open();
            AppendTypes(command, requestParams);
            command.ExecuteNonQuery();
            command.Connection.Close();
        }
    }

    public object ExecuteScalar(RequestParams requestParams)
    {
        object returnScalar;

        NpgsqlCommand command = CreateCommand(requestParams);
        using (command.Connection)
        {
            command.Connection.Open();
            AppendTypes(command, requestParams);
            returnScalar = command.ExecuteScalar();
            command.Connection.Close();
        }

        return returnScalar;
    }

    public TResult ExecuteScalar<TResult>(RequestParams requestParams)
    {
        object returnScalar;

        NpgsqlCommand command = CreateCommand(requestParams);
        using (command.Connection)
        {
            command.Connection.Open();
            AppendTypes(command, requestParams);
            returnScalar = command.ExecuteScalar();
            command.Connection.Close();
        }
        return (TResult)returnScalar;
    }

    private IEnumerable<TEntity> ExecuteSelectMany<TEntity>(SelectType extraSelectType, RequestParamsSelect<TEntity> requestParams)
    {
        if (requestParams.Converters == null || requestParams.Converters.Count() < 1) throw new ArgumentNullException("converters");

        NpgsqlCommand command = CreateCommand(requestParams);
        command.CommandTimeout = command.Connection.ConnectionTimeout;

        IList<TEntity> selectResult = extraSelectType.CreateList<TEntity>();

        using (command.Connection)
        {
            command.Connection.Open();
            AppendTypes(command, requestParams);
            using (NpgsqlTransaction tran = command.Connection.BeginTransaction())
            {
                using (IDataReader readerFirst = command.ExecuteReader())
                {
                    var dataTypeName = readerFirst.GetDataTypeName(0); // Для первого столбца
                    if (dataTypeName == "refcursor")
                    {
                        ProcessForCursors(readerFirst, command, requestParams.Converters, selectResult, extraSelectType);
                    }
                    else
                    {
                        ProcessForSimple(readerFirst, requestParams.Converters, selectResult, extraSelectType);
                    }

                }
                tran.Commit();
            }
        }
        return selectResult;
    }

    private void ProcessForSimple<TEntity>(IDataReader reader, IEnumerable<Action<IDataRecordSpecified, IList<TEntity>>> converters, IList<TEntity> selectResult, SelectType extraSelectType = SelectType.Default)
    {
        IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator);
        foreach (var converter in converters)
        {
            while (reader.Read())
            {
                converter(recordSpecified, selectResult);
            }
            reader.NextResult();
        }
        reader.ProcessExtraSelect(Translator, selectResult, extraSelectType);
    }

    private void CheckCursorCount(int cursorCount, int converterCount, SelectType extraSelectType)
    {
        switch (extraSelectType)
        {
            case SelectType.Page:
            case SelectType.Offer:
                Check.IsTrue(cursorCount >= converterCount + 1, "Количество конвертеров больше количества возвращаемых курсоров");
                break;
            case SelectType.Default:
            default:
                Check.IsTrue(cursorCount >= converterCount, "Количество конвертеров больше количества возвращаемых курсоров");
                break;
        }
    }

    private void ProcessForCursors<TEntity>(IDataReader readerFirst, NpgsqlCommand command, IEnumerable<Action<IDataRecordSpecified, IList<TEntity>>> converters, IList<TEntity> selectResult, SelectType extraSelectType = SelectType.Default)
    {
        var cursors = new List<string>();
        while (readerFirst.Read())
        {
            cursors.Add(readerFirst[0].ToString()!);
        }
        readerFirst.Close();

        CheckCursorCount(cursors.Count, converters.Count(), extraSelectType);

        for (int i = 0; i < converters.Count(); i++)
        {
            command.CommandText = $"fetch all in \"{cursors[i]}\"";
            command.CommandType = CommandType.Text;

            using (NpgsqlDataReader reader = command.ExecuteReader())
            {
                IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator);
                while (reader.Read())
                {
                    converters.ElementAt(i)(recordSpecified, selectResult);
                }
            }
        }
        if (extraSelectType != SelectType.Default)
        {
            // считывание данных постраничного вывода
            command.CommandText = $"fetch all in \"{cursors.Last()}\"";
            command.CommandType = CommandType.Text;

            using (NpgsqlDataReader reader = command.ExecuteReader())
            {
                reader.ProcessExtraSelect(Translator, selectResult, extraSelectType);
            }
        }
    }

    public IEnumerable<TEntity> ExecuteSelectMany<TEntity>(RequestParamsSelect<TEntity> requestParams) => ExecuteSelectMany(SelectType.Default, requestParams);
    public IEnumerableWithPage<TEntity> ExecuteSelectManyWithPage<TEntity>(RequestParamsSelect<TEntity> requestParams) => (ListWithPage<TEntity>)ExecuteSelectMany(SelectType.Page, requestParams);
    public IEnumerableWithOffer<TEntity> ExecuteSelectManyWithOffer<TEntity>(RequestParamsSelect<TEntity> requestParams) => (ListWithOffer<TEntity>)ExecuteSelectMany(SelectType.Offer, requestParams);
}

public static class SqlHelperExtensions
{
    public static TValue Get<TValue>(this IDataRecordSpecified record, string fieldName) => record.Get<TValue>(fieldName);
    public static TValue GetN<TValue>(this IDataRecordSpecified record, string fieldName) => record.GetN<TValue>(fieldName);
}