Imports System.Collections.Generic
Imports System.Data
Imports System.IO
Imports DigitalData.Modules.Base
Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Logging
Imports GdPicture14
Imports System.Drawing
Imports System.Linq
Imports System.Text.RegularExpressions
'11.11.2025

Public Class XRechnungViewDocument
    Private ReadOnly _logger As Logger
    Private ReadOnly _logConfig As LogConfig
    Private ReadOnly _filesystem As FilesystemEx
    Private ReadOnly _file As ZUGFeRD.FileFunctions
    Private ReadOnly _gdpictureLicenseKey As String
    Private fontResName As String
    Private fontResNameBold As String
    Private fontResNameItalic As String

    ' Layout-Konstanten
    Private Const MARGIN_LEFT As Integer = 10
    Private Const MARGIN_TOP As Integer = 15
    Private Const LINE_WIDTH As Integer = 200
    Private Const PAGE_HEIGHT_LIMIT As Integer = 278  ' ← war fälschlich auf 275 zurückgesetzt: bei 275 bleiben
    ' bis zu 2 Zeilen (10mm) ungenutzter Platz vor dem Footer
    ' (FOOTER_Y=280), wodurch Inhalt unnötig auf die nächste
    ' Seite umbricht, obwohl noch Platz vorhanden wäre.
    Private Const FOOTER_Y As Integer = 280
    Private Const FOOTER_TEXT_Y As Integer = 285
    Private Const LINE_HEIGHT As Integer = 5

    ' Spalten-Positionen für Tabellen
    Private Const COL_POS_NUMBER As Integer = 10
    Private Const COL_POS_AMOUNT As Integer = 19
    Private Const COL_POS_UNIT As Integer = 35
    Private Const COL_POS_TEXT As Integer = 50
    Private Const COL_POS_REASON As Integer = 20
    Private Const COL_POS_TAX As Integer = 163
    Private Const COL_POS_SUM As Integer = 181
    Private Const COL_VALUE_X As Integer = 70

    ' Spalten für TAXPOS-Tabelle (Steuerbeträge):
    ' BT 116  <TAB> 2.116,36 €                              (BASEAMOUNT-Zeile)
    ' BT 119  <TAB> 7.00 %  <TAB> 148,14 €  <TAB> VAT        (RATE-Zeile)
    Private Const COL_TAXPOS_LABEL As Integer = MARGIN_LEFT   ' "BT 116" / "BT 119"
    Private Const COL_TAXPOS_VALUE1 As Integer = 50           ' Basisbetrag ODER Steuersatz (%)
    Private Const COL_TAXPOS_AMOUNT As Integer = 100          ' Steuerbetrag (nur RATE-Zeile)
    Private Const COL_TAXPOS_TYPE As Integer = 150            ' VAT-Kennzeichen (nur RATE-Zeile)

    ' Text-Größen
    Private Const TEXT_SIZE_TITLE As Integer = 18
    Private Const TEXT_SIZE_NORMAL As Integer = 10

    ' Text-Längen für Umbruch
    Private Const MAX_TEXT_LENGTH_FULL As Integer = 112
    Private Const MAX_TEXT_LENGTH_POSITION As Integer = 64
    Private Const MAX_TEXT_LENGTH_NOTE As Integer = 70

    Public Sub New(LogConfig As LogConfig, MSSQL As MSSQLServer, GDPictureLicenseKey As String)
        _logConfig = LogConfig
        _logger = LogConfig.GetLogger()
        _filesystem = New FilesystemEx(_logConfig)
        _file = New ZUGFeRD.FileFunctions(LogConfig, MSSQL)
        _gdpictureLicenseKey = GDPictureLicenseKey
    End Sub

    Public Function Create_PDFfromXML(pXmlFile As FileInfo, pDTItemValues As DataTable) As FileInfo
        _logger.Debug("Create_PDFfromXML() Start")

        Try
            ' 1. Initialisierung der Dateipfade
            Dim paths As FilePaths = InitializeFilePaths(pXmlFile)
            If paths Is Nothing Then Return Nothing

            ' 2. PDF erstellen und konfigurieren
            Dim pdfDoc As GdPicturePDF = InitializePDF()
            If pdfDoc Is Nothing Then Return Nothing

            ' 3. Rendering-Context erstellen
            Dim context As New PdfRenderContext(pdfDoc, paths.CreatedString)

            ' 4. Erste Seite mit Header/Footer
            CreateNewPage(context)

            ' 5. Daten verarbeiten und rendern
            ProcessInvoiceData(context, pDTItemValues)

            ' 6. XML einbetten und speichern
            Return FinalizePDF(pdfDoc, paths)

        Catch ex As Exception
            _logger.Error(ex)
            Return Nothing
        End Try
    End Function

#Region "Initialisierung"

    Private Function InitializeFilePaths(xmlFile As FileInfo) As FilePaths
        Try
            Dim xmlPath As String = xmlFile.FullName
            Dim tempPath As String = Path.Combine(Path.GetDirectoryName(xmlPath), "temp")

            ' Temp-Verzeichnis erstellen
            If Not Directory.Exists(tempPath) Then
                Directory.CreateDirectory(tempPath)
            End If

            Dim tempXmlPath As String = Path.Combine(tempPath, "xrechnung.xml")
            If File.Exists(tempXmlPath) Then
                File.Delete(tempXmlPath)
            End If

            Dim pdfFilename As String = Regex.Replace(xmlFile.Name, ".xml", ".pdf", RegexOptions.IgnoreCase)
            Dim outputPath As String = Path.Combine(Path.GetDirectoryName(xmlPath), pdfFilename)

            If File.Exists(outputPath) Then
                File.Delete(outputPath)
            End If

            Dim paths As New FilePaths With {
                .XmlPath = xmlPath,
                .TempPath = tempPath,
                .TempXmlPath = tempXmlPath,
                .PdfFilename = pdfFilename,
                .OutputPath = outputPath,
                .CreatedString = $"Maschinell erstellt durch / Automatically created by Digital Data E-Rechnung Parser: {Now.ToString}"
            }

            _logger.Debug("Create_PDFfromXML() Resulting PDF Filepath: [{0}]", paths.OutputPath)
            Return paths

        Catch ex As Exception
            _logger.Error("Error initializing file paths", ex)
            Return Nothing
        End Try
    End Function

    Private Function InitializePDF() As GdPicturePDF
        Try
            Dim licManager As New LicenseManager()
            licManager.RegisterKEY(_gdpictureLicenseKey)

            Dim pdf As New GdPicturePDF()
            Dim status As GdPictureStatus = pdf.NewPDF(PdfConformance.PDF_A_3a)

            If status <> GdPictureStatus.OK Then
                _logger.Warn($"Error initializing PDF: {status}")
                Return Nothing
            End If

            ' PDF-Einstellungen
            pdf.SetOrigin(PdfOrigin.PdfOriginTopLeft)
            pdf.SetMeasurementUnit(PdfMeasurementUnit.PdfMeasurementUnitMillimeter)
            pdf.SetLineWidth(1)
            pdf.SetTitle("xInvoice VisualReceipt")
            pdf.SetAuthor("Digital Data GmbH, Ludwig Rinn Str. 16, 35452 Heuchelheim")

            ' Fonts initialisieren
            fontResName = pdf.AddStandardFont(PdfStandardFont.PdfStandardFontHelvetica)
            fontResNameBold = pdf.AddStandardFont(PdfStandardFont.PdfStandardFontHelveticaBold)
            fontResNameItalic = pdf.AddStandardFont(PdfStandardFont.PdfStandardFontHelveticaBoldOblique)

            Return pdf

        Catch ex As Exception
            _logger.Error("Error initializing PDF", ex)
            Return Nothing
        End Try
    End Function

