<?php
declare(strict_types=1);

require_once __DIR__ . '/subscriber_contracts_lib.php';

const SC_PDF_W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const SC_PDF_R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';

function sc_pdf_h(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function sc_pdf_attr(?DOMElement $element, string $name, string $namespace = SC_PDF_W_NS): string
{
    return $element instanceof DOMElement ? (string)$element->getAttributeNS($namespace, $name) : '';
}

function sc_pdf_first(DOMXPath $xpath, DOMNode $context, string $query): ?DOMElement
{
    $node = $xpath->query($query, $context)->item(0);
    return $node instanceof DOMElement ? $node : null;
}

function sc_pdf_enabled(?DOMElement $element): bool
{
    if (!$element) return false;
    $value = strtolower(sc_pdf_attr($element, 'val'));
    return !in_array($value, ['0', 'false', 'off', 'none'], true);
}

function sc_pdf_twips_pt(string $value): ?float
{
    return is_numeric($value) ? ((float)$value / 20.0) : null;
}

function sc_pdf_twips_mm(string $value, float $fallback): float
{
    return is_numeric($value) ? ((float)$value * 25.4 / 1440.0) : $fallback;
}

function sc_pdf_css_declarations(array $properties): string
{
    $css = [];
    foreach ($properties as $name => $value) {
        if ($value === null || $value === '') continue;
        $css[] = $name . ':' . $value;
    }
    return implode(';', $css);
}

function sc_pdf_paragraph_properties(DOMXPath $xpath, ?DOMElement $properties): array
{
    if (!$properties) return [];
    $css = [];
    $align = strtolower(sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:jc'), 'val'));
    $alignMap = ['center' => 'center', 'left' => 'left', 'right' => 'right', 'both' => 'justify', 'distribute' => 'justify'];
    if (isset($alignMap[$align])) $css['text-align'] = $alignMap[$align];

    $spacing = sc_pdf_first($xpath, $properties, './w:spacing');
    if ($spacing) {
        $before = sc_pdf_twips_pt(sc_pdf_attr($spacing, 'before'));
        $after = sc_pdf_twips_pt(sc_pdf_attr($spacing, 'after'));
        $line = sc_pdf_attr($spacing, 'line');
        $lineRule = strtolower(sc_pdf_attr($spacing, 'lineRule'));
        if ($before !== null) $css['margin-top'] = round($before, 2) . 'pt';
        if ($after !== null) $css['margin-bottom'] = round($after, 2) . 'pt';
        if (is_numeric($line)) {
            $css['line-height'] = $lineRule === 'auto'
                ? max(.7, round((float)$line / 240.0, 3))
                : round((float)$line / 20.0, 2) . 'pt';
        }
    }

    $indent = sc_pdf_first($xpath, $properties, './w:ind');
    if ($indent) {
        $left = sc_pdf_twips_pt(sc_pdf_attr($indent, 'left') ?: sc_pdf_attr($indent, 'start'));
        $right = sc_pdf_twips_pt(sc_pdf_attr($indent, 'right') ?: sc_pdf_attr($indent, 'end'));
        $first = sc_pdf_twips_pt(sc_pdf_attr($indent, 'firstLine'));
        $hanging = sc_pdf_twips_pt(sc_pdf_attr($indent, 'hanging'));
        if ($left !== null) $css['padding-left'] = round($left, 2) . 'pt';
        if ($right !== null) $css['padding-right'] = round($right, 2) . 'pt';
        if ($first !== null) $css['text-indent'] = round($first, 2) . 'pt';
        if ($hanging !== null) $css['text-indent'] = -round($hanging, 2) . 'pt';
    }

    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:bidi'))) $css['direction'] = 'rtl';
    return $css;
}

function sc_pdf_run_properties(DOMXPath $xpath, ?DOMElement $properties): array
{
    if (!$properties) return [];
    $css = [];
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:b'))) $css['font-weight'] = '700';
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:i'))) $css['font-style'] = 'italic';
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:strike'))) $css['text-decoration'] = 'line-through';

    $underline = strtolower(sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:u'), 'val'));
    if ($underline !== '' && $underline !== 'none' && $underline !== '0') $css['text-decoration'] = 'underline';

    $size = sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:sz'), 'val');
    if (is_numeric($size)) $css['font-size'] = max(5, (float)$size / 2.0) . 'pt';
    $color = strtoupper(sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:color'), 'val'));
    if (preg_match('/^[0-9A-F]{6}$/', $color)) $css['color'] = '#' . $color;
    $shade = strtoupper(sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:shd'), 'fill'));
    if (preg_match('/^[0-9A-F]{6}$/', $shade) && $shade !== 'FFFFFF') $css['background-color'] = '#' . $shade;

    $fonts = sc_pdf_first($xpath, $properties, './w:rFonts');
    if ($fonts) {
        $font = sc_pdf_attr($fonts, 'cs') ?: sc_pdf_attr($fonts, 'ascii') ?: sc_pdf_attr($fonts, 'hAnsi') ?: sc_pdf_attr($fonts, 'eastAsia');
        if ($font !== '') $css['font-family'] = '"' . str_replace('"', '', $font) . '",Arial,Tahoma,sans-serif';
    }

    $spacing = sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:spacing'), 'val');
    if (is_numeric($spacing)) $css['letter-spacing'] = round((float)$spacing / 20.0, 2) . 'pt';
    $vertical = strtolower(sc_pdf_attr(sc_pdf_first($xpath, $properties, './w:vertAlign'), 'val'));
    if ($vertical === 'superscript') $css['vertical-align'] = 'super';
    if ($vertical === 'subscript') $css['vertical-align'] = 'sub';
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:rtl'))) $css['direction'] = 'rtl';
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:caps'))) $css['text-transform'] = 'uppercase';
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:smallCaps'))) $css['font-variant'] = 'small-caps';
    if (sc_pdf_enabled(sc_pdf_first($xpath, $properties, './w:vanish'))) $css['display'] = 'none';
    return $css;
}

function sc_pdf_style_catalog(ZipArchive $zip): array
{
    $xml = (string)($zip->getFromName('word/styles.xml') ?: '');
    if ($xml === '') return ['defaults' => ['p' => [], 'r' => []], 'styles' => []];
    $dom = new DOMDocument('1.0', 'UTF-8');
    if (!@$dom->loadXML($xml, LIBXML_NONET | LIBXML_COMPACT)) return ['defaults' => ['p' => [], 'r' => []], 'styles' => []];
    $xpath = new DOMXPath($dom);
    $xpath->registerNamespace('w', SC_PDF_W_NS);
    $catalog = [
        'defaults' => [
            'p' => sc_pdf_paragraph_properties($xpath, sc_pdf_first($xpath, $dom, '//w:docDefaults/w:pPrDefault/w:pPr')),
            'r' => sc_pdf_run_properties($xpath, sc_pdf_first($xpath, $dom, '//w:docDefaults/w:rPrDefault/w:rPr')),
        ],
        'styles' => [],
    ];
    foreach ($xpath->query('//w:style') as $style) {
        if (!($style instanceof DOMElement)) continue;
        $id = sc_pdf_attr($style, 'styleId');
        if ($id === '') continue;
        $catalog['styles'][$id] = [
            'type' => sc_pdf_attr($style, 'type'),
            'based_on' => sc_pdf_attr(sc_pdf_first($xpath, $style, './w:basedOn'), 'val'),
            'p' => sc_pdf_paragraph_properties($xpath, sc_pdf_first($xpath, $style, './w:pPr')),
            'r' => sc_pdf_run_properties($xpath, sc_pdf_first($xpath, $style, './w:rPr')),
        ];
    }
    return $catalog;
}

function sc_pdf_resolve_style(array $catalog, string $id, array &$seen = []): array
{
    if ($id === '' || isset($seen[$id]) || !isset($catalog['styles'][$id])) return ['p' => [], 'r' => []];
    $seen[$id] = true;
    $style = $catalog['styles'][$id];
    $base = sc_pdf_resolve_style($catalog, (string)($style['based_on'] ?? ''), $seen);
    return [
        'p' => array_merge($base['p'], (array)$style['p']),
        'r' => array_merge($base['r'], (array)$style['r']),
    ];
}

function sc_pdf_relationships(ZipArchive $zip): array
{
    $xml = (string)($zip->getFromName('word/_rels/document.xml.rels') ?: '');
    if ($xml === '') return [];
    $dom = new DOMDocument('1.0', 'UTF-8');
    if (!@$dom->loadXML($xml, LIBXML_NONET | LIBXML_COMPACT)) return [];
    $xpath = new DOMXPath($dom);
    $map = [];
    foreach ($xpath->query('//*[local-name()="Relationship"]') as $relationship) {
        if (!($relationship instanceof DOMElement)) continue;
        $id = (string)$relationship->getAttribute('Id');
        $target = str_replace('\\', '/', (string)$relationship->getAttribute('Target'));
        if ($id !== '' && $target !== '') $map[$id] = 'word/' . ltrim($target, '/');
    }
    return $map;
}

function sc_pdf_save_asset(ZipArchive $zip, string $zipName, string $assetDirectory, string $assetUrlBase): string
{
    $binary = $zip->getFromName($zipName);
    if ($binary === false || $binary === '') return '';
    $extension = strtolower(pathinfo($zipName, PATHINFO_EXTENSION));
    if (!preg_match('/^(png|jpe?g|gif|webp|bmp|svg)$/', $extension)) $extension = 'bin';
    $name = hash('sha256', $binary) . '.' . $extension;
    if (!is_dir($assetDirectory) && !@mkdir($assetDirectory, 0770, true) && !is_dir($assetDirectory)) {
        throw new RuntimeException('تعذر تجهيز صور ملف PDF.');
    }
    $path = $assetDirectory . DIRECTORY_SEPARATOR . $name;
    if (!is_file($path) && file_put_contents($path, $binary, LOCK_EX) === false) {
        throw new RuntimeException('تعذر حفظ صورة مستخدمة في ملف PDF.');
    }
    return rtrim($assetUrlBase, '/') . '/' . rawurlencode($name);
}

function sc_pdf_render_drawing(
    DOMXPath $xpath,
    DOMElement $drawing,
    ZipArchive $zip,
    array $relationships,
    string $assetDirectory,
    string $assetUrlBase
): string {
    $blip = sc_pdf_first($xpath, $drawing, './/*[local-name()="blip"]');
    $relationshipId = sc_pdf_attr($blip, 'embed', SC_PDF_R_NS);
    $target = (string)($relationships[$relationshipId] ?? '');
    if ($target === '') return '';
    $src = sc_pdf_save_asset($zip, $target, $assetDirectory, $assetUrlBase);
    if ($src === '') return '';
    $extent = sc_pdf_first($xpath, $drawing, './/*[local-name()="extent"]');
    $cx = $extent ? (string)$extent->getAttribute('cx') : '';
    $cy = $extent ? (string)$extent->getAttribute('cy') : '';
    $style = [];
    if (is_numeric($cx) && (float)$cx > 0) $style['width'] = round((float)$cx / 36000.0, 3) . 'mm';
    if (is_numeric($cy) && (float)$cy > 0) $style['height'] = round((float)$cy / 36000.0, 3) . 'mm';
    $style['object-fit'] = 'contain';
    return '<img class="sc-docx-image" src="' . sc_pdf_h($src) . '" style="' . sc_pdf_h(sc_pdf_css_declarations($style)) . '">';
}

function sc_pdf_render_run(
    DOMXPath $xpath,
    DOMElement $run,
    ZipArchive $zip,
    array $catalog,
    array $paragraphRunProperties,
    array $relationships,
    string $assetDirectory,
    string $assetUrlBase
): string {
    $runProperties = sc_pdf_first($xpath, $run, './w:rPr');
    $runStyleId = sc_pdf_attr(sc_pdf_first($xpath, $run, './w:rPr/w:rStyle'), 'val');
    $seen = [];
    $runStyle = sc_pdf_resolve_style($catalog, $runStyleId, $seen);
    $css = array_merge(
        (array)($catalog['defaults']['r'] ?? []),
        $paragraphRunProperties,
        (array)$runStyle['r'],
        sc_pdf_run_properties($xpath, $runProperties)
    );
    $content = '';
    foreach ($run->childNodes as $child) {
        if (!($child instanceof DOMElement) || $child->localName === 'rPr') continue;
        if (in_array($child->localName, ['t', 'delText'], true)) {
            $text = str_replace(["\u{00FF}", "\u{F0FF}"], '☐', (string)$child->textContent);
            $content .= sc_pdf_h($text);
        } elseif ($child->localName === 'tab') {
            $content .= '<span class="sc-docx-tab"></span>';
        } elseif ($child->localName === 'br') {
            if (strtolower(sc_pdf_attr($child, 'type')) !== 'page') $content .= '<br>';
        } elseif ($child->localName === 'drawing') {
            $content .= sc_pdf_render_drawing($xpath, $child, $zip, $relationships, $assetDirectory, $assetUrlBase);
        } elseif ($child->localName === 'sym') {
            $hex = sc_pdf_attr($child, 'char');
            $content .= $hex !== '' && function_exists('mb_chr') ? sc_pdf_h((string)mb_chr((int)hexdec($hex), 'UTF-8')) : '';
        }
    }
    if ($content === '') return '';
    return '<span style="' . sc_pdf_h(sc_pdf_css_declarations($css)) . '">' . $content . '</span>';
}

function sc_pdf_render_paragraph(
    DOMXPath $xpath,
    DOMElement $paragraph,
    ZipArchive $zip,
    array $catalog,
    array $relationships,
    string $assetDirectory,
    string $assetUrlBase
): string {
    $properties = sc_pdf_first($xpath, $paragraph, './w:pPr');
    $styleId = sc_pdf_attr(sc_pdf_first($xpath, $paragraph, './w:pPr/w:pStyle'), 'val');
    $seen = [];
    $style = sc_pdf_resolve_style($catalog, $styleId, $seen);
    $paragraphCss = array_merge(
        (array)($catalog['defaults']['p'] ?? []),
        (array)$style['p'],
        sc_pdf_paragraph_properties($xpath, $properties)
    );
    $paragraphRunProperties = (array)$style['r'];
    $content = '';
    foreach ($paragraph->childNodes as $child) {
        if (!($child instanceof DOMElement) || $child->localName === 'pPr') continue;
        if ($child->localName === 'r') {
            $content .= sc_pdf_render_run($xpath, $child, $zip, $catalog, $paragraphRunProperties, $relationships, $assetDirectory, $assetUrlBase);
        } else {
            foreach ($xpath->query('.//w:r', $child) as $run) {
                if ($run instanceof DOMElement) {
                    $content .= sc_pdf_render_run($xpath, $run, $zip, $catalog, $paragraphRunProperties, $relationships, $assetDirectory, $assetUrlBase);
                }
            }
        }
    }
    return '<p style="' . sc_pdf_h(sc_pdf_css_declarations($paragraphCss)) . '">' . ($content !== '' ? $content : '&nbsp;') . '</p>';
}

function sc_pdf_border_css(DOMXPath $xpath, ?DOMElement $borders): string
{
    if (!$borders) return '0.5pt solid #7b8794';
    $edge = sc_pdf_first($xpath, $borders, './w:top') ?: sc_pdf_first($xpath, $borders, './w:insideH');
    if (!$edge) return '0.5pt solid #7b8794';
    $value = strtolower(sc_pdf_attr($edge, 'val'));
    if (in_array($value, ['nil', 'none'], true)) return 'none';
    $size = sc_pdf_attr($edge, 'sz');
    $width = is_numeric($size) ? max(.25, (float)$size / 8.0) : .5;
    $color = strtoupper(sc_pdf_attr($edge, 'color'));
    if (!preg_match('/^[0-9A-F]{6}$/', $color)) $color = '7B8794';
    return round($width, 2) . 'pt solid #' . $color;
}

function sc_pdf_render_table(
    DOMXPath $xpath,
    DOMElement $table,
    ZipArchive $zip,
    array $catalog,
    array $relationships,
    string $assetDirectory,
    string $assetUrlBase
): string {
    $tableProperties = sc_pdf_first($xpath, $table, './w:tblPr');
    $tableCss = ['width' => '100%', 'border-collapse' => 'collapse', 'table-layout' => 'fixed'];
    $alignment = strtolower(sc_pdf_attr(sc_pdf_first($xpath, $tableProperties, './w:jc'), 'val'));
    if ($alignment === 'center') $tableCss['margin-left'] = $tableCss['margin-right'] = 'auto';
    $tableBorder = sc_pdf_border_css($xpath, sc_pdf_first($xpath, $tableProperties, './w:tblBorders'));
    $html = '<table style="' . sc_pdf_h(sc_pdf_css_declarations($tableCss)) . '">';

    $gridWidths = [];
    foreach ($xpath->query('./w:tblGrid/w:gridCol', $table) as $column) {
        $gridWidths[] = max(1, (int)sc_pdf_attr($column instanceof DOMElement ? $column : null, 'w'));
    }
    if ($gridWidths) {
        $totalWidth = array_sum($gridWidths);
        $html .= '<colgroup>';
        foreach ($gridWidths as $width) $html .= '<col style="width:' . round(($width / $totalWidth) * 100, 4) . '%">';
        $html .= '</colgroup>';
    }

    foreach ($xpath->query('./w:tr', $table) as $row) {
        if (!($row instanceof DOMElement)) continue;
        $rowHeight = sc_pdf_first($xpath, $row, './w:trPr/w:trHeight');
        $rowCss = [];
        $height = sc_pdf_twips_pt(sc_pdf_attr($rowHeight, 'val'));
        if ($height !== null) $rowCss['height'] = round($height, 2) . 'pt';
        $html .= '<tr style="' . sc_pdf_h(sc_pdf_css_declarations($rowCss)) . '">';
        foreach ($xpath->query('./w:tc', $row) as $cell) {
            if (!($cell instanceof DOMElement)) continue;
            $cellProperties = sc_pdf_first($xpath, $cell, './w:tcPr');
            $spanValue = sc_pdf_attr(sc_pdf_first($xpath, $cellProperties, './w:gridSpan'), 'val');
            $colspan = is_numeric($spanValue) ? max(1, (int)$spanValue) : 1;
            $cellCss = [
                'border' => sc_pdf_border_css($xpath, sc_pdf_first($xpath, $cellProperties, './w:tcBorders')) ?: $tableBorder,
                'padding' => '3pt 4pt',
                'vertical-align' => 'middle',
                'overflow-wrap' => 'anywhere',
            ];
            $shade = strtoupper(sc_pdf_attr(sc_pdf_first($xpath, $cellProperties, './w:shd'), 'fill'));
            if (preg_match('/^[0-9A-F]{6}$/', $shade) && $shade !== 'FFFFFF') $cellCss['background'] = '#' . $shade;
            $vertical = strtolower(sc_pdf_attr(sc_pdf_first($xpath, $cellProperties, './w:vAlign'), 'val'));
            if (in_array($vertical, ['top', 'center', 'bottom'], true)) $cellCss['vertical-align'] = $vertical === 'center' ? 'middle' : $vertical;
            $html .= '<td' . ($colspan > 1 ? ' colspan="' . $colspan . '"' : '') . ' style="' . sc_pdf_h(sc_pdf_css_declarations($cellCss)) . '">';
            foreach ($xpath->query('./w:p', $cell) as $paragraph) {
                if ($paragraph instanceof DOMElement) {
                    $html .= sc_pdf_render_paragraph($xpath, $paragraph, $zip, $catalog, $relationships, $assetDirectory, $assetUrlBase);
                }
            }
            $html .= '</td>';
        }
        $html .= '</tr>';
    }
    return $html . '</table>';
}

function sc_pdf_document_opening(): string
{
    return <<<'HTML'
<!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8"><style>
@page{size:A4 portrait;margin:0}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:#fff;color:#111}
body{font-family:Arial,Tahoma,"Segoe UI",sans-serif;print-color-adjust:exact;-webkit-print-color-adjust:exact}
.sc-pdf-page{position:relative;display:block;overflow:hidden;background:#fff;break-after:page;page-break-after:always}
.sc-pdf-page:last-child{break-after:auto;page-break-after:auto}
.sc-pdf-page p{min-height:1em;margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
.sc-pdf-page table{direction:rtl}
.sc-docx-tab{display:inline-block;width:12mm}
.sc-docx-image{display:inline-block;max-width:100%;vertical-align:middle}
</style></head><body>
HTML;
}

function sc_pdf_begin_document(string $htmlRelative): void
{
    $path = sc_safe_storage_path($htmlRelative);
    $directory = dirname($path);
    if (!is_dir($directory) && !@mkdir($directory, 0770, true) && !is_dir($directory)) {
        throw new RuntimeException('تعذر تجهيز مجلد PDF الجماعي.');
    }
    if (file_put_contents($path, sc_pdf_document_opening(), LOCK_EX) === false) {
        throw new RuntimeException('تعذر بدء ملف PDF الجماعي.');
    }
}

function sc_pdf_append_docx(string $docxRelative, string $htmlRelative, string $assetRelative): int
{
    $docxPath = sc_safe_storage_path($docxRelative);
    $htmlPath = sc_safe_storage_path($htmlRelative);
    $assetDirectory = sc_safe_storage_path($assetRelative);
    $assetUrlBase = rawurlencode(basename($assetRelative));
    $zip = new ZipArchive();
    if (!is_file($docxPath) || $zip->open($docxPath) !== true) throw new RuntimeException('تعذر قراءة عقد Word لإضافته إلى PDF.');
    try {
        $xml = (string)($zip->getFromName('word/document.xml') ?: '');
        if ($xml === '') throw new RuntimeException('عقد Word لا يحتوي صفحات قابلة للتصدير.');
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->preserveWhiteSpace = true;
        if (!@$dom->loadXML($xml, LIBXML_NONET | LIBXML_COMPACT)) throw new RuntimeException('تعذر تحليل صفحات عقد Word.');
        $xpath = new DOMXPath($dom);
        $xpath->registerNamespace('w', SC_PDF_W_NS);
        $xpath->registerNamespace('r', SC_PDF_R_NS);
        $layout = sc_docx_page_layout_dom($dom);
        $catalog = sc_pdf_style_catalog($zip);
        $relationships = sc_pdf_relationships($zip);
        $section = sc_pdf_first($xpath, $dom, '/w:document/w:body/w:sectPr');
        $pageSize = sc_pdf_first($xpath, $section ?? $dom->documentElement, './w:pgSz');
        $margins = sc_pdf_first($xpath, $section ?? $dom->documentElement, './w:pgMar');
        $pageWidth = sc_pdf_twips_mm(sc_pdf_attr($pageSize, 'w'), 210.0);
        $pageHeight = sc_pdf_twips_mm(sc_pdf_attr($pageSize, 'h'), 297.0);
        if (strtolower(sc_pdf_attr($pageSize, 'orient')) === 'landscape') [$pageWidth, $pageHeight] = [$pageHeight, $pageWidth];
        $top = sc_pdf_twips_mm(sc_pdf_attr($margins, 'top'), 12.7);
        $right = sc_pdf_twips_mm(sc_pdf_attr($margins, 'right'), 12.7);
        $bottom = sc_pdf_twips_mm(sc_pdf_attr($margins, 'bottom'), 12.7);
        $left = sc_pdf_twips_mm(sc_pdf_attr($margins, 'left'), 12.7);
        $pageStyle = sc_pdf_css_declarations([
            'width' => round($pageWidth, 3) . 'mm',
            'height' => round($pageHeight, 3) . 'mm',
            'padding' => round($top, 3) . 'mm ' . round($right, 3) . 'mm ' . round($bottom, 3) . 'mm ' . round($left, 3) . 'mm',
        ]);
        $pages = [];
        foreach ((array)$layout['nodes'] as $entry) {
            $node = $entry['node'] ?? null;
            if (!($node instanceof DOMElement)) continue;
            $page = max(1, (int)($entry['page'] ?? 1));
            $pages[$page] ??= '';
            if ($node->localName === 'p') {
                $pages[$page] .= sc_pdf_render_paragraph($xpath, $node, $zip, $catalog, $relationships, $assetDirectory, $assetUrlBase);
            } elseif ($node->localName === 'tbl') {
                $pages[$page] .= sc_pdf_render_table($xpath, $node, $zip, $catalog, $relationships, $assetDirectory, $assetUrlBase);
            }
        }
        if (!$pages) $pages = [1 => '<p>لا توجد بيانات قابلة للطباعة.</p>'];
        ksort($pages);
        $fragment = '';
        foreach ($pages as $content) {
            $fragment .= '<section class="sc-pdf-page" style="' . sc_pdf_h($pageStyle) . '">' . $content . '</section>' . PHP_EOL;
        }
        if (file_put_contents($htmlPath, $fragment, FILE_APPEND | LOCK_EX) === false) {
            throw new RuntimeException('تعذر إضافة العقد إلى ملف PDF الجماعي.');
        }
        return count($pages);
    } finally {
        $zip->close();
    }
}

function sc_pdf_chrome_path(): string
{
    $candidates = [
        'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
        'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
        'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
        'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
    ];
    foreach ($candidates as $candidate) if (is_file($candidate)) return $candidate;
    throw new RuntimeException('محرك PDF المحلي غير متوفر. ثبّت Chrome أو Edge ثم أعد المحاولة.');
}

function sc_pdf_remove_tree(string $directory): void
{
    if (!is_dir($directory)) return;
    $items = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    foreach ($items as $item) {
        if ($item->isDir()) @rmdir($item->getPathname()); else @unlink($item->getPathname());
    }
    @rmdir($directory);
}

function sc_pdf_finalize_document(string $htmlRelative, string $pdfRelative): void
{
    $htmlPath = sc_safe_storage_path($htmlRelative);
    $pdfPath = sc_safe_storage_path($pdfRelative);
    if (file_put_contents($htmlPath, '</body></html>', FILE_APPEND | LOCK_EX) === false) {
        throw new RuntimeException('تعذر إغلاق ملف PDF الجماعي.');
    }
    $profileDirectory = dirname($pdfPath) . DIRECTORY_SEPARATOR . '.chrome-pdf-' . bin2hex(random_bytes(4));
    if (!@mkdir($profileDirectory, 0770, true) && !is_dir($profileDirectory)) {
        throw new RuntimeException('تعذر تجهيز جلسة محرك PDF.');
    }
    $fileUrl = 'file:///' . str_replace(['\\', ' '], ['/', '%20'], $htmlPath);
    $command = [
        sc_pdf_chrome_path(),
        '--headless=new',
        '--disable-gpu',
        '--disable-extensions',
        '--no-first-run',
        '--no-default-browser-check',
        '--allow-file-access-from-files',
        '--user-data-dir=' . $profileDirectory,
        '--print-to-pdf=' . $pdfPath,
        '--no-pdf-header-footer',
        $fileUrl,
    ];
    $pipes = [];
    $process = @proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, dirname($htmlPath), null, ['bypass_shell' => true]);
    if (!is_resource($process)) {
        sc_pdf_remove_tree($profileDirectory);
        throw new RuntimeException('تعذر تشغيل محرك إنشاء PDF.');
    }
    foreach ($pipes as $pipe) stream_set_blocking($pipe, false);
    $started = microtime(true);
    $stderr = '';
    do {
        $status = proc_get_status($process);
        $stderr .= isset($pipes[2]) ? (string)stream_get_contents($pipes[2]) : '';
        if (!$status['running']) break;
        if (microtime(true) - $started > 180) {
            proc_terminate($process);
            foreach ($pipes as $pipe) fclose($pipe);
            proc_close($process);
            sc_pdf_remove_tree($profileDirectory);
            throw new RuntimeException('استغرق إنشاء PDF وقتاً أطول من الحد المسموح.');
        }
        usleep(100000);
    } while (true);
    foreach ($pipes as $pipe) fclose($pipe);
    $exitCode = proc_close($process);
    sc_pdf_remove_tree($profileDirectory);
    clearstatcache(true, $pdfPath);
    $signature = is_file($pdfPath) ? (string)file_get_contents($pdfPath, false, null, 0, 5) : '';
    if (!is_file($pdfPath) || filesize($pdfPath) < 1000 || $signature !== '%PDF-') {
        throw new RuntimeException('تعذر إنشاء ملف PDF صالح.' . ($exitCode !== 0 && $stderr !== '' ? ' ' . trim($stderr) : ''));
    }
}
