<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Controller;
use App\Core\Request;
use App\Core\Tenant;
use App\Models\AccountReceivable;
use App\Models\Customer;
use App\Services\AuditService;
use RuntimeException;

class ReceivableController extends Controller
{
    public function index(Request $request): void
    {
        $companyId = (int) Tenant::companyId();
        if ($companyId <= 0) {
            flash('error', 'Selecciona una empresa antes de consultar cuentas por cobrar.');
            redirect('empresas/seleccionar');
        }

        $filters = $this->readFilters($request);
        $query = $this->buildFilterQuery($filters);
        $queryString = $query !== '' ? ('?' . $query) : '';
        $report = $this->loadReportData($companyId, $filters);

        $this->render('cuentas_cobrar/index', [
            'title' => 'Cuentas por cobrar',
            'summary' => $report['summary'],
            'accounts' => $report['accounts'],
            'aging' => $report['aging'],
            'payments' => (new AccountReceivable())->recentPayments($companyId),
            'customers' => (new Customer())->all(['activo' => 1], $companyId, 'nombre ASC'),
            'filters' => $filters,
            'exportCsvUrl' => url('cuentas-cobrar/exportar') . $queryString,
            'exportXlsxUrl' => url('cuentas-cobrar/exportar-xlsx') . $queryString,
            'exportPdfUrl' => url('cuentas-cobrar/exportar-pdf') . $queryString,
        ]);
    }

    public function collect(Request $request): never
    {
        $companyId = (int) Tenant::companyId();
        if ($companyId <= 0) {
            flash('error', 'Selecciona una empresa antes de registrar abonos.');
            redirect('empresas/seleccionar');
        }

        $saleId = (int) $request->param('id', 0);
        if ($saleId <= 0) {
            flash('error', 'Venta invalida para registrar abono.');
            redirect('cuentas-cobrar');
        }

        try {
            $paymentId = (new AccountReceivable())->registerPayment(
                $companyId,
                $saleId,
                (float) $request->input('monto', 0),
                (string) $request->input('metodo_pago', 'efectivo'),
                (string) $request->input('referencia', '')
            );

            AuditService::track(
                'cuentas_cobrar.abonar',
                'Abono registrado ID: ' . $paymentId . ' en venta ID: ' . $saleId
            );
            flash('success', 'Abono registrado correctamente.');
            redirect('cuentas-cobrar');
        } catch (\Throwable $e) {
            flash('error', 'No fue posible registrar el abono: ' . $e->getMessage());
            redirect('cuentas-cobrar');
        }
    }

