#if DEBUG
//#define liveW4
#endif
using FirebirdSql.Data.FirebirdClient;
using KarleyLibrary.Erweiterungen;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using System.Linq;
using WK5.Core.Models;
using System.Reflection;
using WK5.Core.Models.Statistik;

namespace WK5.Core
{
    public class BoolTypeHandler : SqlMapper.TypeHandler<bool>
    {
        public override bool Parse(object value)
        {
            if(value is null)
            {
                return false;
            }

            return value.ToString() == "Y";
        }

        public override void SetValue(IDbDataParameter parameter, bool value)
        {
            parameter.Value = value.ToFirebirdBool();
        }
    }
    /// <summary>
    /// Stellt Funktionalitäten zur Arbeit mit einer Firebird-Datenbank zur Verfügung. Diese Klasse kann nicht vererbt werden.
    /// </summary>
    public sealed class FbController2 : IDisposable
    {
        public static List<char> RegexEscapeCharacters = new List<char>()
        {
            '[',
            ']',
            '(',
            ')',
            '|',
            '^',
            '-',
            '+',
            '*',
            '%',
            '_',
            '?',
            '{',
            '}'
        };


        private bool _disposedValue;
        private bool _userInitialized;
        private FbConnection Connection => Command.Connection;
        public string CommandText => Command.CommandText ?? String.Empty;
        public FbTransaction? Transaction => Command.Transaction;
        private FbCommand Command { get; set; }

