<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
 * GPL v3+
 */

/**
 * Hilfs-Funktionen für das Bericht-Modul.
 */

require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';

/**
 * Erzeugt die Tab-Header-Leiste für die Editor-Seite (Übersicht / Editor).
 *
 * @param Bericht $object
 * @return array
 */
function bericht_prepare_head($object)
{
    global $langs, $conf;
    $langs->load("bericht@bericht");

    $h = 0;
    $head = array();

    $head[$h][0] = dol_buildpath('/bericht/bericht_card.php', 1).'?id='.$object->id;
    $head[$h][1] = $langs->trans("BerichtEditor");
    $head[$h][2] = 'editor';
    $h++;

    return $head;
}

/**
 * Lädt das Parent-Objekt anhand des element_type.
 *
 * @return CommonObject|null
 */
function bericht_fetch_parent(DoliDB $db, $element_type, $id)
{
    if ($element_type === 'invoice') {
        require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
        $o = new Facture($db);
    } elseif ($element_type === 'order') {
        require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
        $o = new Commande($db);
    } elseif ($element_type === 'propal') {
        require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
        $o = new Propal($db);
    } elseif ($element_type === 'shipment') {
        require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
        $o = new Expedition($db);
    } else {
        return null;
    }
    if ($o->fetch($id) <= 0) return null;
    $o->fetch_thirdparty();
    if (method_exists($o, 'fetch_optionals')) {
        $o->fetch_optionals();
    }
    return $o;
}

/**
 * Liefert die Expedition zu einer Lieferschein-ID und holt zusaetzlich den verknuepften Auftrag (commande)
 * ueber llx_element_element. Resultat enthaelt: expedition, commande (optional), thirdparty.
 *
 * @return array|null  ['expedition'=>Expedition,'commande'=>Commande|null,'thirdparty'=>Societe|null]
 */
function bericht_fetch_shipment_with_order(DoliDB $db, $expedition_id)
{
    require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
    require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';

    $shipment = new Expedition($db);
    if ($shipment->fetch((int) $expedition_id) <= 0) return null;
    $shipment->fetch_thirdparty();
    if (method_exists($shipment, 'fetch_optionals')) $shipment->fetch_optionals();

    // Verknuepfung commande → shipping (commande ist source, shipping ist target)
    $commande = null;
    $sql = "SELECT fk_source FROM ".$db->prefix()."element_element"
        ." WHERE sourcetype = 'commande' AND targettype = 'shipping'"
        ." AND fk_target = ".((int) $expedition_id)
        ." LIMIT 1";
    $r = $db->query($sql);
    if ($r && ($row = $db->fetch_object($r))) {
        $cmd = new Commande($db);
        if ($cmd->fetch((int) $row->fk_source) > 0) {
            $cmd->fetch_thirdparty();
            if (method_exists($cmd, 'fetch_optionals')) $cmd->fetch_optionals();
            $commande = $cmd;
        }
    }

    return array(
        'expedition' => $shipment,
        'commande'   => $commande,
        'thirdparty' => $shipment->thirdparty ?: ($commande ? $commande->thirdparty : null),
    );
}

/**
 * Sucht das aktuell auf einer Expedition abgelegte Lieferschein-PDF.
 * Liefert den absoluten Pfad oder null wenn nicht vorhanden.
 *
 * Mit $include_signed=false (Default) wird das "-signed.pdf" (das von uns gestempelte
 * Resultat) ausgeschlossen — fuer die PWA-Vorschau wollen wir das Original, nicht den
 * bereits signierten Lieferschein.
 *
 * @param bool $include_signed  Wenn true, koennen -signed.pdf-Dateien als Treffer dienen
 */
function bericht_get_shipment_pdf(DoliDB $db, $shipment, $include_signed = false)
{
    global $conf, $user, $langs;
    require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';

    if (empty($shipment) || empty($shipment->ref)) return null;

    $ref_sane = dol_sanitizeFileName($shipment->ref);
    $shipment_dir = $conf->expedition->multidir_output[$shipment->entity ?: $conf->entity].'/sending/'.$ref_sane;
    if (!is_dir($shipment_dir)) {
        $shipment_dir = $conf->expedition->multidir_output[$shipment->entity ?: $conf->entity].'/'.$ref_sane;
    }

    $is_signed = function ($path) {
        return (bool) preg_match('/-signed\.pdf$/i', $path);
    };

    // 1) Bevorzugt <ref>.pdf direkt
    $candidate = $shipment_dir.'/'.$ref_sane.'.pdf';
    if (file_exists($candidate) && ($include_signed || !$is_signed($candidate))) return $candidate;

    // 2) Wenn nichts da: generieren lassen
    if (!is_dir($shipment_dir) || empty(glob($shipment_dir.'/*.pdf'))) {
        $template = getDolGlobalString('EXPEDITION_ADDON_PDF', 'merou');
        try { $shipment->generateDocument($template, $langs); }
        catch (Throwable $e) { return null; }
    }

    // 3) PDFs im Verzeichnis filtern — signed ausschließen falls nicht erwünscht
    if (is_dir($shipment_dir)) {
        $pdfs = glob($shipment_dir.'/*.pdf') ?: array();
        if (!$include_signed) {
            $pdfs = array_values(array_filter($pdfs, function ($f) use ($is_signed) {
                return !$is_signed($f);
            }));
        }
        if (!empty($pdfs)) {
            // Neueste zuerst (frisch generiert > alt)
            usort($pdfs, function ($a, $b) { return filemtime($b) <=> filemtime($a); });
            return $pdfs[0];
        }
    }
    return null;
}

