Imports System.ComponentModel
Imports System.Data.Common
Imports System.Data.SqlClient
Imports DigitalData.Modules.Encryption
Imports DigitalData.Modules.Logging

Public Class MSSQLServer
    Implements IDatabase

    Public DBInitialized As Boolean = False
    Public CurrentSQLConnectionString As String = ""

    Public Const TIMEOUT_DEFAULT As Integer = 120
    Public Const TABLE_DEFAULT As String = "DDRESULT"

    Private ReadOnly _Timeout As Integer
    Private ReadOnly _Logger As Logger

    Public Enum TransactionMode
        <Description("Use no transaction, neither internal nor external")>
        NoTransaction
        <Description("Use the transaction supplied in the Transaction Parameter")>
        ExternalTransaction
        <Description("Create an internal transaction on the fly")>
        WithTransaction
    End Enum

    Public Sub New(LogConfig As LogConfig, ConnectionString As String, Optional Timeout As Integer = 120)
        _Logger = LogConfig.GetLogger()
        _Timeout = Timeout

        CurrentSQLConnectionString = ConnectionString

        Try
            DBInitialized = TestCanConnect()
        Catch ex As Exception
            DBInitialized = False
            _Logger.Error(ex)
        End Try
    End Sub

    Public Sub New(LogConfig As LogConfig, Server As String, Database As String, UserId As String, Password As String, Optional Timeout As Integer = 120)
        _Logger = LogConfig.GetLogger()
        _Timeout = Timeout

        CurrentSQLConnectionString = GetConnectionString(Server, Database, UserId, Password)

        Try
            DBInitialized = TestCanConnect()
        Catch ex As Exception
            DBInitialized = False
            _Logger.Error(ex)
        End Try
    End Sub

    ''' <summary>
    ''' Encrypts a connection string password.
    ''' </summary>
    ''' <param name="ConnectionString">A connection string with a plain-text password</param>
    ''' <returns>The connection string with the password encrypted.</returns>
    <DebuggerStepThrough()>
    Public Shared Function EncryptConnectionString(ConnectionString As String) As String
        Dim oEncryption As New EncryptionLegacy()
        Dim oBuilder As New SqlConnectionStringBuilder() With {.ConnectionString = ConnectionString}
        Dim oEncryptedPassword = oEncryption.EncryptData(oBuilder.Password)
        oBuilder.Password = oEncryptedPassword

        Return oBuilder.ToString()
    End Function

    ''' <summary>
    ''' Decrypts a connection string password.
    ''' </summary>
    ''' <param name="ConnectionString">A connection string with a encrypted password</param>
    ''' <returns>The connection string with the password decrypted.</returns>
    <DebuggerStepThrough()>
    Public Shared Function DecryptConnectionString(ConnectionString As String) As String
        Dim oEncryption As New EncryptionLegacy()
        Dim oBuilder As New SqlConnectionStringBuilder() With {.ConnectionString = ConnectionString}
        Dim oDecryptedPassword = oEncryption.DecryptData(oBuilder.Password)
        oBuilder.Password = oDecryptedPassword

        Return oBuilder.ToString()
    End Function

    <DebuggerStepThrough()>
    Public Function GetConnectionString(Server As String, Database As String, UserId As String, Password As String) As String
        Dim oConnectionStringBuilder As New SqlConnectionStringBuilder() With {
            .DataSource = Server,
            .InitialCatalog = Database,
            .UserId = UserId,
            .Password = Password
        }

        Return oConnectionStringBuilder.ToString
    End Function

    <DebuggerStepThrough()>
    Public Function GetConnection() As SqlConnection
        Try
            Dim oConnection = GetSQLConnection()
            Return oConnection
        Catch ex As Exception
            _Logger.Error(ex)
            Return Nothing
        End Try
    End Function

    <DebuggerStepThrough()>
    Private Function MaybeGetTransaction(Connection As SqlConnection, Mode As TransactionMode, Transaction As SqlTransaction) As SqlTransaction
        If Connection Is Nothing Then
            Throw New ArgumentNullException("Connection")
        End If

        If Mode = TransactionMode.NoTransaction Then
            Return Nothing
        ElseIf Mode = TransactionMode.ExternalTransaction Then
            Return Transaction
        Else
            Return Connection.BeginTransaction()
        End If
    End Function

    <DebuggerStepThrough()>
    Private Function MaybeCommitTransaction(Transaction As SqlTransaction, TransactionMode As TransactionMode) As Boolean
        Select Case TransactionMode
            Case TransactionMode.NoTransaction
                Return True
            Case TransactionMode.ExternalTransaction
                Return True
            Case TransactionMode.WithTransaction
                Try
                    Transaction.Commit()
                    Return True
                Catch ex As Exception
                    _Logger.Error(ex)
                    Return False
                End Try
            Case Else
                Return True
        End Select
    End Function

    Public Function GetConnectionStringForId(pConnectionId As Integer) As String
        Return Get_ConnectionStringforID(pConnectionId)
    End Function

    Public Function Get_ConnectionStringforID(pConnectionId As Integer) As String
        Dim oConnectionString As String = ""

        Try
            Dim oTable As DataTable = GetDatatable($"SELECT * FROM TBDD_CONNECTION WHERE GUID = {pConnectionId}")
            If oTable.Rows.Count = 1 Then
                Dim oRow As DataRow = oTable.Rows(0)
                Dim oProvider = oRow.Item("SQL_PROVIDER").ToString.ToUpper
                Dim oServer = oRow.Item("SERVER")
                Dim oDatabase = oRow.Item("DATENBANK")
                Dim oUser = oRow.Item("USERNAME")
                Dim oPassword = oRow.Item("PASSWORD")

                Select Case oProvider
                    Case "MS-SQL"
                        If oUser = "WINAUTH" Then
                            oConnectionString = $"Server={oServer};Database={oDatabase};Trusted_Connection=True;"
                        Else
                            oConnectionString = $"Server={oServer};Database={oDatabase};User Id={oUser};Password={oPassword};"
                        End If

                    Case "ORACLE"
                        If oRow.Item("BEMERKUNG").ToString.Contains("without tnsnames") Then
                            oConnectionString = $"Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST={oServer})(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME={oDatabase})));User Id={oUser};Password={oPassword};"
                        Else
                            oConnectionString = $"Data Source={oServer};Persist Security Info=True;User Id={oUser};Password={oPassword};Unicode=True"
                        End If

                    Case Else
                        _Logger.Warn("Provider {0} nicht unterstützt!", oProvider)

                End Select

            Else
                _Logger.Info("No entry for Connection-ID: " & pConnectionId.ToString)
            End If

        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Info("Error in bei Get_ConnectionStringforID")
        End Try

        Return DecryptConnectionString(oConnectionString)
    End Function

    <DebuggerStepThrough()>
    Private Function TestCanConnect() As Boolean
        Return TestCanConnect(CurrentSQLConnectionString)
    End Function

    Private Function TestCanConnect(ConnectionString As String) As Boolean
        Try
            _Logger.Debug("Testing connection to [{0}]", MaskConnectionString(ConnectionString))

            Dim oDecryptedConnectionString = DecryptConnectionString(ConnectionString)
            Dim oConnection As New SqlConnection(oDecryptedConnectionString)
            OpenSQLConnection(oConnection)
            oConnection.Close()
            Return True
        Catch ex As Exception
            _Logger.Error(ex)
            Return False
        End Try
    End Function

    Private Function TestCanConnect(Connection As SqlConnection) As Boolean
        Try
            If Connection Is Nothing Then
                _Logger.Warn("TestCanConnect: Connection is nothing!")
                Return False
            End If

            _Logger.Debug("Testing connection to [{0}]", MaskConnectionString(Connection.ConnectionString))

            OpenSQLConnection(Connection)
            Connection.Close()
            Return True
        Catch ex As Exception
            _Logger.Error(ex)
            Return False
        End Try
    End Function

    ''' <summary>
    ''' This Function intentionally has no try..catch block to have any errors caught outside
    ''' </summary>
    ''' <param name="Connection"></param>
    ''' <returns></returns>
    Private Function OpenSQLConnection(Connection As SqlConnection) As SqlConnection
        If Connection.State = ConnectionState.Closed Then
            Connection.Open()
        End If

        Return Connection
    End Function

    <DebuggerStepThrough()>
    Private Function GetSQLConnection() As SqlConnection
        Return GetConnection(CurrentSQLConnectionString)
    End Function

    Private Function GetConnection(ConnectionString As String) As SqlConnection
        Try
            Dim oConnection As New SqlConnection(ConnectionString)
            oConnection = OpenSQLConnection(oConnection)

            Dim oMaskedConnectionString = MaskConnectionString(ConnectionString)
            _Logger.Debug("The Following Connection is open: {0}", oMaskedConnectionString)

            Return oConnection
        Catch ex As Exception
            _Logger.Error(ex)

            Return Nothing
        End Try
    End Function

    <DebuggerStepThrough()>
    Private Function MaskConnectionString(ConnectionString As String) As String
        Try
            If ConnectionString Is Nothing OrElse ConnectionString.Length = 0 Then
                Throw New ArgumentNullException("ConnectionString")
            End If

            Dim oBuilder As New SqlConnectionStringBuilder() With {.ConnectionString = ConnectionString}
            Dim oConnectionString = ConnectionString.Replace(oBuilder.Password, "XXXXX")
            Return oConnectionString
        Catch ex As Exception
            _Logger.Error(ex)
            Return "Invalid ConnectionString"
        End Try
    End Function

    '<DebuggerStepThrough()>
    Public Function GetDatatable(SqlCommand As String) As DataTable Implements IDatabase.GetDatatable
        Return GetDatatable(SqlCommand, _Timeout)
    End Function

    ''' <summary>
    ''' Returns a datatable for a sql-statement 
    ''' </summary>
    ''' <param name="SqlCommand">sqlcommand for datatable (select XYZ from TableORView)</param>
    ''' <returns>Returns a datatable</returns>
    Public Function GetDatatable(SqlCommand As String, Timeout As Integer) As DataTable Implements IDatabase.GetDatatable
        Using oSqlConnection = GetSQLConnection()
            Return GetDatatableWithConnectionObject(SqlCommand, oSqlConnection, TransactionMode.WithTransaction, Nothing, Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Function GetDatatable(SqlCommand As String, Transaction As SqlTransaction, Optional Timeout As Integer = 120) As DataTable
        Using oSqlConnection = GetSQLConnection()
            Return GetDatatableWithConnectionObject(SqlCommand, oSqlConnection, TransactionMode.ExternalTransaction, Transaction, Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Async Function GetDatatableAsync(SqlCommand As String, Optional Timeout As Integer = 120) As Task(Of DataTable)
        Return Await Task.Run(Function() GetDatatable(SqlCommand, Timeout))
    End Function

    '<DebuggerStepThrough()>
    Public Function GetDatatableWithConnection(SqlCommand As String, ConnectionString As String, Optional Timeout As Integer = 120) As DataTable
        Using oConnection = GetConnection(ConnectionString)
            Return GetDatatableWithConnectionObject(SqlCommand, oConnection, Timeout:=Timeout)
        End Using
    End Function

    Public Function GetDatatableWithConnectionObject(SqlCommand As String, SqlConnection As SqlConnection,
                                                     Optional TransactionMode As TransactionMode = TransactionMode.WithTransaction,
                                                     Optional Transaction As SqlTransaction = Nothing,
                                                     Optional Timeout As Integer = 120) As DataTable
        Dim oTransaction As SqlTransaction = MaybeGetTransaction(SqlConnection, TransactionMode, Transaction)
        Dim oTable As New DataTable() With {.TableName = TABLE_DEFAULT}

        Try
            Dim oAdapter As New SqlDataAdapter(New SqlCommand With {
                .CommandText = SqlCommand,
                .Connection = SqlConnection,
                .Transaction = oTransaction,
                .CommandTimeout = Timeout
            })

            _Logger.Debug("GetDatatableWithConnectionObject: Running Query [{0}]", SqlCommand)

            oAdapter.Fill(oTable)
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn("GetDatatableWithConnectionObject: Error in GetDatatableWithConnection while executing command: [{0}]", SqlCommand)
            Throw ex
        Finally
            MaybeCommitTransaction(oTransaction, TransactionMode)
        End Try

        Return oTable
    End Function

    '<DebuggerStepThrough()>
    Public Function ExecuteNonQuery(SQLCommand As String) As Boolean Implements IDatabase.ExecuteNonQuery
        Using oConnection = GetSQLConnection()
            Return ExecuteNonQueryWithConnectionObject(SQLCommand, oConnection, TransactionMode.WithTransaction, Nothing, _Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Function ExecuteNonQuery(SQLCommand As String, Timeout As Integer) As Boolean Implements IDatabase.ExecuteNonQuery
        Using oConnection = GetSQLConnection()
            Return ExecuteNonQueryWithConnectionObject(SQLCommand, oConnection, TransactionMode.WithTransaction, Nothing, Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Function ExecuteNonQuery(SQLCommand As String, Transaction As SqlTransaction, Optional Timeout As Integer = 120) As Boolean
        Using oConnection = GetSQLConnection()
            Return ExecuteNonQueryWithConnectionObject(SQLCommand, Transaction.Connection, TransactionMode.ExternalTransaction, Transaction, Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Async Function ExecuteNonQueryAsync(SQLCommand As String, Optional Timeout As Integer = 120) As Task(Of Boolean)
        Return Await Task.Run(Function() ExecuteNonQuery(SQLCommand, Timeout))
    End Function

    '<DebuggerStepThrough()>
    Public Function ExecuteNonQueryWithConnection(pSQLCommand As String, ConnString As String, Optional Timeout As Integer = 120) As Boolean
        Using oConnection = GetConnection(ConnString)
            Return ExecuteNonQueryWithConnectionObject(pSQLCommand, oConnection, TransactionMode.WithTransaction, Nothing, Timeout)
        End Using
    End Function

    Public Function ExecuteNonQueryWithConnectionObject(SqlCommand As String, SqlConnection As SqlConnection,
                                                        Optional TransactionMode As TransactionMode = TransactionMode.WithTransaction,
                                                        Optional Transaction As SqlTransaction = Nothing,
                                                        Optional Timeout As Integer = 120) As Boolean
        Dim oTransaction As SqlTransaction = MaybeGetTransaction(SqlConnection, TransactionMode, Transaction)

        Try
            Using oSQLCOmmand = SqlConnection.CreateCommand()
                oSQLCOmmand.CommandText = SqlCommand
                oSQLCOmmand.CommandTimeout = Timeout
                oSQLCOmmand.Transaction = oTransaction
                oSQLCOmmand.ExecuteNonQuery()
            End Using

            Return True
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn("ExecuteNonQueryWithConnectionObject: Error in ExecuteNonQueryWithConnectionObject while executing command: [{0}]-[{1}]", SqlCommand, SqlConnection.ConnectionString)
            Return False
        Finally
            MaybeCommitTransaction(oTransaction, TransactionMode)
        End Try
    End Function

    '<DebuggerStepThrough()>
    Public Function GetScalarValue(SQLQuery As String) As Object Implements IDatabase.GetScalarValue
        Using oConnection As SqlConnection = GetSQLConnection()
            Return GetScalarValueWithConnectionObject(SQLQuery, oConnection)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Function GetScalarValue(SQLCommand As String, Timeout As Integer) As Object Implements IDatabase.GetScalarValue
        Using oConnection = GetSQLConnection()
            Return GetScalarValueWithConnectionObject(SQLCommand, oConnection, TransactionMode.WithTransaction, Nothing, Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Function GetScalarValue(SQLCommand As String, Transaction As SqlTransaction, Optional Timeout As Integer = 120) As Object
        Using oConnection = GetSQLConnection()
            Return GetScalarValueWithConnectionObject(SQLCommand, oConnection, TransactionMode.ExternalTransaction, Transaction, Timeout)
        End Using
    End Function

    '<DebuggerStepThrough()>
    Public Async Function GetScalarValueAsync(SQLQuery As String, Optional Timeout As Integer = 120) As Task(Of Object)
        Return Await Task.Run(Function() GetScalarValue(SQLQuery, Timeout))
    End Function

    '<DebuggerStepThrough()>
    Public Function GetScalarValueWithConnection(SQLCommand As String, ConnectionString As String, Optional Timeout As Integer = 120) As Object
        Using oConnection = GetConnection(ConnectionString)
            Return GetScalarValueWithConnectionObject(SQLCommand, oConnection, TransactionMode.WithTransaction, Nothing, Timeout)
        End Using
    End Function

    Public Function GetScalarValueWithConnectionObject(SqlCommand As String, SqlConnection As SqlConnection,
                                                       Optional TransactionMode As TransactionMode = TransactionMode.WithTransaction,
                                                       Optional Transaction As SqlTransaction = Nothing,
                                                       Optional Timeout As Integer = 120) As Object

        Dim oTransaction As SqlTransaction = MaybeGetTransaction(SqlConnection, TransactionMode, Transaction)
        Dim oResult As Object = Nothing

        Try
            Using oSQLCOmmand = SqlConnection.CreateCommand()
                oSQLCOmmand.CommandText = SqlCommand
                oSQLCOmmand.CommandTimeout = Timeout
                oSQLCOmmand.Transaction = oTransaction

                oResult = oSQLCOmmand.ExecuteScalar()
            End Using
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn("GetDatatableWithConnectionObject: Error in GetDatatableWithConnection while executing command: [{0}]", SqlCommand)
        Finally
            MaybeCommitTransaction(oTransaction, TransactionMode)
        End Try

        Return oResult
    End Function

    Public Function GetScalarValue(SQLCommand As SqlCommand, OutputParameter As String, Timeout As Integer) As Object
        Try
            If TestCanConnect() = False Then
                Return Nothing
            End If

            _Logger.Debug("GetScalarValue: Running Query [{0}]", SQLCommand)

            If SQLCommand.CommandText.Contains(" ") Then
                SQLCommand.CommandType = CommandType.Text
            Else
                SQLCommand.CommandType = CommandType.StoredProcedure
            End If

            Using oConnection As SqlConnection = GetSQLConnection()

                SQLCommand.Connection = oConnection
                SQLCommand.Parameters(OutputParameter).Direction = ParameterDirection.Output
                SQLCommand.CommandTimeout = Timeout
                SQLCommand.ExecuteNonQuery()
                oConnection.Close()

                Return SQLCommand.Parameters(OutputParameter).Value
            End Using
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn($"GetScalarValue failed SQLCommand [{SQLCommand}]")

            Return Nothing
        End Try
    End Function

    '<DebuggerStepThrough()>
    Public Function GetScalarValue(SQLCommand As SqlCommand, OutputParameter As String) As Object
        Return GetScalarValue(SQLCommand, OutputParameter, _Timeout)
    End Function

    ''' <summary>
    ''' Executes the passed sql-statement in asyncmode
    ''' </summary>
    ''' <param name="SqlCommand">the sql statement</param>
    ''' <param name="commandtimeout">Optional Timeout</param>
    ''' <remarks></remarks>
    Public Sub NewExecuteNonQueryAsync(SqlCommand As String, Optional commandtimeout As Integer = 120)
        _Logger.Debug("NewExecuteNonQueryAsync: Running Query [{0}]", SqlCommand)

        Try
            Dim oCallback As New AsyncCallback(AddressOf NewExecuteNonQueryAsync_Callback)

            Using oConnection As SqlConnection = GetSQLConnection()
                Using oSQLCOmmand = oConnection.CreateCommand()
                    oSQLCOmmand.CommandText = SqlCommand
                    oSQLCOmmand.CommandTimeout = commandtimeout
                    oSQLCOmmand.BeginExecuteNonQuery(oCallback, oSQLCOmmand)
                End Using
            End Using
        Catch ex As Exception
            _Logger.Error(ex)
            _Logger.Warn($"NewExecuteNonQueryAsync failed SQLCommand [{SqlCommand}]")

        End Try
    End Sub

    '<DebuggerStepThrough()>
    Private Sub NewExecuteNonQueryAsync_Callback(ByVal result As IAsyncResult)
        Dim command As SqlCommand = CType(result.AsyncState, SqlCommand)
        Dim res = command.EndExecuteNonQuery(result)
        _Logger.Info("Finished executing Async database operation: {0}", command.CommandText)
    End Sub
End Class