#End Region

#Region "Seiten-Management"

    Private Sub CreateNewPage(context As PdfRenderContext)
        Dim status As GdPictureStatus = context.PDF.NewPage(PdfPageSizes.PdfPageSizeA4)
        If status <> GdPictureStatus.OK Then
            Throw New Exception($"Could not create page: {status}")
        End If

        DrawHeader(context.PDF)
        DrawFooter(context.PDF, context.CreatedString)
        context.YPosition = MARGIN_TOP + 30 ' Nach Header
        ' WICHTIG: CurrentPositionPage wird hier NICHT gesetzt.
        ' Es gehört zum Positions-Anker und wird nur in HandleArticleTextFollowUp
        ' und HandlePositionAmountFollowUp gesetzt – nicht beim Seitenbruch selbst.
    End Sub

    Private Sub DrawHeader(pdf As GdPicturePDF)
        Dim y As Integer = MARGIN_TOP

        pdf.SetTextSize(TEXT_SIZE_TITLE)
        pdf.DrawText(fontResName, MARGIN_LEFT, y, "xRechnung Sichtbeleg - xInvoice Visual Receipt")
        y += 10

        pdf.SetTextSize(TEXT_SIZE_NORMAL)
        pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_DE_Row1)
        y += LINE_HEIGHT
        pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_DE_Row2)
        y += LINE_HEIGHT
        pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_EN_Row1)
        y += LINE_HEIGHT
        pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_EN_Row2)
    End Sub

    Private Sub DrawFooter(pdf As GdPicturePDF, createdString As String)
        pdf.DrawLine(MARGIN_LEFT, FOOTER_Y, LINE_WIDTH, FOOTER_Y)
        pdf.DrawText(fontResName, MARGIN_LEFT, FOOTER_TEXT_Y, createdString)
    End Sub

    Private Sub CheckAndCreateNewPageIfNeeded(context As PdfRenderContext)
        ' ← NEU: Prüfe BEIDE Positionen
        Dim maxY As Integer = Math.Max(context.YPosition, context.YDynamic)

        If maxY >= PAGE_HEIGHT_LIMIT Then
            Dim status As GdPictureStatus = context.PDF.NewPage(PdfPageSizes.PdfPageSizeA4)
            If status <> GdPictureStatus.OK Then
                _logger.Warn($"Could not create a second page. The error was: {status}")
                Throw New Exception($"Could not create page: {status}")
            End If

            DrawHeader(context.PDF)
            DrawFooter(context.PDF, context.CreatedString)
            context.YPosition = MARGIN_TOP + 30
            context.YDynamic = context.YPosition  ' ← WICHTIG: Reset YDynamic!
        End If
    End Sub

#End Region

#Region "Datenverarbeitung"

    Private Sub ProcessInvoiceData(context As PdfRenderContext, dataTable As DataTable)
        Dim formerItemSpecName As String = ""

        For Each oRow As DataRow In dataTable.Rows
            ' Prüfen ob neue Seite benötigt wird
            CheckAndCreateNewPageIfNeeded(context)

            Dim itemData As New InvoiceItemData(oRow)

            ' Interne Zeilen behandeln
            If itemData.NormalizedArea = "INTERNAL" Then
                HandleInternalRow(context, itemData)
                Continue For
            End If

            _logger.Debug($"WorkingItem: Area=[{itemData.NormalizedArea}] SpecName=[{itemData.SpecName}] Value=[{itemData.Value}] Caption=[{itemData.Caption}] Display=[{itemData.Display}] LastRow=[{itemData.IsLastRowSameArea}]")

            ' WICHTIG: Area-Switch-Flag VORHER setzen!
            Dim isAreaSwitch As Boolean = (context.CurrentArea <> itemData.NormalizedArea)

            ' Area-Wechsel behandeln
            If isAreaSwitch Then
                formerItemSpecName = ""  ' Reset bei Area-Wechsel
                HandleAreaSwitch(context, itemData)
            Else
                HandleFollowUpItem(context, itemData, formerItemSpecName)
            End If

            ' Item anzeigen - MIT Area-Switch-Info übergeben!
            If itemData.Display AndAlso Not String.IsNullOrEmpty(itemData.Value) Then
                RenderDisplayItem(context, itemData, isAreaSwitch)  ' ← Parameter hinzugefügt!
            End If
        Next
    End Sub

    Private Sub HandleInternalRow(context As PdfRenderContext, itemData As InvoiceItemData)
        _logger.Debug("Next Item as Area is internal")
        If itemData.SpecName = "STATIC_Y_SWITCH" Then
            context.YPosition = CInt(itemData.Value)
        End If
    End Sub

#End Region

