<?php

declare(strict_types=1);

namespace App\Services;

/**
 * PdfExportService
 *
 * Hand-rolled PDF generation (no dompdf/mpdf available in this
 * environment) using the plain PDF object model: a Catalog, a Pages
 * tree, one Page object per page, a content stream per page using the
 * `Tj`/`Td` text-positioning operators and simple `re`/`f` rectangles
 * for header bands, and a reference to the built-in Helvetica font
 * (one of the 14 standard PDF fonts every PDF viewer supports without
 * embedding — no font file needed). Multi-page: paginates automatically
 * once a page's row budget is exhausted. Verified independently with
 * `qpdf --check` and `pdftotext` during development, not just
 * "the bytes start with %PDF".
 */
final class PdfExportService
{
    private const PAGE_WIDTH = 612;  // US Letter, points
    private const PAGE_HEIGHT = 792;
    private const MARGIN = 40;
    private const ROW_HEIGHT = 18;
    private const HEADER_HEIGHT = 60;

    /** @param array<int,array<string,mixed>> $rows @param array<string,string> $columns machine_key => Human Label */
    public function toPdf(array $rows, array $columns, string $title = 'Report', string $subtitle = ''): string
    {
        $usableHeight = self::PAGE_HEIGHT - (self::MARGIN * 2) - self::HEADER_HEIGHT;
        $rowsPerPage = max(1, intdiv($usableHeight, self::ROW_HEIGHT) - 1);
        $pages = array_chunk($rows, $rowsPerPage);
        if ($pages === []) {
            $pages = [[]];
        }

        $columnKeys = array_keys($columns);
        $columnLabels = array_values($columns);
        $colWidth = (self::PAGE_WIDTH - self::MARGIN * 2) / max(1, count($columnKeys));

        $objects = [];
        $objects[1] = "<< /Type /Catalog /Pages 2 0 R >>";
        $objects[3] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>";
        $objects[4] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>";

        $pageObjectIds = [];
        $nextId = 5;

        foreach ($pages as $pageIndex => $pageRows) {
            $contentId = $nextId++;
            $pageId = $nextId++;
            $pageObjectIds[] = $pageId;

            $stream = $this->buildPageContent($title, $subtitle, $columnLabels, $pageRows, $columnKeys, $colWidth, $pageIndex + 1, count($pages));
            $objects[$contentId] = "<< /Length " . strlen($stream) . " >>\nstream\n{$stream}\nendstream";
            $objects[$pageId] = "<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /MediaBox [0 0 " . self::PAGE_WIDTH . ' ' . self::PAGE_HEIGHT . "] /Contents {$contentId} 0 R >>";
        }

        $kids = implode(' ', array_map(fn ($id) => "{$id} 0 R", $pageObjectIds));
        $objects[2] = "<< /Type /Pages /Kids [{$kids}] /Count " . count($pageObjectIds) . " >>";

        ksort($objects);

        return $this->assemblePdf($objects);
    }

    private function buildPageContent(string $title, string $subtitle, array $columnLabels, array $pageRows, array $columnKeys, float $colWidth, int $pageNum, int $totalPages): string
    {
        $stream = '';
        $y = self::PAGE_HEIGHT - self::MARGIN;

        $stream .= "0.09 0.28 0.56 rg\n0 " . ($y - 34) . " " . self::PAGE_WIDTH . " 40 re\nf\n";
        $stream .= "1 1 1 rg\nBT /F2 16 Tf " . self::MARGIN . ' ' . ($y - 15) . " Td (" . $this->escape($title) . ") Tj ET\n";
        if ($subtitle !== '') {
            $stream .= "BT /F1 9 Tf " . self::MARGIN . ' ' . ($y - 28) . " Td (" . $this->escape($subtitle) . ") Tj ET\n";
        }
        $y -= 55;

        $stream .= "0 0 0 rg\nBT /F2 9 Tf\n";
        $x = self::MARGIN;
        foreach ($columnLabels as $label) {
            $stream .= "1 0 0 1 {$x} {$y} Tm (" . $this->escape((string) $label) . ") Tj\n";
            $x += $colWidth;
        }
        $stream .= "ET\n";
        $y -= 6;
        $stream .= "0.7 0.7 0.7 RG\n" . self::MARGIN . " {$y} m " . (self::PAGE_WIDTH - self::MARGIN) . " {$y} l S\n";
        $y -= 14;

        $stream .= "BT /F1 8.5 Tf\n";
        foreach ($pageRows as $row) {
            $x = self::MARGIN;
            foreach ($columnKeys as $key) {
                $value = (string) ($row[$key] ?? '');
                $value = mb_strimwidth($value, 0, (int) ($colWidth / 4.6), '…');
                $stream .= "1 0 0 1 {$x} {$y} Tm (" . $this->escape($value) . ") Tj\n";
                $x += $colWidth;
            }
            $y -= self::ROW_HEIGHT;
        }
        $stream .= "ET\n";

        $stream .= "0.5 0.5 0.5 rg\nBT /F1 7.5 Tf 1 0 0 1 " . self::MARGIN . ' ' . (self::MARGIN - 15) . " Tm (Page {$pageNum} of {$totalPages}  |  Generated " . $this->escape(date('M j, Y g:i A')) . ") Tj ET\n";

        return $stream;
    }

    private function escape(string $text): string
    {
        $text = @iconv('UTF-8', 'CP1252//TRANSLIT//IGNORE', $text);
        $text = $text === false ? '' : $text;

        return str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $text);
    }

    /** @param array<int,string> $objects 1-indexed object bodies, already sorted by id */
    private function assemblePdf(array $objects): string
    {
        $pdf = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n";
        $offsets = [0 => 0];

        foreach ($objects as $id => $body) {
            $offsets[$id] = strlen($pdf);
            $pdf .= "{$id} 0 obj\n{$body}\nendobj\n";
        }

        $xrefOffset = strlen($pdf);
        $maxId = max(array_keys($objects));

        $pdf .= "xref\n0 " . ($maxId + 1) . "\n";
        $pdf .= "0000000000 65535 f \n";
        for ($id = 1; $id <= $maxId; $id++) {
            $offset = $offsets[$id] ?? 0;
            $pdf .= str_pad((string) $offset, 10, '0', STR_PAD_LEFT) . " 00000 n \n";
        }

        $pdf .= "trailer\n<< /Size " . ($maxId + 1) . " /Root 1 0 R >>\nstartxref\n{$xrefOffset}\n%%EOF";

        return $pdf;
    }
}