        /// <summary>
        /// Ruft die PersonalNummer ab, in dessen Namen die Abfragen durchgeführt werden sollen.
        /// </summary>
        public int UserId { get; } = 2;
        /// <summary>
        /// Ruft den ConnectionString zur aktuellen Datenbank ab.
        /// </summary>
        public string ConnectionString { get; }
        #region Konstruktoren
        /// <summary>
        /// Statischer Konstruktor zum Initialisieren der Dapper SQL-Mapper
        /// </summary>
        static FbController2()
        {
            //SqlMapper.SetTypeMap(typeof(Artikel), new CustomPropertyTypeMap(
            //    typeof(Artikel),
            //    (type, columnName) =>
            //    {
            //        var tmp = columnName switch
            //        {
            //            "ARTI_A_NR" => type.GetProperty(nameof(Artikel.Artikelnummer)),
            //            "ARTI_A_BEZ1" => type.GetProperty(nameof(Artikel.Bezeichnung1)),
            //            "ARTI_A_BEZ2" => type.GetProperty(nameof(Artikel.Bezeichnung2)),
            //            "ARTI_A_BEZ3" => type.GetProperty(nameof(Artikel.Bezeichnung3)),
            //            "ARTI_A_BEZ4" => type.GetProperty(nameof(Artikel.Bezeichnung4)),
            //            "ARTI_A_BEZ5" => type.GetProperty(nameof(Artikel.Bezeichnung5)),
            //            "ARTI_A_HERST_NR" => type.GetProperty(nameof(Artikel.Herstellernummer)),
            //            "ARTI_A_HERSTELLER" => type.GetProperty(nameof(Artikel.Hersteller)),
            //            "ARTI_A_LAGER" => type.GetProperty(nameof(Artikel.Lagerplatz)),
            //            "ARTI_A_PREISGRUPPE" => type.GetProperty(nameof(Artikel.Preisgruppe)),
            //            "ARTI_A_SELEKTION1" => type.GetProperty(nameof(Artikel.Selektionsmerkmal1)),
            //            "ARTI_A_SELEKTION2" => type.GetProperty(nameof(Artikel.Selektionsmerkmal2)),
            //            "ARTI_A_SELEKTION3" => type.GetProperty(nameof(Artikel.Selektionsmerkmal3)),
            //            "ARTI_A_UNTERWARENG" => type.GetProperty(nameof(Artikel.Unterwarengruppe)),
            //            "ARTI_A_WARENGRUPPE" => type.GetProperty(nameof(Artikel.Warengruppe)),
            //            "ARTI_B_LANGTEXT" => type.GetProperty(nameof(Artikel.Langtext)),
            //            "ARTI_B_NOTIZ" => type.GetProperty(nameof(Artikel.Notiz)),
            //            "ARTI_N_BREITE" => type.GetProperty(nameof(Artikel.Breite)),
            //            "ARTI_N_MINBESTAND" => type.GetProperty(nameof(Artikel.Mindestbestand)),
            //            "ARTI_N_ZOLLTARIF" => type.GetProperty(nameof(Artikel.ZolltarifId)),
            //            "WK5_ARTI_N_ANGEBOTVORLAGE" => type.GetProperty(nameof(Artikel.AngebotsnummerVorlage)),
            //            "WK5_ARTI_D_LAST_PREIS_CHANGE" => type.GetProperty(nameof(Artikel.LetzePreisÄnderung)),
            //            "ARTI_N_WARTUNGSINTERVALL" => type.GetProperty(nameof(Artikel.Wartungsintervall)),
            //            "ARWE_N_BESTAND" => type.GetProperty(nameof(Artikel.Bestand)),
            //            "ARWE_N_BEDARF" => type.GetProperty(nameof(Artikel.Bedarf)),
            //            "ARWE_N_OFFBESTELL" => type.GetProperty(nameof(Artikel.Bestellt)),
            //            "ARTI_B_WARTUNGSTEXT" => type.GetProperty(nameof(Artikel.WartungsText)),
            //            "ARTI_N_PREISOPTION_ONLINE" => type.GetProperty(nameof(Artikel.PreisoptionOnline)),
            //            "ARTI_L_BUNDLE" => type.GetProperty(nameof(Artikel.IstBundle)),
            //            _ => type.GetProperty(columnName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
            //        };

            //        return tmp;
            //    }
            //));

 
           

            //Creates Type Maps for all Loaded Assemblies in Appdomain

            foreach (Type type in SingletonTypeAttributeCache.CacheAll<KarleyLibrary.Attributes.CompareFieldAttribute>((att) => att.DatenbankFeld))
            {
                SqlMapper.SetTypeMap(type, new CustomPropertyTypeMap(
                    type,
                    (type, columnName) =>
                    {
                        PropertyInfo? prop = SingletonTypeAttributeCache.Get(type, columnName);

                        return prop is null ? type.GetProperty(columnName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) : prop;

                    }
                ));
            }


            SqlMapper.AddTypeHandler(new BoolTypeHandler());
        }
        /// <summary>
        /// Erstellt einen neuen <see cref="FbController2"/> für den Systemuser.
        /// </summary>
        public FbController2()
        {
#if DEBUG && !liveW4
            ConnectionString = GlobalConfig.W4LocalDebugConnectionString;
#else
            ConnectionString = GlobalConfig.W4ConnectionString;
#endif

            Command = new FbCommand
            {
                Connection = new FbConnection(ConnectionString)
            };

            Command.Connection.Open();

        }

        public FbController2(string connectionString)
        {
            ConnectionString = connectionString;
            Command = new FbCommand
            {
                Connection = new FbConnection(ConnectionString)
            };

            Command.Connection.Open();
        }


        /// <summary>
        /// Erstellt einen neuen <see cref="FbController2"/> der alle Anfragen für die übergebene UserId ausführt.
        /// </summary>
        /// <param name="userId"></param>
        public FbController2(int userId)
        {
            if (userId <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(userId), "Must be greater than 0.");
            }
#if DEBUG && !liveW4
            ConnectionString = GlobalConfig.W4LocalDebugConnectionString;
#else
            ConnectionString = GlobalConfig.W4ConnectionString;
#endif
            UserId = userId;
            Command = new FbCommand
            {
                Connection = new FbConnection(ConnectionString)
            };

