<?php

declare(strict_types=1);

require_once __DIR__ . '/pdf_generator.php';
require_once __DIR__ . '/invoice_contract.php';
require_once __DIR__ . '/invoice_pdf_content.php';

class InvoiceOfferPDF extends FPDF
{
    public const MARGIN_L = 14;
    public const MARGIN_R = 14;
    public const MARGIN_T = 40;
    public const PAGE_W = 210;
    public const RIGHT_X = self::PAGE_W - self::MARGIN_R;
    public const CONTENT_W = self::PAGE_W - self::MARGIN_L - self::MARGIN_R;
    public const FOOTER_H = 45;
    public const AUDIT_STRIP_H = 14;
    private const FOOTER_TEXT_H = 18.0;
    private const FOOTER_LINE_GAP = 6.5;
    private const HEADER_H = 26;
    private const TOP_LINE_Y = 8;
    private const LOGO_Y = 10;
    private const LOGO_W = 32;
    private const LOGO_GAP_BELOW = 8;
    private const BOTTOM_LINE_OFFSET = 14;
    private const LINE_W = 0.9;

    private string $logoPath = '';

    /** @return array<string, array{0:int,1:int,2:int}> */
    private function colors(): array
    {
        return invoice_pdf_colors();
    }

    public function setLogoPath(string $logoPath): void
    {
        $this->logoPath = $logoPath;
    }

    public function Header(): void
    {
        $colors = $this->colors();
        $this->SetFillColor(...$colors['white']);
        $this->Rect(0, 0, 210, self::HEADER_H + 8, 'F');

        $this->SetDrawColor(...$colors['gold']);
        $this->SetLineWidth(self::LINE_W);
        $this->Line(self::MARGIN_L, self::TOP_LINE_Y, self::RIGHT_X, self::TOP_LINE_Y);

        if ($this->logoPath !== '' && is_file($this->logoPath)) {
            $logoX = self::RIGHT_X - self::LOGO_W;
            $this->Image($this->logoPath, $logoX, self::LOGO_Y, self::LOGO_W, 0);
        }

        $this->SetY(self::HEADER_H + self::LOGO_GAP_BELOW);
    }

    public function Footer(): void
    {
        $colors = $this->colors();
        $pageHeight = $this->GetPageHeight();
        $bottomLineY = $pageHeight - self::BOTTOM_LINE_OFFSET;
        $footerTop = $bottomLineY - self::FOOTER_LINE_GAP - self::FOOTER_TEXT_H;

        $cols = invoice_pdf_company_footer();
        $this->SetFont('Arial', '', 8);
        $this->SetTextColor(...$colors['muted']);
        $colW = self::CONTENT_W / 4.0;
        $xStart = self::MARGIN_L;
        $y = $footerTop;

        foreach (array_values($cols) as $index => $text) {
            $this->SetXY($xStart + ($index * $colW), $y);
            $this->MultiCell($colW, 3.8, pdf_text($text), 0, 'L');
        }

        $this->SetDrawColor(...$colors['gold']);
        $this->SetLineWidth(self::LINE_W);
        $this->Line(self::MARGIN_L, $bottomLineY, self::RIGHT_X, $bottomLineY);
    }

    public function bottomLimit(): float
    {
        return $this->GetPageHeight() - self::FOOTER_H - 2;
    }

    public function contentTopY(): float
    {
        return self::HEADER_H + self::LOGO_GAP_BELOW;
    }

    public function remainingHeight(): float
    {
        return max(0.3, $this->bottomLimit() - $this->GetY());
    }

    public function ensureSpace(float $height): void
    {
        if ($this->GetY() + $height <= $this->bottomLimit()) {
            return;
        }

        if ($this->GetY() <= $this->contentTopY() + 4.0) {
            return;
        }

        $this->AddPage();
    }

    public function advanceGap(float $preferred): void
    {
        $remaining = $this->remainingHeight();
        if ($remaining <= 0.0 || $preferred <= 0.0) {
            return;
        }

        $gap = $preferred;
        if ($remaining < 22.0) {
            $gap = min($preferred, max(1.0, $remaining * 0.4));
        } elseif ($remaining < 45.0) {
            $gap = min($preferred, max(1.5, $preferred * 0.55));
        } elseif ($remaining < 80.0) {
            $gap = min($preferred, max(2.0, $preferred * 0.75));
        }

        $this->Ln(min($gap, $remaining));
    }

    public function beginBlock(float $minHeight, float $gapBefore = 4.0): void
    {
        if ($this->remainingHeight() >= ($minHeight + $gapBefore)) {
            $this->advanceGap($gapBefore);

            return;
        }

        if ($this->GetY() <= $this->contentTopY() + 4.0) {
            return;
        }

        $this->AddPage();
    }

    public function drawGoldRule(float $y): void
    {
        $colors = $this->colors();
        $this->SetDrawColor(...$colors['gold']);
        $this->SetLineWidth(self::LINE_W);
        $this->Line(self::MARGIN_L, $y, self::RIGHT_X, $y);
    }

    public function drawTableAccentRule(float $y, float $width = 0): void
    {
        if ($width <= 0) {
            $width = self::CONTENT_W;
        }
        $colors = $this->colors();
        $this->SetDrawColor(...$colors['table_accent']);
        $this->SetLineWidth(0.35);
        $this->Line(self::MARGIN_L, $y, self::MARGIN_L + $width, $y);
    }