    public function export(Request $request): never
    {
        $companyId = $this->resolveCompanyIdForExport();
        $filters = $this->readFilters($request);
        $report = $this->loadReportData($companyId, $filters);

        $filename = 'cartera_' . date('Ymd_His') . '.csv';

        if (ob_get_length() > 0) {
            ob_clean();
        }

        header('Content-Type: text/csv; charset=UTF-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Pragma: no-cache');
        header('Expires: 0');

        $output = fopen('php://output', 'wb');
        if ($output === false) {
            echo 'No fue posible generar el archivo.';
            exit;
        }

        fwrite($output, "\xEF\xBB\xBF");
        fputcsv($output, ['Reporte de cartera - Veltrix POS'], ';');
        fputcsv($output, ['Generado', date('Y-m-d H:i:s')], ';');
        fputcsv($output, ['Filtros', $this->filterDescription($filters)], ';');
        fputcsv($output, [''], ';');

        fputcsv($output, [
            'Venta',
            'Fecha',
            'Cliente',
            'Documento',
            'Cajero',
            'Total',
            'Abonado',
            'Saldo',
            'Dias cartera',
            'Rango',
            'Estado',
        ], ';');

        foreach ($report['accounts'] as $row) {
            fputcsv($output, [
                (string) ($row['id'] ?? ''),
                (string) ($row['fecha'] ?? ''),
                $this->sanitizeCsvString((string) ($row['cliente'] ?? 'Consumidor final')),
                $this->sanitizeCsvString((string) ($row['cliente_documento'] ?? '')),
                $this->sanitizeCsvString((string) ($row['cajero'] ?? '')),
                number_format((float) ($row['total'] ?? 0), 2, '.', ''),
                number_format((float) ($row['pagado'] ?? 0), 2, '.', ''),
                number_format((float) ($row['saldo'] ?? 0), 2, '.', ''),
                (string) (int) ($row['dias_cartera'] ?? 0),
                (string) ($row['rango_antiguedad'] ?? ''),
                (string) ($row['estado_cobro'] ?? ''),
            ], ';');
        }

        fputcsv($output, [''], ';');
        fputcsv($output, [
            'Totales',
            '',
            '',
            '',
            '',
            number_format((float) ($report['summary']['total_credito'] ?? 0), 2, '.', ''),
            number_format((float) ($report['summary']['total_abonado'] ?? 0), 2, '.', ''),
            number_format((float) ($report['summary']['total_saldo'] ?? 0), 2, '.', ''),
            '',
            '',
            '',
        ], ';');

        fputcsv($output, ['Antiguedad', 'Saldo', 'Cuentas'], ';');
        fputcsv($output, ['0-30', number_format((float) ($report['aging']['saldo_0_30'] ?? 0), 2, '.', ''), (string) (int) ($report['aging']['cuentas_0_30'] ?? 0)], ';');
        fputcsv($output, ['31-60', number_format((float) ($report['aging']['saldo_31_60'] ?? 0), 2, '.', ''), (string) (int) ($report['aging']['cuentas_31_60'] ?? 0)], ';');
        fputcsv($output, ['61-90', number_format((float) ($report['aging']['saldo_61_90'] ?? 0), 2, '.', ''), (string) (int) ($report['aging']['cuentas_61_90'] ?? 0)], ';');
        fputcsv($output, ['91+', number_format((float) ($report['aging']['saldo_91_mas'] ?? 0), 2, '.', ''), (string) (int) ($report['aging']['cuentas_91_mas'] ?? 0)], ';');

        fclose($output);
        exit;
    }

    public function exportXlsx(Request $request): never
    {
        $companyId = $this->resolveCompanyIdForExport();
        $filters = $this->readFilters($request);
        try {
            $report = $this->loadReportData($companyId, $filters);
            $rows = $this->buildSpreadsheetRows($filters, $report);
            $binary = $this->buildXlsxBinary($rows, 'Cartera');
            $filename = 'cartera_' . date('Ymd_His') . '.xlsx';

            if (ob_get_length() > 0) {
                ob_clean();
            }

            header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
            header('Content-Disposition: attachment; filename="' . $filename . '"');
            header('Cache-Control: max-age=0');

            echo $binary;
            exit;
        } catch (\Throwable $e) {
            flash('error', 'No fue posible exportar a XLSX: ' . $e->getMessage());
            redirect('cuentas-cobrar');
        }
    }

    public function exportPdf(Request $request): never
    {
        $companyId = $this->resolveCompanyIdForExport();
        $filters = $this->readFilters($request);
        try {
            $report = $this->loadReportData($companyId, $filters);
            $lines = $this->buildPdfLines($filters, $report);
            $binary = $this->buildPdfBinary($lines);
            $filename = 'cartera_' . date('Ymd_His') . '.pdf';

            if (ob_get_length() > 0) {
                ob_clean();
            }

            header('Content-Type: application/pdf');
            header('Content-Disposition: attachment; filename="' . $filename . '"');
            header('Cache-Control: private, max-age=0, must-revalidate');
            header('Pragma: public');

            echo $binary;
            exit;
        } catch (\Throwable $e) {
            flash('error', 'No fue posible exportar a PDF: ' . $e->getMessage());
            redirect('cuentas-cobrar');
        }
    }