/**
 * Liefert die Box-Geometrie fuer ein Lieferschein-Template.
 * Nimmt zuerst llx_bericht_signature_box (template_name match), sonst Default-JSON-Konstante.
 *
 * @return array  ['page'=>'first'|'last'|int, 'x_mm'=>float, 'y_mm'=>float, 'w_mm'=>float, 'h_mm'=>float, 'label'=>string]
 */
/**
 * Rendert ein ODT-Lieferschein-Template mit Substitution:
 *   - Bild-Platzhalter "signature" (im ODT als Bild-Name gesetzt) → Kunden-Unterschrifts-PNG
 *   - Textvariablen werden via Odf::setVars befuellt:
 *       {signer_name}, {signed_at}, {gps}, {kunde_name}, {kunde_adresse}, {shipment_ref}, {order_ref}
 *   - LibreOffice headless konvertiert ODT → PDF
 *
 * @param string $odt_template_path  absoluter Pfad zum Quell-ODT (doctemplates/shipments/*.odt)
 * @param string $signature_png      absoluter Pfad zum Unterschrifts-PNG
 * @param array  $meta               Substitutions-Daten: signer_name, signed_at, gps_lat, gps_lon, shipment, order, kunde
 * @param string $out_pdf            absoluter Zielpfad fuer das fertige PDF
 *
 * @return bool true bei Erfolg
 */
function bericht_render_signed_shipment_odt($odt_template_path, $signature_png, array $meta, $out_pdf, &$debug = '')
{
    if (!file_exists($odt_template_path)) { $debug = 'ODT-Template fehlt: '.$odt_template_path; return false; }
    if (!file_exists($signature_png))     { $debug = 'Signatur-PNG fehlt: '.$signature_png; return false; }

    $odt_loader = DOL_DOCUMENT_ROOT.'/includes/odtphp/odf.php';
    if (!file_exists($odt_loader))        { $debug = 'odf.php-Loader fehlt: '.$odt_loader; return false; }
    require_once $odt_loader;

    $tempdir = DOL_DATA_ROOT.'/bericht/temp';
    if (!is_dir($tempdir)) dol_mkdir($tempdir);

    try {
        $odf = new Odf($odt_template_path, array('PATH_TO_TMP' => $tempdir));

        // Text-Platzhalter {signature} im ODT durch ein draw:frame mit der Kunden-Unterschrift ersetzen.
        // setImage() rechnet Pixel × PIXEL_TO_CM × ratio — ratio steuert die Endgroesse im PDF.
        // 1.0 = 1:1 bei 96dpi (zu gross fuer Signaturen). 0.3 ergibt ca. 6×3 cm bei 800×400 Canvas.
        $ratio = (float) getDolGlobalString('BERICHT_SIGNATURE_IMAGE_RATIO', '0.35');
        try {
            $odf->setImage('signature', $signature_png, $ratio);
        } catch (Throwable $e) {
            // Wenn {signature}-Platzhalter im ODT fehlt, ist das ein Konfigurationsproblem —
            // wir loggen es aber rendern trotzdem (die Textvars werden weiter ersetzt).
            dol_syslog('bericht_render_signed_shipment_odt: setImage(signature) failed: '.$e->getMessage(), LOG_WARNING);
        }

        // Textvariablen — werden nur ersetzt wenn sie im ODT als {key} stehen, sonst ignoriert.
        $shipment  = $meta['shipment']  ?? null;
        $order     = $meta['order']     ?? null;
        $kunde     = $meta['kunde']     ?? null;
        $gps_str   = (isset($meta['gps_lat'], $meta['gps_lon']) && $meta['gps_lat'] !== null && $meta['gps_lon'] !== null)
            ? sprintf('%.6f, %.6f', $meta['gps_lat'], $meta['gps_lon'])
            : '';

        // Auftragsnummer: zuerst Extrafeld der Expedition (Eddys Computed-Field),
        // dann Fallback Extrafeld der Commande, dann ref_client, dann commande->ref.
        $auftragsnummer = '';
        if ($shipment && !empty($shipment->array_options['options_auftragsnummer'])) {
            $auftragsnummer = $shipment->array_options['options_auftragsnummer'];
        } elseif ($order && !empty($order->array_options['options_auftragsnummer'])) {
            $auftragsnummer = $order->array_options['options_auftragsnummer'];
        } elseif ($order && !empty($order->ref_client)) {
            $auftragsnummer = $order->ref_client;
        } elseif ($order) {
            $auftragsnummer = $order->ref;
        }

        $vars = array(
            'signer_name'    => (string) ($meta['signer_name'] ?? ''),
            'signed_at'      => (string) ($meta['signed_at'] ?? ''),
            'gps'            => $gps_str,
            'shipment_ref'   => $shipment ? $shipment->ref : '',
            'order_ref'      => $order ? $order->ref : '',
            'auftragsnummer' => $auftragsnummer,
            'kunde_name'     => $kunde->name ?? '',
            'kunde_adresse'  => $kunde
                ? trim(($kunde->address ?? '')."\n".(($kunde->zip ?? '').' '.($kunde->town ?? '')))
                : '',
            'datum'          => dol_print_date(dol_now(), 'day'),
        );

        // Alle Expedition-Extrafelder als {options_<name>} zusaetzlich bereitstellen
        // (deckt Sonderfaelle ab wenn Eddy weitere Extrafelder im ODT nutzen will)
        if ($shipment && !empty($shipment->array_options) && is_array($shipment->array_options)) {
            foreach ($shipment->array_options as $k => $v) {
                if (is_scalar($v) || $v === null) $vars[$k] = (string) ($v ?? '');
            }
        }
        foreach ($vars as $k => $v) {
            try { $odf->setVars($k, $v, true, 'UTF-8'); } catch (Throwable $e) { /* nicht alle Keys muessen im ODT existieren */ }
        }

        $odt_out = $tempdir.'/signed_shipment_'.uniqid().'.odt';
        $odf->saveToDisk($odt_out);
        if (!file_exists($odt_out)) { $debug = 'saveToDisk hat kein ODT erzeugt: '.$odt_out; return false; }

        // LibreOffice headless ODT → PDF
        // Pfad ermitteln: BERICHT_LIBREOFFICE_BIN oder via 'which'
        $lobin = getDolGlobalString('BERICHT_LIBREOFFICE_BIN', '');
        if (!$lobin || !is_executable($lobin)) {
            // Try 'soffice' (Dolibarr-Standard) und 'libreoffice'
            foreach (array('soffice', 'libreoffice') as $candidate) {
                $found = @trim(@shell_exec('command -v '.escapeshellarg($candidate).' 2>/dev/null'));
                if ($found && is_executable($found)) { $lobin = $found; break; }
            }
        }
        if (!$lobin) { $debug = 'LibreOffice-Binary nicht gefunden. Setze BERICHT_LIBREOFFICE_BIN auf vollen Pfad zu soffice.'; return false; }

        $tmp_outdir = $tempdir.'/lo_'.uniqid();
        dol_mkdir($tmp_outdir);

        // -env:UserInstallation noetig damit LO unter Apache-User starten kann (sonst /root/.config-Konflikte)
        $user_profile = $tempdir.'/lo_profile_'.uniqid();
        dol_mkdir($user_profile);
        $cmd = escapeshellcmd($lobin)
            .' --headless'
            .' -env:UserInstallation=file://'.escapeshellarg($user_profile)
            .' --convert-to pdf --outdir '.escapeshellarg($tmp_outdir)
            .' '.escapeshellarg($odt_out).' 2>&1';
        $lo_out = @shell_exec($cmd);

        $pdf_generated = preg_replace('/\.odt$/i', '.pdf', basename($odt_out));
        $pdf_full = $tmp_outdir.'/'.$pdf_generated;

        $ok = false;
        if (file_exists($pdf_full)) {
            $out_dir = dirname($out_pdf);
            if (!is_dir($out_dir)) dol_mkdir($out_dir);
            $ok = @rename($pdf_full, $out_pdf);
            if (!$ok) $ok = @copy($pdf_full, $out_pdf);
            if (!$ok) $debug = 'rename/copy PDF nach Zielpfad fehlgeschlagen: '.$out_pdf;
        } else {
            $debug = 'LibreOffice hat kein PDF erzeugt. Befehl: '.$cmd.' | Output: '.substr((string) $lo_out, 0, 500);
        }

        // Aufraeumen
        @unlink($odt_out);
        if (is_dir($tmp_outdir)) {
            foreach (glob($tmp_outdir.'/*') as $f) @unlink($f);
            @rmdir($tmp_outdir);
        }
        if (is_dir($user_profile)) {
            // rekursiv loeschen, LO erzeugt Subfolder
            @exec('rm -rf '.escapeshellarg($user_profile));
        }

        return $ok && file_exists($out_pdf);
    } catch (Throwable $e) {
        $debug = 'Exception: '.$e->getMessage().' @ '.basename($e->getFile()).':'.$e->getLine();
        dol_syslog('bericht_render_signed_shipment_odt: '.$debug, LOG_ERR);
        return false;
    }
}