    public function nbLines(float $width, string $text): int
    {
        if (!isset($this->CurrentFont)) {
            $this->Error('No font has been set');
        }

        $charWidths = &$this->CurrentFont['cw'];
        if ($width === 0.0) {
            $width = $this->w - $this->rMargin - $this->x;
        }

        $maxWidth = ($width - 2 * $this->cMargin) * 1000 / $this->FontSize;
        $normalized = str_replace("\r", '', $text);
        $length = strlen($normalized);
        if ($length > 0 && $normalized[$length - 1] === "\n") {
            $length--;
        }

        $separator = -1;
        $index = 0;
        $lineStart = 0;
        $lineWidth = 0;
        $lineCount = 1;

        while ($index < $length) {
            $character = $normalized[$index];
            if ($character === "\n") {
                $index++;
                $separator = -1;
                $lineStart = $index;
                $lineWidth = 0;
                $lineCount++;
                continue;
            }

            if ($character === ' ') {
                $separator = $index;
            }

            $lineWidth += $charWidths[ord($character)] ?? 0;
            if ($lineWidth > $maxWidth) {
                if ($separator === -1) {
                    if ($index === $lineStart) {
                        $index++;
                    }
                } else {
                    $index = $separator + 1;
                }

                $separator = -1;
                $lineStart = $index;
                $lineWidth = 0;
                $lineCount++;
            } else {
                $index++;
            }
        }

        return $lineCount;
    }
}

function invoice_pdf_x(): float
{
    return (float)InvoiceOfferPDF::MARGIN_L;
}

function invoice_pdf_w(): float
{
    return (float)InvoiceOfferPDF::CONTENT_W;
}

function invoice_pdf_right(): float
{
    return (float)InvoiceOfferPDF::RIGHT_X;
}

function invoice_pdf_ensure_text_block(InvoiceOfferPDF $pdf, string $text, float $width, float $lineHeight, float $extra = 6.0): void
{
    $pdf->SetFont('Arial', '', 10);
    $lines = max(1, $pdf->nbLines($width, pdf_text($text)));
    $pdf->ensureSpace(($lines * $lineHeight) + $extra);
}

function invoice_pdf_estimate_payment_plan_height(array $contract, array $totals): float
{
    $pos1Count = count(invoice_payment_plan_pos1_schedule($contract, $totals));
    $pos2Count = invoice_should_show_payment_plan_pos2($contract) ? count(invoice_maintenance_plan_rows($contract)) : 0;
    $tseCount = invoice_should_show_payment_plan_pos3($contract) ? count(invoice_tse_monthly_plan_rows($contract)) : 0;

    $height = 6.5 + 18.0 + 14.0 + 10.2 + ($pos1Count * 7.2);

    if ($pos2Count > 0) {
        $height += 4.0 + 14.0 + 10.2 + ($pos2Count * 7.2);
    }

    if ($tseCount > 0) {
        $height += 4.0 + 14.0 + 10.2 + ($tseCount * 7.2);
    }

    return $height + 4.0;
}

function invoice_pdf_write_paragraph(InvoiceOfferPDF $pdf, string $text, float $lineHeight = 5.3): void
{
    $colors = invoice_pdf_colors();
    $pdf->SetFont('Arial', '', 10.5);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->MultiCell(0, $lineHeight, pdf_text($text), 0, 'L');
}

function invoice_pdf_write_fixed_price_notice(InvoiceOfferPDF $pdf, string $text): void
{
    $colors = invoice_pdf_colors();
    $pdf->SetFont('Arial', '', 11);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->MultiCell(0, 5.6, pdf_text($text), 0, 'L');
}

function invoice_pdf_customer_block(InvoiceOfferPDF $pdf, array $contract): void
{
    $colors = invoice_pdf_colors();
    $pdf->SetFont('Arial', 'B', 11.5);
    $pdf->SetTextColor(...$colors['body']);

    foreach (invoice_customer_detail_lines($contract) as $line) {
        $pdf->SetX(invoice_pdf_x());
        $pdf->Cell(invoice_pdf_w(), 5.8, pdf_text($line), 0, 1, 'L');
    }

    $pdf->advanceGap(6);
}

function invoice_pdf_section_body_indent_x(): float
{
    return invoice_pdf_x() + 8.0;
}

function invoice_pdf_section_body_width(): float
{
    return invoice_pdf_right() - invoice_pdf_section_body_indent_x();
}

function invoice_pdf_write_section_body(InvoiceOfferPDF $pdf, string $text, float $lineHeight = 4.4): void
{
    $colors = invoice_pdf_colors();
    $indentX = invoice_pdf_section_body_indent_x();
    $width = invoice_pdf_section_body_width();
    $paragraphs = preg_split('/\R+/', trim($text)) ?: [];

    $pdf->SetFont('Arial', '', 10);
    $pdf->SetTextColor(...$colors['body']);

    $first = true;
    foreach ($paragraphs as $paragraph) {
        $paragraph = trim((string)$paragraph);
        if ($paragraph === '') {
            continue;
        }

        if (!$first) {
            $pdf->advanceGap(1.5);
        }
        $first = false;

        $pdf->SetX($indentX);
        $pdf->MultiCell($width, $lineHeight, pdf_text($paragraph), 0, 'L');
    }
}

function invoice_pdf_render_section_heading(InvoiceOfferPDF $pdf, string $heading): void
{
    $colors = invoice_pdf_colors();
    $pdf->SetFont('Arial', 'B', 10.5);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetX(invoice_pdf_x());
    $pdf->Cell(invoice_pdf_w(), 5.8, pdf_text($heading), 0, 1, 'L');
    $pdf->advanceGap(1.2);
}

