Imports DigitalData.Modules.Database
Imports DigitalData.Modules.EDMI.API
Imports DigitalData.Modules.EDMI.API.Constants
Imports DigitalData.Modules.EDMI.API.EDMIServiceReference
Imports DigitalData.Modules.Logging

Public Class DatabaseWithFallback
    Private ReadOnly _Logger As Logger
    Private ReadOnly _Client As Client
    Private ReadOnly _ClientConfig As GlobalStateClientConfiguration
    Private ReadOnly _DatabaseECM As MSSQLServer
    Private _DatabaseIDB As MSSQLServer

    ''' <summary>
    ''' Options for GetDatatable
    ''' </summary>
    Public Class GetDatatableOptions
        Public ReadOnly FallbackSQL
        Public ReadOnly FallbackType

        ''' <summary>
        ''' Filter expression for the cached Datatable
        ''' </summary>
        Public Property FilterExpression As String = ""
        ''' <summary>
        ''' Columns to sort the cached Datatable by
        ''' </summary>
        Public Property SortByColumn As String = ""
        ''' <summary>
        ''' Force the fallback, skipping the service completely
        ''' </summary>
        Public Property ForceFallback As Boolean = False
        ''' <summary>
        ''' Connection Id to use, references TBDD_CONNECTION
        ''' </summary>
        Public Property ConnectionId As Integer = 0

        ''' <summary>
        ''' Creates a new options object for GetDatatable options
        ''' </summary>
        ''' <param name="pFallbackSQL">SQL Command to execute as fallback</param>
        ''' <param name="pFallbackType">Named Database to use for the fallback SQL Command</param>
        Public Sub New(pFallbackSQL As String, pFallbackType As Constants.DatabaseType)
            FallbackSQL = pFallbackSQL
            FallbackType = pFallbackType
        End Sub
    End Class

    Public Sub New(LogConfig As LogConfig, Client As Client, DatabaseECM As MSSQLServer, DatabaseIDB As MSSQLServer)
        _Logger = LogConfig.GetLogger()
        _Client = Client
        _DatabaseECM = DatabaseECM
        _DatabaseIDB = DatabaseIDB

        _Logger.Debug("Client exists: [{0}]", Not IsNothing(Client))
        _Logger.Debug("DatabaseECM exists: [{0}]", Not IsNothing(DatabaseECM))
        _Logger.Debug("DatabaseIDB exists: [{0}]", Not IsNothing(DatabaseIDB))

        ' Load client config, will throw if client is not yet connected to service
        _ClientConfig = Client?.ClientConfig
    End Sub

    ''' <summary>
    ''' Set the IDB Database class after initializing the class. 
    ''' It is now your responsibility to make sure to not use any IDB calls before calling this method.
    ''' </summary>
    Public Sub InitializeIDB(pDatabaseIDB As MSSQLServer)
        _DatabaseIDB = pDatabaseIDB
    End Sub

    Public Function GetConnectionString(pConnectionId As Integer) As String
        Return _DatabaseECM.GetConnectionStringForId(pConnectionId)
    End Function

#Region "GetDatatable"
    Public Function GetDatatableECM(pSQL As String) As DataTable
        Return GetDatatable(New GetDatatableOptions(pSQL, Constants.DatabaseType.ECM))
    End Function

    Public Async Function GetDatatableECMAsync(pSQL As String) As Task(Of DataTable)
        Return Await Task.Run(Function() GetDatatableECM(pSQL))
    End Function

    Public Function GetDatatableIDB(pSQL As String) As DataTable
        Return GetDatatable(New GetDatatableOptions(pSQL, Constants.DatabaseType.IDB))
    End Function

    Public Async Function GetDatatableIDBAsync(pSQL As String) As Task(Of DataTable)
        Return Await Task.Run(Function() GetDatatableIDB(pSQL))
    End Function

    Public Function GetDatatableWithConnection(pSQL As String, pConnectionId As Integer) As DataTable
        Return GetDatatable(New GetDatatableOptions(pSQL, Constants.DatabaseType.None) With {.ConnectionId = pConnectionId})
    End Function

    Public Async Function GetDatatableWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of DataTable)
        Return Await Task.Run(Function() GetDatatableWithConnection(pSQL, pConnectionId))
    End Function
#End Region

