<?php
namespace App\Services;

use App\Core\Dates;
use App\Core\Num;
use App\Core\Settings;

/**
 * Generator PDF tanpa dependensi eksternal.
 *
 * Menghasilkan PDF 1.4 yang valid dengan font inti (Helvetica) sehingga tidak
 * memerlukan Dompdf maupun wkhtmltopdf. Dirancang khusus untuk laporan tabel:
 *  - kop laporan (nama perusahaan, judul, periode, filter, dibuat oleh/tanggal)
 *  - tabel dengan header berulang di setiap halaman
 *  - baris subtotal & grand total
 *  - orientasi portrait/landscape, nomor halaman "Halaman X dari Y"
 *  - blok tanda tangan
 *  - format Rupiah dan tanggal Indonesia
 *
 * Bila Dompdf tersedia melalui Composer, ExportService akan memilihnya untuk
 * keluaran HTML kompleks; untuk laporan tabel kelas ini lebih cepat & ringan.
 */
class PdfWriter
{
    private array $pages = [];
    private string $buf = '';
    private float $w;
    private float $h;
    private float $mLeft = 28.0;
    private float $mRight = 28.0;
    private float $mTop = 34.0;
    private float $mBottom = 44.0;
    private float $y;
    private float $fontSize = 8;
    private string $orientation;

    private array $header = [];
    private array $columns = [];
    private bool $repeatHeader = true;
    private array $signature = [];

    // Lebar karakter Helvetica (per 1000 unit em) — cukup akurat untuk tabel
    private const WIDTHS = [
        32=>278,33=>278,34=>355,35=>556,36=>556,37=>889,38=>667,39=>191,40=>333,41=>333,42=>389,43=>584,
        44=>278,45=>333,46=>278,47=>278,48=>556,49=>556,50=>556,51=>556,52=>556,53=>556,54=>556,55=>556,
        56=>556,57=>556,58=>278,59=>278,60=>584,61=>584,62=>584,63=>556,64=>1015,65=>667,66=>667,67=>722,
        68=>722,69=>667,70=>611,71=>778,72=>722,73=>278,74=>500,75=>667,76=>556,77=>833,78=>722,79=>778,
        80=>667,81=>778,82=>722,83=>667,84=>611,85=>722,86=>667,87=>944,88=>667,89=>667,90=>611,91=>278,
        92=>278,93=>278,94=>469,95=>556,96=>333,97=>556,98=>556,99=>500,100=>556,101=>556,102=>278,103=>556,
        104=>556,105=>222,106=>222,107=>500,108=>222,109=>833,110=>556,111=>556,112=>556,113=>556,114=>333,
        115=>500,116=>278,117=>556,118=>500,119=>722,120=>500,121=>500,122=>500,123=>334,124=>260,125=>334,126=>584,
    ];

    public function __construct(string $orientation = 'portrait', string $size = 'A4')
    {
        $sizes = ['A4' => [595.28, 841.89], 'A3' => [841.89, 1190.55], 'LETTER' => [612.0, 792.0], 'LEGAL' => [612.0, 1008.0]];
        [$pw, $ph] = $sizes[strtoupper($size)] ?? $sizes['A4'];
        $this->orientation = strtolower($orientation) === 'landscape' ? 'landscape' : 'portrait';
        if ($this->orientation === 'landscape') { $this->w = $ph; $this->h = $pw; }
        else { $this->w = $pw; $this->h = $ph; }
        $this->y = $this->h - $this->mTop;
    }

    public function setSignature(array $sig): void { $this->signature = $sig; }

    // ------------------------------------------------------------------ primitif
    private function esc(string $s): string
    {
        // PDF WinAnsi: konversi UTF-8 -> CP1252, ganti karakter tak terwakili
        $s = str_replace(['–', '—', '’', '‘', '“', '”', '…', '•', '≤', '≥', '×', '→'],
                         ['-', '-', "'", "'", '"', '"', '...', '-', '<=', '>=', 'x', '->'], $s);
        $conv = @iconv('UTF-8', 'CP1252//TRANSLIT', $s);
        if ($conv === false) $conv = preg_replace('/[^\x20-\x7E]/', '?', $s) ?? $s;
        return str_replace(['\\', '(', ')', "\r", "\n"], ['\\\\', '\\(', '\\)', '', ' '], $conv);
    }

    private function textWidth(string $s, ?float $size = null): float
    {
        $size = $size ?? $this->fontSize;
        $conv = @iconv('UTF-8', 'CP1252//TRANSLIT', $s);
        if ($conv === false) $conv = $s;
        $w = 0;
        for ($i = 0, $n = strlen($conv); $i < $n; $i++) {
            $w += self::WIDTHS[ord($conv[$i])] ?? 556;
        }
        return $w * $size / 1000;
    }