function invoice_pdf_offer_meta_block(InvoiceOfferPDF $pdf, array $contract, string $offerNo): void
{
    $colors = invoice_pdf_colors();
    $pdf->ensureSpace(22);
    $pdf->advanceGap(10);
    $y = $pdf->GetY();
    $contractDate = invoice_pdf_format_date((string)($contract['contract_date'] ?? ''));
    $validUntil = invoice_pdf_valid_until_date($contract);
    $dateCol1X = 128.0;
    $dateCol2X = 172.0;
    $dateColW = 26.0;

    $pdf->SetFont('Arial', 'B', 22);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetXY(invoice_pdf_x(), $y);
    $pdf->Cell(100, 9.5, pdf_text('Angebot Nr. ' . $offerNo), 0, 0, 'L');

    $pdf->SetFont('Arial', 'B', 10);
    $pdf->SetTextColor(...$colors['meta']);
    $pdf->SetXY($dateCol1X, $y);
    $pdf->Cell($dateColW, 4.5, pdf_text('Angebotsdatum'), 0, 0, 'R');
    $pdf->SetXY($dateCol2X, $y);
    $pdf->Cell($dateColW, 4.5, pdf_text('Gültig bis'), 0, 0, 'R');

    $pdf->SetFont('Arial', 'B', 10.5);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetXY($dateCol1X, $y + 4.8);
    $pdf->Cell($dateColW, 5, pdf_text($contractDate), 0, 0, 'R');
    $pdf->SetXY($dateCol2X, $y + 4.8);
    $pdf->Cell($dateColW, 5, pdf_text($validUntil), 0, 0, 'R');

    $pdf->SetY($y + 14);
    $pdf->advanceGap(5);
}

function invoice_pdf_draw_table_header(InvoiceOfferPDF $pdf): void
{
    $colors = invoice_pdf_colors();
    $y = $pdf->GetY();
    $pdf->SetFillColor(...$colors['table_header']);
    $pdf->SetTextColor(...$colors['table_header_text']);
    $pdf->SetFont('Arial', '', 10);
    $pdf->Cell(12, 7, pdf_text('Pos.'), 0, 0, 'C', true);
    $pdf->Cell(84, 7, pdf_text('Bezeichnung'), 0, 0, 'L', true);
    $pdf->Cell(18, 7, pdf_text('Menge'), 0, 0, 'C', true);
    $pdf->Cell(34, 7, pdf_text('Einzel (€)'), 0, 0, 'R', true);
    $pdf->Cell(34, 7, pdf_text('Gesamt (€)'), 0, 1, 'R', true);

    $pdf->SetDrawColor(...$colors['table_accent']);
    $pdf->SetLineWidth(0.35);
    $pdf->Line(invoice_pdf_x(), $y + 7, invoice_pdf_right(), $y + 7);
}

function invoice_pdf_estimate_table_row_height(InvoiceOfferPDF $pdf, string $description, bool $bold = false): float
{
    $pdf->SetFont('Arial', $bold ? 'B' : '', 10);
    $lineHeight = 5.5;
    $vPad = 2.0;
    $lines = $pdf->nbLines(84.0, pdf_text($description));

    return max(9.5, ($lines * $lineHeight) + ($vPad * 2) + 1.0);
}

function invoice_pdf_draw_table_row(InvoiceOfferPDF $pdf, int $pos, string $description, int $qty, float $unitNet, float $totalNet, bool $alt, bool $boldProduct = false): void
{
    $colors = invoice_pdf_colors();
    $fillColor = $alt ? $colors['row_alt'] : $colors['white'];
    $fontStyle = $boldProduct ? 'B' : '';
    $pdf->SetFont('Arial', $fontStyle, 10);

    $x = $pdf->GetX();
    $y = $pdf->GetY();
    $posW = 12.0;
    $descX = $x + $posW;
    $descW = 84.0;
    $qtyX = $x + 96.0;
    $unitX = $x + 114.0;
    $totalX = $x + 148.0;
    $descText = pdf_text($description);
    $lineHeight = 5.5;
    $vPad = 2.0;

    // Measure wrapped height with the same MultiCell logic used for rendering.
    $pdf->SetFont('Arial', $fontStyle, 10);
    $pdf->SetTextColor(...$fillColor);
    $pdf->SetXY($descX, $y + $vPad);
    $pdf->MultiCell($descW, $lineHeight, $descText, 0, 'L', false);
    $rowH = max(9.5, ($pdf->GetY() - $y) + $vPad);

    $pdf->SetFillColor(...$fillColor);
    $pdf->Rect($x, $y, $posW, $rowH, 'F');
    $pdf->Rect($descX, $y, $descW, $rowH, 'F');
    $pdf->Rect($qtyX, $y, 18.0, $rowH, 'F');
    $pdf->Rect($unitX, $y, 34.0, $rowH, 'F');
    $pdf->Rect($totalX, $y, 34.0, $rowH, 'F');

    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetFont('Arial', '', 10);
    $pdf->SetXY($x, $y + $vPad);
    $pdf->Cell($posW, $rowH - ($vPad * 2), pdf_text((string)$pos), 0, 0, 'C');

    $pdf->SetFont('Arial', $fontStyle, 10);
    $pdf->SetXY($descX, $y + $vPad);
    $pdf->MultiCell($descW, $lineHeight, $descText, 0, 'L');

    $pdf->SetFont('Arial', '', 10);
    $pdf->SetXY($qtyX, $y + $vPad);
    $pdf->Cell(18.0, $rowH - ($vPad * 2), pdf_text((string)$qty), 0, 0, 'C');
    $pdf->SetFont('Arial', $fontStyle, 10);
    $pdf->SetXY($unitX, $y + $vPad);
    $pdf->Cell(34.0, $rowH - ($vPad * 2), pdf_text(invoice_pdf_format_money_plain($unitNet)), 0, 0, 'R');
    $pdf->SetXY($totalX, $y + $vPad);
    $pdf->Cell(34.0, $rowH - ($vPad * 2), pdf_text(invoice_pdf_format_money_plain($totalNet)), 0, 0, 'R');

    $pdf->SetY($y + $rowH + 1.0);
}

