<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Support;

/**
 * Минимальный генератор .xlsx без зависимостей (нужно только ext-zip).
 *
 * Реальный Excel-файл — это ZIP с XML-файлами внутри по схеме OOXML.
 * Этот writer пишет один лист с inline-strings, без sharedStrings.xml,
 * с минимальными стилями (жирный заголовок + формат для дат).
 *
 * Usage:
 *   $w = new XlsxWriter('Products');
 *   $w->writeRow(['ID', 'Title', 'Price'], header: true);
 *   foreach ($rows as $r) $w->writeRow([$r['id'], $r['title'], $r['price']]);
 *   $bytes = $w->finish();
 *
 * Поддерживаемые типы ячеек определяются автоматически:
 *   - int/float → число
 *   - bool      → 1/0
 *   - null      → пустая ячейка
 *   - string    → inlineStr
 *
 * Для больших выгрузок (десятки тысяч строк) держим строки в памяти —
 * для прайс-листа интернет-магазина этого достаточно. Для миллионов
 * нужен streaming-writer (план v2).
 */
final class XlsxWriter
{
    private string $sheetName;
    /** @var list<string> Накопленные XML-фрагменты <row>...</row>. */
    private array $rows = [];
    private int $rowIndex = 0;

    public function __construct(string $sheetName = 'Sheet1')
    {
        // Excel ограничивает имя листа 31 символом, без [ ] : / \ ? *.
        $clean = preg_replace('/[\\[\\]\\:\\/\\\\\\?\\*]/', '_', $sheetName) ?? 'Sheet1';
        $this->sheetName = mb_substr($clean, 0, 31, 'UTF-8') ?: 'Sheet1';
    }

    /**
     * Записывает одну строку.
     * @param list<mixed> $cells
     */
    public function writeRow(array $cells, bool $header = false): void
    {
        $this->rowIndex++;
        $xml = '<row r="' . $this->rowIndex . '">';
        $col = 0;
        foreach ($cells as $value) {
            $col++;
            $ref = $this->columnLetter($col) . $this->rowIndex;
            $xml .= $this->cellXml($ref, $value, $header);
        }
        $xml .= '</row>';
        $this->rows[] = $xml;
    }

    /** Возвращает бинарное содержимое .xlsx-файла. */
    public function finish(): string
    {
        $tmp = tempnam(sys_get_temp_dir(), 'xlsx');
        if ($tmp === false) {
            throw new \RuntimeException('Cannot create temp file for XLSX');
        }
        $zip = new \ZipArchive();
        if ($zip->open($tmp, \ZipArchive::OVERWRITE) !== true) {
            throw new \RuntimeException('Cannot open zip for writing');
        }
        $zip->addFromString('[Content_Types].xml', $this->contentTypesXml());
        $zip->addFromString('_rels/.rels',          $this->rootRelsXml());
        $zip->addFromString('xl/workbook.xml',      $this->workbookXml());
        $zip->addFromString('xl/_rels/workbook.xml.rels', $this->workbookRelsXml());
        $zip->addFromString('xl/styles.xml',        $this->stylesXml());
        $zip->addFromString('xl/worksheets/sheet1.xml', $this->sheetXml());
        $zip->close();
        $bytes = (string) file_get_contents($tmp);
        @unlink($tmp);
        return $bytes;
    }

    private function cellXml(string $ref, mixed $value, bool $header): string
    {
        $styleAttr = $header ? ' s="1"' : '';
        if ($value === null || $value === '') {
            return '<c r="' . $ref . '"' . $styleAttr . '/>';
        }
        if (is_bool($value)) {
            return '<c r="' . $ref . '" t="b"' . $styleAttr . '><v>' . ($value ? '1' : '0') . '</v></c>';
        }
        if (is_int($value) || (is_float($value) && is_finite($value))) {
            return '<c r="' . $ref . '" t="n"' . $styleAttr . '><v>'
                . (is_float($value) ? rtrim(rtrim(number_format($value, 6, '.', ''), '0'), '.') : (string) $value)
                . '</v></c>';
        }
        // Числовая строка — сохраняем как число, чтобы в Excel были суммы/фильтры по числам.
        $s = (string) $value;
        if (preg_match('/^-?\d+(\.\d+)?$/', $s) === 1 && strlen($s) <= 15) {
            return '<c r="' . $ref . '" t="n"' . $styleAttr . '><v>' . $s . '</v></c>';
        }
        return '<c r="' . $ref . '" t="inlineStr"' . $styleAttr . '><is><t xml:space="preserve">'
            . $this->escape($s) . '</t></is></c>';
    }

    /** A, B, …, Z, AA, AB, … */
    private function columnLetter(int $n): string
    {
        $s = '';
        while ($n > 0) {
            $n--;
            $s = chr(65 + ($n % 26)) . $s;
            $n = intdiv($n, 26);
        }
        return $s;
    }

    private function escape(string $s): string
    {
        // Убираем управляющие символы, недопустимые в XML 1.0.
        $s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $s) ?? $s;
        return htmlspecialchars($s, ENT_XML1 | ENT_QUOTES, 'UTF-8');
    }

    private function sheetXml(): string
    {
        $rowsXml = implode('', $this->rows);
        return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            . '<sheetData>' . $rowsXml . '</sheetData>'
            . '</worksheet>';
    }

    private function workbookXml(): string
    {
        return '<?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">'
            . '<sheets><sheet name="' . $this->escape($this->sheetName) . '" sheetId="1" r:id="rId1"/></sheets>'
            . '</workbook>';
    }

    private function workbookRelsXml(): string
    {
        return '<?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/styles" Target="styles.xml"/>'
            . '</Relationships>';
    }

    private function rootRelsXml(): string
    {
        return '<?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"/>'
            . '</Relationships>';
    }

    private function contentTypesXml(): string
    {
        return '<?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/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
            . '</Types>';
    }

    /**
     * Минимальные стили: 0 — обычный, 1 — bold (для заголовка).
     * Без них Excel показывает «file repaired», поэтому нужен валидный styles.xml.
     */
    private function stylesXml(): string
    {
        return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            . '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            . '<fonts count="2">'
            .   '<font><sz val="11"/><name val="Calibri"/></font>'
            .   '<font><b/><sz val="11"/><name val="Calibri"/></font>'
            . '</fonts>'
            . '<fills count="1"><fill><patternFill patternType="none"/></fill></fills>'
            . '<borders count="1"><border/></borders>'
            . '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0"/></cellStyleXfs>'
            . '<cellXfs count="2">'
            .   '<xf numFmtId="0" fontId="0" xfId="0"/>'
            .   '<xf numFmtId="0" fontId="1" xfId="0" applyFont="1"/>'
            . '</cellXfs>'
            . '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
            . '</styleSheet>';
    }
}
