<?php

declare(strict_types=1);

/**
 * Minimal OOXML workbook writer used only for the equipment export.
 * The project has no Composer/vendor runtime. Packaging prefers ZipArchive
 * when available and falls back to the standard PharData ZIP writer.
 */

function xlsx_xml_text($value): string
{
    $text = str_replace("\0", '', (string)($value ?? ''));
    $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $text) ?? '';
    return htmlspecialchars($text, ENT_QUOTES | ENT_XML1, 'UTF-8');
}

function xlsx_cell_inline(string $ref, $value, int $style = 0): string
{
    // inlineStr forces text semantics, protecting identifiers and values that
    // begin with = + - @ from Excel formula interpretation.
    $text = xlsx_xml_text($value);
    return '<c r="' . $ref . '" s="' . $style . '" t="inlineStr"><is><t xml:space="preserve">' . $text . '</t></is></c>';
}

function xlsx_cell_number(string $ref, float|int $value, int $style = 0): string
{
    return '<c r="' . $ref . '" s="' . $style . '"><v>' . rtrim(rtrim(number_format((float)$value, 10, '.', ''), '0'), '.') . '</v></c>';
}

function xlsx_excel_serial(?string $value): ?float
{
    $value = trim((string)$value);
    if ($value === '') {
        return null;
    }
    try {
        $dt = new DateTimeImmutable($value);
    } catch (Throwable $e) {
        return null;
    }
    $utc = new DateTimeZone('UTC');
    $normalized = new DateTimeImmutable($dt->format('Y-m-d H:i:s'), $utc);
    $base = new DateTimeImmutable('1899-12-30 00:00:00', $utc);
    return ($normalized->getTimestamp() - $base->getTimestamp()) / 86400;
}

function xlsx_status_style(string $status): int
{
    return match ($status) {
        'assigned' => 6,
        'repair' => 7,
        'returned' => 8,
        'retired' => 9,
        default => 4,
    };
}

function xlsx_styles_xml(): string
{
    return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
        . '<numFmts count="2">'
        . '<numFmt numFmtId="164" formatCode="dd.mm.yyyy."/>'
        . '<numFmt numFmtId="165" formatCode="dd.mm.yyyy. hh:mm"/>'
        . '</numFmts>'
        . '<fonts count="8">'
        . '<font><sz val="11"/><name val="Calibri"/><family val="2"/></font>'
        . '<font><b/><sz val="16"/><color rgb="FFFFFFFF"/><name val="Calibri"/><family val="2"/></font>'
        . '<font><i/><sz val="11"/><name val="Calibri"/><family val="2"/></font>'
        . '<font><b/><sz val="11"/><color rgb="FFFFFFFF"/><name val="Calibri"/><family val="2"/></font>'
        . '<font><sz val="11"/><color rgb="FF274E13"/><name val="Calibri"/></font>'
        . '<font><sz val="11"/><color rgb="FF9C0006"/><name val="Calibri"/></font>'
        . '<font><sz val="11"/><color rgb="FF666666"/><name val="Calibri"/></font>'
        . '<font><sz val="11"/><color rgb="FF9C5700"/><name val="Calibri"/></font>'
        . '</fonts>'
        . '<fills count="10">'
        . '<fill><patternFill patternType="none"/></fill>'
        . '<fill><patternFill patternType="gray125"/></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FF1F4E78"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FFD9EAF7"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FF4472C4"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FFEAF5FB"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FFD9EAD3"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FFF4CCCC"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FFE7E6E6"/><bgColor indexed="64"/></patternFill></fill>'
        . '<fill><patternFill patternType="solid"><fgColor rgb="FFFCE4D6"/><bgColor indexed="64"/></patternFill></fill>'
        . '</fills>'
        . '<borders count="2">'
        . '<border><left/><right/><top/><bottom/><diagonal/></border>'
        . '<border><left style="thin"><color rgb="FFD9E7F0"/></left><right style="thin"><color rgb="FFD9E7F0"/></right><top style="thin"><color rgb="FFD9E7F0"/></top><bottom style="thin"><color rgb="FFD9E7F0"/></bottom><diagonal/></border>'
        . '</borders>'
        . '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
        . '<cellXfs count="14">'
        . '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
        . '<xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="center" vertical="center"/></xf>'
        . '<xf numFmtId="0" fontId="2" fillId="3" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="left" vertical="center"/></xf>'
        . '<xf numFmtId="0" fontId="3" fillId="4" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>'
        . '<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
        . '<xf numFmtId="0" fontId="0" fillId="5" borderId="1" xfId="0" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
        . '<xf numFmtId="0" fontId="4" fillId="6" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
        . '<xf numFmtId="0" fontId="5" fillId="7" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
        . '<xf numFmtId="0" fontId="6" fillId="8" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
        . '<xf numFmtId="0" fontId="7" fillId="9" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
        . '<xf numFmtId="164" fontId="0" fillId="0" borderId="1" xfId="0" applyNumberFormat="1" applyBorder="1" applyAlignment="1"><alignment vertical="top"/></xf>'
        . '<xf numFmtId="165" fontId="0" fillId="0" borderId="1" xfId="0" applyNumberFormat="1" applyBorder="1" applyAlignment="1"><alignment vertical="top"/></xf>'
        . '<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1" applyAlignment="1"><alignment horizontal="right" vertical="center"/></xf>'
        . '<xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="center" vertical="center"/></xf>'
        . '</cellXfs>'
        . '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
        . '</styleSheet>';
}