function invoice_pdf_render_items_table(InvoiceOfferPDF $pdf, array $contract, array $totals): void
{
    invoice_pdf_ensure_text_block($pdf, invoice_pdf_intro_text($contract), invoice_pdf_w(), 5.3, 8.0);
    invoice_pdf_write_paragraph($pdf, invoice_pdf_intro_text($contract));
    $pdf->advanceGap(3);

    $pdf->ensureSpace(14);
    invoice_pdf_draw_table_header($pdf);

    $pos = 1;
    $alt = false;
    foreach ($contract['line_items'] ?? [] as $item) {
        $description = (string)($item['description'] ?? '');
        $pdf->ensureSpace(invoice_pdf_estimate_table_row_height($pdf, $description, true) + 2);
        $qty = max(1, (int)($item['quantity'] ?? 1));
        $unit = invoice_pdf_line_item_unit_net($item);
        $total = invoice_pdf_line_item_total_net($item);
        invoice_pdf_draw_table_row(
            $pdf,
            $pos,
            (string)($item['description'] ?? ''),
            $qty,
            $unit,
            $total,
            $alt,
            true
        );
        $pos++;
        $alt = !$alt;
    }

    if (!empty($contract['optional_printer'])) {
        $mode = (string)($contract['invoice_mode'] ?? 'website');
        $optional = is_array($contract['optional_item'] ?? null)
            ? $contract['optional_item']
            : invoice_contract_mode_config($mode)['optional_item'];
        $unit = (float)($optional['unit_price_net'] ?? 0);
        $optionalDescription = (string)($optional['description'] ?? 'Optional');
        $pdf->ensureSpace(invoice_pdf_estimate_table_row_height($pdf, $optionalDescription, true) + 2);
        invoice_pdf_draw_table_row(
            $pdf,
            $pos,
            (string)($optional['description'] ?? 'Optional'),
            1,
            $unit,
            $unit,
            $alt,
            true
        );
        $alt = !$alt;
    }

    $pdf->advanceGap(4);
    invoice_pdf_render_totals($pdf, $totals, $alt);
    $pdf->advanceGap(3);
    invoice_pdf_write_fixed_price_notice($pdf, invoice_pdf_fixed_price_notice());
    $pdf->advanceGap(4);
}

function invoice_pdf_format_totals_amount(array $row): string
{
    $amount = (float)($row['amount'] ?? 0);
    $formatted = invoice_pdf_format_money_plain($amount) . ' €';

    if (!empty($row['discount']) && $amount > 0) {
        return '- ' . $formatted;
    }

    return $formatted;
}

function invoice_pdf_draw_totals_row(
    InvoiceOfferPDF $pdf,
    string $label,
    string $amount,
    bool $alt,
    float $rowH = 6.8,
    bool $labelBold = false,
    bool $amountBold = false,
    ?array $textColor = null,
    float $fontSize = 10.0
): float {
    $colors = invoice_pdf_colors();
    $fillColor = $alt ? $colors['row_alt'] : $colors['white'];
    $x = $pdf->GetX();
    $y = $pdf->GetY();
    $tableW = invoice_pdf_w();
    $labelW = 130.0;
    $amountW = $tableW - $labelW;

    $pdf->SetFillColor(...$fillColor);
    $pdf->Rect($x, $y, $tableW, $rowH, 'F');

    $pdf->SetTextColor(...($textColor ?? $colors['body']));
    $pdf->SetFont('Arial', $labelBold ? 'B' : '', $fontSize);
    $pdf->SetXY($x, $y);
    $pdf->Cell($labelW, $rowH, pdf_text($label), 0, 0, 'R');
    $pdf->SetFont('Arial', $amountBold ? 'B' : '', $fontSize);
    $pdf->Cell($amountW, $rowH, pdf_text($amount), 0, 0, 'R');
    $pdf->SetY($y + $rowH);

    return $y;
}

function invoice_pdf_render_totals(InvoiceOfferPDF $pdf, array $totals, bool $alt = false): void
{
    $colors = invoice_pdf_colors();
    $discountGreen = [34, 110, 55];
    $pdf->ensureSpace(28);

    foreach (invoice_document_totals_rows($totals) as $row) {
        if (!empty($row['rule_before'])) {
            $pdf->advanceGap(2);
            $pdf->drawTableAccentRule($pdf->GetY());
            $pdf->advanceGap(2);
        }

        $label = (string)($row['label'] ?? '');
        $amount = invoice_pdf_format_totals_amount($row);
        $isDiscount = !empty($row['discount']);
        $isNetTotal = !empty($row['net_total']);
        $isFinalTotal = !empty($row['rule_after']);
        $rowH = $isNetTotal ? 10.0 : 7.2;
        $fontSize = $isNetTotal ? 16.0 : 10.5;
        $labelBold = $isDiscount || $isNetTotal || $isFinalTotal;
        $amountBold = $isDiscount || $isNetTotal || $isFinalTotal;
        $textColor = $isDiscount ? $discountGreen : $colors['body'];

        $rowY = invoice_pdf_draw_totals_row(
            $pdf,
            $label,
            $amount,
            $alt,
            $rowH,
            $labelBold,
            $amountBold,
            $textColor,
            $fontSize
        );
        $alt = !$alt;

        if (!empty($row['rule_after'])) {
            $pdf->SetDrawColor(...$colors['table_accent']);
            $pdf->SetLineWidth(0.35);
            $pdf->Line(invoice_pdf_x(), $rowY + $rowH + 0.2, invoice_pdf_right(), $rowY + $rowH + 0.2);
        }
    }

    $pdf->advanceGap(3);
}

