Imports System.IO
Imports System.Xml
Imports System.Xml.Serialization
Imports DigitalData.Modules.Interfaces.Exceptions
Imports DigitalData.Modules.Interfaces.PDFEmbeds
Imports DigitalData.Modules.Interfaces.ZUGFeRD
Imports DigitalData.Modules.Logging
Imports GdPicture14

Public Class ZUGFeRDInterface
    Private ReadOnly _logConfig As LogConfig
    Private ReadOnly _logger As Logger
    Private ReadOnly _Options As ZugferdOptions
    Private ReadOnly _Validator As Validator

    ' These constants define the specification markers for the different
    ' zugferd document schema versions. These markers need to be used to 
    ' define the property map in the database (column SPECIFICATION).
    Public Const ZUGFERD_SPEC_DEFAULT = "DEFAULT"
    Public Const ZUGFERD_SPEC_10 = "ZUGFERD_10"
    Public Const ZUGFERD_SPEC_2x = "ZUGFERD_2x"
    Public Const ZUGFERD_SPEC_2_3x = "ZUGFERD_2_3x"

    Public Const UBL_SPEC_21 = "UBL_21"

    Public Const XMLSCHEMA_ZUGFERD_10 = "Version1_0"
    Public Const XMLSCHEMA_ZUGFERD_20 = "Version2_0"
    Public Const XMLSCHEMA_ZUGFERD_211 = "Version2_1_1"
    Public Const XMLSCHEMA_ZUGFERD_22 = "Version2_2_FacturX"
    Public Const XMLSCHEMA_ZUGFERD_23 = "Version2_3_FacturX"
    Public Const XMLSCHEMA_ZUGFERD_233 = "Version2_3_3_FacturX"
    Public Const XMLSCHEMA_ZUGFERD_24 = "Version2_4_FacturX"
    Public Const XMLSCHEMA_ZUGFERD_25 = "Version2_5_FacturX"
    Public Const XMLSCHEMA_UBL_21_INVOICE = "UBL2_1_INVOICE"
    Public Const XMLSCHEMA_UBL_21_CREDITNOTE = "UBL2_1_CREDITNOTE"

    Public Const RECEIPT_TYPE_XML = "XML"
    Public Const RECEIPT_TYPE_PDF = "PDF"

    Private ReadOnly ValidFilenames As New List(Of String) From {
        PDFEmbeds.ZUGFERD_XML_FILENAME.ToUpper,
        PDFEmbeds.FACTUR_X_XML_FILENAME_DE.ToUpper,
        PDFEmbeds.FACTUR_X_XML_FILENAME_FR.ToUpper
    }

    Private AllowedFilenames As New List(Of String)

    Public Enum ErrorType
        NoValidFile
        NoZugferd
        NoValidZugferd
        MissingProperties
        UnsupportedFormat
        FileTooBig
        UnknownError
        NotInUse
    End Enum

    Public ReadOnly Property FileGroup As FileGroups
    Public ReadOnly Property PropertyValues As PropertyValues


    Public Class ZugferdOptions
        Public Property AllowFacturX_Filename As Boolean = True
        Public Property AllowXRechnung_Filename As Boolean = True
        Public Property AllowZugferd_1_0_Schema As Boolean = True
        Public Property AllowZugferd_2_x_Schema As Boolean = True
        Public Property AllowZugferd_2_3_x_Schema As Boolean = True
        Public Property AllowPeppol_3_x_Schema As Boolean = False
    End Class

    Public Class ZugferdResult
        Public Property DataFileName As String
        Public Property XElementObject As XElement
        Public Property SchemaObject As Object
        Public Property Specification As String
        Public Property UsedXMLSchema As String
        ''' <summary>
        ''' Dateityp des Belegs: PDF oder XML
        ''' </summary>
        Public Property ReceiptFileType As String
        Public Property ValidationErrors As New List(Of ZugferdValidationError)
    End Class

    Public Class ZugferdValidationError
        Public ElementName As String
        Public ElementValue As String
        Public ErrorMessage As String
        Public ErrorMessageDE As String
        Public ErrorMessageToken As String
    End Class

    ''' <summary>
    ''' Create a new instance of ZUGFeRDInterface
    ''' </summary>
    ''' <param name="pLogConfig">A LogConfig object</param>
    ''' <param name="pGDPictureKey">A valid GDPicture License</param>
    ''' <param name="pOptions">Optional parameters to control various settings</param>
    Public Sub New(pLogConfig As LogConfig, pGDPictureKey As String, Optional pOptions As ZugferdOptions = Nothing)
        _logConfig = pLogConfig
        _logger = _logConfig.GetLogger()
        _Validator = New Validator(_logConfig)

        If pOptions Is Nothing Then
            _Options = New ZugferdOptions()
        Else
            _Options = pOptions
        End If

        ApplyFilenameOptions(_Options)

        FileGroup = New FileGroups(_logConfig)
        PropertyValues = New PropertyValues(_logConfig)

        Try
            Dim oLicenseManager As New LicenseManager
            oLicenseManager.RegisterKEY(pGDPictureKey)
        Catch ex As Exception
            _logger.Warn("GDPicture License could not be registered!")
            _logger.Error(ex)
        End Try
    End Sub

    Private Sub ApplyFilenameOptions(pOptions As ZugferdOptions)
        Dim oAllowedFilenames As List(Of String) = ValidFilenames

        If pOptions.AllowFacturX_Filename = False Then
            oAllowedFilenames = oAllowedFilenames.
                Except(New List(Of String) From {PDFEmbeds.FACTUR_X_XML_FILENAME_FR}).ToList()
        End If

        If pOptions.AllowXRechnung_Filename = False Then
            oAllowedFilenames = oAllowedFilenames.
                Except(New List(Of String) From {PDFEmbeds.FACTUR_X_XML_FILENAME_DE}).ToList()
        End If

        AllowedFilenames = oAllowedFilenames
    End Sub

    Public Function FilterPropertyMap(pPropertyMapList As List(Of XmlItemProperty), pSpecification As String) As Dictionary(Of String, XmlItemProperty)
        _logger.Debug("Filtering Property map list for Specification [{0}]", pSpecification)

        If pSpecification = ZUGFERD_SPEC_10 Then
            Try

                _logger.Debug("Special Case [{0}], including [{1}]", ZUGFERD_SPEC_10, ZUGFERD_SPEC_DEFAULT)

                Dim countElements As Integer = pPropertyMapList.Where(Function(x) x.Specification = pSpecification Or x.Specification = ZUGFERD_SPEC_DEFAULT).Count
                _logger.Debug("Property map list contains [{0}] elements for specification [{1}]", countElements, pSpecification)

                Return pPropertyMapList.
                        Where(Function(kv) kv.Specification = pSpecification Or kv.Specification = ZUGFERD_SPEC_DEFAULT).
                        ToDictionary(Function(kv) kv.XMLPath, Function(kv) kv)

            Catch ex As Exception
                _logger.Error(ex)
            End Try
        Else

            Try

                Dim countElements As Integer = pPropertyMapList.Where(Function(x) x.Specification = pSpecification).Count
                _logger.Debug("Property map list contains [{0}] elements for specification [{1}]", countElements, pSpecification)

                Return pPropertyMapList.
                        Where(Function(kv) kv.Specification = pSpecification).
                        ToDictionary(Function(kv) kv.XMLPath, Function(kv) kv)

            Catch ex As Exception
                _logger.Error(ex)
            End Try
        End If

        Return Nothing
    End Function

    Public Function GetSerializedXMLContentFromFile(oFileInfo As FileInfo) As ZugferdResult
        Dim oResult = New ZugferdResult()
        Dim oMessage As String = String.Empty

        Try
            Dim oFileSize As Integer = oFileInfo.Length
            Dim oFileData As Byte() = File.ReadAllBytes(oFileInfo.FullName)

            Dim oXmlFile As EmbeddedFile = New EmbeddedFile() With {
                .FileName = oFileInfo.Name,
                .FileContents = oFileData
            }

            Using oStream As New MemoryStream(oXmlFile.FileContents)
                oResult = New ZugferdResult With {
                    .DataFileName = oXmlFile.FileName,
                    .XElementObject = XElement.Load(oStream)
            }
            End Using

        Catch ex As ZUGFeRDExecption
            ' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code.
            ' It also produces misleading error messages when checking if an attachment is a zugferd file.
            Throw ex

        Catch ex As Exception
            _logger.Error(ex)
            Throw New ZUGFeRDExecption(ErrorCodes.UnsupportedFerdException, "Datei ist eine ungültige XML Datei.")
        End Try

        If oResult.ValidationErrors.Any() Then
            Throw New ValidationException() With {
                .ValidationErrors = oResult.ValidationErrors
            }
        End If

        Return SerializeZUGFeRDDocument(oResult)
    End Function

    ''' <summary>
    ''' Validates a ZUGFeRD File and extracts the XML Document from it
    ''' </summary>
    ''' <param name="Path"></param>
    Public Function ExtractZUGFeRDFileWithGDPicture(Path As String) As ZugferdResult
        Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path)
        oResult = ValidateZUGFeRDDocument(oResult)

        If oResult.ValidationErrors.Any() Then
            Throw New ValidationException() With {
                .ValidationErrors = oResult.ValidationErrors
            }
        End If

        Return SerializeZUGFeRDDocument(oResult)
    End Function

    ''' <summary>
    ''' Validates a ZUGFeRD File and extracts the XML Document from it
    ''' </summary>
    ''' <param name="Stream"></param>
    Public Function ExtractZUGFeRDFileWithGDPicture(Stream As Stream) As ZugferdResult
        Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream)
        oResult = ValidateZUGFeRDDocument(oResult)

        If oResult.ValidationErrors.Any() Then
            _logger.Info("Validation found [{0}] errors", oResult.ValidationErrors.Count)
            oResult.ValidationErrors.ForEach(
                Sub(e) _logger.Info("Field [{0}] with value [{1}] has error: [{2}]", e.ElementName, e.ElementValue, e.ErrorMessage)
            )

            Throw New ValidationException() With {
                .ValidationErrors = oResult.ValidationErrors
            }
        End If

        Return SerializeZUGFeRDDocument(oResult)
    End Function

    ''' <summary>
    ''' Validates a ZUGFeRD File and extracts the XML Document from it
    ''' </summary>
    ''' <param name="pStream"></param>
    ''' <returns>The embedded xml data as an XPath document</returns>
    Public Function ValidateZUGFeRDFileWithGDPicture(pStream As Stream) As ZugferdResult
        Dim oEmbedExtractor = New PDFEmbeds(_logConfig)

        Try
            ' Extract XML attachments only!
            Dim oFiles = oEmbedExtractor.Extract(pStream, New List(Of String) From {"xml"})

            ' Attachments are in this case the files that are embedded into a pdf file,
            ' like for example the zugferd-invoice.xml file
            Return HandleEmbeddedFiles(oFiles)

        Catch ex As ZUGFeRDExecption
            ' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code.
            ' It also produces misleading error messages when checking if an attachment is a zugferd file.
            Throw ex

        Catch ex As Exception
            _logger.Warn("Error while validating eInvoice file with GDPicture")
            _logger.Error(ex)
            Throw ex
        End Try
    End Function

    ''' <summary>
    ''' Validates a ZUGFeRD File and extracts the XML Document from it
    ''' </summary>
    ''' <param name="pPath"></param>
    ''' <returns>The embedded xml data as an XPath document</returns>
    Public Function ValidateZUGFeRDFileWithGDPicture(pPath As String, Optional Validate As Boolean = False) As ZugferdResult
        Dim oEmbedExtractor = New PDFEmbeds(_logConfig)

        Try
            ' Extract XML attachments only!
            Dim oFiles = oEmbedExtractor.Extract(pPath, New List(Of String) From {"xml"})
            If Validate Then
                Dim oZugferdResult = HandleEmbeddedFiles(oFiles)
                Return _Validator.ValidateZUGFeRDDocument(oZugferdResult)
            Else
                ' Attachments are in this case the files that are embedded into a pdf file,
                ' like for example the zugferd-invoice.xml file
                Return HandleEmbeddedFiles(oFiles)
            End If



        Catch ex As ZUGFeRDExecption
            ' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code.
            ' It also produces misleading error messages when checking if an attachment is a zugferd file.
            Throw ex

        Catch ex As Exception
            _logger.Warn("Error while validating eInvoice file with GDPicture")
            _logger.Error(ex)
            Throw ex
        End Try
    End Function

    Private Function HandleEmbeddedFiles(pResults As List(Of PDFEmbeds.EmbeddedFile)) As ZugferdResult
        If pResults Is Nothing OrElse pResults.Count = 0 Then
            Throw New ZUGFeRDExecption(ErrorCodes.InvalidFerdNoXMLAttachmentFound, "Die Datei ist keine gültige ZUGFeRD-Datei, es wurde kein passender XML-Anhang gefunden.")
        End If

        ' Find the first file which filename matches the valid filenames for embedded invoice files
        Dim oValidResult As PDFEmbeds.EmbeddedFile = pResults.
            Where(Function(f) ValidFilenames.Contains(f.FileName.ToUpper)).
            FirstOrDefault()

        If oValidResult Is Nothing Then
            Throw New ZUGFeRDExecption(ErrorCodes.InvalidFerdNoXMLAttachmentFound, "Die Datei ist keine ZUGFeRD Datei, weil keine passende XML-Datei gefunden wurde.")
        End If

        ' Search the embedded files for the ones which are allowed as per the configuration.
        ' The config might say, allow ZUGFeRD but not Factur-X.
        Dim oAllowedResult As PDFEmbeds.EmbeddedFile = pResults.
            Where(Function(f) AllowedFilenames.Contains(f.FileName.ToUpper)).
            FirstOrDefault()

        If oAllowedResult Is Nothing Then
            Throw New ZUGFeRDExecption(ErrorCodes.InvalidFerdNoXMLAttachmentFound, "Datei enthält einen XML-Anhang mit einem ungültigen Dateinamen.", oAllowedResult.FileName)
        End If

        Try
            Using oStream As New MemoryStream(oAllowedResult.FileContents)
                Return New ZugferdResult With {
                    .DataFileName = oAllowedResult.FileName,
                    .XElementObject = XElement.Load(oStream)
                }
            End Using

        Catch ex As ZUGFeRDExecption
            ' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code.
            ' It also produces misleading error messages when checking if an attachment is a zugferd file.
            Throw ex

        Catch ex As Exception
            _logger.Error(ex)
            Throw New ZUGFeRDExecption(ErrorCodes.InvalidFerdException, "XML-Datei konnte nicht gelesen werden.")
        End Try
    End Function

    Private Class AllowedType
        Public SchemaType As Type
        Public Specification As String
        Public XMLSchema As String
    End Class

    Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult
        Return _Validator.ValidateZUGFeRDDocument(pResult)
    End Function

    Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult
        Try
            Dim oReader As XmlReader

            Dim oObject As Object = Nothing
            Dim oSpecification As String = Nothing
            Dim oUsedXMLSchema As String = Nothing

            Dim oAllowedTypes As New List(Of AllowedType)

            If _Options.AllowZugferd_1_0_Schema Then
                oAllowedTypes.Add(New AllowedType With {
                    .SchemaType = GetType(Version1_0.CrossIndustryDocumentType),
                    .Specification = ZUGFERD_SPEC_10,
                    .XMLSchema = XMLSCHEMA_ZUGFERD_10
                })
            End If

            '' Reihenfolge 2.0 muss unverändert bleiben. Älteste Version immer zuerst!
            If _Options.AllowZugferd_2_x_Schema Then
                oAllowedTypes.AddRange(New List(Of AllowedType) From {
                    New AllowedType With {
                        .SchemaType = GetType(Version2_0.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_20
                    },
                    New AllowedType With {
                        .SchemaType = GetType(Version2_1_1.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_211
                    },
                    New AllowedType With {
                        .SchemaType = GetType(Version2_2_FacturX.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_22
                    }
                })
            End If

            '' Reihenfolge ab 2.3 geändert. Neuste Version immer zuerst bzw. oben
            If _Options.AllowZugferd_2_3_x_Schema Then
                oAllowedTypes.AddRange(New List(Of AllowedType) From {
                    New AllowedType With {
                        .SchemaType = GetType(Version2_5_FacturX.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2_3x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_25
                    },
                    New AllowedType With {
                        .SchemaType = GetType(Version2_4_FacturX.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2_3x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_24
                    },
                    New AllowedType With {
                        .SchemaType = GetType(Version2_3_3_FacturX.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2_3x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_233
                    },
                    New AllowedType With {
                        .SchemaType = GetType(Version2_3_FacturX.CrossIndustryInvoiceType),
                        .Specification = ZUGFERD_SPEC_2_3x,
                        .XMLSchema = XMLSCHEMA_ZUGFERD_23
                    }
                })
            End If

            If _Options.AllowPeppol_3_x_Schema Then
                oAllowedTypes.AddRange(New List(Of AllowedType) From {
                    New AllowedType With {
                        .SchemaType = GetType(UBL_21_Invoice.InvoiceType),
                        .Specification = UBL_SPEC_21,
                        .XMLSchema = XMLSCHEMA_UBL_21_INVOICE
                    },
                    New AllowedType With {
                        .SchemaType = GetType(UBL_21_CreditNote.CreditNoteType),
                        .Specification = UBL_SPEC_21,
                        .XMLSchema = XMLSCHEMA_UBL_21_CREDITNOTE
                    }
                })
            End If

            For Each oType In oAllowedTypes
                Dim oTypeName As String = oType.SchemaType.FullName
                Dim oSerializer As New XmlSerializer(oType.SchemaType)
                _logger.Debug("Trying Type [{0}]", oTypeName)

                Try
                    oReader = pResult.XElementObject.CreateReader()
                    oObject = oSerializer.Deserialize(oReader)
                    oSpecification = oType.Specification
                    oUsedXMLSchema = oType.XMLSchema

                    _logger.Debug("Serializing with type [{0}] succeeded", oTypeName)
                    Exit For
                Catch ex As Exception
                    _logger.Debug("Serializing with type [{0}] failed", oTypeName)
                    _logger.Debug(ex.Message)
                    If IsNothing(ex.InnerException) = False Then
                        _logger.Debug(ex.InnerException.Message)
                    End If
                End Try
            Next

            If oObject Is Nothing Then
                Throw New ZUGFeRDExecption(ErrorCodes.UnsupportedFerdException, "Unsupported Format")
            End If

            pResult.Specification = oSpecification
            pResult.UsedXMLSchema = oUsedXMLSchema
            pResult.SchemaObject = oObject

            Return pResult

        Catch ex As ZUGFeRDExecption
            _logger.Error(ex)
            Throw ex

        Catch ex As Exception
            _logger.Error(ex)
            Dim oMessage = "Datei ist eine ungültige Datei oder das Format wird nicht unterstüzt, oder das Format ist deaktiviert."
            Throw New ZUGFeRDExecption(ErrorCodes.UnsupportedFerdException, oMessage)
        End Try
    End Function
End Class