            Command.Connection.Open();
        }
        #endregion
        #region Hilfsmethoden
        /// <summary>
        /// Setzt den User Context in der Datenbank auf die <see cref="UserId"/>
        /// <para>
        /// Der UserContext wird für Trigger und Prozeduren in der Datenbank benötigt, damit die Historie korrekt geführt wird.
        /// </para>
        /// </summary>
        /// <returns></returns>
        private Task SetUserContext(CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (!_userInitialized)
            {
                //Sorgt dafür das in der SQL Session der richtige User hinterlegt ist.
                Command.CommandText = $@"select rdb$set_context('USER_SESSION', 'USERNUMMER', {UserId}) from rdb$database";
                _userInitialized = true;
                return Command.ExecuteScalarAsync(cancellationToken);
            }
            return Task.CompletedTask;
        }
        /// <summary>
        /// Fügt einen Parameter zum Command hinzu.
        /// </summary>
        /// <param name="parameterName"></param>
        /// <param name="parameterValue"></param>
        public void AddParameter(string parameterName, object? parameterValue)
        {
            Command.Parameters.AddWithValue(
                parameterName: parameterName,
                value: parameterValue is bool fbBool ? fbBool.ToFirebirdBool() : parameterValue
            );
        }
        /// <summary>
        /// Löscht alle gesetzten Parameter vom Command
        /// </summary>
        public void ClearParameters() => Command.Parameters.Clear();
        public string GetParametersWithValues(bool clearParameter)
        {
            StringBuilder sb = new StringBuilder();
            foreach (FbParameter parameter in Command.Parameters)
            {
                sb.AppendLine($"{parameter.ParameterName}: {parameter.Value}");
            }

            if(clearParameter)
            {
                ClearParameters();
            }

            return sb.ToString();
        }
        public static string SanitizeRegex(string value, char escapeCharacter = '\\')
        {
            foreach (char escape in RegexEscapeCharacters)
            {
                value = value.Replace(escape.ToString(), $"{escapeCharacter}{escape}");
            }
            return value;
        }
        #endregion
        #region SQL-Methoden
        /// <summary>
        /// Führt einen beliebigen SQL-Command am Server aus. Diese Methode eignet sich für UPDATE, DELETE und INSERT-Statements.
        /// <para>
        /// Die Methode verwendet für den Befehl <see cref="CommandType.Text"/>
        /// </para>
        /// </summary>
        /// <param name="command">Beliebiges SQL. Es kann Auch ein SQL-String mit Platzhaltern für Parameter übergeben werden.</param>
        public async Task QueryAsync(string sqlCommand, CancellationToken cancellationToken = default)
        {
            try
            {
                await SetUserContext(cancellationToken);
                Command.CommandText = sqlCommand;
                Command.CommandType = CommandType.Text;
                await Command.ExecuteNonQueryAsync(cancellationToken);
            }
            catch (OperationCanceledException) { }
            finally
            {
                ClearParameters();
            }

        }
        /// <summary>
        /// Führt einen beliebigen SQL-Command via Dapper am Server aus. Diese Methode eignet sich für UPDATE, DELETE und INSERT-Statements
        /// </summary>
        /// <param name="sqlCommand"></param>
        /// <param name="param"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task QueryDapperAsync(string sqlCommand, object? param = null, CancellationToken cancellationToken = default)
        {
            try
            {
                await SetUserContext(cancellationToken);
                await Command.Connection.QueryAsync(sqlCommand, param, Transaction);
            }
            catch (OperationCanceledException)
            {
            }
        }


        /// <summary>
        /// Liefert das Ergebnis als Objekt zurück.
        /// <para>
        /// Diese Methode kann nur dann verwendet werden, wenn es maximal eine Row und ein Item gibt, das zurückgegeben werden kann.
        /// </para>
        /// </summary>
        /// <param name="sqlCommand"></param>
        /// <returns></returns>
        public async Task<object?> FetchObjectAsync(string sqlCommand, CancellationToken cancellationToken = default)
        {
            object? returnValue = null;
            try
            {
                await SetUserContext(cancellationToken);
                Command.CommandText = sqlCommand;
                Command.CommandType = CommandType.Text;
                returnValue = await Command.ExecuteScalarAsync(cancellationToken);
            }
            catch (OperationCanceledException) { }
            finally
            {
                ClearParameters();
            }
            return returnValue;
        }


