using System.Data;
using System.Text.Json.Serialization;

namespace Kit.Helpers;

public class OutputValue
{
    public object Value;
}

public class CustomType
{
    public string Name { get; set; }
    public Type Type { get; set; }

    public CustomType(string name, Type type)
    {
        Name = name;
        Type = type;
    }
}

public class RequestParams
{
    public IConnectionString ConnectionString { get; set; } = new ConnectionString();
    public string CommandText { get; set; } = string.Empty;
    public bool IsStoredProcedure { get; set; } = false;
    public bool WithStrictSyntax { get; set; } = false;
    public IEnumerable<KeyValuePair<string, object>>? Parameters { get; set; }
    public IEnumerable<CustomType>? CustomTypes { get; set; }
}

public class RequestParamsSelect<TEntity> : RequestParams
{
    public IEnumerable<Action<IDataRecordSpecified, IList<TEntity>>> Converters { get; set; } = Enumerable.Empty<Action<IDataRecordSpecified, IList<TEntity>>>();
}

public interface ISqlHelper
{
    void ExecuteNonQuery(RequestParams requestParams);
    object ExecuteScalar(RequestParams requestParams);
    TResult ExecuteScalar<TResult>(RequestParams requestParams);
    IEnumerable<TEntity> ExecuteSelectMany<TEntity>(RequestParamsSelect<TEntity> requestParams);
    IEnumerableWithPage<TEntity> ExecuteSelectManyWithPage<TEntity>(RequestParamsSelect<TEntity> requestParams);
    IEnumerableWithOffer<TEntity> ExecuteSelectManyWithOffer<TEntity>(RequestParamsSelect<TEntity> requestParams);
}

public interface ISqlHelperFluent
{
    ISqlHelperFluent AsStoredProcedure();
    ISqlHelperFluent AsSqlText();
    ISqlHelperFluent WithStrictSyntax();
    ISqlHelperFluent AddCustomType(string name, Type type);
    ISqlHelperFluent AddCustomType<TType>(string name);
    ISqlHelperFluent AddFilter(string name, object value);
    ISqlHelperFluent AddFilter(string name, string value);
    ISqlHelperFluent AddParameter(string name, object value);
    ISqlHelperFluent AddParameter(string name, string value);
    ISqlHelperFluent AddParameterNullable(string name, object? value);
    ISqlHelperFluent AddParameterIfNotNull(string name, object? value);
    ISqlHelperFluent AddOutputParameter(string name, object value);
    ISqlHelperFluent AddParameters(IEnumerable<KeyValuePair<string, object>> parameters);
    ISqlHelperFluent AddFilters(IEnumerable<KeyValuePair<string, object>> parameters);
    void ExecuteNonQuery();
    TResult ExecuteScalar<TResult>();
}

public interface ISqlHelperFluent<TEntity>
{
    ISqlHelperFluent<TEntity> AsStoredProcedure();
    ISqlHelperFluent<TEntity> AsSqlText();
    ISqlHelperFluent<TEntity> WithStrictSyntax();
    ISqlHelperFluent<TEntity> AddCustomType(string name, Type type);
    ISqlHelperFluent<TEntity> AddCustomType<TType>(string name);
    ISqlHelperFluent<TEntity> AddFilter(string name, object value);
    ISqlHelperFluent<TEntity> AddFilter(string name, string value);
    ISqlHelperFluent<TEntity> AddParameter(string name, object value);
    ISqlHelperFluent<TEntity> AddParameter(string name, string value);
    ISqlHelperFluent<TEntity> AddParameterNullable(string name, object? value);
    ISqlHelperFluent<TEntity> AddParameterIfNotNull(string name, object? value);
    ISqlHelperFluent<TEntity> AddOutputParameter(string name, object value);
    ISqlHelperFluent<TEntity> AddParameters(IEnumerable<KeyValuePair<string, object>> parameters);
    ISqlHelperFluent<TEntity> AddFilters(IEnumerable<KeyValuePair<string, object>> parameters);
    ISqlHelperFluent<TEntity> AddConverter(Action<IDataRecordSpecified, IList<TEntity>> converter);
    IEnumerable<TEntity> ExecuteSelectMany();
    IEnumerableWithPage<TEntity> ExecuteSelectManyWithPage();
    IEnumerableWithOffer<TEntity> ExecuteSelectManyWithOffer();
}

public enum ConnectionStringType
{
    Undefined = 0,
    Postgres = 1,
    SQLite = 2,
}

public interface IConnectionString
{
    ConnectionStringType Type { get; }
    string Value { get; }
    [JsonIgnore]
    object? DatabaseConnection { get; }
}
public class ConnectionString : IConnectionString
{
    public ConnectionStringType Type { get; set; } = ConnectionStringType.Undefined;