function xlsx_equipment_table_sheet_xml(array $rows, DateTimeImmutable $generatedAt, string $title, string $subtitle, bool $warehouseSheet = false): string
{
    $lastRow = max(4, 4 + count($rows));
    $columnWidths = $warehouseSheet
        ? [22, 30, 27, 18, 18, 25, 19, 20, 17, 16, 32, 36, 20]
        : [24, 30, 30, 18, 16, 28, 20, 22, 16, 17, 32, 42, 22];

    $cols = '';
    foreach ($columnWidths as $index => $width) {
        $column = $index + 1;
        $cols .= '<col min="' . $column . '" max="' . $column . '" width="' . $width . '" customWidth="1"/>';
    }

    $xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
        . '<dimension ref="A1:M' . $lastRow . '"/>'
        . '<sheetViews><sheetView workbookViewId="0"><pane ySplit="4" topLeftCell="A5" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>'
        . '<sheetFormatPr defaultRowHeight="18"/>'
        . '<cols>' . $cols . '</cols><sheetData>';

    $xml .= '<row r="1" ht="28" customHeight="1">' . xlsx_cell_inline('A1', $title, 1) . '</row>';
    $xml .= '<row r="2" ht="' . ($warehouseSheet ? '32' : '22') . '" customHeight="1">' . xlsx_cell_inline('A2', $subtitle, 2) . '</row>';
    $xml .= '<row r="3" ht="10" customHeight="1"></row>';

    $headers = ['Korisnik','Sektor','Lokacija','Pravno lice','Tip opreme','Naziv opreme','Inventarski broj','Serijski broj','Status','Datum dodele','Email','Napomena','Ažurirano'];
    $letters = range('A', 'M');
    $headerCells = '';
    foreach ($headers as $i => $header) {
        $headerCells .= xlsx_cell_inline($letters[$i] . '4', $header, 3);
    }
    $xml .= '<row r="4" ht="26" customHeight="1">' . $headerCells . '</row>';

    foreach ($rows as $index => $row) {
        $r = 5 + $index;
        $bodyStyle = ($index % 2 === 1) ? 5 : 4;
        $cells = '';
        $cells .= xlsx_cell_inline('A' . $r, $row['user_name'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('B' . $r, $row['user_department'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('C' . $r, $row['user_location'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('D' . $r, equipment_organization_unit_label($row['organization_unit'] ?? null), $bodyStyle);
        $cells .= xlsx_cell_inline('E' . $r, $row['asset_type'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('F' . $r, $row['asset_name'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('G' . $r, $row['inventory_no'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('H' . $r, $row['serial_no'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('I' . $r, equipment_status_label((string)($row['status'] ?? '')), xlsx_status_style((string)($row['status'] ?? '')));
        $assignedSerial = xlsx_excel_serial($row['assigned_at'] ?? null);
        $cells .= $assignedSerial !== null ? xlsx_cell_number('J' . $r, $assignedSerial, 10) : xlsx_cell_inline('J' . $r, '', $bodyStyle);
        $cells .= xlsx_cell_inline('K' . $r, $row['user_email'] ?? '', $bodyStyle);
        $cells .= xlsx_cell_inline('L' . $r, $row['notes'] ?? '', $bodyStyle);
        $updatedSerial = xlsx_excel_serial($row['updated_at'] ?? null);
        $cells .= $updatedSerial !== null ? xlsx_cell_number('M' . $r, $updatedSerial, 11) : xlsx_cell_inline('M' . $r, '', $bodyStyle);
        $xml .= '<row r="' . $r . '" ht="30" customHeight="1">' . $cells . '</row>';
    }

    $xml .= '</sheetData>'
        . '<autoFilter ref="A4:M' . $lastRow . '"/>'
        . '<mergeCells count="2"><mergeCell ref="A1:M1"/><mergeCell ref="A2:M2"/></mergeCells>'
        . '<pageMargins left="0.3" right="0.3" top="0.5" bottom="0.5" header="0.3" footer="0.3"/>'
        . '</worksheet>';
    return $xml;
}

function xlsx_equipment_sheet_xml(array $rows, DateTimeImmutable $generatedAt): string
{
    $subtitle = 'Stanje na dan ' . $generatedAt->format('d.m.Y.') . ' | Ukupno evidentirano: ' . count($rows) . ' stavki';
    return xlsx_equipment_table_sheet_xml($rows, $generatedAt, 'PREGLED ICT OPREME', $subtitle, false);
}

function xlsx_equipment_warehouse_xml(array $rows, DateTimeImmutable $generatedAt): string
{
    $subtitle = 'Stanje na dan ' . $generatedAt->format('d.m.Y.')
        . ' | Evidencija opreme na lageru | Za nedodeljenu opremu polja Korisnik, Sektor, Email i Datum dodele ostaju prazna';
    return xlsx_equipment_table_sheet_xml($rows, $generatedAt, 'MAGACIN ICT OPREME', $subtitle, true);
}

function xlsx_equipment_summary_xml(array $rows): string
{
    $typeCounts = [];
    $statusCounts = [];
    $locationCounts = [];
    $warehouseCount = 0;
    foreach ($rows as $row) {
        $type = equipment_summary_type((string)($row['asset_type'] ?? ''));
        $typeCounts[$type] = ($typeCounts[$type] ?? 0) + 1;
        $status = equipment_status_label((string)($row['status'] ?? ''));
        $statusCounts[$status] = ($statusCounts[$status] ?? 0) + 1;
        if ((int)($row['is_in_warehouse'] ?? 0) === 1) {
            $warehouseCount++;
        }
        $location = trim((string)($row['user_location'] ?? ''));
        if ($location === '') {
            $location = 'Nije navedeno';
        }
        $locationCounts[$location] = ($locationCounts[$location] ?? 0) + 1;
    }

    $orderedTypes = [];
    foreach (['Laptop','Telefon','Monitor','Tablet','Ostalo'] as $type) {
        if (isset($typeCounts[$type])) {
            $orderedTypes[$type] = $typeCounts[$type];
        }
    }
    $orderedStatuses = [];
    foreach (equipment_statuses() as $statusKey => $label) {
        if (isset($statusCounts[$label])) {
            $orderedStatuses[$label] = $statusCounts[$label];
        }
    }
    ksort($locationCounts, SORT_NATURAL | SORT_FLAG_CASE);

    // Jedan row element po row broju. Pre v3.24.0 type/status petlje su
    // emitovale duple <row r="4">/<row r="5"> elemente, što Excel popravlja.
    $rowsByNumber = [];
    $rowsByNumber[1] = ['attrs' => ' ht="28" customHeight="1"', 'cells' => xlsx_cell_inline('A1', 'SAŽETAK EVIDENCIJE OPREME', 13)];
    $rowsByNumber[2] = ['attrs' => ' ht="10" customHeight="1"', 'cells' => ''];
    $rowsByNumber[3] = [
        'attrs' => ' ht="24" customHeight="1"',
        'cells' => xlsx_cell_inline('A3', 'Tip opreme', 3)
            . xlsx_cell_inline('B3', 'Broj', 3)
            . xlsx_cell_inline('D3', 'Status', 3)
            . xlsx_cell_inline('E3', 'Broj', 3),
    ];

    $typeRow = 4;
    foreach ($orderedTypes as $label => $count) {
        $rowsByNumber[$typeRow]['cells'] = ($rowsByNumber[$typeRow]['cells'] ?? '')
            . xlsx_cell_inline('A' . $typeRow, $label, 4)
            . xlsx_cell_number('B' . $typeRow, $count, 12);
        $typeRow++;
    }

    $statusRow = 4;
    foreach ($orderedStatuses as $label => $count) {
        $rowsByNumber[$statusRow]['cells'] = ($rowsByNumber[$statusRow]['cells'] ?? '')
            . xlsx_cell_inline('D' . $statusRow, $label, 4)
            . xlsx_cell_number('E' . $statusRow, $count, 12);
        $statusRow++;
    }

    $warehouseRow = max(4, $statusRow + 1);
    $rowsByNumber[$warehouseRow]['cells'] = ($rowsByNumber[$warehouseRow]['cells'] ?? '')
        . xlsx_cell_inline('D' . $warehouseRow, 'Magacin', 4)
        . xlsx_cell_number('E' . $warehouseRow, $warehouseCount, 12);

    $maxTopRows = max(count($orderedTypes), count($orderedStatuses) + 2);
    $locationHeaderRow = max(10, 4 + $maxTopRows + 3, $warehouseRow + 3);
    $rowsByNumber[$locationHeaderRow] = [
        'attrs' => ' ht="24" customHeight="1"',
        'cells' => xlsx_cell_inline('A' . $locationHeaderRow, 'Lokacija', 3)
            . xlsx_cell_inline('B' . $locationHeaderRow, 'Broj', 3),
    ];
    $locationRow = $locationHeaderRow + 1;
    foreach ($locationCounts as $label => $count) {
        $rowsByNumber[$locationRow]['cells'] = xlsx_cell_inline('A' . $locationRow, $label, 4)
            . xlsx_cell_number('B' . $locationRow, $count, 12);
        $locationRow++;
    }

    ksort($rowsByNumber, SORT_NUMERIC);
    $lastRow = max(array_keys($rowsByNumber));
    $sheetRowsXml = '';
    foreach ($rowsByNumber as $rowNo => $rowData) {
        $attrs = (string)($rowData['attrs'] ?? '');
        $cells = (string)($rowData['cells'] ?? '');
        $sheetRowsXml .= '<row r="' . $rowNo . '"' . $attrs . '>' . $cells . '</row>';
    }

    return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
        . '<dimension ref="A1:E' . max(1, $lastRow) . '"/>'
        . '<sheetViews><sheetView workbookViewId="0"/></sheetViews>'
        . '<sheetFormatPr defaultRowHeight="18"/>'
        . '<cols><col min="1" max="1" width="36" customWidth="1"/><col min="2" max="2" width="12" customWidth="1"/><col min="3" max="3" width="5" customWidth="1"/><col min="4" max="4" width="28" customWidth="1"/><col min="5" max="5" width="12" customWidth="1"/></cols>'
        . '<sheetData>' . $sheetRowsXml . '</sheetData>'
        . '<mergeCells count="1"><mergeCell ref="A1:E1"/></mergeCells>'
        . '<pageMargins left="0.4" right="0.4" top="0.5" bottom="0.5" header="0.3" footer="0.3"/>'
        . '</worksheet>';
}

function xlsx_column_index(string $letters): int
{
    $index = 0;
    foreach (str_split($letters) as $letter) {
        $index = ($index * 26) + (ord($letter) - 64);
    }
    return $index;
}

function xlsx_validate_worksheet_xml(string $name, string $xml, int $maxStyleIndex = 13): void
{
    if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $xml)) {
        throw new RuntimeException('XLSX sadrži nedozvoljeni XML kontrolni karakter u ' . $name . '.');
    }
    if (preg_match('/<v>\s*(?:NaN|Infinity|-Infinity|undefined|null)\s*<\/v>/i', $xml)) {
        throw new RuntimeException('XLSX sadrži nevalidnu numeričku vrednost u ' . $name . '.');
    }
    if (class_exists('DOMDocument')) {
        $previous = libxml_use_internal_errors(true);
        $dom = new DOMDocument();
        $ok = $dom->loadXML($xml, LIBXML_NONET | LIBXML_NOBLANKS);
        libxml_clear_errors();
        libxml_use_internal_errors($previous);
        if (!$ok) {
            throw new RuntimeException('XLSX worksheet XML nije parsabilan: ' . $name . '.');
        }
    }

    preg_match_all('/<row\b([^>]*)>(.*?)<\/row>/s', $xml, $rowMatches, PREG_SET_ORDER);
    $seenRows = [];
    $seenCells = [];
    $previousRow = 0;
    foreach ($rowMatches as $rowMatch) {
        if (!preg_match('/\br="(\d+)"/', $rowMatch[1], $rowRef)) {
            throw new RuntimeException('XLSX row nema validan r atribut u ' . $name . '.');
        }
        $rowNo = (int)$rowRef[1];
        if ($rowNo <= 0 || isset($seenRows[$rowNo]) || $rowNo <= $previousRow) {
            throw new RuntimeException('XLSX sadrži dupliran ili neuređen row r="' . $rowNo . '" u ' . $name . '.');
        }
        $seenRows[$rowNo] = true;
        $previousRow = $rowNo;

        preg_match_all('/<c\b([^>]*)\br="([A-Z]+)(\d+)"([^>]*)>/', $rowMatch[2], $cellMatches, PREG_SET_ORDER);
        $previousColumn = 0;
        foreach ($cellMatches as $cellMatch) {
            $cellRef = $cellMatch[2] . $cellMatch[3];
            $cellRow = (int)$cellMatch[3];
            $columnIndex = xlsx_column_index($cellMatch[2]);
            if ($cellRow !== $rowNo) {
                throw new RuntimeException('XLSX cell ' . $cellRef . ' nije u odgovarajućem row elementu u ' . $name . '.');
            }
            if (isset($seenCells[$cellRef]) || $columnIndex <= $previousColumn) {
                throw new RuntimeException('XLSX sadrži dupliranu ili neuređenu ćeliju ' . $cellRef . ' u ' . $name . '.');
            }
            $seenCells[$cellRef] = true;
            $previousColumn = $columnIndex;
            $attrs = $cellMatch[1] . ' ' . $cellMatch[4];
            if (preg_match('/\bs="(\d+)"/', $attrs, $styleMatch) && (int)$styleMatch[1] > $maxStyleIndex) {
                throw new RuntimeException('XLSX ćelija ' . $cellRef . ' koristi nepostojeći style index u ' . $name . '.');
            }
        }
    }
}

function xlsx_validate_package_files(array $files): void
{
    $required = ['[Content_Types].xml', '_rels/.rels', 'xl/workbook.xml', 'xl/_rels/workbook.xml.rels', 'xl/styles.xml', 'xl/worksheets/sheet1.xml', 'xl/worksheets/sheet2.xml'];
    foreach ($required as $part) {
        if (!isset($files[$part]) || $files[$part] === '') {
            throw new RuntimeException('XLSX nema obavezan OpenXML deo: ' . $part . '.');
        }
    }
    foreach ($files as $name => $content) {
        if (str_starts_with($name, 'xl/worksheets/')) {
            xlsx_validate_worksheet_xml($name, $content);
        }
    }
}

function xlsx_validate_zip_package(string $zipPath, array $requiredParts): void
{
    if (!is_file($zipPath) || filesize($zipPath) <= 0) {
        throw new RuntimeException('XLSX ZIP paket nije kreiran.');
    }

    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('XLSX ZIP paket nije moguće ponovo otvoriti.');
        }
        foreach ($requiredParts as $part) {
            if ($zip->locateName($part) === false) {
                $zip->close();
                throw new RuntimeException('XLSX ZIP paket nema deo: ' . $part . '.');
            }
        }
        $zip->close();
        return;
    }

    if (class_exists('PharData')) {
        $phar = new PharData($zipPath);
        foreach ($requiredParts as $part) {
            if (!isset($phar[$part])) {
                throw new RuntimeException('XLSX ZIP paket nema deo: ' . $part . '.');
            }
        }
        unset($phar);
        return;
    }

    throw new RuntimeException('Nije dostupna ZIP validacija za XLSX paket.');
}

function xlsx_package_files(array $files, string $xlsxPath): void
{
    xlsx_validate_package_files($files);

    $zipPath = $xlsxPath . '.zip';
    @unlink($zipPath);
    @unlink($xlsxPath);

    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new RuntimeException('Nije moguće kreirati XLSX arhivu.');
        }
        foreach ($files as $name => $content) {
            $zip->addFromString($name, $content);
        }
        $zip->close();
    } elseif (class_exists('PharData')) {
        $phar = new PharData($zipPath, 0, null, Phar::ZIP);
        foreach ($files as $name => $content) {
            $phar[$name] = $content;
        }
        unset($phar);
    } else {
        throw new RuntimeException('PHP nema ZipArchive ni PharData podršku potrebnu za XLSX export.');
    }

    xlsx_validate_zip_package($zipPath, array_keys($files));
    if (!@rename($zipPath, $xlsxPath)) {
        @unlink($zipPath);
        throw new RuntimeException('XLSX fajl nije mogao biti finalizovan.');
    }
}

function create_equipment_xlsx(array $rows, DateTimeImmutable $generatedAt): string
{
    $token = bin2hex(random_bytes(8));
    $xlsxPath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'sos-pregled-opreme-' . $token . '.xlsx';
    $iso = gmdate('Y-m-d\\TH:i:s\\Z');
    $warehouseRows = array_values(array_filter($rows, static fn(array $row): bool => (int)($row['is_in_warehouse'] ?? 0) === 1));
    $hasWarehouse = count($warehouseRows) > 0;
    $sheetCount = $hasWarehouse ? 3 : 2;

    $contentTypes = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
    if ($hasWarehouse) {
        $contentTypes .= '<Override PartName="/xl/worksheets/sheet3.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
    }
    $contentTypes .= '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>';

    $titleParts = '<vt:lpstr>Pregled opreme</vt:lpstr><vt:lpstr>Sažetak</vt:lpstr>' . ($hasWarehouse ? '<vt:lpstr>Magacin</vt:lpstr>' : '');
    $appXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>SOS Infrastruktura</Application><TitlesOfParts><vt:vector size="' . $sheetCount . '" baseType="lpstr">' . $titleParts . '</vt:vector></TitlesOfParts></Properties>';

    $workbookSheets = '<sheet name="Pregled opreme" sheetId="1" r:id="rId1"/><sheet name="Sažetak" sheetId="2" r:id="rId2"/>' . ($hasWarehouse ? '<sheet name="Magacin" sheetId="3" r:id="rId3"/>' : '');
    $workbookXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><bookViews><workbookView xWindow="0" yWindow="0" windowWidth="24000" windowHeight="14000"/></bookViews><sheets>' . $workbookSheets . '</sheets></workbook>';

    $workbookRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>';
    if ($hasWarehouse) {
        $workbookRels .= '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet3.xml"/><Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';
    } else {
        $workbookRels .= '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';
    }
    $workbookRels .= '</Relationships>';

    $files = [
        '[Content_Types].xml' => $contentTypes,
        '_rels/.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>',
        'docProps/core.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>SOS pregled ICT opreme</dc:title><dc:creator>SOS Infrastruktura</dc:creator><dcterms:created xsi:type="dcterms:W3CDTF">' . $iso . '</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">' . $iso . '</dcterms:modified></cp:coreProperties>',
        'docProps/app.xml' => $appXml,
        'xl/workbook.xml' => $workbookXml,
        'xl/_rels/workbook.xml.rels' => $workbookRels,
        'xl/styles.xml' => xlsx_styles_xml(),
        'xl/worksheets/sheet1.xml' => xlsx_equipment_sheet_xml($rows, $generatedAt),
        'xl/worksheets/sheet2.xml' => xlsx_equipment_summary_xml($rows),
    ];
    if ($hasWarehouse) {
        $files['xl/worksheets/sheet3.xml'] = xlsx_equipment_warehouse_xml($warehouseRows, $generatedAt);
    }

    xlsx_package_files($files, $xlsxPath);
    return $xlsxPath;
}