/**
 * Stempelt eine PNG-Signatur (plus Metadaten-Label) in ein vorhandenes Lieferschein-PDF.
 * Nutzt FPDI zum Importieren der Originalseiten und TCPDF zum Daraufschreiben.
 * Die Box-Geometrie kommt aus llx_bericht_signature_box (template_name match) oder Default.
 *
 * @param string $src_pdf       absoluter Pfad zum Original-Lieferschein-PDF
 * @param string $signature_png absoluter Pfad zum PNG der Unterschrift
 * @param array  $box           ['page','x_mm','y_mm','w_mm','h_mm','label']
 * @param array  $meta          ['signer_name','signed_at','gps_lat','gps_lon','shipment_ref']
 * @param string $out_pdf       absoluter Pfad fuer die Ausgabe
 *
 * @return bool true bei Erfolg
 */
function bericht_stamp_signature_on_pdf($src_pdf, $signature_png, array $box, array $meta, $out_pdf)
{
    if (!file_exists($src_pdf) || !file_exists($signature_png)) return false;

    // FPDI bei Bedarf laden
    if (!class_exists('\\setasign\\Fpdi\\Tcpdf\\Fpdi')) {
        $candidates = array(
            DOL_DOCUMENT_ROOT.'/includes/setasign/vendor/autoload.php',
            DOL_DOCUMENT_ROOT.'/includes/fpdi/autoload.php',
            DOL_DOCUMENT_ROOT.'/includes/setasign/vendor/setasign/fpdi/src/Tcpdf/Fpdi.php',
        );
        foreach ($candidates as $c) {
            if (file_exists($c)) { require_once $c; break; }
        }
    }
    if (!class_exists('\\setasign\\Fpdi\\Tcpdf\\Fpdi')) return false;

    try {
        $pdf = new \setasign\Fpdi\Tcpdf\Fpdi();
        $pdf->setPrintHeader(false);
        $pdf->setPrintFooter(false);
        $pdf->setAutoPageBreak(false, 0);
        $pageCount = $pdf->setSourceFile($src_pdf);

        // Ziel-Seite auswaehlen
        $target_page = 1;
        if (is_numeric($box['page'])) {
            $target_page = max(1, min($pageCount, (int) $box['page']));
        } elseif ($box['page'] === 'first') {
            $target_page = 1;
        } else { // 'last' oder beliebiger nicht-numerischer Wert
            $target_page = $pageCount;
        }

        for ($p = 1; $p <= $pageCount; $p++) {
            $tpl = $pdf->importPage($p);
            $size = $pdf->getTemplateSize($tpl);
            $orient = ($size['width'] > $size['height']) ? 'L' : 'P';
            $pdf->AddPage($orient, array($size['width'], $size['height']));
            $pdf->useTemplate($tpl);

            if ($p === $target_page) {
                $x = (float) $box['x_mm'];
                $y = (float) $box['y_mm'];
                $w = (float) $box['w_mm'];
                $h = (float) $box['h_mm'];
                $label = $box['label'] ?? 'Unterschrift Kunde';

                // Rahmen der Box (zart, mehr als Hilfsmarkierung)
                $pdf->SetDrawColor(120, 120, 120);
                $pdf->SetLineWidth(0.15);
                $pdf->Rect($x, $y, $w, $h, 'D');

                // Label oberhalb der Box
                $pdf->SetFont('helvetica', 'B', 8);
                $pdf->SetTextColor(80, 80, 80);
                $pdf->SetXY($x, max(0, $y - 4));
                $pdf->Cell($w, 3.5, $label, 0, 0, 'L');

                // PNG hinein, mit Padding und proportionaler Skalierung
                $pad = 2;
                $sigBoxW = max(1, $w - 2 * $pad);
                $sigBoxH = max(1, $h - 2 * $pad - 6); // 6 mm fuer Footer-Text
                list($iw, $ih) = @getimagesize($signature_png);
                if ($iw && $ih) {
                    $ratio = min($sigBoxW / $iw, $sigBoxH / $ih);
                    $dw = $iw * $ratio;
                    $dh = $ih * $ratio;
                    $dx = $x + ($w - $dw) / 2;
                    $dy = $y + $pad;
                    $pdf->Image($signature_png, $dx, $dy, $dw, $dh, 'PNG');
                }

                // Trennlinie unter der Unterschrift
                $sepY = $y + $h - 6;
                $pdf->SetDrawColor(180, 180, 180);
                $pdf->Line($x + $pad, $sepY, $x + $w - $pad, $sepY);

                // Footer-Text: Name + Datum + GPS
                $pdf->SetFont('helvetica', '', 6.5);
                $pdf->SetTextColor(60, 60, 60);
                $pdf->SetXY($x + $pad, $sepY + 0.5);
                $line1 = trim(($meta['signer_name'] ?? '') . ($meta['signed_at'] ? '  ·  '.$meta['signed_at'] : ''));
                $pdf->Cell($w - 2 * $pad, 3, $line1, 0, 0, 'L');
                if (!empty($meta['gps_lat']) && !empty($meta['gps_lon'])) {
                    $pdf->SetXY($x + $pad, $sepY + 3);
                    $pdf->Cell($w - 2 * $pad, 3, sprintf('GPS: %.6f, %.6f', $meta['gps_lat'], $meta['gps_lon']), 0, 0, 'L');
                }
            }
        }

        $pdf->Output($out_pdf, 'F');
        return file_exists($out_pdf);
    } catch (Throwable $e) {
        dol_syslog('bericht_stamp_signature_on_pdf: '.$e->getMessage(), LOG_ERR);
        return false;
    }
}