#Region "Area-Switch Handling"

    Private Sub HandleAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
        context.CurrentArea = itemData.NormalizedArea
        context.CreateTextBox = False
        _logger.Debug($"Area-Switch to: {context.CurrentArea}")

        ' Area-Header zeichnen - ÜBERGIBT itemData komplett
        Dim areaCaption As String = GetAreaCaption(context, itemData)
        If Not String.IsNullOrEmpty(areaCaption) Then
            ' ← NEU: VORHER prüfen ob genug Platz für Header + minimale Inhalte (3 Zeilen = 15mm)
            Dim requiredSpace As Integer = 15 ' Header (3x LINE_HEIGHT) + mindestens 1 Inhalt-Zeile

            If (context.YPosition + requiredSpace) >= PAGE_HEIGHT_LIMIT Then
                CreateNewPage(context)
                context.YDynamic = 0  ' Seitenbruch: YDynamic-Korrektur unten darf nicht greifen
                _logger.Debug($"Area-Switch [{context.CurrentArea}]: Page break BEFORE header! New YPosition: {context.YPosition}")
            End If

            ' *** BUGFIX: Leerzeile vor Area-Header bei POSITION/DESCRIPTION-Übergang ***
            '
            ' DrawAreaHeader zeichnet die Trennlinie bei: context.YPosition + LINE_HEIGHT.
            ' Nach Areas die RenderMultiLineText verwenden (POSITION, ALLOWANCE), hat
            ' HandleArticleTextFollowUp YPosition bereits auf YDynamic gesetzt – das ist
            ' eine Zeile NACH dem letzten gerenderten Text (so arbeitet RenderMultiLineText).
            ' DrawAreaHeader addiert nochmals +LINE_HEIGHT → insgesamt 2 Zeilen Abstand
            ' statt der gewünschten 1 Zeile (= Trennlinie direkt nach dem letzten Inhalt).
            '
            ' Erkennungsmerkmal: context.YDynamic > 0 bedeutet, der vorige Bereich hat
            ' YPosition über den YDynamic-Sync bereits um 1 LINE_HEIGHT vorgerückt.
            ' Korrektur: 1 LINE_HEIGHT wieder abziehen, damit DrawAreaHeader den richtigen
            ' Abstand erzeugt. YDynamic selbst wird NICHT zurückgesetzt, damit
            ' yPositionAlreadyAdvanced in HandlePositionAmountFollowUp weiterhin korrekt
            ' erkennen kann, dass kein weiterer Vorschub nötig ist.
            If context.YDynamic > 0 Then
                context.YPosition -= LINE_HEIGHT
            End If

            DrawAreaHeader(context, areaCaption)
            DrawAreaSpecificHeaders(context)
        End If

        ' Area-spezifische Initialisierung - VERWENDET context.CurrentArea (normalisiert)
        Select Case context.CurrentArea
            Case "TYPE"
                HandleTypeAreaSwitch(context, itemData)
            Case "POSITION"
                HandlePositionAreaSwitch(context, itemData)
            Case "ALLOWANCE"
                HandleAllowanceAreaSwitch(context, itemData)
            Case "INCL_NOTE"
                HandleIncludedNoteAreaSwitch(context, itemData)
            Case "TAXPOS"
                HandleTaxPosAreaSwitch(context, itemData)
            Case "AMOUNT"
                context.CreateTextBox = True
        End Select
    End Sub

    Private Function GetAreaCaption(context As PdfRenderContext, itemData As InvoiceItemData) As String
        ' VERWENDET NormalizedArea für Switch, aber AreaCaptionFromPattern für INCL_NOTE
        Select Case itemData.NormalizedArea
            Case "TYPE"
                Return $"{Return_InvType(itemData.Value)} [{itemData.Value}]"
            Case "SELLER"
                Return "Verkäufer / Seller:"
            Case "BUYER"
                Return "Käufer / Buyer:"
            Case "HEAD"
                Return String.Empty
            Case "POSITION"
                Return "Positionen / Positions:"
            Case "INCL_NOTE"
                ' Verwendet den Caption aus dem Pattern INCL_NOTE#...
                If Not String.IsNullOrEmpty(itemData.AreaCaptionFromPattern) Then
                    Return itemData.AreaCaptionFromPattern
                End If
                ' Fallback wenn kein Pattern vorhanden
                Return "Notizen und Hinweise / Notes:"
            Case "ALLOWANCE"
                Return GetAllowanceCaption(context, itemData)
            Case "AMOUNT"
                Return "Beträge / Amounts:"
            Case "TAXPOS"
                Return "Steuerbeträge / Tax amounts:"
            Case "PAYMENT"
                Return "Zahlungsinformationen / Payment details:"
            Case "EXEMPTION"
                Return "UST.-Befreiungsgrund / Exemption reason:"
            Case Else
                Return String.Empty
        End Select
    End Function

    Private Function GetAllowanceCaption(context As PdfRenderContext, itemData As InvoiceItemData) As String
        If itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then
            If itemData.Value = "False" Then
                context.HasDiscount = True
                Return "Rabatt/Discount:"
            Else
                Return "Zuschlag/Surcharge:"
            End If
        End If
        Return "Zu- oder Abschlag/Surcharge or Discount:"
    End Function

    Private Sub DrawAreaHeader(context As PdfRenderContext, caption As String)
        context.YPosition += LINE_HEIGHT
        _logger.Debug($"Drawing area header for [{context.CurrentArea}] with caption [{caption}] at YPosition={context.YPosition}")
        context.PDF.DrawLine(MARGIN_LEFT, context.YPosition, LINE_WIDTH, context.YPosition)
        context.YPosition += LINE_HEIGHT
        context.PDF.DrawText(fontResNameBold, MARGIN_LEFT, context.YPosition, caption)
        context.YPosition += LINE_HEIGHT

        If context.CurrentArea = "TYPE" Then
            context.PDF.DrawLine(MARGIN_LEFT, context.YPosition, LINE_WIDTH, context.YPosition)
            context.YPosition += LINE_HEIGHT
        End If
    End Sub

    Private Sub DrawAreaSpecificHeaders(context As PdfRenderContext)
        'VERWENDET context.CurrentArea (bereits normalisiert)
        Select Case context.CurrentArea
            Case "POSITION"
                DrawPositionTableHeader(context)
            Case "INCL_NOTE"
            ' Kein spezifischer Header für INCL_NOTE mehr nötig
            ' Der Caption kommt bereits aus GetAreaCaption
            Case "ALLOWANCE"  ' ← KORRIGIERT!
                DrawAllowanceTableHeader(context)
        End Select
    End Sub

    Private Sub DrawPositionTableHeader(context As PdfRenderContext)
        context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "Pos#")
        context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, "Anz./am.")
        context.PDF.DrawText(fontResName, COL_POS_UNIT, context.YPosition, "Einh/Unit")
        context.PDF.DrawText(fontResName, COL_POS_TEXT, context.YPosition, "Pos.Text")
        context.PDF.DrawText(fontResName, COL_POS_TAX, context.YPosition, "Steuer/Tax")
        context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, "Betrag/Sum")
    End Sub

    Private Sub DrawAllowanceTableHeader(context As PdfRenderContext)
        context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "Pos#")
        context.PDF.DrawText(fontResName, COL_POS_REASON, context.YPosition, "Grund/Reason")
        context.PDF.DrawText(fontResName, COL_POS_TAX, context.YPosition, "Steuer/Tax")
        context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, "Betrag/Sum")
        context.PositionCount = 0
    End Sub

#End Region

#Region "Area-Specific Switch Handlers"

    Private Sub HandleTypeAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
        If itemData.SpecName = "INVOICE_CURRENCY" Then
            If itemData.Value <> "EUR" Then
                context.CurrencySymbol = itemData.Value
            End If
        End If
    End Sub

    Private Sub HandlePositionAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
        If itemData.SpecName = "INVOICE_POSITION_AMOUNT" Then
            ' KEIN Increment hier - wird in Follow-Up gemacht
            context.YDynamic = 0
            context.YPosition += LINE_HEIGHT  ' Neue Zeile für erste Position
            context.CurrentPositionStartY = context.YPosition
            context.CurrentPositionPage = context.PDF.GetPageCount()
            context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "1")  ' Erste Position ist immer 1
            context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, itemData.Value)
            context.PositionCount = 1  ' Zähler auf 1 setzen statt increment
            itemData.Display = False
        End If
    End Sub

    Private Sub HandleAllowanceAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
        ' Erste Allowance: Prüfe ob es CHARGE_INDICATOR (Metadata) oder ACTUAL_AMOUNT (erste Zeile) ist
        If itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then
            ' Nur Metadata - nichts rendern, nur Flag setzen
            _logger.Debug($"Found RECEIPT_ALLOWANCE_CHARGE_INDICATOR with value [{itemData.Value}]. Setting HasDiscount={itemData.Value = "False"} in context.")
            itemData.Display = False
        ElseIf {"RECEIPT_ALLOWANCE_ACTUAL_AMOUNT", "POSITION_ALLOWANCE_ACTUAL_AMOUNT"}.Contains(itemData.SpecName) Then
            ' Erste Allowance-Zeile
            context.YPosition += LINE_HEIGHT
            context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "1")
            Dim currTerm As String = FormatCurrency(itemData.Value, context.CurrencySymbol)
            If context.HasDiscount AndAlso itemData.SpecName = "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT" AndAlso Not currTerm.StartsWith("-") Then
                currTerm = "-" + currTerm
            End If
            context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, currTerm)  ' ← In COL_POS_SUM!
            context.PositionCount = 1
            itemData.Display = False
            _logger.Debug($"First Allowance/Charge rendered with amount [{currTerm}]. PositionCount set to 1. HasDiscount={context.HasDiscount}. Display set to False.")
        End If
    End Sub

    Private Sub HandleIncludedNoteAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
        ' Erste Note direkt ausgeben
        context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, itemData.Value)
        itemData.Display = False
    End Sub

    Private Sub HandleTaxPosAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
        ' Die TAXPOS-Area kann pro Steuersatz-Gruppe mit BASEAMOUNT ODER RATE beginnen:
        '   Gruppe: [BASEAMOUNT] RATE AMOUNT TYPE   (BASEAMOUNT optional, ggf. nicht vorhanden)
        ' Jede Gruppe ergibt GENAU EINE Zeile, sobald TYPE (VAT) eintrifft - außer der
        ' optionalen BASEAMOUNT-Zeile, die SOFORT und SEPARAT gerendert wird.
        ' WICHTIG: Dies ist die allererste Zeile der Area - kein zusätzlicher Zeilenvorschub.
        context.IsFirstTaxPosDisplay = True

        If itemData.SpecName = "INVOICE_TAXPOS_BASEAMOUNT" Then
            RenderTaxposBaseAmountRow(context, itemData)
            itemData.Display = False
        ElseIf itemData.SpecName = "INVOICE_TAXPOS_RATE" Then
            context.TaxPosRate = itemData.Value
            context.TaxPosRateCaption = itemData.Caption
            itemData.Display = False
            _logger.Debug($"TAXPOS RATE in AreaSwitch (group start, no BASEAMOUNT) accumulated: [{context.TaxPosRate}]")
        End If
    End Sub

    ''' <summary>
    ''' Rendert die Basisbetrag-Zeile sofort in eigener Zeile: "BT 116 <TAB> 2.116,36 €"
    ''' Diese Zeile ist unabhängig von der nachfolgenden RATE/AMOUNT/TYPE-Zeile.
    ''' </summary>
    Private Sub RenderTaxposBaseAmountRow(context As PdfRenderContext, itemData As InvoiceItemData)
        ' Erste Zeile der TAXPOS-Area braucht keinen zusätzlichen Zeilenvorschub
        ' (Area-Header hat YPosition bereits korrekt positioniert); alle weiteren Zeilen schon.
        If Not context.IsFirstTaxPosDisplay Then
            context.YPosition += LINE_HEIGHT
        End If
        context.IsFirstTaxPosDisplay = False

        Dim baseAmountFormatted As String = FormatCurrency(itemData.Value, context.CurrencySymbol)
        context.PDF.DrawText(fontResName, COL_TAXPOS_LABEL, context.YPosition, itemData.Caption)
        context.PDF.DrawText(fontResName, COL_TAXPOS_VALUE1, context.YPosition, baseAmountFormatted)
        _logger.Debug($"TAXPOS BASEAMOUNT row rendered: [{itemData.Caption}] [{baseAmountFormatted}] at Y={context.YPosition}")
    End Sub