    private string _value = string.Empty;
    public string Value
    {
        get => _value;
        set => _value = value.ServerMapPath();
    }
    [JsonIgnore]
    public object? DatabaseConnection { get; set; }

    public void CreateAndOpenConnection()
    {
        switch (Type)
        {
            case ConnectionStringType.Postgres:
                DatabaseConnection = new Npgsql.NpgsqlConnection(Value.CreateSQLiteConnectionString());
                (DatabaseConnection as Npgsql.NpgsqlConnection)!.Open();
                break;
            case ConnectionStringType.SQLite:
                DatabaseConnection = new Microsoft.Data.Sqlite.SqliteConnection(Value.CreateSQLiteConnectionString());
                (DatabaseConnection as Microsoft.Data.Sqlite.SqliteConnection)!.Open();
                break;
            default:
                throw new NotImplementedException();
        }

    }

    ~ConnectionString()
    {
        if (DatabaseConnection is IDisposable disposable) disposable.Dispose();
        DatabaseConnection = null;
    }
}

public class SqlHelperFluent : ISqlHelperFluent
{
    protected readonly IConnectionString _connectionString;
    protected readonly string _sqlText;
    protected bool _isStoredProcedure;
    protected bool _isOutputParameter;
    protected bool _withStrictSyntax;
    protected List<CustomType> _customTypes;
    protected List<KeyValuePair<string, object>> _parameters;

    public SqlHelperFluent(IConnectionString connectionString, string sqlText)
    {
        _connectionString = connectionString;
        _sqlText = sqlText;
        _isStoredProcedure = true;
        _isOutputParameter = false;
        _withStrictSyntax = false;
        _customTypes = new List<CustomType>();
        _parameters = new List<KeyValuePair<string, object>>();
    }

    protected ISqlHelper GetSqlHelper()
    {
        switch (_connectionString.Type)
        {
            case ConnectionStringType.Postgres:
                return Postgres.SqlHelper.Instance;
            case ConnectionStringType.SQLite:
                return SQLite.SqlHelper.Instance;
            default:
                throw new NotImplementedException();
        }
    }


    #region fluent methods

    public ISqlHelperFluent AsStoredProcedure()
    {
        _isStoredProcedure = true;
        return this;
    }

    public ISqlHelperFluent AsSqlText()
    {
        _isStoredProcedure = false;
        return this;
    }

    public ISqlHelperFluent WithStrictSyntax()
    {
        _withStrictSyntax = true;
        return this;
    }

    public ISqlHelperFluent AddCustomType(string name, Type type)
    {
        if (type != null)
        {
            _customTypes.Add(new CustomType(name, type));
        }
        return this;
    }

    public ISqlHelperFluent AddCustomType<TType>(string name)
    {
        _customTypes.Add(new CustomType(name, typeof(TType)));
        return this;
    }

    public ISqlHelperFluent AddFilter(string name, object value)
    {
        if (value != null)
        {
            _parameters.Add(new KeyValuePair<string, object>(name, value));
        }
        return this;
    }

    public ISqlHelperFluent AddFilter(string name, string value)
    {
        if (!value.IsNullOrEmpty())
        {
            _parameters.Add(new KeyValuePair<string, object>(name, value));
        }
        return this;
    }

    public ISqlHelperFluent AddParameter(string name, object value)
    {
        _parameters.Add(new KeyValuePair<string, object>(name, value));
        return this;
    }

    public ISqlHelperFluent AddParameter(string name, string value)
    {

        _parameters.Add(new KeyValuePair<string, object>(name, value ?? string.Empty));
        return this;
    }

    public ISqlHelperFluent AddParameterNullable(string name, object? value)
    {
        _parameters.Add(new KeyValuePair<string, object>(name, value ?? DBNull.Value));
        return this;
    }

    public ISqlHelperFluent AddParameterIfNotNull(string name, object? value)
    {
        if (value != null)
        {
            _parameters.Add(new KeyValuePair<string, object>(name, value));
        }
        return this;
    }

    public ISqlHelperFluent AddOutputParameter(string name, object value)
    {
        _parameters.Add(new KeyValuePair<string, object>(name, new OutputValue { Value = value ?? DBNull.Value }));
        return this;
    }

    public ISqlHelperFluent AddParameters(IEnumerable<KeyValuePair<string, object>> parameters)
    {
        _parameters.AddRange(parameters);
        return this;
    }

    public ISqlHelperFluent AddFilters(IEnumerable<KeyValuePair<string, object>> parameters)
    {
        _parameters.AddRange(parameters.Where(x => x.Value != null));
        return this;
    }

    #endregion