function bericht_get_signature_box(DoliDB $db, $template_name = '')
{
    global $conf;

    if ($template_name !== '') {
        $sql = "SELECT page, x_mm, y_mm, w_mm, h_mm, label FROM ".$db->prefix()."bericht_signature_box"
            ." WHERE entity = ".((int) $conf->entity)
            ." AND template_name = '".$db->escape($template_name)."'"
            ." LIMIT 1";
        $r = $db->query($sql);
        if ($r && ($row = $db->fetch_object($r))) {
            return array(
                'page'   => $row->page,
                'x_mm'   => (float) $row->x_mm,
                'y_mm'   => (float) $row->y_mm,
                'w_mm'   => (float) $row->w_mm,
                'h_mm'   => (float) $row->h_mm,
                'label'  => $row->label ?: 'Unterschrift Kunde',
            );
        }
    }
    // Default aus Konstante
    $json = getDolGlobalString('BERICHT_SIGNATURE_BOX_DEFAULT', '');
    $def = $json ? json_decode($json, true) : null;
    if (!is_array($def)) {
        $def = array('page' => 'last', 'x_mm' => 120, 'y_mm' => 230, 'w_mm' => 70, 'h_mm' => 35, 'label' => 'Unterschrift Kunde');
    }
    return $def;
}

/**
 * Mappt einen element_type-Code auf den Dolibarr-internen Element-Namen
 * für das Verzeichnis der Anhänge (multidir_output).
 */
function bericht_element_to_dir_key($element_type)
{
    return array(
        'invoice' => 'facture',
        'order'   => 'commande',
        'propal'  => 'propal',
    )[$element_type] ?? null;
}

/**
 * Sammelt alle Anhänge eines Parent-Objekts UND der direkt verknüpften Objekte
 * (z. B. der Auftrag/Angebot, die mit dieser Rechnung verknüpft sind).
 *
 * @return array  Liste mit ['source','source_id','source_ref','filename','fullpath','relpath','size','mime','date']
 */