#End Region


#Region "Follow-Up Item Handling"
    Private Sub HandleFollowUpItem(context As PdfRenderContext, itemData As InvoiceItemData, ByRef formerItemSpecName As String)
        _logger.Debug($"FollowItem START - CurrentArea: [{context.CurrentArea}] - ItemArea: [{itemData.NormalizedArea}] - SpecName: [{itemData.SpecName}] - Value: [{itemData.Value}] - YPos: [{context.YPosition}]")

        Dim descriptionFollowup As Boolean = False

        Select Case context.CurrentArea
            Case "POSITION", "ALLOWANCE"
                _logger.Debug($"FollowItem: Entering POSITION/ALLOWANCE handler")
                HandlePositionOrAllowanceFollowUp(context, itemData, formerItemSpecName, descriptionFollowup)
            Case "HEAD"
                _logger.Debug($"FollowItem: Entering HEAD handler")
                HandleHeadFollowUp(itemData)
            Case "TAXPOS"
                _logger.Debug($"FollowItem: Entering TAXPOS handler")
                HandleTaxPosFollowUp(context, itemData)
            Case "INCL_NOTE"
                _logger.Debug($"FollowItem: Entering INCL_NOTE handler - YPosition before={context.YPosition}")
                context.YPosition += LINE_HEIGHT
                context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, itemData.Value)
                itemData.Display = False
                _logger.Debug($"FollowItem: INCL_NOTE handler - YPosition after={context.YPosition}, Display set to False")
            Case Else
                _logger.Warn($"FollowItem: UNHANDLED CurrentArea=[{context.CurrentArea}] for SpecName=[{itemData.SpecName}]")
        End Select

        _logger.Debug($"FollowItem END - CurrentArea: [{context.CurrentArea}] - YPos: [{context.YPosition}] - Display: [{itemData.Display}]")
    End Sub

    Private Sub HandlePositionOrAllowanceFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, ByRef formerItemSpecName As String, ByRef descriptionFollowup As Boolean)
        ' Former Item Tracking
        If itemData.SpecName <> formerItemSpecName AndAlso formerItemSpecName <> "" Then
            If itemData.SpecName = "INVOICE_POSITION_ARTICLE_DESCRIPTION" AndAlso formerItemSpecName = "INVOICE_POSITION_ARTICLE" Then
                descriptionFollowup = True
            Else
                formerItemSpecName = itemData.SpecName
            End If
        Else
            formerItemSpecName = itemData.SpecName
        End If

        ' Spezifische Item-Behandlung
        If {"INVOICE_POSITION_AMOUNT", "POSITION_ALLOWANCE_ACTUAL_AMOUNT", "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT"}.Contains(itemData.SpecName) Then
            HandlePositionAmountFollowUp(context, itemData, descriptionFollowup)
        ElseIf itemData.SpecName = "INVOICE_POSITION_UNIT_TYPE" Then
            HandleUnitTypeFollowUp(context, itemData)
        ElseIf {"POSITION_ALLOWANCE_REASON", "RECEIPT_ALLOWANCE_REASON"}.Contains(itemData.SpecName) Then
            context.PDF.DrawText(fontResName, COL_POS_REASON, context.YPosition, itemData.Value)
            itemData.Display = False
        ElseIf {"INVOICE_POSITION_ARTICLE", "INVOICE_POSITION_ARTICLE_DESCRIPTION"}.Contains(itemData.SpecName) Then
            HandleArticleTextFollowUp(context, itemData, descriptionFollowup)
        ElseIf itemData.SpecName = "INVOICE_POSITION_NOTE" Then
            HandlePositionNoteFollowUp(context, itemData)
        ElseIf {"INVOICE_TAXPOS_TAX_RATE", "INVOICE_TAXPOS_RATE"}.Contains(itemData.SpecName) Then
            HandleTaxRateFollowUp(context, itemData)
        ElseIf {"RECEIPT_ALLOWANCE_VAT_RATE", "POSITION_ALLOWANCE_VAT_RATE"}.Contains(itemData.SpecName) Then
            HandleTaxRateFollowUp(context, itemData)
        ElseIf itemData.SpecName = "INVOICE_POSITION_TAX_AMOUNT" Then
            HandlePositionTaxAmountFollowUp(context, itemData)
        ElseIf {"RECEIPT_ALLOWANCE_VAT_CODE", "POSITION_ALLOWANCE_VAT_CODE"}.Contains(itemData.SpecName) Then
            itemData.Display = False
        ElseIf {"RECEIPT_ALLOWANCE_CALCULATION_PERCENT", "POSITION_ALLOWANCE_CALCULATION_PERCENT"}.Contains(itemData.SpecName) Then
            itemData.Display = False
        ElseIf itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then
            itemData.Display = False
        End If
    End Sub

    Private Sub HandlePositionAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean)
        context.PositionCount += 1

        ' *** BUGFIX: Unnötige Leerzeile zwischen Positionen ***
        '
        ' HandleArticleTextFollowUp synct am Ende jeder Position:
        '   context.YPosition = Math.Max(context.YPosition, context.YDynamic)
        ' Nach einer mehrzeiligen DESCRIPTION (z.B. 2 Teile) zeigt YDynamic bereits auf die
        ' NÄCHSTE freie Zeile (RenderMultiLineText erhöht YDynamic nach jedem Teil). D.h.
        ' YPosition steht zu Beginn dieser Methode oft schon korrekt auf der nächsten freien
        ' Zeile - ein zusätzliches "+= LINE_HEIGHT" weiter unten würde dann eine echte
        ' Leerzeile zwischen letzter DESCRIPTION-Zeile und der neuen Position erzeugen.
        '
        ' Erkennungsmerkmal: YDynamic > 0 UND YDynamic = YPosition bedeutet, die vorherige
        ' Position hat bereits über RenderMultiLineText/HandleArticleTextFollowUp die
        ' YPosition auf die korrekte nächste freie Zeile gesetzt - dann KEIN weiterer Vorschub.
        Dim yPositionAlreadyAdvanced As Boolean = (context.YDynamic > 0) AndAlso (context.YDynamic = context.YPosition)

        If Not descriptionFollowup Then
            context.YPlus = 0
            context.YDynamic = 0
        End If

        If Not yPositionAlreadyAdvanced Then
            context.YPosition += LINE_HEIGHT
        End If
        context.CurrentPositionStartY = context.YPosition
        context.CurrentPositionPage = context.PDF.GetPageCount()

        ' ✓ OPTIMIERT: Von 11mm auf 6mm
        ' WARUM 6mm?
        ' - Position#-Zeile: 5mm (wird SOFORT gerendert)
        ' - Tax/Amount: 0mm Extra (werden auf DERSELBEN Y-Position gerendert via CurrentPositionStartY)
        ' - Minimaler Puffer: 1mm (für Textbox-Rendering-Toleranzen)
        Dim requiredSpace As Integer = 6

        If (context.YPosition + requiredSpace) >= PAGE_HEIGHT_LIMIT Then
            CreateNewPage(context)
            context.YDynamic = context.YPosition
            context.CurrentPositionStartY = context.YPosition
            context.CurrentPositionPage = context.PDF.GetPageCount()  ' neue Seite ist der Anker
            _logger.Debug($"HandlePositionAmountFollowUp: Page break! New YPosition={context.YPosition}, CurrentPositionStartY={context.CurrentPositionStartY}, Page={context.CurrentPositionPage}")
        End If

        context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, context.PositionCount.ToString())

        If itemData.SpecName = "INVOICE_POSITION_AMOUNT" Then
            context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, itemData.Value)
        ElseIf {"POSITION_ALLOWANCE_ACTUAL_AMOUNT", "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT"}.Contains(itemData.SpecName) Then
            Dim term As String = FormatCurrency(itemData.Value, context.CurrencySymbol)
            If context.HasDiscount AndAlso itemData.SpecName = "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT" AndAlso Not term.StartsWith("-") Then
                term = "-" + term
            End If
            context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, term)
        Else
            If context.YDynamic = 0 Then
                context.YDynamic = context.YPosition
            End If
            RenderMultiLineText(context, itemData.Value, COL_POS_AMOUNT, MAX_TEXT_LENGTH_POSITION)
        End If

        itemData.Display = False
    End Sub

    Private Sub HandleUnitTypeFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
        context.YPlus = 0
        Dim unit As String = Return_UnitType(itemData.Value)
        context.PDF.DrawText(fontResName, COL_POS_UNIT, context.YPosition, unit)
        itemData.Display = False
    End Sub

    Private Sub HandleArticleTextFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean)
        If Not descriptionFollowup Then
            context.YPlus = 0
        End If

        If context.YDynamic = 0 Then
            context.YDynamic = context.YPosition
        End If

        Dim xPos As Integer = COL_POS_TEXT
        If itemData.SpecName.Contains("ALLOWANCE") Then
            xPos = COL_POS_REASON
        End If

        ' *** BUGFIX: CurrentPositionStartY auf die ERSTE Zeile von INVOICE_POSITION_ARTICLE verankern ***
        '
        ' Tax (COL_POS_TAX) und Amount (COL_POS_SUM) müssen in der ERSTEN Zeile des Artikeltexts
        ' erscheinen - dort wo der Artikelname und damit auch der Steuersatz/Betrag laut Layout
        ' hingehören. Bei kurzen (einzeiligen) Artikeln ist "erste Zeile" = "letzte Zeile", daher
        ' lieferte die alte Berechnung (YDynamic - LINE_HEIGHT, NACH dem Rendern) bislang dasselbe
        ' Ergebnis. Bei LANGEN, mehrzeiligen Artikeltexten (z.B. 15 Zeilen Prüfbeschreibung, die
        ' selbst über einen Seitenbruch laufen) zeigte "YDynamic - LINE_HEIGHT NACH dem Rendern"
        ' auf die LETZTE Zeile des Artikeltexts (ggf. auf einer späteren Seite) - das war falsch.
        '
        ' Die neuen ByRef-Parameter firstAnchorY/firstAnchorPage liefern stattdessen exakt den
        ' Y-Wert und die Seite des ALLERERSTEN tatsächlich gezeichneten Textteils, erfasst
        ' INNERHALB von RenderMultiLineText direkt vor dem ersten DrawText-Aufruf - das ist in
        ' allen Fällen korrekt, auch wenn der Seitenbruch noch VOR dem ersten Zeichnen eintritt
        ' (Szenario B: Artikel beginnt komplett auf neuer Seite).
        '
        '   Szenario A – kein Seitenbruch, kurzer Artikel:  erste Zeile=240/Seite1 → Anker=240/1 (wie bisher) ✓
        '   Szenario B – FIRST-part-Break (Artikel beginnt auf neuer Seite): erste Zeile=45/Seite2 → Anker=45/2 (wie bisher) ✓
        '   Szenario C – CONTINUATION-Break in DESCRIPTION (Artikel einzeilig, bleibt auf alter Seite):
        '                erste Zeile=265/Seite1 → Anker=265/1 (wie bisher, unverändert) ✓
        '   Szenario D – NEU: langer mehrzeiliger ARTIKEL, der selbst über mehrere Seiten läuft:
        '                erste Zeile=220/Seite1 → Anker=220/1 (KORRIGIERT, vorher fälschlich letzte Zeile/spätere Seite)
        If itemData.SpecName = "INVOICE_POSITION_ARTICLE" Then
            Dim firstAnchorY As Integer = -1
            Dim firstAnchorPage As Integer = -1
            RenderMultiLineText(context, itemData.Value, xPos, MAX_TEXT_LENGTH_POSITION, firstAnchorY, firstAnchorPage)

            context.CurrentPositionStartY = firstAnchorY
            context.CurrentPositionPage = firstAnchorPage
            _logger.Debug($"HandleArticleTextFollowUp: CurrentPositionStartY anchored to FIRST ARTICLE row Y={context.CurrentPositionStartY} on page {context.CurrentPositionPage}")
        Else
            RenderMultiLineText(context, itemData.Value, xPos, MAX_TEXT_LENGTH_POSITION)
        End If

        ' YPosition immer synchronisieren
        context.YPosition = Math.Max(context.YPosition, context.YDynamic)

        itemData.Display = False
    End Sub

    Private Sub HandlePositionNoteFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
        If context.YDynamic = 0 Then
            context.YDynamic = context.YPosition
        End If

        RenderMultiLineText(context, itemData.Value, COL_POS_TEXT, MAX_TEXT_LENGTH_NOTE)

        ' ✓ KORRIGIERT: YPosition synchronisieren
        context.YPosition = Math.Max(context.YPosition, context.YDynamic)

        itemData.Display = False
    End Sub

    Private Sub HandleTaxRateFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
        Dim yPos As Integer = context.CurrentPositionStartY
        Dim targetPage As Integer = context.CurrentPositionPage
        Dim activePage As Integer = context.PDF.GetPageCount()

        _logger.Debug($"Handling Tax Rate Follow-Up: Value=[{itemData.Value}] at YPos={yPos}, targetPage={targetPage}, activePage={activePage}")

        If yPos < MARGIN_TOP + 30 Then
            _logger.Warn($"TaxRate: yPos={yPos} ungültig. Verwende YPosition={context.YPosition}.")
            yPos = context.YPosition
            targetPage = activePage
        End If

        If targetPage <> activePage Then
            context.PDF.SelectPage(targetPage)
            _logger.Debug($"TaxRate: SelectPage({targetPage}) to draw at Y={yPos}, then back to page {activePage}")
        End If

        ' SetTextSize explizit setzen: GdPicturePDF hält Font-Zustand global;
        ' nach SelectPage ist er undefiniert und muss vor jedem DrawText normalisiert werden.
        context.PDF.SetTextSize(TEXT_SIZE_NORMAL)
        context.PDF.DrawText(fontResName, COL_POS_TAX, yPos, $"{itemData.Value} %")

        If targetPage <> activePage Then
            context.PDF.SelectPage(activePage)
            context.PDF.SetTextSize(TEXT_SIZE_NORMAL)  ' Font-Zustand der aktiven Seite wiederherstellen
        End If

        itemData.Display = False
    End Sub

    Private Sub HandlePositionTaxAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
        Dim yPos As Integer = context.CurrentPositionStartY
        Dim targetPage As Integer = context.CurrentPositionPage
        Dim activePage As Integer = context.PDF.GetPageCount()

        If yPos < MARGIN_TOP + 30 Then
            _logger.Warn($"TaxAmount: yPos={yPos} ungültig. Verwende YPosition={context.YPosition}.")
            yPos = context.YPosition
            targetPage = activePage
        End If

        Dim yPosAdjusted As Double = yPos - 3.5
        Dim taxTerm As String = FormatCurrency(itemData.Value, context.CurrencySymbol)
        _logger.Debug($"Handling Position Tax Amount Follow-Up: Value=[{itemData.Value}] Formatted=[{taxTerm}] at YPos={yPos}, Adjusted={yPosAdjusted}, targetPage={targetPage}, activePage={activePage}")

        If targetPage <> activePage Then
            context.PDF.SelectPage(targetPage)
            _logger.Debug($"TaxAmount: SelectPage({targetPage}) to draw at Y={yPosAdjusted}, then back to page {activePage}")
        End If

        ' SetTextSize explizit setzen – gleicher Grund wie in HandleTaxRateFollowUp.
        context.PDF.SetTextSize(TEXT_SIZE_NORMAL)
        context.PDF.DrawTextBox(fontResName, 177, yPosAdjusted, 198, YCoo_TextBoxPlus5(yPosAdjusted),
                       TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentNear,
                       taxTerm)

        If targetPage <> activePage Then
            context.PDF.SelectPage(activePage)
            context.PDF.SetTextSize(TEXT_SIZE_NORMAL)  ' Font-Zustand der aktiven Seite wiederherstellen
        End If

        itemData.Display = False
    End Sub

    Private Sub HandleHeadFollowUp(itemData As InvoiceItemData)
        If {"INVOICE_DATE", "INVOICE_SERVICE_DATE"}.Contains(itemData.SpecName) Then
            itemData.Value = StringFunctions.DatetimeStringToGermanStringConverter(itemData.Value, _logger)
        End If
    End Sub

    Private Sub HandleTaxPosFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
        If itemData.SpecName = "INVOICE_TAXPOS_BASEAMOUNT" Then
            ' BASEAMOUNT kann auch als Follow-Up (zweite und weitere Steuersatz-Gruppen)
            ' auftreten, nicht nur im allerersten Area-Switch. Eigene Zeile, sofort gerendert.
            RenderTaxposBaseAmountRow(context, itemData)
            itemData.Display = False
        ElseIf itemData.SpecName = "INVOICE_TAXPOS_RATE" Then
            context.PositionCount += 1
            context.TaxPosRate = itemData.Value
            context.TaxPosRateCaption = itemData.Caption
            itemData.Display = False
            _logger.Debug($"TAXPOS RATE accumulated (new group): [{context.TaxPosRate}]")
        ElseIf itemData.SpecName = "INVOICE_TAXPOS_AMOUNT" Then
            context.TaxPosAmount = FormatCurrency(itemData.Value, context.CurrencySymbol)
            itemData.Display = False
            _logger.Debug($"TAXPOS AMOUNT accumulated: [{context.TaxPosAmount}]")
        ElseIf itemData.SpecName = "INVOICE_TAXPOS_TYPE" Then
            ' Vollständige Gruppe (RATE + AMOUNT + TYPE) ist da → eine Zeile rendern:
            ' "BT 119 <TAB> 7.00 % <TAB> 148,14 € <TAB> VAT"
            If Not String.IsNullOrEmpty(context.TaxPosRate) Then
                ' Nur Zeilenvorschub wenn dies NICHT die allererste Zeile der Area ist
                ' (z.B. wenn die Gruppe direkt mit RATE begann, ohne BASEAMOUNT davor).
                If Not context.IsFirstTaxPosDisplay Then
                    context.YPosition += LINE_HEIGHT
                End If
                context.IsFirstTaxPosDisplay = False

                context.PDF.DrawText(fontResName, COL_TAXPOS_LABEL, context.YPosition, context.TaxPosRateCaption)
                context.PDF.DrawText(fontResName, COL_TAXPOS_VALUE1, context.YPosition, $"{context.TaxPosRate} %")
                context.PDF.DrawText(fontResName, COL_TAXPOS_AMOUNT, context.YPosition, context.TaxPosAmount)
                context.PDF.DrawText(fontResName, COL_TAXPOS_TYPE, context.YPosition, itemData.Value)

                _logger.Debug($"TAXPOS row rendered: [{context.TaxPosRateCaption}] [{context.TaxPosRate} %] [{context.TaxPosAmount}] [{itemData.Value}] at Y={context.YPosition}")

                ' Reset für nächste TAXPOS-Gruppe
                context.TaxPosRate = ""
                context.TaxPosRateCaption = ""
                context.TaxPosAmount = ""
            Else
                _logger.Debug($"TAXPOS TYPE DUPLICATE/incomplete ignored: [{itemData.Value}]")
            End If
            itemData.Display = False
        ElseIf itemData.Value.Contains("EXEMPTION") Then
            _logger.Debug($"We got an Exemption: {itemData.Value}")
        End If
    End Sub