    private function fit(string $s, float $maxW, ?float $size = null): string
    {
        if ($this->textWidth($s, $size) <= $maxW) return $s;
        $ell = '..';
        while ($s !== '' && $this->textWidth($s . $ell, $size) > $maxW) $s = mb_substr($s, 0, mb_strlen($s) - 1);
        return $s . $ell;
    }

    private function out(string $s): void { $this->buf .= $s . "\n"; }

    private function font(string $style = '', ?float $size = null): void
    {
        $size = $size ?? $this->fontSize;
        $f = match ($style) { 'B' => '/F2', 'I' => '/F3', 'BI' => '/F4', default => '/F1' };
        $this->out(sprintf('BT %s %.2F Tf ET', $f, $size));
        $this->currentFont = $f;
        $this->currentSize = $size;
    }
    private string $currentFont = '/F1';
    private float $currentSize = 8;

    private function text(float $x, float $y, string $s, string $style = '', ?float $size = null, array $rgb = [0, 0, 0]): void
    {
        if ($s === '') return;
        $size = $size ?? $this->fontSize;
        $f = match ($style) { 'B' => '/F2', 'I' => '/F3', 'BI' => '/F4', default => '/F1' };
        $c = sprintf('%.3f %.3f %.3f rg', $rgb[0] / 255, $rgb[1] / 255, $rgb[2] / 255);
        $this->out(sprintf('%s BT %s %.2F Tf %.2f %.2f Td (%s) Tj ET 0 0 0 rg', $c, $f, $size, $x, $y, $this->esc($s)));
    }

    private function textRight(float $xRight, float $y, string $s, string $style = '', ?float $size = null, array $rgb = [0, 0, 0]): void
    {
        $this->text($xRight - $this->textWidth($s, $size ?? $this->fontSize), $y, $s, $style, $size, $rgb);
    }

    private function rect(float $x, float $y, float $w, float $h, array $fill, ?array $stroke = null): void
    {
        $this->out(sprintf('%.3f %.3f %.3f rg', $fill[0] / 255, $fill[1] / 255, $fill[2] / 255));
        if ($stroke) {
            $this->out(sprintf('%.3f %.3f %.3f RG 0.4 w', $stroke[0] / 255, $stroke[1] / 255, $stroke[2] / 255));
            $this->out(sprintf('%.2f %.2f %.2f %.2f re B', $x, $y, $w, $h));
        } else {
            $this->out(sprintf('%.2f %.2f %.2f %.2f re f', $x, $y, $w, $h));
        }
        $this->out('0 0 0 rg 0 0 0 RG');
    }

    private function line(float $x1, float $y1, float $x2, float $y2, array $rgb = [190, 190, 190], float $lw = 0.4): void
    {
        $this->out(sprintf('%.3f %.3f %.3f RG %.2f w %.2f %.2f m %.2f %.2f l S 0 0 0 RG',
            $rgb[0] / 255, $rgb[1] / 255, $rgb[2] / 255, $lw, $x1, $y1, $x2, $y2));
    }

    private function contentWidth(): float { return $this->w - $this->mLeft - $this->mRight; }

    // ------------------------------------------------------------------ halaman
    private function newPage(): void
    {
        if ($this->buf !== '') $this->pages[] = $this->buf;
        $this->buf = '';
        $this->y = $this->h - $this->mTop;
        $this->drawHeader();
        if ($this->columns && $this->repeatHeader) $this->drawTableHeader();
    }

    /**
     * Menetapkan kop laporan.
     * @param array $h ['perusahaan','judul','subjudul','periode','filter'=>array,'dibuat_oleh','tanggal_lapor','metode']
     */
    public function setHeader(array $h): void { $this->header = $h; }