    private function resolveCompanyIdForExport(): int
    {
        $companyId = (int) Tenant::companyId();
        if ($companyId <= 0) {
            flash('error', 'Selecciona una empresa antes de exportar cartera.');
            redirect('empresas/seleccionar');
        }

        return $companyId;
    }

    private function loadReportData(int $companyId, array $filters): array
    {
        $model = new AccountReceivable();

        return [
            'summary' => $model->summary($companyId, $filters),
            'accounts' => $model->openSales($companyId, $filters),
            'aging' => $model->agingSummary($companyId, $filters),
        ];
    }

    private function readFilters(Request $request): array
    {
        $status = strtolower(trim((string) $request->query('estado', 'todos')));
        if (!in_array($status, ['todos', 'pendiente', 'parcial'], true)) {
            $status = 'todos';
        }

        $dateFrom = trim((string) $request->query('fecha_desde', ''));
        if ($dateFrom !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
            $dateFrom = '';
        }

        $dateTo = trim((string) $request->query('fecha_hasta', ''));
        if ($dateTo !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) {
            $dateTo = '';
        }

        return [
            'cliente_id' => max(0, (int) $request->query('cliente_id', 0)),
            'fecha_desde' => $dateFrom,
            'fecha_hasta' => $dateTo,
            'estado' => $status,
        ];
    }

    private function buildFilterQuery(array $filters): string
    {
        $params = [];

        $clienteId = (int) ($filters['cliente_id'] ?? 0);
        if ($clienteId > 0) {
            $params['cliente_id'] = $clienteId;
        }

        $fechaDesde = trim((string) ($filters['fecha_desde'] ?? ''));
        if ($fechaDesde !== '') {
            $params['fecha_desde'] = $fechaDesde;
        }

        $fechaHasta = trim((string) ($filters['fecha_hasta'] ?? ''));
        if ($fechaHasta !== '') {
            $params['fecha_hasta'] = $fechaHasta;
        }

        $estado = trim((string) ($filters['estado'] ?? 'todos'));
        if ($estado !== '' && $estado !== 'todos') {
            $params['estado'] = $estado;
        }

        return http_build_query($params);
    }

    private function filterDescription(array $filters): string
    {
        return 'Cliente ID: ' . (string) (($filters['cliente_id'] ?? 0) > 0 ? (int) $filters['cliente_id'] : 'Todos')
            . ' | Desde: ' . (($filters['fecha_desde'] ?? '') !== '' ? (string) $filters['fecha_desde'] : 'N/A')
            . ' | Hasta: ' . (($filters['fecha_hasta'] ?? '') !== '' ? (string) $filters['fecha_hasta'] : 'N/A')
            . ' | Estado: ' . (string) ($filters['estado'] ?? 'todos');
    }

    private function sanitizeCsvString(string $value): string
    {
        $trimmed = ltrim($value);
        if ($trimmed !== '' && preg_match('/^[=\-+@]/', $trimmed) === 1) {
            return "'" . $value;
        }

        return $value;
    }