#End Region

#Region "Rendering"
    Private Sub RenderDisplayItem(context As PdfRenderContext, itemData As InvoiceItemData, isAreaSwitch As Boolean)
        ' Spezielle Behandlung für TAXPOS: Erstes Display-Item nach Area-Switch
        Dim skipLineHeight As Boolean = isAreaSwitch OrElse context.IsFirstTaxPosDisplay

        If context.IsFirstTaxPosDisplay Then
            context.IsFirstTaxPosDisplay = False  ' Reset nach Verwendung
            _logger.Debug($"RenderDisplayItem: TAXPOS first display item - skipping line height")
        End If

        ' Y-Position anpassen - ABER NICHT nach Area-Switch oder erstem TAXPOS Display!
        If Not itemData.IsLastRowSameArea AndAlso Not skipLineHeight Then
            context.YPosition += LINE_HEIGHT
            _logger.Debug($"RenderDisplayItem: Adding line height. New YPosition: {context.YPosition}")
        End If

        ' Formatierung für Währungsfelder
        Dim displayValue As String = itemData.Value
        If ShouldFormatAsCurrency(context.CurrentArea, itemData.SpecName) Then
            displayValue = FormatCurrency(itemData.Value, context.CurrencySymbol)
        End If

        ' Rendern basierend auf Layout
        If Not String.IsNullOrEmpty(itemData.Caption) Then
            RenderLabelValuePair(context, itemData.Caption, displayValue)
        Else
            If itemData.IsLastRowSameArea Then
                context.PDF.DrawText(fontResName, itemData.XPosition, context.YPosition, displayValue)
            Else
                RenderTextWithWrapping(context, displayValue)
            End If
        End If
    End Sub

    Private Function ShouldFormatAsCurrency(area As String, specName As String) As Boolean
        If area <> "AMOUNT" AndAlso area <> "ALLOWANCE" Then Return False

        Dim currencyFields As String() = {
            "INVOICE_TOTAL_TAX", "INVOICE_TOTAL_NET", "INVOICE_TOTAL_GROSS",
            "INVOICE_TOTAL_CHARGE_AMOUNT", "POSITION_ALLOWANCE_ACTUAL_AMOUNT",
            "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT", "POSITION_ALLOWANCE_CALCULATION_PERCENT",
            "RECEIPT_ALLOWANCE_CALCULATION_PERCENT"
        }

        Return currencyFields.Contains(specName)
    End Function

    Private Sub RenderLabelValuePair(context As PdfRenderContext, label As String, value As String)
        context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, label)

        If context.CreateTextBox Then
            Dim textBoxYPos As Integer = context.YPosition - 3
            context.PDF.DrawTextBox(fontResName, COL_VALUE_X, textBoxYPos, 90, YCoo_TextBoxPlus5(textBoxYPos),
                                   TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentCenter,
                                   value)
        Else
            context.PDF.DrawText(fontResName, COL_VALUE_X, context.YPosition, value)
        End If
    End Sub

    Private Sub RenderTextWithWrapping(context As PdfRenderContext, text As String)
        If text.Length > MAX_TEXT_LENGTH_FULL Then
            For i As Integer = 0 To text.Length - 1 Step MAX_TEXT_LENGTH_FULL
                Dim length As Integer = Math.Min(MAX_TEXT_LENGTH_FULL, text.Length - i)
                Dim part As String = text.Substring(i, length)
                context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, part)
                context.YPosition += LINE_HEIGHT
            Next
        Else
            context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, text)
        End If
    End Sub

    Private Sub RenderMultiLineText(context As PdfRenderContext, text As String, xPos As Integer, maxLength As Integer,
                                     Optional ByRef firstRenderedY As Integer = -1,
                                     Optional ByRef firstRenderedPage As Integer = -1)
        Dim partsNL As List(Of String) = StringFunctions.SplitTextByNewLine(text)
        Dim isFirstPart As Boolean = True

        For Each linePart As String In partsNL
            Dim parts As List(Of String) = StringFunctions.SplitText_Length(linePart, maxLength)

            For Each part As String In parts
                ' ✓ PROBLEM GEFUNDEN: requiredSpace=10 ist zu viel!
                ' Bei YDynamic=230 prüft es: 230 + 10 = 240 >= 270 → TRUE → Seitenumbruch
                ' ABER: Die Zeile selbst benötigt nur 5mm! 230 + 5 = 235 wäre OK!

                Dim requiredSpace As Integer = 6 ' ← REDUZIERT: Nur 1mm Puffer (Zeile=5mm + 1mm)

                If (context.YDynamic + requiredSpace) >= PAGE_HEIGHT_LIMIT Then
                    ' Neue Seite erstellen
                    CreateNewPage(context)
                    context.YDynamic = context.YPosition

                    ' WICHTIG: CurrentPositionStartY wird hier NICHT geändert.
                    ' Der korrekte Anker ist die Zeile von INVOICE_POSITION_ARTICLE,
                    ' gesetzt in HandleArticleTextFollowUp VOR dem RenderMultiLineText-Aufruf.
                    ' - CONTINUATION-Break: Artikel auf alter Seite → CurrentPositionStartY
                    '   bleibt auf der alten Seite (Tax/Amount landen korrekt neben Artikel).
                    ' - FIRST-part-Break im ARTICLE-Feld: HandleArticleTextFollowUp hat
                    '   CurrentPositionStartY = context.YDynamic gesetzt, BEVOR CreateNewPage
                    '   aufgerufen wurde → nach dem Seitenbruch zeigt YDynamic auf die neue
                    '   Seite und der Anker ist bereits korrekt (45).
                    If isFirstPart Then
                        _logger.Debug($"RenderMultiLineText: Page break at FIRST part! YDynamic={context.YDynamic}, CurrentPositionStartY unchanged={context.CurrentPositionStartY}")
                    Else
                        _logger.Debug($"RenderMultiLineText: Page break at continuation! YDynamic={context.YDynamic}, CurrentPositionStartY unchanged={context.CurrentPositionStartY}")
                    End If
                End If

                ' *** BUGFIX (lange mehrzeilige ARTICLE-Texte über Seitenbruch hinweg) ***
                ' Erfasst Y/Seite des ALLERERSTEN tatsächlich gezeichneten Textteils - das ist
                ' immer die korrekte Ankerzeile, unabhängig davon, ob danach noch weitere Zeilen
                ' folgen oder ein Seitenbruch mitten im Text auftritt. Wird nur befüllt, wenn der
                ' Aufrufer (HandleArticleTextFollowUp) die Parameter explizit übergibt.
                If firstRenderedY = -1 Then
                    firstRenderedY = context.YDynamic
                    firstRenderedPage = context.PDF.GetPageCount()
                End If

                _logger.Debug($"RenderMultiLineText: Rendering part: [{part}] at Y position: {context.YDynamic}")
                context.PDF.DrawText(fontResName, xPos, context.YDynamic, part)
                context.YDynamic += LINE_HEIGHT
                context.YPlus += LINE_HEIGHT
                isFirstPart = False
            Next
        Next
    End Sub

