using Microsoft.Data.Sqlite;
using Npgsql.NameTranslation;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Kit.Helpers.SQLite;

internal class SQLiteDataRecordSpecified : DataRecordSpecified
{
    public SQLiteDataRecordSpecified(IDataReader dataReader, Npgsql.INpgsqlNameTranslator nameTranslator) : base(dataReader, nameTranslator) { }

    public override TValue Convert<TValue>(object value) => SQliteConvert.Convert<TValue>(value);
}

internal static class SQliteConvert
{
    private static readonly IEnumerable<ConverterItem> _typeConverters = new List<ConverterItem>
    {
        new ConverterItem(typeof(bool),           typeof(long),   (value) => System.Convert.ToBoolean(value)),
        new ConverterItem(typeof(byte),           typeof(long),   (value) => System.Convert.ToByte(value)),
        new ConverterItem(typeof(char),           typeof(string), (value) => System.Convert.ToChar(value.ToString()!)),
        new ConverterItem(typeof(DateOnly),       typeof(string), (value) => DateOnly.ParseExact(value.ToString()!, "yyyy-MM-dd")),
        new ConverterItem(typeof(DateTime),       typeof(string), (value) => DateTime.ParseExact(value.ToString()!, "yyyy-MM-dd HH:mm:ss.FFFFFFF", CultureInfo.InvariantCulture)),
        new ConverterItem(typeof(DateTimeOffset), typeof(string), (value) => DateTimeOffset.ParseExact(value.ToString()!, "yyyy-MM-dd HH:mm:ss.FFFFFFFzzz", CultureInfo.InvariantCulture)),
        new ConverterItem(typeof(decimal),        typeof(string), (value) => System.Convert.ToDecimal(value.ToString()!)),
        new ConverterItem(typeof(double),         typeof(double), (value) => value),
        new ConverterItem(typeof(Guid),           typeof(string), (value) => Guid.Parse(value.ToString()!)),
        new ConverterItem(typeof(short),          typeof(long),   (value) => System.Convert.ToInt16(value)),
        new ConverterItem(typeof(int),            typeof(long),   (value) => System.Convert.ToInt32(value)),
        new ConverterItem(typeof(long),           typeof(long),   (value) => value),
        new ConverterItem(typeof(sbyte),          typeof(long),   (value) => System.Convert.ToSByte(value)),
        new ConverterItem(typeof(float),          typeof(double), (value) => System.Convert.ToSingle(value)),
        new ConverterItem(typeof(string),         typeof(string), (value) => value.ToString()!),
        new ConverterItem(typeof(TimeOnly),       typeof(string), (value) => TimeOnly.ParseExact(value.ToString()!, "HH:mm:ss.fffffff", CultureInfo.InvariantCulture)),
        new ConverterItem(typeof(TimeSpan),       typeof(string), (value) => TimeSpan.ParseExact(value.ToString()!, "d.hh:mm:ss.fffffff", CultureInfo.InvariantCulture)),
        new ConverterItem(typeof(ushort),         typeof(long),   (value) => System.Convert.ToUInt16(value)),
        new ConverterItem(typeof(uint),           typeof(long),   (value) => System.Convert.ToUInt32(value)),
        new ConverterItem(typeof(ulong),          typeof(long),   (value) => System.Convert.ToUInt64(value)),
    };

    private class ConverterItem
    {
        public Type Tgt { get; set; }
        public Type Src { get; set; }
        public Func<object, object> Func { get; set; }

        public ConverterItem(Type tgt, Type src, Func<object, object> func)
        {
            this.Tgt = tgt;
            this.Src = src;
            this.Func = func;
        }
    }

    public static TValue Convert<TValue>(object value)
    {
        Type valueType = value.GetType();
        Type targetType = typeof(TValue);

        return (TValue)(_typeConverters.FirstOrDefault(x => x.Tgt == targetType && x.Src == valueType)?.Func(value) ?? value);
    }
}

public class SqlHelper : ISqlHelper
{
    private readonly ILockService<string> _lockService = new LockService<string>();
    private SqlHelper() { }

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

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