    private function buildSpreadsheetRows(array $filters, array $report): array
    {
        $rows = [];
        $rows[] = ['Reporte de cartera - Veltrix POS'];
        $rows[] = ['Generado', date('Y-m-d H:i:s')];
        $rows[] = ['Filtros', $this->filterDescription($filters)];
        $rows[] = [''];
        $rows[] = ['Venta', 'Fecha', 'Cliente', 'Documento', 'Cajero', 'Total', 'Abonado', 'Saldo', 'Dias cartera', 'Rango', 'Estado'];

        foreach ((array) ($report['accounts'] ?? []) as $row) {
            $rows[] = [
                (int) ($row['id'] ?? 0),
                (string) ($row['fecha'] ?? ''),
                (string) ($row['cliente'] ?? 'Consumidor final'),
                (string) ($row['cliente_documento'] ?? ''),
                (string) ($row['cajero'] ?? ''),
                (float) ($row['total'] ?? 0),
                (float) ($row['pagado'] ?? 0),
                (float) ($row['saldo'] ?? 0),
                (int) ($row['dias_cartera'] ?? 0),
                (string) ($row['rango_antiguedad'] ?? ''),
                (string) ($row['estado_cobro'] ?? ''),
            ];
        }

        $rows[] = [''];
        $rows[] = ['Totales', '', '', '', '', (float) ($report['summary']['total_credito'] ?? 0), (float) ($report['summary']['total_abonado'] ?? 0), (float) ($report['summary']['total_saldo'] ?? 0), '', '', ''];
        $rows[] = [''];
        $rows[] = ['Antiguedad', 'Saldo', 'Cuentas'];
        $rows[] = ['0-30', (float) ($report['aging']['saldo_0_30'] ?? 0), (int) ($report['aging']['cuentas_0_30'] ?? 0)];
        $rows[] = ['31-60', (float) ($report['aging']['saldo_31_60'] ?? 0), (int) ($report['aging']['cuentas_31_60'] ?? 0)];
        $rows[] = ['61-90', (float) ($report['aging']['saldo_61_90'] ?? 0), (int) ($report['aging']['cuentas_61_90'] ?? 0)];
        $rows[] = ['91+', (float) ($report['aging']['saldo_91_mas'] ?? 0), (int) ($report['aging']['cuentas_91_mas'] ?? 0)];

        return $rows;
    }

    private function buildXlsxBinary(array $rows, string $sheetName): string
    {
        if (!class_exists(\ZipArchive::class)) {
            throw new RuntimeException('La extension ZIP de PHP no esta habilitada para exportar XLSX.');
        }

        $tempFile = tempnam(sys_get_temp_dir(), 'veltrix_xlsx_');
        if ($tempFile === false) {
            throw new RuntimeException('No fue posible crear archivo temporal para XLSX.');
        }

        $zip = new \ZipArchive();
        if ($zip->open($tempFile, \ZipArchive::OVERWRITE) !== true) {
            @unlink($tempFile);
            throw new RuntimeException('No fue posible abrir archivo ZIP para XLSX.');
        }

        $sheetXml = $this->buildWorksheetXml($rows);
        $safeSheetName = htmlspecialchars(substr($sheetName, 0, 31), ENT_XML1 | ENT_QUOTES, 'UTF-8');

        $zip->addFromString('[Content_Types].xml', '<?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"/>'
            . '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>'
            . '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>'
            . '</Types>');

        $zip->addFromString('_rels/.rels', '<?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"/>'
            . '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
            . '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>'
            . '</Relationships>');

        $zip->addFromString('docProps/core.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            . '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"'
            . ' xmlns:dc="http://purl.org/dc/elements/1.1/"'
            . ' xmlns:dcterms="http://purl.org/dc/terms/"'
            . ' xmlns:dcmitype="http://purl.org/dc/dcmitype/"'
            . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
            . '<dc:creator>Veltrix POS</dc:creator>'
            . '<cp:lastModifiedBy>Veltrix POS</cp:lastModifiedBy>'
            . '<dcterms:created xsi:type="dcterms:W3CDTF">' . gmdate('Y-m-d\TH:i:s\Z') . '</dcterms:created>'
            . '<dcterms:modified xsi:type="dcterms:W3CDTF">' . gmdate('Y-m-d\TH:i:s\Z') . '</dcterms:modified>'
            . '</cp:coreProperties>');

        $zip->addFromString('docProps/app.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            . '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"'
            . ' xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
            . '<Application>Veltrix POS</Application>'
            . '</Properties>');

        $zip->addFromString('xl/workbook.xml', '<?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="' . $safeSheetName . '" sheetId="1" r:id="rId1"/></sheets>'
            . '</workbook>');

        $zip->addFromString('xl/_rels/workbook.xml.rels', '<?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>');

        $zip->addFromString('xl/styles.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            . '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            . '<fonts count="1"><font><sz val="11"/><name val="Calibri"/><family val="2"/></font></fonts>'
            . '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
            . '<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>'
            . '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
            . '<cellXfs count="2">'
            . '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
            . '<xf numFmtId="4" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>'
            . '</cellXfs>'
            . '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
            . '</styleSheet>');

        $zip->addFromString('xl/worksheets/sheet1.xml', $sheetXml);

        $zip->close();

        $binary = file_get_contents($tempFile);
        @unlink($tempFile);

        if ($binary === false) {
            throw new RuntimeException('No fue posible leer el archivo XLSX generado.');
        }

        return $binary;
    }