#End Region

#Region "Finalisierung"

    Private Function FinalizePDF(pdf As GdPicturePDF, paths As FilePaths) As FileInfo
        Try
            ' XML einbetten
            If File.Exists(paths.XmlPath) Then
                pdf.EmbedFile(paths.XmlPath, "E-invoice XML attachment")
            Else
                _logger.Info("XML File is not existing and could not be embedded!")
                Return Nothing
            End If

            pdf.EnableCompression(True)

            ' Speichern
            Dim status As GdPictureStatus = pdf.SaveToFile(paths.OutputPath)
            If status = GdPictureStatus.OK Then
                _logger.Info("PDF VisualReceipt generated successfully!")
                _logger.Debug("Vor MOVE... oxmlFilePath: [{0}] / oTempFilePath: [{1}]", paths.XmlPath, paths.TempXmlPath)
                File.Move(paths.XmlPath, paths.TempXmlPath)

                pdf.CloseDocument()
                Dim result As New FileInfo(paths.OutputPath)
                _logger.Info("Create_PDFfromXML() End successfully. File [{0}] written.", result.FullName)
                Return result
            Else
                _logger.Warn($"Error generating PDF VisualReceipt: {status}")
                pdf.CloseDocument()
                Return Nothing
            End If

        Catch ex As Exception
            pdf.CloseDocument()
            _logger.Error("Error finalizing PDF", ex)
            Return Nothing
        End Try
    End Function