    private function drawHeader(): void
    {
        $h = $this->header;
        if (!$h) return;
        $x = $this->mLeft;
        $right = $this->w - $this->mRight;

        if (!empty($h['perusahaan'])) {
            $this->text($x, $this->y, (string)$h['perusahaan'], 'B', 12, [31, 56, 100]);
            $this->y -= 13;
        }
        if (!empty($h['judul'])) {
            $this->text($x, $this->y, (string)$h['judul'], 'B', 10, [0, 0, 0]);
            $this->y -= 12;
        }
        if (!empty($h['subjudul'])) {
            $this->text($x, $this->y, (string)$h['subjudul'], '', 8, [90, 90, 90]);
            $this->y -= 10;
        }

        $meta = [];
        if (!empty($h['periode']))       $meta[] = 'Periode: ' . $h['periode'];
        if (!empty($h['tanggal_lapor'])) $meta[] = 'Tanggal Pelaporan: ' . $h['tanggal_lapor'];
        if (!empty($h['metode']))        $meta[] = 'Metode: ' . $h['metode'];
        if ($meta) { $this->text($x, $this->y, implode('   |   ', $meta), '', 7.5, [70, 70, 70]); $this->y -= 10; }

        $filters = array_filter((array)($h['filter'] ?? []));
        if ($filters) {
            $parts = [];
            foreach ($filters as $k => $v) $parts[] = $k . ': ' . $v;
            $txt = 'Filter: ' . implode('  |  ', $parts);
            foreach ($this->wrap($txt, $this->contentWidth(), 7) as $ln) {
                $this->text($x, $this->y, $ln, 'I', 7, [110, 110, 110]);
                $this->y -= 9;
            }
        }
        $gen = [];
        if (!empty($h['dibuat_oleh'])) $gen[] = 'Dibuat oleh: ' . $h['dibuat_oleh'];
        $gen[] = 'Dicetak: ' . Dates::fmtDateTime(Dates::now());
        $this->text($x, $this->y, implode('   |   ', $gen), '', 7, [130, 130, 130]);
        $this->y -= 6;
        $this->line($x, $this->y, $right, $this->y, [31, 56, 100], 0.8);
        $this->y -= 12;
    }

    private function wrap(string $text, float $maxW, ?float $size = null): array
    {
        $words = preg_split('/\s+/', $text) ?: [];
        $lines = []; $cur = '';
        foreach ($words as $wd) {
            $try = $cur === '' ? $wd : $cur . ' ' . $wd;
            if ($this->textWidth($try, $size) <= $maxW) { $cur = $try; }
            else { if ($cur !== '') $lines[] = $cur; $cur = $wd; }
        }
        if ($cur !== '') $lines[] = $cur;
        return $lines ?: [''];
    }

    /**
     * Menetapkan definisi kolom tabel.
     * @param array $cols [['label','key','type'=>'text|money|date|int|dec|pct','width'=>float(proporsi),'align'=>'left|right|center'], ...]
     */
    public function setColumns(array $cols): void
    {
        $totalW = 0.0;
        foreach ($cols as $c) $totalW += (float)($c['width'] ?? 1);
        $avail = $this->contentWidth();
        foreach ($cols as &$c) {
            $c['_w'] = $avail * ((float)($c['width'] ?? 1) / max(0.0001, $totalW));
            $c['align'] = $c['align'] ?? (in_array((string)($c['type'] ?? 'text'), ['money', 'int', 'dec', 'pct'], true) ? 'right'
                          : (((string)($c['type'] ?? '')) === 'date' ? 'center' : 'left'));
        }
        unset($c);
        $this->columns = $cols;
    }

    private function drawTableHeader(): void
    {
        $rowH = 16.0;
        $this->rect($this->mLeft, $this->y - $rowH + 4, $this->contentWidth(), $rowH, [31, 56, 100]);
        $x = $this->mLeft;
        foreach ($this->columns as $c) {
            $label = $this->fit((string)($c['label'] ?? ''), $c['_w'] - 6, 7);
            $tx = match ($c['align']) {
                'right'  => $x + $c['_w'] - 3 - $this->textWidth($label, 7),
                'center' => $x + ($c['_w'] - $this->textWidth($label, 7)) / 2,
                default  => $x + 3,
            };
            $this->text($tx, $this->y - $rowH + 9, $label, 'B', 7, [255, 255, 255]);
            $x += $c['_w'];
        }
        $this->y -= $rowH + 1;
    }

    /** Memulai dokumen (halaman pertama). */
    public function start(): void { $this->newPage(); }

    private function ensureSpace(float $need): void
    {
        if ($this->y - $need < $this->mBottom) $this->newPage();
    }