        /// <summary>
        /// Führt eine Prozedur in der Datenbank aus
        /// </summary>
        /// <example>
        /// using FbController2 fbController = new FbController2();
        /// fbController.AddParameter("@INP_BELEGNR", Beleg.BELE_N_NR);
        /// fbController.AddParameter("@INP_BELEGTYP", "AU");
        /// await fbController.RunProcedureAsync("PROZ_BELEGCOPY");
        /// </example>
        /// <param name="procedureName">Der Name der Prozedur. Dieser kann aus der Datenbank entnommen werden.</param>
        public async Task RunProcedureAsync(string procedureName, CancellationToken cancellationToken = default)
        {
            try
            {
                await SetUserContext(cancellationToken);
                Command.CommandType = CommandType.StoredProcedure;
                Command.CommandText = procedureName;
                await Command.ExecuteNonQueryAsync(cancellationToken);
            }
            catch (OperationCanceledException){}
            finally
            {
                ClearParameters();
            }
        }



        /// <summary>
        /// Liefert alle Datensätze für eine beliebige SQL-Abfrage zurück. Diese Methode eignet sich für SELECT-Statements, wenn man mehr als ein Ergebnis erwartet. 
        /// <para>
        /// Benötigt man nur einen Datensatz, dann eignet sich besser die Methode
        /// <seealso cref="SelectRow(string)"/>
        /// </para>
        /// </summary>
        /// <param name="selectCommand">Beliebiges SQL. Es kann Auch ein SQL-String mit Platzhaltern für Parameter übergeben werden.</param>
        /// <returns>Alle Datensätze in einer DataTable. Wenn keine Datensätze gefunden wurden, dann gibt die Methode eine leere DataTable zurück.</returns>
        public async Task<DataTable> SelectDataAsync(string selectCommand, CancellationToken cancellationToken = default)
        {
            DataTable data = new DataTable();
            try
            {
                await SetUserContext(cancellationToken);
                Command.CommandType = CommandType.Text;
                Command.CommandText = selectCommand;
                var reader = await Command.ExecuteReaderAsync(cancellationToken);
                await Task.Run(() => { data.Load(reader); }, cancellationToken);
            }
            catch (OperationCanceledException){}
            finally
            {
                ClearParameters();
            }
            
            
            return data;
        }
        /// <summary>
        /// Liefert den ersten gefunden Datensatz für eine SQL-Abfrage zurück. Diese Methode eignet sich für SELECT-Statements, wenn man nur ein Ergebnis erwartet.
        /// <para>
        /// Benötigt man mehr als einen Datensatz, dann eignet sich besser die Methode
        /// <seealso cref="SelectDataAsync(string)"/>
        /// </para>
        /// </summary>
        /// <param name="selectCommand">Beliebiges SQL. Es kann Auch ein SQL-String mit Platzhaltern für Parameter übergeben werden.</param>
        /// <returns>null wenn kein Ergebnis gefunden wurde, ansonsten die erste gefundene DataRow</returns>
        public async Task<DataRow?> SelectRowAsync(string selectCommand, CancellationToken cancellationToken = default)
        {
            DataTable data = await SelectDataAsync(selectCommand, cancellationToken);
            ClearParameters();
            return data.Rows.Count > 0 ? data.Rows[0] : null;
        }

        public Task<T?> SelectRowAsync<T>(string selectCommand, object? param = null)
        {
            Task<T?> result = Command.Connection.QueryFirstOrDefaultAsync<T?>(selectCommand, param, Transaction);
            ClearParameters();
            return result;
        }
        public async Task<List<T>> SelectDataAsync<T>(string selectCommand, object? param = null)
        {
            IEnumerable<T> enumerable = await Command.Connection.QueryAsync<T>(selectCommand, param, Transaction);
            ClearParameters();
            return enumerable.ToList();
        }