#End Region

#Region "Helper Classes"

    Private Class FilePaths
        Public Property XmlPath As String
        Public Property TempPath As String
        Public Property TempXmlPath As String
        Public Property PdfFilename As String
        Public Property OutputPath As String
        Public Property CreatedString As String
    End Class

    Private Class PdfRenderContext
        Public Property PDF As GdPicturePDF
        Public Property YPosition As Integer
        Public Property CurrentArea As String
        Public Property CurrencySymbol As String
        Public Property PositionCount As Integer
        Public Property HasDiscount As Boolean
        Public Property YDynamic As Integer
        Public Property YPlus As Integer
        Public Property CreateTextBox As Boolean
        Public Property CreatedString As String
        Public Property TaxPosText As String  ' Legacy, nicht mehr aktiv genutzt - bleibt für Kompatibilität
        Public Property TaxPosRate As String
        Public Property TaxPosRateCaption As String
        Public Property TaxPosAmount As String
        Public Property IsFirstTaxPosDisplay As Boolean
        Public Property CurrentPositionStartY As Integer
        ' Seitennummer auf der CurrentPositionStartY gültig ist.
        ' Wird beim Setzen von CurrentPositionStartY immer mitgesetzt.
        Public Property CurrentPositionPage As Integer

        Public Sub New(pdf As GdPicturePDF, createdString As String)
            Me.PDF = pdf
            Me.CreatedString = createdString
            YPosition = MARGIN_TOP
            CurrentArea = String.Empty
            CurrencySymbol = "€"
            PositionCount = 0
            HasDiscount = False
            YDynamic = 0
            YPlus = 0
            CreateTextBox = False
            TaxPosText = ""
            TaxPosRate = ""
            TaxPosRateCaption = ""
            TaxPosAmount = ""
            IsFirstTaxPosDisplay = False
            CurrentPositionStartY = 0
            CurrentPositionPage = 1
        End Sub
    End Class

    Private Class InvoiceItemData
        Public Property Area As String
        Public Property SpecName As String
        Public Property Value As String
        Public Property Caption As String
        Public Property Display As Boolean
        Public Property IsLastRowSameArea As Boolean
        Public Property XPosition As Integer
        Public Property AreaSwitch As Boolean

        ' Neue Property für normalisierte Area
        Public ReadOnly Property NormalizedArea As String
            Get
                Return NormalizeAreaName(Area)
            End Get
        End Property

        ' Neue Property für Area-Caption (nur bei INCL_NOTE#)
        Public ReadOnly Property AreaCaptionFromPattern As String
            Get
                If Area.StartsWith("INCL_NOTE#") Then
                    Return Area.Substring(10) ' Entfernt "INCL_NOTE#"
                End If
                Return String.Empty
            End Get
        End Property

        Public Sub New(row As DataRow)
            Area = GetSafeString(row, "Area")
            SpecName = GetSafeString(row, "SPEC_NAME")
            Value = GetSafeString(row, "ITEM_VALUE")
            Caption = GetSafeString(row, "Row_Caption")
            Display = GetSafeBoolean(row, "Display", False)
            IsLastRowSameArea = GetSafeBoolean(row, "Y_eq_lastrow", False)
            XPosition = GetSafeInteger(row, "xPosition", MARGIN_LEFT)
            AreaSwitch = False
        End Sub

        Private Shared Function GetSafeString(row As DataRow, columnName As String) As String
            If row.Table.Columns.Contains(columnName) AndAlso Not IsDBNull(row.Item(columnName)) Then
                Return row.Item(columnName).ToString()
            End If
            Return String.Empty
        End Function

        Private Shared Function GetSafeBoolean(row As DataRow, columnName As String, defaultValue As Boolean) As Boolean
            If row.Table.Columns.Contains(columnName) AndAlso Not IsDBNull(row.Item(columnName)) Then
                Return CBool(row.Item(columnName))
            End If
            Return defaultValue
        End Function

        Private Shared Function GetSafeInteger(row As DataRow, columnName As String, defaultValue As Integer) As Integer
            If row.Table.Columns.Contains(columnName) AndAlso Not IsDBNull(row.Item(columnName)) Then
                Return CInt(row.Item(columnName))
            End If
            Return defaultValue
        End Function

        ' Statische Hilfsmethode für Area-Normalisierung
        Private Shared Function NormalizeAreaName(area As String) As String
            If area.StartsWith("INCL_NOTE#") Then
                Return "INCL_NOTE"
            End If
            Return area
        End Function
    End Class

