<?php
namespace App\Services;

/**
 * A minimal, dependency-free PDF writer — just enough to lay out a titled,
 * paginated data table. Written by hand (not a vendored library) so a PDF
 * download works on any cPanel host with nothing beyond stock PHP: no
 * Composer install, no extension requirement.
 *
 * Only what a tabular report needs: Helvetica/Helvetica-Bold text (the PDF
 * standard 14 fonts need no embedding), horizontal rules, and automatic page
 * breaks with a repeated header row. Not a general-purpose PDF library.
 */
class SimplePdf
{
    private const PAGE_W = 595.28; // A4 pt
    private const PAGE_H = 841.89;
    private const MARGIN = 36;

    private array $objects = [];
    private int $nextId = 1;
    private array $pageIds = [];
    private array $pageContent = [];
    private string $title = '';
    private string $subtitle = '';
    /** @var array<int,array{label:string,width:float}> */
    private array $columns = [];
    private float $y = 0;
    private int $pageIndex = -1;
    private int $rowsOnPage = 0;

    public function __construct(string $title, string $subtitle = '')
    {
        $this->title = $title;
        $this->subtitle = $subtitle;
    }

    /** @param array<int,array{label:string,width?:float}> $columns relative widths, auto-normalised */
    public function setColumns(array $columns): void
    {
        $totalWeight = array_sum(array_map(fn($c) => $c['width'] ?? 1, $columns));
        $usableWidth = self::PAGE_W - (2 * self::MARGIN);
        $this->columns = array_map(fn($c) => [
            'label' => $c['label'],
            'width' => $usableWidth * (($c['width'] ?? 1) / $totalWeight),
        ], $columns);
    }

    /** @param array<int,array<string,mixed>> $rows */
    public function render(array $rows, string $footerNote = ''): void
    {
        $this->newPage();
        $this->drawHeaderRow();

        foreach ($rows as $row) {
            if ($this->y < self::MARGIN + 40) {
                $this->newPage();
                $this->drawHeaderRow();
            }
            $this->drawDataRow(array_values($row));
        }

        if (empty($rows)) {
            $this->text(self::MARGIN, $this->y - 14, 'No data matches the current filters.', 9, false, true);
        }

        foreach ($this->pageContent as $i => $_) {
            $this->text(self::MARGIN, self::MARGIN - 14, $footerNote . ' · Page ' . ($i + 1) . ' of ' . count($this->pageContent), 7, false, false, $i);
        }
    }

    private function newPage(): void
    {
        $this->pageIndex++;
        $this->pageContent[$this->pageIndex] = '';
        $this->y = self::PAGE_H - self::MARGIN;

        $this->text(self::MARGIN, $this->y, $this->title, 15, true);
        $this->y -= 18;
        if ($this->subtitle !== '') {
            $this->text(self::MARGIN, $this->y, $this->subtitle, 9, false, true);
            $this->y -= 16;
        } else {
            $this->y -= 6;
        }
        $this->line(self::MARGIN, $this->y, self::PAGE_W - self::MARGIN, $this->y);
        $this->y -= 14;
    }

    private function drawHeaderRow(): void
    {
        $x = self::MARGIN;
        foreach ($this->columns as $col) {
            $this->text($x, $this->y, $this->truncate($col['label'], $col['width'], 8, true), 8, true);
            $x += $col['width'];
        }
        $this->y -= 5;
        $this->line(self::MARGIN, $this->y, self::PAGE_W - self::MARGIN, $this->y);
        $this->y -= 12;
    }

    private function drawDataRow(array $values): void
    {
        $x = self::MARGIN;
        foreach ($this->columns as $i => $col) {
            $val = $values[$i] ?? '';
            $this->text($x, $this->y, $this->truncate((string)$val, $col['width'], 8, false), 8, false);
            $x += $col['width'];
        }
        $this->y -= 14;
    }