function invoice_pdf_render_section_subtitle(InvoiceOfferPDF $pdf, string $title): void
{
    $colors = invoice_pdf_colors();
    $pdf->ensureSpace(12);
    $pdf->SetFont('Arial', 'B', 10.5);
    $pdf->SetTextColor(...$colors['body']);
    $titleY = $pdf->GetY();
    $pdf->Cell(0, 5.8, pdf_text($title), 0, 1, 'L');
    $pdf->drawTableAccentRule($titleY + 5.8);
    $pdf->advanceGap(2);
}

function invoice_pdf_draw_stacked_table_header(InvoiceOfferPDF $pdf, array $headers, array $widths, ?array $aligns = null): void
{
    $colors = invoice_pdf_colors();
    $headerH = 10.2;
    $x = invoice_pdf_x();
    $y = $pdf->GetY();

    $pdf->SetFillColor(...$colors['table_header']);
    $pdf->SetTextColor(...$colors['table_header_text']);

    foreach ($headers as $index => $header) {
        $width = (float)($widths[$index] ?? 30);
        $align = $aligns[$index] ?? ($index === count($headers) - 1 ? 'R' : 'L');

        $pdf->Rect($x, $y, $width, $headerH, 'F');

        if (is_array($header)) {
            $amountAlign = $align === 'R' ? 'R' : 'C';
            $pdf->SetFont('Arial', '', 10);
            $pdf->SetXY($x, $y + 1.0);
            $pdf->Cell($width, 4.2, pdf_text((string)($header[0] ?? '')), 0, 0, $amountAlign);
            $pdf->SetFont('Arial', '', 9.5);
            $pdf->SetXY($x, $y + 5.2);
            $pdf->Cell($width, 4.0, pdf_text((string)($header[1] ?? '')), 0, 0, $amountAlign);
        } else {
            $pdf->SetFont('Arial', '', 10);
            $pdf->SetXY($x, $y + 2.6);
            $pdf->Cell($width, 5.0, pdf_text((string)$header), 0, 0, $align);
        }

        $x += $width;
    }

    $pdf->SetY($y + $headerH);
}

function invoice_pdf_draw_simple_table_header(InvoiceOfferPDF $pdf, array $headers, array $widths, ?array $aligns = null): void
{
    $colors = invoice_pdf_colors();
    $pdf->SetFillColor(...$colors['table_header']);
    $pdf->SetTextColor(...$colors['table_header_text']);
    $pdf->SetFont('Arial', '', 10);

    foreach ($headers as $index => $header) {
        $width = (float)($widths[$index] ?? 30);
        $align = $aligns[$index] ?? ($index === count($headers) - 1 ? 'R' : 'L');
        $pdf->Cell($width, 7, pdf_text((string)$header), 0, 0, $align, true);
    }
    $pdf->Ln();
}

function invoice_pdf_draw_simple_table_row(InvoiceOfferPDF $pdf, array $cells, array $widths, bool $alt, ?array $aligns = null): void
{
    $colors = invoice_pdf_colors();
    $fillColor = $alt ? $colors['row_alt'] : $colors['white'];
    $pdf->SetFillColor(...$fillColor);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetFont('Arial', '', 10);

    foreach ($cells as $index => $cell) {
        $width = (float)($widths[$index] ?? 30);
        $align = $aligns[$index] ?? ($index === count($cells) - 1 ? 'R' : 'L');
        $pdf->Cell($width, 7.2, pdf_text((string)$cell), 0, 0, $align, true);
    }
    $pdf->Ln();
}

function invoice_pdf_begin_section_page(InvoiceOfferPDF $pdf, float $minHeight): void
{
    $pdf->beginBlock($minHeight, 5.0);
}