    private function buildWorksheetXml(array $rows): string
    {
        $sheetData = '';

        foreach ($rows as $rowIndex => $rowValues) {
            $excelRow = $rowIndex + 1;
            $sheetData .= '<row r="' . $excelRow . '">';

            foreach ((array) $rowValues as $columnIndex => $value) {
                $col = $this->xlsxColumnName($columnIndex + 1);
                $cellRef = $col . $excelRow;

                if (is_int($value) || is_float($value)) {
                    $sheetData .= '<c r="' . $cellRef . '" s="1"><v>' . (string) $value . '</v></c>';
                    continue;
                }

                $stringValue = $this->sanitizeSpreadsheetString((string) $value);
                $escaped = htmlspecialchars($stringValue, ENT_XML1 | ENT_QUOTES, 'UTF-8');
                $needsPreserve = preg_match('/^\s|\s$/', $stringValue) === 1;
                $spaceAttr = $needsPreserve ? ' xml:space="preserve"' : '';

                $sheetData .= '<c r="' . $cellRef . '" t="inlineStr"><is><t' . $spaceAttr . '>' . $escaped . '</t></is></c>';
            }

            $sheetData .= '</row>';
        }

        return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
            . ' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
            . '<sheetData>' . $sheetData . '</sheetData>'
            . '</worksheet>';
    }

    private function xlsxColumnName(int $index): string
    {
        $name = '';
        while ($index > 0) {
            $mod = ($index - 1) % 26;
            $name = chr(65 + $mod) . $name;
            $index = (int) floor(($index - 1) / 26);
        }

        return $name;
    }

    private function sanitizeSpreadsheetString(string $value): string
    {
        $trimmed = ltrim($value);
        if ($trimmed !== '' && preg_match('/^[=\-+@]/', $trimmed) === 1) {
            return "'" . $value;
        }

        return $value;
    }

    private function buildPdfLines(array $filters, array $report): array
    {
        $lines = [];
        $lines[] = 'Reporte de cartera - Veltrix POS';
        $lines[] = 'Generado: ' . date('Y-m-d H:i:s');
        $lines[] = $this->filterDescription($filters);
        $lines[] = str_repeat('-', 110);
        $lines[] = sprintf(
            '%-7s %-16s %-24s %12s %12s %12s %6s %-7s %-8s',
            'Venta',
            'Fecha',
            'Cliente',
            'Total',
            'Abonado',
            'Saldo',
            'Dias',
            'Rango',
            'Estado'
        );
        $lines[] = str_repeat('-', 110);

        foreach ((array) ($report['accounts'] ?? []) as $row) {
            $lines[] = sprintf(
                '%-7s %-16s %-24s %12s %12s %12s %6s %-7s %-8s',
                '#' . (string) ($row['id'] ?? ''),
                substr((string) ($row['fecha'] ?? ''), 0, 16),
                substr((string) ($row['cliente'] ?? 'Consumidor final'), 0, 24),
                number_format((float) ($row['total'] ?? 0), 2, '.', ''),
                number_format((float) ($row['pagado'] ?? 0), 2, '.', ''),
                number_format((float) ($row['saldo'] ?? 0), 2, '.', ''),
                (string) (int) ($row['dias_cartera'] ?? 0),
                (string) ($row['rango_antiguedad'] ?? ''),
                (string) ($row['estado_cobro'] ?? '')
            );
        }

        $lines[] = str_repeat('-', 110);
        $lines[] = 'Totales -> Credito: ' . number_format((float) ($report['summary']['total_credito'] ?? 0), 2, '.', '')
            . ' | Abonado: ' . number_format((float) ($report['summary']['total_abonado'] ?? 0), 2, '.', '')
            . ' | Saldo: ' . number_format((float) ($report['summary']['total_saldo'] ?? 0), 2, '.', '');
        $lines[] = 'Antiguedad -> 0-30: ' . number_format((float) ($report['aging']['saldo_0_30'] ?? 0), 2, '.', '')
            . ' | 31-60: ' . number_format((float) ($report['aging']['saldo_31_60'] ?? 0), 2, '.', '')
            . ' | 61-90: ' . number_format((float) ($report['aging']['saldo_61_90'] ?? 0), 2, '.', '')
            . ' | 91+: ' . number_format((float) ($report['aging']['saldo_91_mas'] ?? 0), 2, '.', '');

        return $lines;
    }