    private function truncate(string $text, float $widthPt, int $fontSize, bool $bold): string
    {
        $charWidth = $fontSize * ($bold ? 0.62 : 0.52);
        $maxChars = max(3, (int)floor($widthPt / $charWidth));
        if (mb_strlen($text) <= $maxChars) return $text;
        return mb_substr($text, 0, max(0, $maxChars - 1)) . '…';
    }

    private function text(float $x, float $y, string $text, int $size, bool $bold = false, bool $gray = false, ?int $pageIndex = null): void
    {
        $font = $bold ? 'F2' : 'F1';
        $color = $gray ? '0.45 0.45 0.45 rg' : '0 0 0 rg';
        $escaped = $this->pdfString($text);
        $idx = $pageIndex ?? $this->pageIndex;
        $this->pageContent[$idx] .= "BT {$color} /{$font} {$size} Tf {$x} {$y} Td ({$escaped}) Tj ET\n0 0 0 rg\n";
    }

    private function line(float $x1, float $y1, float $x2, float $y2): void
    {
        $this->pageContent[$this->pageIndex] .= "0.75 0.75 0.75 RG 0.5 w {$x1} {$y1} m {$x2} {$y2} l S\n";
    }

    private function pdfString(string $text): string
    {
        // Standard-14 fonts only cover WinAnsi/Latin-1 — downgrade UTF-8 rather than corrupt the stream.
        $converted = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT//IGNORE', $text);
        if ($converted === false) $converted = preg_replace('/[^\x20-\x7E]/', '?', $text);
        return str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $converted);
    }

    public function output(): string
    {
        $this->objects = [];
        $this->nextId = 1;

        $fontRegularId = $this->addObject("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
        $fontBoldId = $this->addObject("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>");

        $pagesId = $this->nextId++; // reserved, filled in after pages are built
        $this->pageIds = [];
        $contentIds = [];

        foreach ($this->pageContent as $content) {
            $contentIds[] = $this->addObject(null, "stream\n{$content}endstream", strlen($content));
        }

        foreach ($this->pageContent as $i => $_) {
            $pageId = $this->addObject(
                "<< /Type /Page /Parent {$pagesId} 0 R /MediaBox [0 0 " . self::PAGE_W . ' ' . self::PAGE_H . "] " .
                "/Resources << /Font << /F1 {$fontRegularId} 0 R /F2 {$fontBoldId} 0 R >> >> " .
                "/Contents {$contentIds[$i]} 0 R >>"
            );
            $this->pageIds[] = $pageId;
        }

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

        $catalogId = $this->addObject("<< /Type /Catalog /Pages {$pagesId} 0 R >>");

        return $this->assemble($catalogId);
    }

    private function addObject(?string $dict, ?string $streamBody = null, int $streamLen = 0): int
    {
        $id = $this->nextId++;
        if ($streamBody !== null) {
            $this->objects[$id] = "<< /Length {$streamLen} >>\n{$streamBody}";
        } else {
            $this->objects[$id] = $dict;
        }
        return $id;
    }

    private function assemble(int $catalogId): string
    {
        $out = "%PDF-1.4\n";
        $offsets = [];
        foreach ($this->objects as $id => $body) {
            $offsets[$id] = strlen($out);
            $out .= "{$id} 0 obj\n{$body}\nendobj\n";
        }
        $xrefStart = strlen($out);
        $count = max($this->objects ? array_key_last($this->objects) : 0, $catalogId) + 1;

        $out .= "xref\n0 {$count}\n";
        $out .= "0000000000 65535 f \n";
        for ($id = 1; $id < $count; $id++) {
            $offset = $offsets[$id] ?? 0;
            $out .= sprintf("%010d 00000 n \n", $offset);
        }
        $out .= "trailer\n<< /Size {$count} /Root {$catalogId} 0 R >>\nstartxref\n{$xrefStart}\n%%EOF";
        return $out;
    }
}