function bericht_collect_attachments(DoliDB $db, CommonObject $parent, $element_type)
{
    global $conf;

    $result = array();

    // 1) Eigene Anhänge
    $dir_key = bericht_element_to_dir_key($element_type);
    if ($dir_key && !empty($conf->{$dir_key}->multidir_output[$parent->entity])) {
        $upload_dir = $conf->{$dir_key}->multidir_output[$parent->entity].'/'.dol_sanitizeFileName($parent->ref);
        if (is_dir($upload_dir)) {
            $files = dol_dir_list($upload_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$');
            foreach ($files as $f) {
                $result[] = array(
                    'source'      => $element_type,
                    'source_id'   => $parent->id,
                    'source_ref'  => $parent->ref,
                    'filename'    => $f['name'],
                    'fullpath'    => $f['fullname'],
                    'relpath'     => str_replace(DOL_DATA_ROOT.'/', '', $f['fullname']),
                    'size'        => $f['size'],
                    'mime'        => dol_mimetype($f['name']),
                    'date'        => $f['date'],
                );
            }
        }
    }

    // 2) Verknüpfte Objekte: order, propal — auch deren Anhänge anbieten
    $parent->fetchObjectLinked();
    $linked_types = array('commande' => 'order', 'propal' => 'propal', 'facture' => 'invoice');
    foreach ($linked_types as $linked_dolibarr_type => $linked_module_type) {
        if (empty($parent->linkedObjects[$linked_dolibarr_type])) continue;
        foreach ($parent->linkedObjects[$linked_dolibarr_type] as $lobj) {
            if (empty($conf->{$linked_dolibarr_type}->multidir_output[$lobj->entity])) continue;
            $linked_dir = $conf->{$linked_dolibarr_type}->multidir_output[$lobj->entity].'/'.dol_sanitizeFileName($lobj->ref);
            if (!is_dir($linked_dir)) continue;
            $files = dol_dir_list($linked_dir, 'files', 1, '', '(\.meta|_preview.*\.png|thumbs)$');
            foreach ($files as $f) {
                $result[] = array(
                    'source'      => $linked_module_type,
                    'source_id'   => $lobj->id,
                    'source_ref'  => $lobj->ref,
                    'filename'    => $f['name'],
                    'fullpath'    => $f['fullname'],
                    'relpath'     => str_replace(DOL_DATA_ROOT.'/', '', $f['fullname']),
                    'size'        => $f['size'],
                    'mime'        => dol_mimetype($f['name']),
                    'date'        => $f['date'],
                );
            }
        }
    }

    return $result;
}

/**
 * Liest die Auftragsnummer aus dem Parent-Objekt.
 * Reihenfolge: extrafield 'auftragsnummer' → ref_client → ref
 */
function bericht_get_auftragsnummer(CommonObject $parent)
{
    if (!empty($parent->array_options['options_auftragsnummer'])) {
        return $parent->array_options['options_auftragsnummer'];
    }
    if (!empty($parent->ref_client)) {
        return $parent->ref_client;
    }
    return $parent->ref;
}

/**
 * Listet alle ODT-Templates im Templates-Verzeichnis auf.
 *
 * @return string[]  Dateinamen
 */
function bericht_list_templates()
{
    $dir = DOL_DATA_ROOT.'/bericht/templates';
    if (!is_dir($dir)) return array();
    $files = scandir($dir);
    return array_values(array_filter($files, function ($f) {
        return preg_match('/\.odt$/i', $f);
    }));
}

/**
 * Sicherer absolute-Pfad-Resolver für Dateien unterhalb DOL_DATA_ROOT.
 * Verhindert Path-Traversal.
 */
function bericht_resolve_data_path($relpath)
{
    $base = realpath(DOL_DATA_ROOT);
    $full = realpath($base.'/'.$relpath);
    if ($full === false) return null;
    if (strpos($full, $base) !== 0) return null;
    return $full;
}

function bericht_hex_to_rgb($hex)
{
    $hex = ltrim($hex, '#');
    if (strlen($hex) === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
    if (strlen($hex) !== 6) return array(255, 0, 0);
    return array(hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2)));
}

/**
 * Rendert Fabric.js-Annotationen in ein TCPDF-Objekt.
 * Nutzt die wichtigsten Shape-Typen: rect, circle/ellipse, line, path, text, group (Pfeil).
 */