    #region finish methods

    private RequestParams BuildParams() => new RequestParams
    {
        ConnectionString = _connectionString,
        CommandText = _sqlText,
        IsStoredProcedure = _isStoredProcedure,
        WithStrictSyntax = _withStrictSyntax,
        Parameters = _parameters,
        CustomTypes = _customTypes,
    };

    public void ExecuteNonQuery()
    {
        GetSqlHelper().ExecuteNonQuery(BuildParams());
    }

    public TResult ExecuteScalar<TResult>()
    {
        return GetSqlHelper().ExecuteScalar<TResult>(BuildParams());
    }

    #endregion
}

public class SqlHelperFluent<TEntity> : SqlHelperFluent, ISqlHelperFluent<TEntity>
{
    protected List<Action<IDataRecordSpecified, IList<TEntity>>> _converters;

    public SqlHelperFluent(IConnectionString connectionString, string sqlText) : base(connectionString, sqlText)
    {
        _converters = new List<Action<IDataRecordSpecified, IList<TEntity>>>();
    }
    #region base fluent methods

    public new ISqlHelperFluent<TEntity> AddCustomType(string name, Type type)
    {
        base.AddCustomType(name, type);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddCustomType<TType>(string name)
    {
        base.AddCustomType<TType>(name);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddFilter(string name, object value)
    {
        base.AddFilter(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddFilter(string name, string value)
    {
        base.AddFilter(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddParameter(string name, object value)
    {
        base.AddParameter(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddParameter(string name, string value)
    {
        base.AddParameter(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddParameterNullable(string name, object? value)
    {
        base.AddParameterNullable(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddParameterIfNotNull(string name, object? value)
    {
        base.AddParameterIfNotNull(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddOutputParameter(string name, object value)
    {
        base.AddParameter(name, value);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddParameters(IEnumerable<KeyValuePair<string, object>> parameters)
    {
        base.AddParameters(parameters);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AddFilters(IEnumerable<KeyValuePair<string, object>> parameters)
    {
        base.AddFilters(parameters);
        return this;
    }

    public new ISqlHelperFluent<TEntity> AsStoredProcedure()
    {
        base.AsStoredProcedure();
        return this;
    }

    public new ISqlHelperFluent<TEntity> AsSqlText()
    {
        base.AsSqlText();
        return this;
    }

    public new ISqlHelperFluent<TEntity> WithStrictSyntax()
    {
        base.WithStrictSyntax();
        return this;
    }

    #endregion

    private RequestParamsSelect<TEntity> BuildParams() => new RequestParamsSelect<TEntity>
    {
        ConnectionString = _connectionString,
        CommandText = _sqlText,
        IsStoredProcedure = _isStoredProcedure,
        WithStrictSyntax = _withStrictSyntax,
        Parameters = _parameters,
        CustomTypes = _customTypes,
        Converters = _converters,
    };

    public ISqlHelperFluent<TEntity> AddConverter(Action<IDataRecordSpecified, IList<TEntity>> converter)
    {
        _converters.Add(converter);
        return this;
    }

    public IEnumerable<TEntity> ExecuteSelectMany()
    {
        return GetSqlHelper().ExecuteSelectMany(BuildParams());
    }

    public IEnumerableWithPage<TEntity> ExecuteSelectManyWithPage()
    {
        return GetSqlHelper().ExecuteSelectManyWithPage(BuildParams());
    }

    public IEnumerableWithOffer<TEntity> ExecuteSelectManyWithOffer()
    {
        return GetSqlHelper().ExecuteSelectManyWithOffer(BuildParams());
    }
}

public static class SqLiteHelperFluentExtentions
{
    public static ISqlHelperFluent PrepareExecute(this IConnectionString connectionString, string sqlText)
    {
        return new SqlHelperFluent(connectionString, sqlText);
    }

    public static ISqlHelperFluent<TEntity> PrepareExecute<TEntity>(this IConnectionString connectionString, string sqlText)
    {
        return new SqlHelperFluent<TEntity>(connectionString, sqlText);
    }
}

public interface IDataRecordSpecified
{
    IDataRecord Src { get; }
    Npgsql.INpgsqlNameTranslator NameTranslator { get; }
    TValue Convert<TValue>(object value);
}

internal class DataRecordSpecified : IDataRecordSpecified
{
    public IDataRecord Src { get; }
    public Npgsql.INpgsqlNameTranslator NameTranslator { get; }
    public virtual TValue Convert<TValue>(object value)
    {
        return (TValue)value;
    }

    public DataRecordSpecified(IDataReader dataReader, Npgsql.INpgsqlNameTranslator nameTranslator)
    {
        Src = dataReader;
        NameTranslator = nameTranslator;
    }
}

public static class SqlHelperExtensions
{
    /// <summary>
    /// Проверка на наличие столбца в запросе
    /// </summary>
    /// <param name="reader"></param>
    /// <param name="name"></param>
    /// <returns></returns>
    public static bool HasColumn(this IDataRecordSpecified reader, string name)
    {
        string nameTranslated = reader.NameTranslator.TranslateMemberName(name).ToLower();
        for (int n = 0; n < reader.Src.FieldCount; n++)
        {
            string fieldNameTranslated = reader.NameTranslator.TranslateMemberName(reader.Src.GetName(n)).ToLower();
            if (fieldNameTranslated == nameTranslated) return true;
        }
        return false;
    }

    private static string GetTranslatedName(this IDataRecordSpecified record, string fieldName) => record.NameTranslator.TranslateMemberName(fieldName);

    private static TValue? TryGet<TValue>(this IDataRecordSpecified record, string fieldName, bool isStrict)
    {
        fieldName = GetTranslatedName(record, fieldName);
        object value = record.Src[fieldName];

        if (value == DBNull.Value)
        {
            if (isStrict)
                throw new InvalidCastException(string.Format("{0} {1} нельзя привести к {2}", fieldName, typeof(DBNull).FullName, typeof(TValue).FullName));

            return default;
        }

        try
        {
            return record.Convert<TValue>(value);
        }
        catch (InvalidCastException ex)
        {
            throw new InvalidCastException(string.Format("{0} {1} нельзя привести к {2}", fieldName, value.GetType().FullName, typeof(TValue).FullName), ex);
        }
    }

    public static TValue Get<TValue>(this IDataRecordSpecified record, string fieldName) => record.TryGet<TValue>(fieldName, isStrict: true)!;
    public static TValue GetN<TValue>(this IDataRecordSpecified record, string fieldName) => record.TryGet<TValue>(fieldName, isStrict: false)!;

    internal static IList<TEntity> CreateList<TEntity>(this SelectType extraSelectType)
    {
        switch (extraSelectType)
        {
            case SelectType.Page:
                return new ListWithPage<TEntity>();
            case SelectType.Offer:
                return new ListWithOffer<TEntity>();
            case SelectType.Default:
            default:
                return new List<TEntity>();
        }
    }

    internal static void ProcessExtraSelect<TEntity>(this IDataReader reader, Npgsql.INpgsqlNameTranslator translator, IList<TEntity> selectResult, SelectType extraSelectType = SelectType.Default)
    {
        IDataRecordSpecified readerSpecified = new DataRecordSpecified(reader, translator);
        switch (extraSelectType)
        {
            case SelectType.Page:
                {
                    var result = (IEnumerableWithPage<TEntity>)selectResult;
                    // считывание данных постраничного вывода
                    while (reader.Read())
                    {
                        if (readerSpecified.HasColumn("FilteredRows")) result.FilteredRows = readerSpecified.Get<int>("filteredRows");
                        if (readerSpecified.HasColumn("TotalRows")) result.TotalRows = readerSpecified.Get<int>("totalRows");
                        if (readerSpecified.HasColumn("PageNo")) result.PageNo = readerSpecified.Get<int>("pageNo");
                        if (readerSpecified.HasColumn("PageSize")) result.PageSize = readerSpecified.Get<int>("pageSize");
                        if (readerSpecified.HasColumn("Sort")) result.Sort = readerSpecified.Get<string>("sort");
                        if (readerSpecified.HasColumn("Dir")) result.Dir = readerSpecified.Get<string>("dir");
                    }
                }
                break;
            case SelectType.Offer:
                {
                    var result = (IEnumerableWithOffer<TEntity>)selectResult;
                    // считывание данных постраничного вывода
                    while (reader.Read())
                    {
                        if (readerSpecified.HasColumn("FilteredRows")) result.FilteredRows = readerSpecified.Get<int>("FilteredRows");
                        if (readerSpecified.HasColumn("TotalRows")) result.TotalRows = readerSpecified.Get<int>("TotalRows");
                        if (readerSpecified.HasColumn("Start")) result.Start = readerSpecified.Get<int>("Start");
                        if (readerSpecified.HasColumn("Length")) result.Lenght = readerSpecified.Get<int>("Length");
                        if (readerSpecified.HasColumn("Sort")) result.Sort = readerSpecified.Get<string>("Sort");
                        if (readerSpecified.HasColumn("Dir")) result.Dir = readerSpecified.Get<string>("Dir");
                    }
                }
                break;
            case SelectType.Default:
            default:
                break;
        }
    }

}

internal enum SelectType
{
    Default,
    Page,
    Offer
}