    /** Menambahkan satu baris data. */
    public function row(array $data, array $opts = []): void
    {
        $bold  = !empty($opts['bold']);
        $bg    = $opts['bg'] ?? null;
        $rowH  = (float)($opts['height'] ?? 13);
        $size  = (int)($opts['size'] ?? 7.5);
        $this->ensureSpace($rowH + 2);

        if ($bg) $this->rect($this->mLeft, $this->y - $rowH + 4, $this->contentWidth(), $rowH, $bg);

        $x = $this->mLeft;
        foreach ($this->columns as $c) {
            $key = $c['key'] ?? null;
            $raw = $key !== null ? ($data[$key] ?? null) : null;
            $txt = self::formatValue($raw, (string)($c['type'] ?? 'text'));
            $neg = in_array((string)($c['type'] ?? ''), ['money', 'dec'], true) && (float)Num::parse((string)$raw) < 0;
            $txt = $this->fit($txt, $c['_w'] - 6, $size);
            $tx = match ($c['align']) {
                'right'  => $x + $c['_w'] - 3 - $this->textWidth($txt, $size),
                'center' => $x + ($c['_w'] - $this->textWidth($txt, $size)) / 2,
                default  => $x + 3,
            };
            $this->text($tx, $this->y - $rowH + 8, $txt, $bold ? 'B' : '', $size, $neg ? [200, 30, 30] : [20, 20, 20]);
            $x += $c['_w'];
        }
        $this->line($this->mLeft, $this->y - $rowH + 3, $this->w - $this->mRight, $this->y - $rowH + 3, [225, 225, 225], 0.3);
        $this->y -= $rowH;
    }

    /** Baris subtotal kelompok. */
    public function subtotal(array $data, string $label = 'Subtotal'): void
    {
        $this->row($data, ['bold' => true, 'bg' => [235, 240, 248], 'height' => 14]);
    }

    /** Baris grand total. */
    public function grandTotal(array $data): void
    {
        $this->ensureSpace(20);
        $this->line($this->mLeft, $this->y + 4, $this->w - $this->mRight, $this->y + 4, [31, 56, 100], 0.8);
        $this->row($data, ['bold' => true, 'bg' => [220, 230, 241], 'height' => 16, 'size' => 8]);
    }

    /** Judul kelompok (grouping). */
    public function groupHeading(string $text): void
    {
        $this->ensureSpace(20);
        $this->y -= 4;
        $this->text($this->mLeft, $this->y - 8, $text, 'B', 8.5, [31, 56, 100]);
        $this->y -= 14;
    }

    /** Paragraf bebas. */
    public function paragraph(string $text, string $style = '', float $size = 8, array $rgb = [40, 40, 40]): void
    {
        foreach ($this->wrap($text, $this->contentWidth(), $size) as $ln) {
            $this->ensureSpace(12);
            $this->text($this->mLeft, $this->y - 8, $ln, $style, $size, $rgb);
            $this->y -= 11;
        }
    }

    public function spacer(float $h = 8): void { $this->ensureSpace($h); $this->y -= $h; }

    /** Kotak ringkasan (KPI) berjajar. */
    public function kpiBoxes(array $items, int $perRow = 4): void
    {
        $gap = 8.0;
        $bw = ($this->contentWidth() - $gap * ($perRow - 1)) / $perRow;
        $bh = 34.0;
        $i = 0;
        foreach (array_chunk($items, $perRow) as $chunk) {
            $this->ensureSpace($bh + 8);
            $x = $this->mLeft;
            foreach ($chunk as $it) {
                $this->rect($x, $this->y - $bh, $bw, $bh, [246, 248, 252], [214, 222, 235]);
                $this->text($x + 5, $this->y - 12, $this->fit((string)$it['label'], $bw - 10, 6.5), '', 6.5, [110, 110, 110]);
                $this->text($x + 5, $this->y - 26, $this->fit((string)$it['value'], $bw - 10, 10), 'B', 10, [31, 56, 100]);
                $x += $bw + $gap;
            }
            $this->y -= $bh + $gap;
        }
    }

    private function drawSignature(): void
    {
        if (!$this->signature) return;
        $need = 78.0;
        if ($this->y - $need < $this->mBottom) $this->newPage();
        $this->y -= 16;
        $n = max(1, count($this->signature));
        $colW = $this->contentWidth() / $n;
        $x = $this->mLeft;
        $top = $this->y;
        foreach ($this->signature as $s) {
            $this->text($x, $top - 8, (string)($s['peran'] ?? ''), '', 7.5, [90, 90, 90]);
            $this->line($x, $top - 56, $x + $colW - 24, $top - 56, [120, 120, 120], 0.5);
            $this->text($x, $top - 66, (string)($s['nama'] ?? '(...................................)'), '', 7.5, [40, 40, 40]);
            if (!empty($s['jabatan'])) $this->text($x, $top - 75, (string)$s['jabatan'], 'I', 6.5, [120, 120, 120]);
            $x += $colW;
        }
        $this->y = $top - 84;
    }