function invoice_pdf_render_payment_plan_section(InvoiceOfferPDF $pdf, array $contract, array $totals): void
{
    $colors = invoice_pdf_colors();
    $pos1Schedule = invoice_payment_plan_pos1_schedule($contract, $totals);
    $pos2Rows = invoice_should_show_payment_plan_pos2($contract) ? invoice_maintenance_plan_rows($contract) : [];
    $tseRows = invoice_should_show_payment_plan_pos3($contract) ? invoice_tse_monthly_plan_rows($contract) : [];
    $pos1Widths = [12, 68, 51, 51];
    $pos1Aligns = ['L', 'L', 'R', 'R'];
    $feeWidths = [82, 51, 49];
    $feeAligns = ['L', 'R', 'R'];

    invoice_pdf_begin_section_page($pdf, invoice_pdf_estimate_payment_plan_height($contract, $totals));

    $pdf->SetFont('Arial', 'B', 11.5);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->Cell(0, 6.5, pdf_text('1.1 Zahlungsplan'), 0, 1, 'L');
    $pdf->advanceGap(1);
    invoice_pdf_write_paragraph($pdf, invoice_pdf_payment_plan_intro($contract, $totals), 4.8);
    $pdf->advanceGap(2);

    invoice_pdf_render_section_subtitle($pdf, invoice_payment_plan_pos1_title($contract));
    invoice_pdf_draw_stacked_table_header(
        $pdf,
        array_merge(['Nr.', 'Fälligkeit'], invoice_payment_plan_amount_headers()),
        $pos1Widths,
        $pos1Aligns
    );

    $alt = false;
    foreach ($pos1Schedule as $row) {
        $pdf->ensureSpace(9);
        invoice_pdf_draw_simple_table_row(
            $pdf,
            [
                (string)($row['number'] ?? ''),
                (string)($row['due_label'] ?? ''),
                invoice_pdf_format_money_plain((float)($row['net'] ?? 0)),
                invoice_pdf_format_money_plain((float)($row['gross'] ?? 0)),
            ],
            $pos1Widths,
            $alt,
            $pos1Aligns
        );
        $alt = !$alt;
    }

    if ($pos2Rows !== []) {
        $pdf->advanceGap(3);
        invoice_pdf_render_section_subtitle($pdf, invoice_payment_plan_pos2_title());
        invoice_pdf_draw_stacked_table_header(
            $pdf,
            array_merge(['Fälligkeit'], invoice_payment_plan_amount_headers()),
            $feeWidths,
            $feeAligns
        );

        $alt = false;
        foreach ($pos2Rows as $row) {
            $pdf->ensureSpace(9);
            invoice_pdf_draw_simple_table_row(
                $pdf,
                [
                    (string)($row['due'] ?? ''),
                    (string)($row['net'] ?? ''),
                    invoice_pdf_format_money_plain((float)($row['gross'] ?? 0)),
                ],
                $feeWidths,
                $alt,
                $feeAligns
            );
            $alt = !$alt;
        }
    }

    if ($tseRows !== []) {
        $pdf->advanceGap(3);
        invoice_pdf_render_section_subtitle($pdf, invoice_payment_plan_tse_title());
        invoice_pdf_draw_stacked_table_header(
            $pdf,
            array_merge(['Fälligkeit'], invoice_payment_plan_amount_headers()),
            $feeWidths,
            $feeAligns
        );

        $alt = false;
        foreach ($tseRows as $row) {
            $pdf->ensureSpace(9);
            invoice_pdf_draw_simple_table_row(
                $pdf,
                [
                    (string)($row['due'] ?? ''),
                    (string)($row['net'] ?? ''),
                    invoice_pdf_format_money_plain((float)($row['gross'] ?? 0)),
                ],
                $feeWidths,
                $alt,
                $feeAligns
            );
            $alt = !$alt;
        }
    }

    $pdf->advanceGap(3);
}

function invoice_pdf_write_underlined_title(InvoiceOfferPDF $pdf, string $title, ?float $x = null): void
{
    $colors = invoice_pdf_colors();
    $x ??= invoice_pdf_x();
    $width = invoice_pdf_right() - $x;
    $pdf->SetFont('Arial', 'BU', 10);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetX($x);
    $pdf->Cell($width, 4.8, pdf_text($title), 0, 1, 'L');
}

function invoice_pdf_write_centered_paragraph(InvoiceOfferPDF $pdf, string $text, float $lineHeight = 4.8): void
{
    $colors = invoice_pdf_colors();
    $pdf->SetFont('Arial', '', 10);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetX(invoice_pdf_x());
    $pdf->MultiCell(invoice_pdf_w(), $lineHeight, pdf_text($text), 0, 'C');
}

function invoice_pdf_render_terms(InvoiceOfferPDF $pdf, array $contract): void
{
    $colors = invoice_pdf_colors();
    $bodyIndentX = invoice_pdf_section_body_indent_x();

    foreach (invoice_document_terms_sections($contract) as $section) {
        $pdf->ensureSpace(18);
        invoice_pdf_render_section_heading($pdf, (string)($section['heading'] ?? ''));

        if (!empty($section['intro'])) {
            invoice_pdf_write_section_body($pdf, (string)$section['intro'], 4.6);
            $pdf->advanceGap(2);
        }

        if (!empty($section['items']) && is_array($section['items'])) {
            foreach ($section['items'] as $item) {
                $pdf->ensureSpace(16);
                invoice_pdf_write_underlined_title($pdf, (string)($item['title'] ?? ''), $bodyIndentX);
                invoice_pdf_write_section_body($pdf, (string)($item['text'] ?? ''), 4.6);
                $pdf->advanceGap(2);
            }
        }

        if (!empty($section['body'])) {
            invoice_pdf_write_section_body($pdf, (string)$section['body'], 4.6);
        }

        $pdf->advanceGap(2.5);
    }

    $pdf->advanceGap(2);
    $closingLines = invoice_pdf_closing_lines();
    $closingCount = count($closingLines);
    foreach ($closingLines as $index => $line) {
        $pdf->ensureSpace(10);
        $pdf->SetFont('Arial', '', 10);
        $pdf->SetTextColor(...$colors['body']);
        $pdf->Cell(0, 5.3, pdf_text($line), 0, 1, 'L');
        if ($index < $closingCount - 1) {
            $pdf->advanceGap(2.5);
        }
    }
}

function invoice_pdf_owner_name(array $contract): string
{
    $ownerName = trim((string)contract_owner_name_for_pdf($contract));
    if ($ownerName !== '' && $ownerName !== '-') {
        return $ownerName;
    }

    return invoice_customer_label((string)($contract['customer_details'] ?? ''));
}