function bericht_burn_annotations($pdf, $fabric_json, $x, $y, $w, $h)
{
    if (empty($fabric_json)) return;
    $data = json_decode($fabric_json, true);
    if (!is_array($data) || empty($data['objects'])) return;

    $cw = $data['width'] ?? null;
    $ch = $data['height'] ?? null;
    if (!$cw || !$ch) return;
    $sx = $w / $cw;
    $sy = $h / $ch;

    foreach ($data['objects'] as $obj) {
        $stroke = isset($obj['stroke']) ? bericht_hex_to_rgb($obj['stroke']) : array(255, 0, 0);
        $sw = ($obj['strokeWidth'] ?? 2) * $sx;
        $pdf->SetDrawColor($stroke[0], $stroke[1], $stroke[2]);
        $pdf->SetLineWidth(max(0.2, $sw));

        $type = $obj['type'] ?? '';
        $ox = ($obj['left'] ?? 0) * $sx + $x;
        $oy = ($obj['top'] ?? 0) * $sy + $y;
        $ow = ($obj['width'] ?? 0) * ($obj['scaleX'] ?? 1) * $sx;
        $oh = ($obj['height'] ?? 0) * ($obj['scaleY'] ?? 1) * $sy;

        switch ($type) {
            case 'rect':
                $pdf->Rect($ox, $oy, $ow, $oh, 'D');
                break;
            case 'circle':
            case 'ellipse':
                $rx = ($obj['rx'] ?? ($ow / 2)) * $sx;
                $ry = ($obj['ry'] ?? ($oh / 2)) * $sy;
                $pdf->Ellipse($ox + $rx, $oy + $ry, $rx, $ry, 0, 0, 360, 'D');
                break;
            case 'line':
                $x1 = ($obj['x1'] ?? 0) * $sx + $x + ($obj['left'] ?? 0) * $sx;
                $y1 = ($obj['y1'] ?? 0) * $sy + $y + ($obj['top'] ?? 0) * $sy;
                $x2 = ($obj['x2'] ?? 0) * $sx + $x + ($obj['left'] ?? 0) * $sx;
                $y2 = ($obj['y2'] ?? 0) * $sy + $y + ($obj['top'] ?? 0) * $sy;
                $pdf->Line($x1, $y1, $x2, $y2);
                break;
            case 'path':
                if (!empty($obj['path']) && is_array($obj['path'])) {
                    $prev = null;
                    foreach ($obj['path'] as $seg) {
                        if (!is_array($seg) || count($seg) < 3) continue;
                        $cmd = $seg[0];
                        if ($cmd === 'M') {
                            $prev = array($seg[1] * $sx + $x, $seg[2] * $sy + $y);
                        } elseif (($cmd === 'L' || $cmd === 'Q') && $prev) {
                            $px = $seg[1] * $sx + $x;
                            $py = $seg[2] * $sy + $y;
                            $pdf->Line($prev[0], $prev[1], $px, $py);
                            $prev = array($px, $py);
                        }
                    }
                }
                break;
            case 'group':
                // Pfeil = Group(Line + Triangle) — als Pfeillinie rendern
                if (!empty($obj['objects']) && is_array($obj['objects'])) {
                    $angle = ($obj['angle'] ?? 0) * M_PI / 180;
                    $startX = $ox; $startY = $oy;
                    // Länge aus den Children abschätzen
                    $len = 0;
                    foreach ($obj['objects'] as $child) {
                        if (($child['type'] ?? '') === 'line') {
                            $lx2 = ($child['x2'] ?? 0) - ($child['x1'] ?? 0);
                            $len = max($len, abs($lx2));
                        }
                    }
                    $len *= $sx * ($obj['scaleX'] ?? 1);
                    $endX = $startX + cos($angle) * $len;
                    $endY = $startY + sin($angle) * $len;
                    $pdf->Line($startX, $startY, $endX, $endY);
                    // Pfeilspitze
                    $headLen = max(2, $sw * 4);
                    $a1 = $angle + M_PI - 0.4;
                    $a2 = $angle + M_PI + 0.4;
                    $pdf->Line($endX, $endY, $endX + cos($a1) * $headLen, $endY + sin($a1) * $headLen);
                    $pdf->Line($endX, $endY, $endX + cos($a2) * $headLen, $endY + sin($a2) * $headLen);
                }
                break;
            case 'i-text':
            case 'text':
            case 'textbox':
                $fontsize = max(6, ($obj['fontSize'] ?? 16) * $sx * 2.83);
                $pdf->SetFont(bericht_ensure_hack_font($pdf), '', $fontsize);
                $pdf->SetTextColor($stroke[0], $stroke[1], $stroke[2]);
                $pdf->Text($ox, $oy + $fontsize * 0.35, $obj['text'] ?? '');
                break;
        }
    }
}

/**
 * Rendert ein ODT-Template als PDF (Deckblatt).
 * Nutzt Dolibarrs odtphp + LibreOffice headless für die Konvertierung.
 *
 * @return string|null  Pfad zum erzeugten PDF oder null
 */
function bericht_render_cover_for_preview($template_path, $bericht, $parent, $tempdir)
{
    return bericht_render_cover_internal($template_path, $bericht, $parent, $tempdir);
}

/**
 * Rendert eine BerichtPage in das aktuelle TCPDF-Objekt.
 * Behandelt: single (mit image_scale/align), grid_2, grid_2v, grid_4, grid_6, PDF-Quelle.
 *
 * @param TCPDF       $pdf
 * @param BerichtPage $page
 * @param string      $ori    'P' oder 'L'
 * @param string      $fmt    A4/A3/A5/Letter
 * @param bool        $fpdi_loaded
 */
/**
 * Registriert den Hack-Font (Eddys Corporate-Font) in TCPDF und gibt den Font-Key zurück.
 * Konvertiert die TTFs beim ersten Aufruf via TCPDF_FONTS::addTTFfont ins TCPDF-Font-Verzeichnis.
 * Fallback auf 'helvetica' wenn etwas schiefgeht.
 *
 * @return string  Font-Key für $pdf->SetFont()
 */
function bericht_ensure_hack_font($pdf = null)
{
    // Der Font wird in BerichtPdfTrait::berichtInit registriert; hier nur Key zurückgeben.
    if ($pdf && isset($pdf->hack_font_key) && $pdf->hack_font_key) {
        return $pdf->hack_font_key;
    }
    return 'helvetica';
}

/**
 * Rendert eine HTML-Notiz (CKEditor-Output) mit TCPDF::writeHTMLCell
 * in einen reservierten Bereich unterhalb des Bildes.
 */
function bericht_write_note_html($pdf, $html, $x, $y, $w, $h)
{
    if (empty($html)) return;
    $font = bericht_ensure_hack_font($pdf);
    $pdf->SetFont($font, '', 9);
    $pdf->SetTextColor(40, 40, 40);
    $autoPB = $pdf->getAutoPageBreak();
    $bMargin = $pdf->getBreakMargin();
    $pdf->SetAutoPageBreak(false, 0);
    // HTML in Font-Wrapper packen, damit TCPDF den Hack-Font für den ganzen Block nutzt
    $wrapped = '<span style="font-family:'.$font.';font-size:9pt;">'.$html.'</span>';
    $pdf->writeHTMLCell($w, $h, $x, $y, $wrapped, 0, 1, false, true, 'L', true);
    $pdf->SetAutoPageBreak($autoPB, $bMargin);
}