    public static function formatValue($v, string $type): string
    {
        if ($v === null || $v === '') return $type === 'text' ? '' : '-';
        return match ($type) {
            'money' => Num::rupiah($v),
            'money_plain' => Num::fmt($v, 0),
            'int'   => Num::fmt($v, 0),
            'dec'   => Num::fmt($v, 2),
            'pct'   => Num::percent($v, 1),
            'date'  => Dates::fmt(substr((string)$v, 0, 10)),
            'days'  => Num::fmt($v, 0),
            default => (string)$v,
        };
    }

    /** Menutup dokumen dan menghasilkan biner PDF. */
    public function render(): string
    {
        $this->drawSignature();
        if ($this->buf !== '') { $this->pages[] = $this->buf; $this->buf = ''; }
        if (!$this->pages) { $this->pages[] = ''; }

        $total = count($this->pages);
        // Footer nomor halaman
        $company = Settings::str('umum.nama_perusahaan', '');
        foreach ($this->pages as $i => &$p) {
            $save = $this->buf; $this->buf = '';
            $this->line($this->mLeft, $this->mBottom - 6, $this->w - $this->mRight, $this->mBottom - 6, [210, 210, 210], 0.4);
            $this->text($this->mLeft, $this->mBottom - 17, $company, '', 6.5, [140, 140, 140]);
            $lbl = 'Halaman ' . ($i + 1) . ' dari ' . $total;
            $this->textRight($this->w - $this->mRight, $this->mBottom - 17, $lbl, '', 6.5, [140, 140, 140]);
            $p .= $this->buf;
            $this->buf = $save;
        }
        unset($p);

        return $this->buildPdf();
    }

    private function buildPdf(): string
    {
        $objects = [];
        $nPages = count($this->pages);

        // 1 Catalog, 2 Pages, 3..(2+n) Page, then contents, then fonts
        $pageIds = [];
        $contentIds = [];
        $id = 3;
        for ($i = 0; $i < $nPages; $i++) { $pageIds[] = $id++; }
        for ($i = 0; $i < $nPages; $i++) { $contentIds[] = $id++; }
        $fontIds = ['/F1' => $id++, '/F2' => $id++, '/F3' => $id++, '/F4' => $id++];

        $fontRes = '';
        $fontNames = ['/F1' => 'Helvetica', '/F2' => 'Helvetica-Bold', '/F3' => 'Helvetica-Oblique', '/F4' => 'Helvetica-BoldOblique'];
        foreach ($fontIds as $k => $fid) $fontRes .= "$k $fid 0 R ";

        $objects[1] = "<< /Type /Catalog /Pages 2 0 R >>";
        $kids = implode(' ', array_map(fn($p) => "$p 0 R", $pageIds));
        $objects[2] = "<< /Type /Pages /Count $nPages /Kids [$kids] >>";
        for ($i = 0; $i < $nPages; $i++) {
            $objects[$pageIds[$i]] = sprintf(
                "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %.2f %.2f] /Resources << /Font << %s>> /ProcSet [/PDF /Text] >> /Contents %d 0 R >>",
                $this->w, $this->h, $fontRes, $contentIds[$i]
            );
            $stream = $this->pages[$i];
            $objects[$contentIds[$i]] = "<< /Length " . strlen($stream) . " >>\nstream\n" . $stream . "\nendstream";
        }
        foreach ($fontIds as $k => $fid) {
            $objects[$fid] = "<< /Type /Font /Subtype /Type1 /BaseFont /{$fontNames[$k]} /Encoding /WinAnsiEncoding >>";
        }

        ksort($objects);
        $pdf = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
        $offsets = [];
        foreach ($objects as $num => $body) {
            $offsets[$num] = strlen($pdf);
            $pdf .= "$num 0 obj\n$body\nendobj\n";
        }
        $xrefPos = strlen($pdf);
        $maxId = max(array_keys($objects));
        $pdf .= "xref\n0 " . ($maxId + 1) . "\n";
        $pdf .= "0000000000 65535 f \n";
        for ($i = 1; $i <= $maxId; $i++) {
            $pdf .= isset($offsets[$i]) ? sprintf("%010d 00000 n \n", $offsets[$i]) : "0000000000 65535 f \n";
        }
        $pdf .= "trailer\n<< /Size " . ($maxId + 1) . " /Root 1 0 R >>\nstartxref\n$xrefPos\n%%EOF";
        return $pdf;
    }

    public function save(string $path): string
    {
        $dir = dirname($path);
        if (!is_dir($dir)) @mkdir($dir, 0775, true);
        file_put_contents($path, $this->render());
        return $path;
    }
}