    private List<KeyValuePair<string, object>> UpdateParamNames(RequestParams requestParams)
    {
        var result = new List<KeyValuePair<string, object>>();
        if (requestParams.Parameters.IsNullOrEmpty()) return result;

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

    private SqliteConnection CreateConnection(IConnectionString fileName)
    {
        var conn = new SqliteConnection(fileName.Value.CreateSQLiteConnectionString());
        conn.Open();
        return conn;
    }

    private Func<string, string, string> _funcStrToIntJsonArr = (string input, string delimiter) =>
    {
        if (string.IsNullOrWhiteSpace(input))
        {
            return new List<int>().ToJSON();
        }
        if (string.IsNullOrWhiteSpace(delimiter))
        {
            delimiter = ",";
        }

        HashSet<int> ints = input.Split(delimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(x => int.TryParse(x.Trim(), out int result) ? result : 0).ToHashSet();
        return ints.ToJSON();
    };

    private class SQLiteProcedureParam
    {
        public string Name { get; set; } = string.Empty;
        public string Type { get; set; } = string.Empty;
        public bool IsOptional { get; set; }
        [MemberNotNullWhen(true, "DefaultValueParsed")]
        public bool HasDefault { get; set; }
        public string DefaultValue { get; set; } = string.Empty;

        public SqliteType TypeParsed { get; set; } = SqliteType.Blob;
        public object? DefaultValueParsed { get; set; }
    }

    private void ValidateAndCompleteParams(string procedureName, string procedureParams, string procedureText, List<KeyValuePair<string, object>> parametersUpdated)
    {
        var errors = new List<string>();

        IEnumerable<string> textParams = Regex.Matches(procedureText, @"(:\w+)").Select(x => x.Value.ToLower()).Distinct().ToList();
        var paramParsedList = new List<SQLiteProcedureParam>();

        procedureParams.Split(",", StringSplitOptions.TrimEntries).ForEach(x =>
        {
            // Обязательный, без значения по умолчанию
            var regexReq = new Regex(@"^(:\w+)\s+(\S+)$");

            // Необязательный, по умолчанию null
            var regexOpt = new Regex(@"^(:\w+)\s+(\S+)(?i:\s*=\s*|\s+default\s+)(?i:null)$");
            // Необязательный, указано значение по умолчанию 
            var regexOptW = new Regex(@"^(:\w+)\s+(\S+)(?i:\s*=\s*|\s+default\s+)(.*)");

            Match match = null!;
            var param = new SQLiteProcedureParam();

            if ((match = regexReq.Match(x)).Success)
            {
                param.Name = match.Groups[1].Value;
                param.Type = match.Groups[2].Value;
                param.IsOptional = false;
                param.HasDefault = false;
                param.DefaultValue = string.Empty;
            }
            else if ((match = regexOpt.Match(x)).Success)
            {
                param.Name = match.Groups[1].Value;
                param.Type = match.Groups[2].Value;
                param.IsOptional = true;
                param.HasDefault = false;
                param.DefaultValue = string.Empty;
            }
            else if ((match = regexOptW.Match(x)).Success)
            {
                param.Name = match.Groups[1].Value;
                param.Type = match.Groups[2].Value;
                param.IsOptional = true;
                param.HasDefault = true;
                param.DefaultValue = match.Groups[3].Value;
            }
            else
            {
                errors.Add($"Invalid parameter: {x}");
                return;
            }

            param.Name = param.Name.ToLower();

            try
            {
                param.TypeParsed = param.Type.ParseToEnum<SqliteType>(ignoreCase: true);
            }
            catch
            {
                errors.Add($"parameter '{param.Name}': invalid type ({param.Type})");
                return;
            }

            try
            {
                if (param.HasDefault)
                {
                    switch (param.TypeParsed)
                    {
                        case SqliteType.Integer:
                            param.DefaultValueParsed = int.Parse(param.DefaultValue);
                            break;
                        case SqliteType.Real:
                            param.DefaultValueParsed = double.Parse(param.DefaultValue);
                            break;
                        case SqliteType.Text:
                            if (param.DefaultValue.StartsWith('\'') == false || param.DefaultValue.EndsWith('\'') == false)
                                throw new Exception();

                            param.DefaultValueParsed = param.DefaultValue.Trim('\'').Replace("''", "'");
                            if (((string)param.DefaultValueParsed).IndexOf("''") != -1)
                                throw new Exception();
                            break;
                        case SqliteType.Blob:
                        default:
                            break;
                    }
                }
            }
            catch
            {
                errors.Add($"parameter '{param.Name}': invalid default value ({param.DefaultValue})");
                param.DefaultValueParsed = null;

                return;
            }

            paramParsedList.Add(param);
        });

        IEnumerable<string> delacrationDuplicates = paramParsedList.GroupBy(x => x.Name.ToLower()).Where(x => x.Count() > 1).Select(x => x.Key).ToList();
        if (delacrationDuplicates.Count() > 0)
        {
            errors.AddRange(delacrationDuplicates.Select(x => $"There are duplicates of the parameter declaration: {x}"));
        }

        IEnumerable<string> notDescribedParams = textParams.Where(x => paramParsedList.Any(y => y.Name == x) == false).ToList();
        if (notDescribedParams.Count() > 0)
        {
            errors.AddRange(notDescribedParams.Select(x => $"parameter not declared: {x}"));
        }

        if (errors.Count > 0)
        {
            throw new Exception(errors.Join(Environment.NewLine));
        }

        // Ловим неуказанные обязательные параметры
        IEnumerable<string> notSendedRequired = paramParsedList.Where(x => x.IsOptional == false && parametersUpdated.Any(y => y.Key == x.Name) == false).Select(x => x.Name).ToList();
        if (notSendedRequired.Count() > 0)
        {
            errors.AddRange(notSendedRequired.Select(x => $"required parameter is not sended: {x}"));
        }

        if (errors.Count > 0)
        {
            throw new Exception(errors.Join(Environment.NewLine));
        }

        // Ловим неуказанные необязательные параметры
        paramParsedList.Where(x => x.IsOptional && parametersUpdated.Any(y => y.Key == x.Name) == false).ForEach(x =>
        {
            if (x.HasDefault)
            {
                parametersUpdated.Add(new KeyValuePair<string, object>(x.Name, x.DefaultValueParsed));
            }
            else
            {
                parametersUpdated.Add(new KeyValuePair<string, object>(x.Name, DBNull.Value));
            }
        });
    }

    private void GetStoredProcedureContent(SqliteCommand command, string input, out string procedureName, out string procedureParams, out string procedureText)
    {
        string oldText = command.CommandText;
        CommandType oldType = command.CommandType;

        command.CommandType = CommandType.Text;
        command.CommandText = "select procedure_name, params, text from procedures where procedure_name = :procedure_name";
        command.Parameters.AddWithValue(":procedure_name", input);

        using (var reader = command.ExecuteReader())
        {
            if (reader.Read() == false)
            {
                throw new Exception($"procedure '{input}' not found");
            }

            var readerD = new SQLiteDataRecordSpecified(reader, Translator);

            procedureName = readerD.Get<string>("ProcedureName");
            procedureParams = readerD.Get<string>("Params");
            procedureText = readerD.Get<string>("Text");

            if (procedureName != input)
            {
                throw new Exception($"invalid procedure name (requested: {input}; received: {procedureName})");
            }
        }
        command.CommandText = oldText;
        command.CommandType = oldType;
        command.Parameters.Clear();
    }

    private string GetCommandText(RequestParams requestParams)
    {
        if (requestParams.WithStrictSyntax)
        {
            return requestParams.CommandText;
        }

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

    private SqliteCommand CreateCommand(SqliteConnection connection, RequestParams requestParams)
    {
        if (requestParams.CommandText.IsNullOrEmpty()) throw new ArgumentNullException("sql");

        string sql = GetCommandText(requestParams);

        var parametersUpdated = UpdateParamNames(requestParams);

        SqliteCommand command = connection.CreateCommand();
        command.CommandTimeout = connection.ConnectionTimeout;

        // Регистрируем пользовательские функции
        connection.CreateFunction("str_to_int_json_arr", _funcStrToIntJsonArr, true);

        if (requestParams.IsStoredProcedure)
        {
            GetStoredProcedureContent(command, sql, out string procedureName, out string procedureParams, out string procedureText);
            ValidateAndCompleteParams(procedureName, procedureParams, procedureText, parametersUpdated);

            sql = procedureText;
        }
        if (sql.IsNullOrEmpty()) throw new ArgumentNullException("sql");

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

        if (parametersUpdated.Count > 0)
        {
            foreach (var keyValuePair in parametersUpdated)
            {
                command.Parameters.AddWithValue(keyValuePair.Key, keyValuePair.Value ?? (keyValuePair.Value is Guid ? Guid.Empty : DBNull.Value));
            }
        }

        return command;
    }

    private TResult ExecuteOnConnect<TResult>(RequestParams requestParams, Func<SqliteCommand, TResult> func)
    {
        return _lockService.Lock(requestParams.ConnectionString.Value, () =>
        {
            bool needNewConnect = requestParams.ConnectionString.DatabaseConnection == null || requestParams.ConnectionString.DatabaseConnection is SqliteConnection == false;
            SqliteConnection connection = needNewConnect ? CreateConnection(requestParams.ConnectionString) : (SqliteConnection)requestParams.ConnectionString.DatabaseConnection!;

            try
            {
                using (SqliteCommand command = CreateCommand(connection, requestParams))
                    return func(command);
            }
            finally
            {
                if (needNewConnect)
                    connection?.Dispose();

                connection = null!;
            }
        });
    }

    public void ExecuteNonQuery(RequestParams requestParams)
    {
        ExecuteOnConnect(requestParams, (SqliteCommand command) => command.ExecuteNonQuery());
    }

    public object ExecuteScalar(RequestParams requestParams)
    {
        return ExecuteOnConnect(requestParams, (SqliteCommand command) => command.ExecuteScalar()!);
    }

    public TResult ExecuteScalar<TResult>(RequestParams requestParams)
    {
        return ExecuteOnConnect(requestParams, (SqliteCommand command) => SQliteConvert.Convert<TResult>(command.ExecuteScalar()!));
    }

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

        return ExecuteOnConnect(requestParams, (SqliteCommand command) =>
        {
            IList<TEntity> selectResult = extraSelectType.CreateList<TEntity>();
            using (IDataReader reader = command.ExecuteReader())
            {
                IDataRecordSpecified readerSpecified = new SQLiteDataRecordSpecified(reader, Translator);
                foreach (var converter in requestParams.Converters)
                {
                    while (reader.Read())
                    {
                        converter(readerSpecified, selectResult);
                    }
                    reader.NextResult();
                }
                reader.ProcessExtraSelect(Translator, selectResult, extraSelectType);
            }
            return selectResult;
        });
    }

    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);
}