#Region "GetScalarValue"
    Public Function GetScalarValueECM(pSQL As String) As Object
        Return GetScalarValue(pSQL, Constants.DatabaseType.ECM)
    End Function

    Public Async Function GetScalarValueECMAsync(pSQL As String) As Task(Of Object)
        Return Await Task.Run(Function() GetScalarValueECM(pSQL))
    End Function

    Public Function GetScalarValueIDB(pSQL As String) As Object
        Return GetScalarValue(pSQL, Constants.DatabaseType.IDB)
    End Function

    Public Async Function GetScalarValueIDBAsync(pSQL As String) As Task(Of Object)
        Return Await Task.Run(Function() GetScalarValueIDB(pSQL))
    End Function

    Public Function GetScalarValueWithConnection(pSQL As String, pConnectionId As Integer) As Object
        Return GetScalarValue(pSQL, Constants.DatabaseType.None, pConnectionId:=pConnectionId)
    End Function

    Public Async Function GetScalarValueWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of Object)
        Return Await Task.Run(Function() GetScalarValueWithConnection(pSQL, pConnectionId))
    End Function
#End Region

#Region "ExecuteNonQuery"
    Public Function ExecuteNonQueryECM(pSQL As String) As Boolean
        Return ExecuteNonQuery(pSQL, Constants.DatabaseType.ECM)
    End Function

    Public Async Function ExecuteNonQueryECMAsync(pSQL As String) As Task(Of Boolean)
        Return Await Task.Run(Function() ExecuteNonQueryECM(pSQL))
    End Function

    Public Function ExecuteNonQueryIDB(pSQL As String) As Boolean
        Return ExecuteNonQuery(pSQL, Constants.DatabaseType.IDB)
    End Function

    Public Async Function ExecuteNonQueryIDBAsync(pSQL As String) As Task(Of Boolean)
        Return Await Task.Run(Function() ExecuteNonQueryIDB(pSQL))
    End Function

    Public Function ExecuteNonQueryWithConnection(pSQL As String, pConnectionId As Integer) As Boolean
        Return ExecuteNonQuery(pSQL, Constants.DatabaseType.None, pConnectionId:=pConnectionId)
    End Function

    Public Async Function ExecuteNonQueryWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of Boolean)
        Return Await Task.Run(Function() ExecuteNonQueryWithConnection(pSQL, pConnectionId))
    End Function