function invoice_pdf_store_signature_layout(array $layout): void
{
    $GLOBALS['invoice_pdf_signature_layout'] = $layout;
}

function invoice_pdf_get_signature_layout(): ?array
{
    $layout = $GLOBALS['invoice_pdf_signature_layout'] ?? null;

    return is_array($layout) ? $layout : null;
}

function invoice_pdf_reset_signature_layout(): void
{
    unset($GLOBALS['invoice_pdf_signature_layout']);
}

function invoice_pdf_signature_required_height(): float
{
    return 6.0 + 7.0 + 52.0 + 3.0 + 14.0;
}

/** @return array<string, float|int|string> */
function invoice_pdf_signature_layout(float $yStart = 44.0): array
{
    $contentX = invoice_pdf_x();
    $contentW = invoice_pdf_w();
    $gap = 4.0;
    $colW = ($contentW - $gap) / 2.0;
    $rightX = $contentX + $colW + $gap;
    $pad = 4.0;
    $headingH = 7.0;
    $colsY = $yStart + $headingH;
    $boxHeight = 52.0;
    $sigLineY = $colsY + $boxHeight - 11.0;
    $sigBoxH = 15.0;
    $sigBoxY = $sigLineY - $sigBoxH - 1.0;
    return [
        'style' => 'invoice',
        'content_x' => $contentX,
        'content_w' => $contentW,
        'y_start' => $yStart,
        'cols_y' => $colsY,
        'box_height' => $boxHeight,
        'left_x' => $contentX,
        'left_w' => $colW,
        'right_x' => $rightX,
        'right_w' => $colW,
        'right_pad_x' => $rightX + $pad,
        'right_inner_w' => $colW - (2 * $pad),
        'sig_box_y' => $sigBoxY,
        'sig_box_h' => $sigBoxH,
        'sig_line_y' => $sigLineY,
        'center_y' => $sigBoxY + ($sigBoxH / 2.0),
    ];
}

function invoice_pdf_signature_start_y(InvoiceOfferPDF $pdf, float $requiredHeight): float
{
    $contentTop = $pdf->contentTopY() + 4.0;
    $remaining = $pdf->remainingHeight();

    if ($remaining >= $requiredHeight + 1.5) {
        $gap = min(3.5, max(1.2, ($remaining - $requiredHeight) * 0.15));

        return $pdf->GetY() + $gap;
    }

    if ($pdf->GetY() > $contentTop + 6.0) {
        $pdf->AddPage();
    }

    return $pdf->GetY() + 2.0;
}

function invoice_pdf_render_signature_section(InvoiceOfferPDF $pdf, array $contract): array
{
    $colors = invoice_pdf_colors();
    $karvi = contract_template_karvi();
    $contractDate = pdf_contract_date_dash($contract['contract_date'] ?? null);
    $ownerName = invoice_pdf_owner_name($contract);
    $requiredHeight = invoice_pdf_signature_required_height();
    $pdf->SetAutoPageBreak(false);

    $yStart = invoice_pdf_signature_start_y($pdf, $requiredHeight);

    $layout = invoice_pdf_signature_layout($yStart);
    invoice_pdf_store_signature_layout($layout);

    $contentX = (float)$layout['content_x'];
    $contentW = (float)$layout['content_w'];
    $colsY = (float)$layout['cols_y'];
    $boxHeight = (float)$layout['box_height'];
    $leftX = (float)$layout['left_x'];
    $leftW = (float)$layout['left_w'];
    $rightPadX = (float)$layout['right_pad_x'];
    $leftPadX = $leftX + 4.0;
    $leftTextW = $leftW - 8.0;
    $rightTextW = (float)$layout['right_w'] - 8.0;
    $sigBoxY = (float)$layout['sig_box_y'];
    $sigBoxH = (float)$layout['sig_box_h'];
    $sigLineY = (float)$layout['sig_line_y'];
    $headingH = 7.0;
    $totalGrayH = $headingH + $boxHeight;

    $pdf->SetFillColor(242, 242, 242);
    $pdf->Rect($contentX, $yStart, $contentW, $totalGrayH, 'F');

    $pdf->SetFont('Arial', 'B', 10.5);
    $pdf->SetTextColor(...$colors['meta']);
    $pdf->SetXY($contentX + 4.0, $yStart + 2.5);
    $pdf->Cell($contentW - 8.0, 5, pdf_text('UNTERSCHRIFTEN'), 0, 1, 'L');

    $pdf->SetFont('Arial', 'B', 9.5);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetXY($leftPadX, $colsY + 3.0);
    $pdf->Cell($leftTextW, 4.5, pdf_text($karvi['name']), 0, 2);
    $pdf->SetFont('Arial', '', 9);
    $pdf->Cell($leftTextW, 4.5, pdf_text($karvi['signatory_name']), 0, 2);
    $pdf->Cell($leftTextW, 4.5, pdf_text($karvi['signatory_title']), 0, 2);
    $pdf->Cell($leftTextW, 4.5, pdf_text('Unterschrift:'), 0, 2);

    $pdf->SetFillColor(255, 255, 255);
    $pdf->Rect($leftPadX, $sigBoxY, $leftTextW, $sigBoxH, 'F');
    $pdf->SetDrawColor(0, 0, 0);
    $pdf->SetLineWidth(0.2);
    $pdf->Line($leftPadX, $sigLineY, $leftPadX + $leftTextW, $sigLineY);
    $pdf->SetFont('Arial', '', 9);
    $pdf->SetTextColor(...$colors['body']);
    $pdf->SetXY($leftPadX, $sigLineY + 1.5);
    $pdf->Cell($leftTextW, 4.5, pdf_text('Datum: ' . $contractDate), 0, 0);

    $pdf->SetFont('Arial', 'B', 9.5);
    $pdf->SetXY($rightPadX, $colsY + 3.0);
    $pdf->Cell($rightTextW, 4.5, pdf_text('Franchisepartner / Restaurant'), 0, 2);
    $pdf->SetFont('Arial', '', 9);
    $pdf->Cell($rightTextW, 4.5, pdf_text($ownerName), 0, 2);
    $pdf->Cell($rightTextW, 4.5, pdf_text('Name des Inhabers: ' . $ownerName), 0, 2);
    $pdf->Cell($rightTextW, 4.5, pdf_text('Unterschrift:'), 0, 2);

    $rightInnerW = (float)$layout['right_inner_w'];
    $pdf->SetFillColor(255, 255, 255);
    $pdf->Rect($rightPadX, $sigBoxY, $rightInnerW, $sigBoxH, 'F');
    $pdf->Line($rightPadX, $sigLineY, $rightPadX + $rightInnerW, $sigLineY);
    $pdf->SetXY($rightPadX, $sigLineY + 1.5);
    $pdf->Cell($rightInnerW, 4.5, pdf_text('Datum: ' . $contractDate), 0, 0);

    $pdf->SetY($yStart + $totalGrayH + 2);

    return $layout;
}