function bericht_render_page_to_pdf($pdf, $page, $ori, $fmt, $fpdi_loaded)
{
    // Phase 6: Wenn der Editor ein Composite-PNG hochgeladen hat, nutzen wir das direkt
    if (!empty($page->composite_path)) {
        $full = bericht_resolve_data_path($page->composite_path);
        if ($full && file_exists($full)) {
            $pdf->AddPage($ori, $fmt);
            $pageW = $pdf->getPageWidth();
            $pageH = $pdf->getPageHeight();
            // Margins: oben Platz für Header (Logo/Titel/Firmenname), unten für Footer
            $mL = 10; $mR = 10; $mT = 30; $mB = 16;
            // Notiz-Bereich reservieren wenn eine Notiz existiert
            $note_h = 0;
            if (!empty($page->note)) {
                $note_h = max(20, min(80, $pageH * 0.25));
            }
            $maxW = $pageW - $mL - $mR;
            $maxH = $pageH - $mT - $mB - $note_h - ($note_h ? 4 : 0);
            list($iw, $ih) = @getimagesize($full);
            if ($iw && $ih) {
                $ratio = min($maxW / $iw, $maxH / $ih);
                $w = $iw * $ratio; $h = $ih * $ratio;
                $x = ($pageW - $w) / 2;
                $y = $mT;
                $pdf->Image($full, $x, $y, $w, $h);
            }
            if (!empty($page->note)) {
                $note_y = $mT + ($ih && $iw ? ($ih * min($maxW / $iw, $maxH / $ih)) : $maxH) + 4;
                bericht_write_note_html($pdf, $page->note, $mL, $note_y, $maxW, $note_h);
            }
            return;
        }
    }

    $layout = $page->layout ?: 'single';

    // Spezial-Fall: reine Titel-Seite (kein Bild, nur großer Titel zentriert)
    if ($layout === 'title_only' || ($layout === 'single' && empty($page->source_path) && !empty($page->title))) {
        $pdf->AddPage($ori, $fmt);
        $pageW = $pdf->getPageWidth();
        $pageH = $pdf->getPageHeight();
        $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 32);
        $pdf->SetTextColor(40, 40, 40);
        $pdf->SetY($pageH / 2 - 20);
        $pdf->MultiCell(0, 20, $page->title, 0, 'C');
        if (!empty($page->note)) {
            $pdf->Ln(12);
            $pdf->SetFont(bericht_ensure_hack_font($pdf), '', 12);
            $pdf->MultiCell(0, 8, $page->note, 0, 'C');
        }
        return;
    }

    /* ----- Vorher/Nachher-Layout ----- */
    if ($layout === 'before_after') {
        $imgs = $page->getImages();
        if (empty($imgs)) return;
        $pdf->AddPage($ori, $fmt);
        $pageW = $pdf->getPageWidth();
        $pageH = $pdf->getPageHeight();
        $margin = 10;
        $title_h = !empty($page->title) ? 12 : 0;
        if ($title_h) {
            $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 16);
            $pdf->SetY($margin);
            $pdf->MultiCell(0, $title_h, $page->title, 0, 'C');
        }
        // Zwei Bilder nebeneinander mit VORHER/NACHHER Labels
        $label_h = 8;
        $gap = 6;
        $top = $margin + $title_h + 4;
        $usable_h = $pageH - $top - 10 - $label_h; // -10 für Notiz, -label_h für Beschriftung
        $w_each = ($pageW - 2 * $margin - $gap) / 2;

        foreach (array(array('Vorher', 0), array('Nachher', 1)) as $slot_info) {
            list($label, $slot) = $slot_info;
            if (!isset($imgs[$slot])) continue;
            $full = bericht_resolve_data_path($imgs[$slot]['source_path']);
            if (!$full || !file_exists($full)) continue;
            list($iw, $ih) = @getimagesize($full);
            if (!$iw || !$ih) continue;

            $x0 = $margin + $slot * ($w_each + $gap);
            // Label
            $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 11);
            $pdf->SetXY($x0, $top);
            $pdf->Cell($w_each, $label_h, $label, 0, 0, 'C');
            // Bild darunter
            $ratio = min($w_each / $iw, $usable_h / $ih);
            $w = $iw * $ratio; $h = $ih * $ratio;
            $x = $x0 + ($w_each - $w) / 2;
            $y = $top + $label_h + ($usable_h - $h) / 2;
            $pdf->Image($full, $x, $y, $w, $h);
        }
        bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $pageW, $pageH);
        if (!empty($page->note)) {
            bericht_write_note_html($pdf, $page->note, 10, $pdf->getPageHeight() - 22, $pdf->getPageWidth() - 20, 18);
        }
        return;
    }

    /* ----- Multi-Image Grid ----- */
    if ($layout !== 'single') {
        $imgs = $page->getImages();
        if (empty($imgs)) return;

        $pdf->AddPage($ori, $fmt);
        $pageW = $pdf->getPageWidth();
        $pageH = $pdf->getPageHeight();

        // Titel oben
        $title_h = 0;
        if (!empty($page->title)) {
            $pdf->SetFont(bericht_ensure_hack_font($pdf), 'B', 16);
            $pdf->SetY(10);
            $pdf->MultiCell(0, 10, $page->title, 0, 'C');
            $title_h = 14;
        }
        $rects = BerichtPage::slotRects($layout, $pageW, $pageH - $title_h, 10 + $title_h);

        foreach ($imgs as $img) {
            $slot = $img['slot'];
            if (!isset($rects[$slot])) continue;
            $r = $rects[$slot];
            $full = bericht_resolve_data_path($img['source_path']);
            if (!$full || !file_exists($full)) continue;

            list($iw, $ih) = @getimagesize($full);
            if (!$iw || !$ih) continue;
            $ratio = min($r['w'] / $iw, $r['h'] / $ih);
            $w = $iw * $ratio; $h = $ih * $ratio;
            $x = $r['x'] + ($r['w'] - $w) / 2;
            $y = $r['y'] + ($r['h'] - $h) / 2;
            $pdf->Image($full, $x, $y, $w, $h);
        }
        // Annotationen über volle Seite
        bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $pageW, $pageH);

        // Notiz unten
        if (!empty($page->note)) {
            bericht_write_note_html($pdf, $page->note, 10, $pdf->getPageHeight() - 22, $pdf->getPageWidth() - 20, 18);
        }
        return;
    }

    /* ----- Single ----- */
    $full = bericht_resolve_data_path($page->source_path);
    if (!$full || !file_exists($full)) return;

    if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
        $pdf->AddPage($ori, $fmt);
        list($iw, $ih) = @getimagesize($full);
        if (!$iw || !$ih) return;

        $pageW = $pdf->getPageWidth();
        $pageH = $pdf->getPageHeight();
        $margin = 10;
        $maxW = ($pageW - 2 * $margin);
        $maxH = ($pageH - 2 * $margin - 10); // Notiz unten

        // image_scale: Anteil der maximalen Größe
        $scale = max(0.2, min(1.0, (float) ($page->image_scale ?: 1.0)));
        $availW = $maxW * $scale;
        $availH = $maxH * $scale;
        $ratio = min($availW / $iw, $availH / $ih);
        $w = $iw * $ratio; $h = $ih * $ratio;

        // image_align bestimmt die Position
        list($x, $y) = bericht_align_position($page->image_align ?: 'fit', $w, $h, $pageW, $pageH, $margin);

        $pdf->Image($full, $x, $y, $w, $h);
        bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
    } elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {
        try {
            $pdf->setSourceFile($full);
            $tpl = $pdf->importPage($page->source_page ?: 1);
            $size = $pdf->getTemplateSize($tpl);
            $pdf->AddPage($size['orientation'] ?? 'P', array($size['width'], $size['height']));
            $pdf->useTemplate($tpl);
            bericht_burn_annotations($pdf, $page->fabric_json, 0, 0, $size['width'], $size['height']);
        } catch (Throwable $e) { /* skip */ }
    }

    if (!empty($page->note)) {
        bericht_write_note_html($pdf, $page->note, 10, $pdf->getPageHeight() - 22, $pdf->getPageWidth() - 20, 18);
    }
}

