<?php

declare(strict_types=1);

namespace App\Core;

/**
 * ZipWriter
 *
 * This server has no `zip` PHP extension (no `ZipArchive` class), so
 * .xlsx export — which is just a ZIP archive of XML files — needs a
 * hand-rolled writer. This implements the well-documented PKZIP
 * format directly: local file headers, DEFLATE-compressed entries
 * (via the zlib extension, which IS available, using raw deflate —
 * no zlib/gzip header — per the ZIP spec), a central directory, and
 * the end-of-central-directory record.
 *
 * Deliberately supports only what XlsxExportService needs: adding
 * whole strings as files. No streaming, no directories-as-entries, no
 * ZIP64 (not needed for report-sized spreadsheets).
 */
final class ZipWriter
{
    /** @var array<int,array{name:string,data:string}> */
    private array $entries = [];

    public function addFile(string $path, string $contents): void
    {
        $this->entries[] = ['name' => $path, 'data' => $contents];
    }

    public function toBinary(): string
    {
        $body = '';
        $centralDirectory = '';
        $offset = 0;

        foreach ($this->entries as $entry) {
            $name = $entry['name'];
            $data = $entry['data'];
            $crc = crc32($data);
            $uncompressedSize = strlen($data);

            $compressed = gzdeflate($data, 6);
            $compressedSize = strlen($compressed);

            $dosTime = 0; // No meaningful mtime needed for generated report files.
            $dosDate = (1 << 5) | 1; // 1980-01-01, the ZIP epoch minimum.

            $localHeader = "PK\x03\x04"
                . pack('v', 20)          // version needed to extract
                . pack('v', 0)           // general purpose flag
                . pack('v', 8)           // compression method: 8 = deflate
                . pack('v', $dosTime)
                . pack('v', $dosDate)
                . pack('V', $crc)
                . pack('V', $compressedSize)
                . pack('V', $uncompressedSize)
                . pack('v', strlen($name))
                . pack('v', 0)           // extra field length
                . $name;

            $body .= $localHeader . $compressed;

            $centralDirectory .= "PK\x01\x02"
                . pack('v', 20)          // version made by
                . pack('v', 20)          // version needed to extract
                . pack('v', 0)
                . pack('v', 8)
                . pack('v', $dosTime)
                . pack('v', $dosDate)
                . pack('V', $crc)
                . pack('V', $compressedSize)
                . pack('V', $uncompressedSize)
                . pack('v', strlen($name))
                . pack('v', 0)           // extra field length
                . pack('v', 0)           // comment length
                . pack('v', 0)           // disk number start
                . pack('v', 0)           // internal file attributes
                . pack('V', 0)           // external file attributes
                . pack('V', $offset)
                . $name;

            $offset += strlen($localHeader) + $compressedSize;
        }

        $centralDirectoryOffset = $offset;
        $endRecord = "PK\x05\x06"
            . pack('v', 0)               // disk number
            . pack('v', 0)               // disk with central directory
            . pack('v', count($this->entries))
            . pack('v', count($this->entries))
            . pack('V', strlen($centralDirectory))
            . pack('V', $centralDirectoryOffset)
            . pack('v', 0);              // comment length

        return $body . $centralDirectory . $endRecord;
    }
}