#End Region

#Region "Hilfsfunktionen"

    Private Function FormatCurrency(ByVal pValue As String, pCurrencySymbol As String) As String
        pValue = pValue.Trim()

        Dim oBetrag As Decimal
        Dim culture As Globalization.CultureInfo

        ' Erkennung des Dezimaltrennzeichens
        If pValue.Contains(",") AndAlso Not pValue.Contains(".") Then
            culture = New Globalization.CultureInfo("de-DE") ' Komma → deutsches Format
        ElseIf pValue.Contains(".") AndAlso Not pValue.Contains(",") Then
            culture = New Globalization.CultureInfo("en-US") ' Punkt → englisches Format
        Else
            ' Mischformat oder Tausendertrennzeichen → Fallback auf aktuelle Culture
            culture = Globalization.CultureInfo.CurrentCulture
        End If

        ' Parsen mit gewählter Culture
        If Not Decimal.TryParse(pValue, Globalization.NumberStyles.Any, culture, oBetrag) Then
            Throw New FormatException($"Ungültiger Zahlenwert: {pValue}")
        End If

        ' Formatieren mit deutscher Culture
        Dim oFormatiert As String = oBetrag.ToString("N2", New Globalization.CultureInfo("de-DE"))
        Return $"{oFormatiert} {pCurrencySymbol}"
    End Function

    Private Function FormatStringT(ByVal text As String) As String
        Return text.Replace(vbCr, " - ").Replace(vbLf, "").Replace(vbTab, " ")
    End Function

    Private Function RemoveNewlinesAndTabs(ByVal text As String) As String
        Return text.Replace(vbCr, " - ").Replace(vbLf, "").Replace(vbTab, " ")
    End Function

    Private Function YCoo_TextBoxMinus5(yPosition As Integer) As Integer
        Return yPosition - 5
    End Function

    Private Function YCoo_TextBoxPlus5(yPosition As Integer) As Integer
        Return yPosition + 5
    End Function

    Function SplitTextByNewLine(text As String) As List(Of String)
        If String.IsNullOrEmpty(text) Then
            Return New List(Of String)()
        End If

        ' Zerlege den Text anhand von Zeilenumbrüchen
        Dim lines As List(Of String) = text.Split({vbCrLf, vbLf, vbCr}, StringSplitOptions.None).ToList()
        Return lines
    End Function

    Private Function Return_InvType(pType As String) As String
        Dim oReturn As String = "Rechnung/invoice"
        If pType = "380" Then
            oReturn = "Handelsrechnung/Commercial invoice"
        ElseIf pType = "381" Then
            oReturn = "Gutschriftanzeige/Credit advice"
        ElseIf pType = "384" Then
            oReturn = "Rechnungskorrektur/Invoice correction"
        ElseIf pType = "386" Then
            oReturn = "Vorauszahlungsrechnung/Prepayment invoice"
        ElseIf pType = "326" Then
            oReturn = "Teilrechnung/Partial invoice"
        ElseIf pType = "84" Then
            oReturn = "Gutschrift/Credit note"
        ElseIf pType = "389" Then
            oReturn = "Gutschriftsverfahren/Credit note procedure"
        End If
        Return oReturn
    End Function

    Private Function Return_UnitType(pType As String) As String
        Dim oReturn As String = "Stück/pc"
        If pType = "C62" Then
            oReturn = "Stück/pc"
        ElseIf pType = "DAY" Then
            oReturn = "Tag/day"
        ElseIf pType = "HAR" Then
            oReturn = "Hek/hec"
        ElseIf pType = "HUR" Then
            oReturn = "h"
        ElseIf pType = "KGM" Then
            oReturn = "kg"
        ElseIf pType = "KTM" Then
            oReturn = "km"
        ElseIf pType = "KWH" Then
            oReturn = pType
        ElseIf pType = "LS" Then
            oReturn = "pausch/flat"
        ElseIf pType = "MIN" Then
            oReturn = "minute"
        ElseIf pType = "MTK" Then
            oReturn = "QM/SM"
        ElseIf pType = "Kubik/CM" Then
            oReturn = "MTR"
        ElseIf pType = "Meter" Then
            oReturn = "minute"
        ElseIf pType = "P1" Then
            oReturn = "%"
        ElseIf pType = "SET" Then
            oReturn = "Set"
        ElseIf pType = "TNE" Then
            oReturn = "Tonne/ton"
        ElseIf pType = "WEE" Then
            oReturn = "Woche/week"
        End If
        Return oReturn
    End Function

#End Region

End Class