function generate_invoice_contract_pdf(array $contract, ?string $outputPath = null): string
{
    $config = app_config();
    $filename = contract_pdf_filename($contract);
    if ($filename === '') {
        $filename = trim((string)($contract['sign_token'] ?? $contract['token'] ?? '')) . '.pdf';
    }
    if ($filename === '.pdf' || $filename === '') {
        $filename = generate_token() . '.pdf';
    }
    $targetPath = $outputPath ?? (rtrim((string)$config['contracts_dir'], '/\\') . DIRECTORY_SEPARATOR . $filename);
    $mode = (string)($contract['invoice_mode'] ?? 'website');
    $totals = is_array($contract['totals'] ?? null) ? $contract['totals'] : invoice_calculate_totals(
        is_array($contract['line_items'] ?? null) ? $contract['line_items'] : [],
        (float)($contract['discount'] ?? 0),
        !empty($contract['optional_printer']),
        is_array($contract['optional_item'] ?? null) ? $contract['optional_item'] : invoice_contract_mode_config($mode)['optional_item']
    );

    $offerNo = invoice_pdf_offer_number($contract);

    invoice_pdf_reset_signature_layout();

    $pdf = new InvoiceOfferPDF();
    $pdf->setLogoPath(invoice_pdf_logo_path());
    $pdf->SetAutoPageBreak(true, InvoiceOfferPDF::FOOTER_H + 6);
    $pdf->SetMargins(InvoiceOfferPDF::MARGIN_L, InvoiceOfferPDF::MARGIN_T, InvoiceOfferPDF::MARGIN_R);
    $pdf->AddPage();

    invoice_pdf_customer_block($pdf, $contract);
    invoice_pdf_offer_meta_block($pdf, $contract, $offerNo);
    invoice_pdf_render_items_table($pdf, $contract, $totals);
    invoice_pdf_render_payment_plan_section($pdf, $contract, $totals);
    invoice_pdf_render_terms($pdf, $contract);
    invoice_pdf_render_signature_section($pdf, $contract);

    $dir = dirname($targetPath);
    if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
        throw new RuntimeException('Cannot create PDF folder. Make uploads/contracts writable on the server.');
    }

    if (!is_writable($dir)) {
        throw new RuntimeException('PDF folder is not writable. Check uploads/contracts permissions.');
    }

    $pdf->Output('F', $targetPath);

    return basename($targetPath);
}

function generate_contract_pdf_for_record(array $contract, ?string $outputPath = null): string
{
    if (invoice_contract_is($contract)) {
        return generate_invoice_contract_pdf($contract, $outputPath);
    }

    return generate_contract_pdf($contract, $outputPath);
}

function invoice_ensure_upload_dirs(): void
{
    $config = app_config();
    foreach (['contracts_dir', 'signatures_dir', 'signed_dir'] as $key) {
        $dir = trim((string)($config[$key] ?? ''));
        if ($dir === '') {
            continue;
        }
        if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
            throw new RuntimeException('Cannot create upload folder: ' . $dir);
        }
    }
}

function invoice_ensure_contract_pdf(array $contract): ?string
{
    if (!invoice_contract_is($contract)) {
        return null;
    }

    $existing = contract_resolve_contract_pdf_disk_path($contract);
    if ($existing !== null && is_file($existing) && filesize($existing) >= 512) {
        return $existing;
    }

    try {
        invoice_ensure_upload_dirs();
    } catch (Throwable) {
        return null;
    }

    $config = app_config();
    $filename = contract_pdf_filename($contract);
    $path = rtrim((string)$config['contracts_dir'], '/\\') . DIRECTORY_SEPARATOR . $filename;

    try {
        generate_invoice_contract_pdf($contract, $path);
    } catch (Throwable) {
        return null;
    }

    if (!is_file($path) || filesize($path) < 512) {
        return null;
    }

    $token = contract_admin_token($contract);
    if ($token !== '' && basename((string)($contract['pdf_path'] ?? '')) !== $filename) {
        require_once __DIR__ . '/api_service.php';
        if (!api_is_configured() || api_feature_enabled('pdf_update')) {
            api_update_contract($token, ['pdf_path' => $filename]);
        }
    }

    return $path;
}