#End Region


    ''' <summary>
    ''' Returns a Datatable by trying to fetch a cached version from the service, then querying the database through the service and querying the database directly as fallback. 
    ''' </summary>
    ''' <param name="pDataTableName">Name of the Cached Datatable</param>
    ''' <param name="pOptions">Options object</param>
    Public Function GetDatatable(pDataTableName As String, pOptions As GetDatatableOptions) As DataTable
        Return GetDatatable(pDataTableName, pOptions.FallbackSQL, pOptions.FallbackType, pOptions.FilterExpression, pOptions.SortByColumn, pOptions.ForceFallback, pOptions.ConnectionId)
    End Function

    Public Function GetDatatable(pSQL As String, pConnectionId As Integer) As DataTable
        Return GetDatatable("FORCE_FALLBACK", pSQL, Constants.DatabaseType.ECM, pSortByColumn:=Nothing, pForceFallback:=False, pConnectionId:=pConnectionId)
    End Function

    ''' <summary>
    ''' Returns a datatable directly from the database.
    ''' </summary>
    ''' <param name="pOptions">Options object</param>
    Public Function GetDatatable(pOptions As GetDatatableOptions) As DataTable
        Dim oForceFallback = True
        Return GetDatatable("FORCE_FALLBACK", pOptions.FallbackSQL, pOptions.FallbackType, pOptions.FilterExpression, pOptions.SortByColumn, oForceFallback, pOptions.ConnectionId)
    End Function

    ''' <summary>
    ''' Returns a Datatable by trying to fetch a cached version from the service, then querying the database through the service and querying the database directly as fallback.
    ''' </summary>
    ''' <param name="pDataTableName">Name of the Cached Datatable</param>
    ''' <param name="pFallbackSQL">SQL Command to execute as fallback</param>
    ''' <param name="pFallbackType">Named Database to use for the fallback SQL Command</param>
    ''' <param name="pFilterExpression">Filter expression for the cached Datatable</param>
    ''' <param name="pSortByColumn">Columns to sort the cached Datatable by</param>
    ''' <param name="pForceFallback">Force the fallback, skipping the service completely</param>
    ''' <param name="pConnectionId">Connection Id to use, references TBDD_CONNECTION</param>
    Public Function GetDatatable(pDataTableName As String, pFallbackSQL As String, pFallbackType As Constants.DatabaseType, Optional pFilterExpression As String = "", Optional pSortByColumn As String = "", Optional pForceFallback As Boolean = False, Optional pConnectionId As Integer = 0) As DataTable
        Try
            Dim oResult As DataTable = Nothing
            Dim oTableResult As TableResult = Nothing

            ' If there is no client, we assume there is no service (configured)
            If _Client Is Nothing Then
                _Logger.Debug("Client is empty, falling back to direct database access.")
                Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId)
            End If

            ' If ForceFallback flag is set, we go to database immediately
            If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then
                _Logger.Debug("ForceFallback is True, falling back to direct database access.")
                Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId)
            End If

            ' If the table is not cached, we try going through the service
            If Not IsTableCached(pDataTableName) Then
                _Logger.Debug("Datatable is not chached, fetching data from service.")
                Return GetDatatableFromService(pFallbackSQL, pFallbackType, pConnectionId)
            End If

            ' If there is a proper ConnectionId, we try going through the service
            If pConnectionId > 0 Then
                _Logger.Debug("ConnectionId is set, fetching data from service.")
                Return GetDatatableFromService(pFallbackSQL, pFallbackType, pConnectionId)
            End If

            Try
                _Logger.Debug("Datatable is chached, fetching data from cache.")
                oTableResult = _Client.GetDatatableByName(pDataTableName, pFilterExpression, pSortByColumn)
            Catch ex As Exception
                _Logger.Error(ex)
                oTableResult = Nothing
            End Try

            If oTableResult Is Nothing OrElse oTableResult.OK = False Then
                _Logger.Warn("Datatable [{0}] could not be fetched from AppServer Cache. Falling back to direct Database Access.", pDataTableName)
                Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId)
            Else
                Return oTableResult.Table
            End If

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

        End Try
    End Function

    ''' <summary>
    ''' Returns a Scalar Value by querying the database through the service and querying the database directly as fallback.
    ''' </summary>
    ''' <param name="pSQL">SQL Command to execute as fallback</param>
    ''' <param name="pDatabaseType">Named Database to use for the fallback SQL Command</param>
    ''' <param name="pForceFallback">Force the fallback, skipping the service completely</param>
    ''' <param name="pConnectionId">Connection Id to use, references TBDD_CONNECTION</param>
    Public Function GetScalarValue(pSQL As String, pDatabaseType As Constants.DatabaseType, Optional pForceFallback As Boolean = False, Optional pConnectionId As Integer = 0) As Object
        Try
            ' If there is no client, we assume there is no service (configured)
            If _Client Is Nothing Then
                Return GetScalarValueFromDatabase(pSQL, pDatabaseType, pConnectionId)
            End If

            ' If ForceFallback flag is set, we go to database immediately
            If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then
                Return GetScalarValueFromDatabase(pSQL, pDatabaseType, pConnectionId)
            End If

            Return GetScalarValueFromService(pSQL, pDatabaseType, pConnectionId)

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

        End Try
    End Function

    ''' <summary>
    ''' Returns a Scalar Value by querying the database through the service and querying the database directly as fallback.
    ''' </summary>
    ''' <param name="pSQL">SQL Command to execute as fallback</param>
    ''' <param name="pDatabaseType">Named Database to use for the fallback SQL Command</param>
    ''' <param name="pForceFallback">Force the fallback, skipping the service completely</param>
    ''' <param name="pConnectionId">Connection Id to use, references TBDD_CONNECTION</param>
    Public Function ExecuteNonQuery(pSQL As String, pDatabaseType As Constants.DatabaseType, Optional pForceFallback As Boolean = False, Optional pConnectionId As Integer = 0) As Boolean
        Try
            Dim oResult As DataTable = Nothing
            Dim oTableResult As TableResult = Nothing

            ' If there is no client, we assume there is no service (configured)
            If _Client Is Nothing Then
                Return ExecuteNonQueryFromDatabase(pSQL, pDatabaseType, pConnectionId)
            End If

            ' If ForceFallback flag is set, we go to database immediately
            If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then
                Return ExecuteNonQueryFromDatabase(pSQL, pDatabaseType, pConnectionId)
            End If

            Return ExecuteNonQueryFromService(pSQL, pDatabaseType, pConnectionId)

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

        End Try
    End Function

    Private Function IsTableCached(pName As String) As Boolean
        If _Client Is Nothing Then
            Return False
        End If

        Return _Client.CachedTables.Contains(pName.ToUpper)
    End Function

    Private Function GetDatatableFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable
        Try
            Dim oResult As GetDatatableResponse = Nothing

            Select Case DatabaseType
                Case Constants.DatabaseType.ECM
                    oResult = _Client.GetDatatableFromECM(pSQLCommand)

                Case Constants.DatabaseType.IDB
                    oResult = _Client.GetDatatableFromIDB(pSQLCommand)

                Case Else
                    Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)

            End Select

            If oResult Is Nothing Then
                Throw New ApplicationException("Unexpected server error ocurred!")
            End If

            If oResult.OK = False Then
                Throw New ApplicationException(oResult.ErrorMessage)
            End If

            Return oResult.Table

        Catch ex As Exception
            _Logger.Warn("GetDatatableFromService failed. Falling back to direct database access.")
            _Logger.Error(ex)
            Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)

        End Try
    End Function


    Private Function GetDatatableFromDatabase(pSQLCommand As String, pDatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable
        Try
            _Logger.Debug("Fetching data from database [{0}] with Connection Id [{0}]", pDatabaseType.ToString, pConnectionId)

            Select Case pDatabaseType
                Case Constants.DatabaseType.IDB
                    Return _DatabaseIDB.GetDatatable(pSQLCommand)

                Case Else
                    If pConnectionId > 0 Then
                        _Logger.Debug("Retrieving Connection String from Connection Id [{0}]", pConnectionId)
                        Dim oConnectionString = _DatabaseECM.Get_ConnectionStringforID(pConnectionId)
                        Return _DatabaseECM.GetDatatableWithConnection(pSQLCommand, oConnectionString)
                    Else
                        Return _DatabaseECM.GetDatatable(pSQLCommand)
                    End If

            End Select

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

        End Try
    End Function

    Private Function GetScalarValueFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object
        Try
            Dim oResult As GetScalarValueResponse = Nothing

            Select Case DatabaseType
                Case Constants.DatabaseType.ECM
                    oResult = _Client.GetScalarValueFromECM(pSQLCommand)

                Case Constants.DatabaseType.IDB
                    oResult = _Client.GetScalarValueFromIDB(pSQLCommand)

                Case Else
                    Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)

            End Select

            If oResult Is Nothing Then
                Throw New ApplicationException("Unexpected server error ocurred!")
            End If

            If oResult.OK = False Then
                Throw New ApplicationException(oResult.ErrorMessage)
            End If

            Return oResult.Scalar

        Catch ex As Exception
            _Logger.Warn("GetScalarValueFromService failed. Falling back to direct database access.")
            _Logger.Error(ex)
            Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)

        End Try
    End Function


    Private Function GetScalarValueFromDatabase(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object
        Try
            Select Case DatabaseType
                Case Constants.DatabaseType.IDB
                    Return _DatabaseIDB.GetScalarValue(pSQLCommand)

                Case Else
                    If pConnectionId > 0 Then
                        Dim oConnectionString = _DatabaseECM.Get_ConnectionStringforID(pConnectionId)
                        Return _DatabaseECM.GetScalarValueWithConnection(pSQLCommand, oConnectionString)
                    Else
                        Return _DatabaseECM.GetScalarValue(pSQLCommand)
                    End If

            End Select

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

        End Try
    End Function

    Private Function ExecuteNonQueryFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean
        Try
            Dim oResult As ExecuteNonQueryResponse = Nothing

            Select Case DatabaseType
                Case Constants.DatabaseType.ECM
                    oResult = _Client.ExecuteNonQueryFromECM(pSQLCommand)

                Case Constants.DatabaseType.IDB
                    oResult = _Client.ExecuteNonQueryFromIDB(pSQLCommand)

                Case Else
                    Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)

            End Select

            If oResult Is Nothing Then
                Throw New ApplicationException("Unexpected server error ocurred!")
            End If

            If oResult.OK = False Then
                Throw New ApplicationException(oResult.ErrorMessage)
            End If

            Return oResult.Result

        Catch ex As Exception
            _Logger.Warn("ExecuteNonQueryFromService failed. Falling back to direct database access.")
            _Logger.Error(ex)
            Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)

        End Try
    End Function

    Private Function ExecuteNonQueryFromDatabase(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean
        Try
            Select Case DatabaseType
                Case Constants.DatabaseType.IDB
                    Return _DatabaseIDB.ExecuteNonQuery(pSQLCommand)

                Case Else
                    If pConnectionId > 0 Then
                        Dim oConnectionString = _DatabaseECM.Get_ConnectionStringforID(pConnectionId)
                        Return _DatabaseECM.ExecuteNonQueryWithConnection(pSQLCommand, oConnectionString)
                    Else
                        Return _DatabaseECM.ExecuteNonQuery(pSQLCommand)
                    End If

            End Select

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

        End Try
    End Function
End Class