    private function buildPdfBinary(array $lines): string
    {
        $linesPerPage = 52;
        $lineChunks = array_chunk($lines, $linesPerPage);
        if ($lineChunks === []) {
            $lineChunks = [['Reporte de cartera']];
        }

        $objects = [
            1 => '',
            2 => '',
            3 => '<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>',
        ];

        $nextId = 4;
        $pageIds = [];

        foreach ($lineChunks as $chunk) {
            $content = "BT\n/F1 9 Tf\n12 TL\n40 800 Td\n";
            foreach ($chunk as $index => $line) {
                $content .= '(' . $this->escapePdfText($line) . ") Tj\n";
                if ($index < (count($chunk) - 1)) {
                    $content .= "T*\n";
                }
            }
            $content .= "ET";

            $contentId = $nextId++;
            $objects[$contentId] = "<< /Length " . strlen($content) . " >>\nstream\n" . $content . "\nendstream";

            $pageId = $nextId++;
            $objects[$pageId] = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents " . $contentId . " 0 R >>";
            $pageIds[] = $pageId;
        }

        $kids = implode(' ', array_map(static fn (int $id): string => $id . ' 0 R', $pageIds));
        $objects[2] = '<< /Type /Pages /Count ' . count($pageIds) . ' /Kids [ ' . $kids . ' ] >>';
        $objects[1] = '<< /Type /Catalog /Pages 2 0 R >>';

        ksort($objects);
        $maxId = (int) max(array_keys($objects));

        $pdf = "%PDF-1.4\n";
        $offsets = [];

        for ($i = 1; $i <= $maxId; $i++) {
            if (!isset($objects[$i])) {
                $objects[$i] = '<<>>';
            }

            $offsets[$i] = strlen($pdf);
            $pdf .= $i . " 0 obj\n" . $objects[$i] . "\nendobj\n";
        }

        $xrefPos = strlen($pdf);
        $pdf .= 'xref' . "\n";
        $pdf .= '0 ' . ($maxId + 1) . "\n";
        $pdf .= "0000000000 65535 f \n";

        for ($i = 1; $i <= $maxId; $i++) {
            $pdf .= sprintf('%010d 00000 n ', $offsets[$i]) . "\n";
        }

        $pdf .= "trailer\n<< /Size " . ($maxId + 1) . " /Root 1 0 R >>\n";
        $pdf .= "startxref\n" . $xrefPos . "\n%%EOF";

        return $pdf;
    }

    private function escapePdfText(string $text): string
    {
        $normalized = $text;
        if (function_exists('iconv')) {
            $converted = iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text);
            if ($converted !== false) {
                $normalized = $converted;
            }
        }

        $normalized = preg_replace('/[^\x20-\x7E\x80-\xFF]/', '', $normalized) ?? '';
        $normalized = str_replace('\\', '\\\\', $normalized);
        $normalized = str_replace('(', '\(', $normalized);
        $normalized = str_replace(')', '\)', $normalized);

        return $normalized;
    }
}