        /// <summary>
        /// Liefert alle Datensätze für eine beliebige SQL-Abfrage zurück. Diese Methode eignet sich für SELECT-Statements, wenn man mehr als ein Ergebnis erwartet. 
        /// <para>
        /// Benötigt man nur einen Datensatz, dann eignet sich besser die Methode
        /// <seealso cref="SelectRow(string)"/>
        /// </para>
        /// </summary>
        /// <param name="selectCommand">Beliebiges SQL. Es kann Auch ein SQL-String mit Platzhaltern für Parameter übergeben werden.</param>
        /// <returns>Alle Datensätze in einer DataTable. Wenn keine Datensätze gefunden wurden, dann gibt die Methode eine leere DataTable zurück.</returns>
        public DataTable SelectData(string selectCommand, string tableName = "")
        {
            DataTable data = new DataTable(tableName);
            Command.CommandType = CommandType.Text;
            Command.CommandText = selectCommand;
            using FbDataAdapter adapter = new FbDataAdapter(Command);
            adapter.Fill(data);
            ClearParameters();
            return data;
        }

        /// <summary>
        /// Liefert den ersten gefunden Datensatz für eine SQL-Abfrage zurück. Diese Methode eignet sich für SELECT-Statements, wenn man nur ein Ergebnis erwartet.
        /// <para>
        /// Benötigt man mehr als einen Datensatz, dann eignet sich besser die Methode
        /// <seealso cref="SelectData(string)"/>
        /// </para>
        /// </summary>
        /// <param name="selectCommand">Beliebiges SQL. Es kann Auch ein SQL-String mit Platzhaltern für Parameter übergeben werden.</param>
        /// <returns>null wenn kein Ergebnis gefunden wurde, ansonsten die erste gefundene DataRow</returns>
        public DataRow? SelectRow(string selectCommand)
        {
            DataTable data = SelectData(selectCommand);
            ClearParameters();
            return data.Rows.Count > 0 ? data.Rows[0] : null;
        }


        


        #endregion
        #region Transaction
        /// <summary>
        /// Startet eine neue Transaction.
        /// </summary>
        /// <exception cref="InvalidOperationException">Tritt auf, wenn bereits eine Transaction läuft.</exception>
        /// <param name="transactionName">Hilft bei der Identifizierung der Abfragen in Database Workbench.</param>
        public async Task StartTransactionAsync()
        {
            await SetUserContext();

            if (Command.Transaction is not null)
            {
                throw new InvalidOperationException($"Es konnte keine Transaction gestartet werden, da bereits eine Transaction läuft");
            }

            Command.Transaction = (FbTransaction)await Connection.BeginTransactionAsync();
        }
        /// <summary>
        /// Committed alle Änderungen aus der Pipe-Line
        /// <para>
        /// Beim Aufruf der Methode wird die Transaction abgeschlossen und kann nicht mehr verwendet werden.
        /// </para>
        /// </summary>
        public async Task CommitChangesAsync()
        {
            try
            {
                await Command.Transaction.CommitAsync();
            }
            catch (Exception)
            {

                throw;
            }
            finally
            {
                Command.Transaction?.Dispose();
            }
        }
        /// <summary>
        /// Hebt alle ausstehenden Änderungen der Transaction auf.
        /// </summary>
        public async Task RollbackChangesAsync()
        {
            try
            {
                await Command.Transaction.RollbackAsync();
            }
            catch (Exception)
            {

                throw;
            }
        }
        #endregion
        #region IDisposable
        private void Dispose(bool disposing)
        {
            if (!_disposedValue)
            {
                if (disposing)
                {
                    // Dispose schließt die Verbindung automatisch
                    Connection.Dispose();
                    Command.Dispose();
                }

                _disposedValue = true;
            }
        }

        ~FbController2()
        {
            // Ändern Sie diesen Code nicht. Fügen Sie Bereinigungscode in der Methode "Dispose(bool disposing)" ein.
            Dispose(disposing: false);
        }

        public void Dispose()
        {
            // Ändern Sie diesen Code nicht. Fügen Sie Bereinigungscode in der Methode "Dispose(bool disposing)" ein.
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}