/**
 * Berechnet die x/y-Position für ein Bild auf einer Seite anhand des image_align-Werts.
 */
function bericht_align_position($align, $w, $h, $pageW, $pageH, $margin = 10)
{
    $usableW = $pageW - 2 * $margin;
    $usableH = $pageH - 2 * $margin - 10; // Notiz unten

    switch ($align) {
        case 'topleft':
            return array($margin, $margin);
        case 'topright':
            return array($pageW - $margin - $w, $margin);
        case 'bottomleft':
            return array($margin, $margin + $usableH - $h);
        case 'bottomright':
            return array($pageW - $margin - $w, $margin + $usableH - $h);
        case 'center':
            return array(($pageW - $w) / 2, $margin + ($usableH - $h) / 2);
        case 'fit':
        default:
            return array(($pageW - $w) / 2, $margin);
    }
}

function bericht_render_cover_internal($template_path, $bericht, $parent, $tempdir)
{
    global $user, $langs;

    $odt_loader = DOL_DOCUMENT_ROOT.'/includes/odtphp/odf.php';
    if (!file_exists($odt_loader)) return null;
    require_once $odt_loader;

    try {
        $odf = new Odf($template_path, array('PATH_TO_TMP' => $tempdir));
        $vars = array(
            'auftragsnummer'  => $bericht->auftragsnummer ?: '',
            'angebotsnummer'  => $parent->array_options['options_angebotsnummer'] ?? '',
            'rechnungsnummer' => $parent->ref ?? '',
            'kunde_name'      => $parent->thirdparty->name ?? '',
            'kunde_adresse'   => trim(($parent->thirdparty->address ?? '')."\n".($parent->thirdparty->zip ?? '').' '.($parent->thirdparty->town ?? '')),
            'datum'           => dol_print_date(dol_now(), 'day'),
            'beschreibung'    => $parent->array_options['options_beschreibung'] ?? '',
            'hinweis'         => $parent->array_options['options_hinweis'] ?? '',
            'bericht_titel'   => $bericht->titel ?? '',
            'ersteller'       => $user->getFullName($langs ?? null) ?: $user->login,
        );
        foreach ($vars as $k => $v) {
            try { $odf->setVars($k, $v, true, 'UTF-8'); } catch (Throwable $e) {}
        }
        $odt_out = $tempdir.'/cover_'.$bericht->id.'_'.uniqid().'.odt';
        $odf->saveToDisk($odt_out);

        $lobin = getDolGlobalString('BERICHT_LIBREOFFICE_BIN', '/usr/bin/libreoffice');
        $cmd = escapeshellcmd($lobin)
            .' --headless --convert-to pdf --outdir '.escapeshellarg($tempdir).' '.escapeshellarg($odt_out).' 2>&1';
        @shell_exec($cmd);

        $pdf_out = preg_replace('/\.odt$/i', '.pdf', $odt_out);
        return file_exists($pdf_out) ? $pdf_out : null;
    } catch (Throwable $e) {
        return null;
    }
}
