<?php
defined('BASEPATH') or exit('No direct script access allowed');

use Dompdf\Dompdf;
use Dompdf\Options;

/**
 * Shared Dompdf PDF generation for Koha reports.
 * Large tables must be rendered in small chunks (Cellmap OOM otherwise).
 */
class Pdf_export
{
    /**
     * Render HTML and stream a PDF download.
     *
     * @param string $html
     * @param string $filename
     * @param string $orientation portrait|landscape
     * @param string $paper
     * @param array  $extra Options: page_numbers (bool), default_font (string), page_text, page_x, page_y, page_size
     */
    public function stream($html, $filename, $orientation = 'landscape', $paper = 'A4', $extra = array())
    {
        @ini_set('memory_limit', '2048M');
        @set_time_limit(0);
        $token = $this->progress_token($extra);
        if ($token !== '') {
            $this->load_progress();
            Pdf_progress::update($token, 'rendering', 0, 1, array(
                'message' => 'Generating PDF…',
                'rows_total' => isset($extra['progress_rows']) ? (int) $extra['progress_rows'] : 0,
            ));
        }
        $pdf = $this->render_html_to_string($html, $orientation, $paper, $extra);
        unset($html);
        if ($token !== '') {
            Pdf_progress::update($token, 'done', 1, 1, array('message' => 'PDF ready — download starting…'));
        }
        $this->send_download_cookie();
        $this->send_pdf_headers($filename);
        echo $pdf;
        exit;
    }

    /**
     * Render large reports in chunks (avoids Dompdf Cellmap OOM), merge when possible, else ZIP.
     *
     * @param array  $html_chunks
     * @param string $filename
     * @param string $orientation
     * @param string $paper
     * @param array  $extra
     */
    public function stream_chunks(array $html_chunks, $filename, $orientation = 'landscape', $paper = 'A4', $extra = array())
    {
        $index = 0;
        $this->stream_chunks_callback(function () use (&$html_chunks, &$index) {
            if (!isset($html_chunks[$index])) {
                return null;
            }
            $html = $html_chunks[$index];
            $html_chunks[$index] = null;
            $index++;
            return $html;
        }, $filename, $orientation, $paper, $extra);
    }

    /**
     * Lazily render Dompdf parts via callback (one HTML string at a time).
     * Always delivers a single merged PDF. Never ZIP.
     * Throws RuntimeException('PDF_MERGE_FAILED') if parts cannot be merged.
     *
     * Callback signature: function(int $chunk_index): string|null — return null when done.
     *
     * @param callable $next_html
     * @param string   $filename
     * @param string   $orientation
     * @param string   $paper
     * @param array    $extra
     */
    public function stream_chunks_callback($next_html, $filename, $orientation = 'landscape', $paper = 'A4', $extra = array())
    {
        @ini_set('memory_limit', '2048M');
        @set_time_limit(0);

        require_once FCPATH . 'vendor/autoload.php';
        $this->load_progress();

        $token = $this->progress_token($extra);
        $known_total = isset($extra['progress_total_chunks']) ? (int) $extra['progress_total_chunks'] : 0;
        $rows_total = isset($extra['progress_rows']) ? (int) $extra['progress_rows'] : 0;
        $chunk_size = isset($extra['progress_chunk_size']) ? max(1, (int) $extra['progress_chunk_size']) : 35;

        if ($token !== '') {
            Pdf_progress::update($token, 'starting', 0, $known_total, array(
                'rows_total' => $rows_total,
                'rows_done' => 0,
                'message' => $rows_total > 0
                    ? ('Preparing PDF for ' . number_format($rows_total) . ' rows…')
                    : 'Preparing PDF…',
            ));
        }

        $tmp_files = array();
        $dir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'koha_pdf_' . uniqid('', true);
        if (!@mkdir($dir, 0700, true) && !is_dir($dir)) {
            if ($token !== '') {
                Pdf_progress::update($token, 'error', 0, $known_total, array(
                    'message' => 'Unable to create temporary PDF folder.',
                ));
            }
            throw new RuntimeException('PDF_MERGE_FAILED');
        }

        $merge_failed = false;
        try {
            $chunk_extra = $extra;
            $chunk_extra['page_numbers'] = false;
            $i = 0;

            while (true) {
                $html = call_user_func($next_html, $i);
                if ($html === null || $html === false) {
                    break;
                }
                if (!is_string($html) || trim($html) === '') {
                    $i++;
                    continue;
                }

                $total_for_ui = $known_total > 0 ? $known_total : max($known_total, $i + 1);
                if ($token !== '') {
                    $rows_done = min($rows_total, ($i + 1) * $chunk_size);
                    Pdf_progress::update($token, 'rendering', $i + 1, $total_for_ui, array(
                        'rows_total' => $rows_total,
                        'rows_done' => $rows_done,
                    ));
                }

                $bin = $this->render_html_to_string($html, $orientation, $paper, $chunk_extra);
                unset($html);

                $path = $dir . DIRECTORY_SEPARATOR . sprintf('part_%04d.pdf', $i);
                if (file_put_contents($path, $bin) === false) {
                    unset($bin);
                    throw new RuntimeException('Unable to write PDF chunk.');
                }
                unset($bin);
                $tmp_files[] = $path;
                $i++;

                if (function_exists('gc_collect_cycles')) {
                    gc_collect_cycles();
                }
            }

            if (empty($tmp_files)) {
                $this->stream('<p>No records</p>', $filename, $orientation, $paper, $extra);
                return;
            }

            $part_count = count($tmp_files);
            if ($known_total <= 0) {
                $known_total = $part_count;
            }

            if ($part_count === 1) {
                if ($token !== '') {
                    Pdf_progress::update($token, 'done', 1, 1, array(
                        'rows_total' => $rows_total,
                        'rows_done' => $rows_total,
                        'message' => 'PDF ready — download starting…',
                    ));
                }
                $this->send_download_cookie();
                $this->send_pdf_headers($filename);
                readfile($tmp_files[0]);
                exit;
            }

            if ($token !== '') {
                Pdf_progress::update($token, 'merging', $part_count, $known_total, array(
                    'rows_total' => $rows_total,
                    'rows_done' => $rows_total,
                    'message' => 'Merging ' . number_format($part_count) . ' parts into one PDF…',
                ));
            }

            $merged = null;
            if (class_exists('\\setasign\\Fpdi\\Fpdi')) {
                try {
                    $merged = $this->merge_pdf_files($tmp_files, $orientation, $paper, $extra);
                } catch (Throwable $e) {
                    $merged = null;
                    log_message('error', 'PDF FPDI merge failed: ' . $e->getMessage());
                }
            }

            if ($merged === null || $merged === '') {
                try {
                    $merged = $this->merge_pdf_files_external($tmp_files);
                } catch (Throwable $e) {
                    $merged = null;
                    log_message('error', 'PDF external merge failed: ' . $e->getMessage());
                }
            }

            if ($merged !== null && $merged !== '') {
                if ($token !== '') {
                    Pdf_progress::update($token, 'done', $part_count, $known_total, array(
                        'rows_total' => $rows_total,
                        'rows_done' => $rows_total,
                        'message' => 'PDF ready — download starting…',
                    ));
                }
                $this->send_download_cookie();
                $this->send_pdf_headers($filename);
                echo $merged;
                unset($merged);
                exit;
            }

            $merge_failed = true;
        } catch (Throwable $e) {
            if ($token !== '') {
                Pdf_progress::update($token, 'error', 0, $known_total, array(
                    'message' => 'PDF export failed. Please try Excel or retry.',
                ));
            }
            if ($e->getMessage() === 'PDF_MERGE_FAILED') {
                $merge_failed = true;
            } else {
                log_message('error', 'PDF chunk export failed: ' . $e->getMessage());
                $merge_failed = true;
            }
        } finally {
            foreach ($tmp_files as $f) {
                if (is_file($f)) {
                    @unlink($f);
                }
            }
            if (is_dir($dir)) {
                @rmdir($dir);
            }
        }

        if ($merge_failed) {
            throw new RuntimeException('PDF_MERGE_FAILED');
        }
    }

    protected function load_progress()
    {
        if (!class_exists('Pdf_progress', false)) {
            require_once APPPATH . 'libraries/Pdf_progress.php';
        }
    }

    protected function progress_token(array $extra)
    {
        $token = '';
        if (!empty($extra['progress_token'])) {
            $token = (string) $extra['progress_token'];
        } else {
            $CI =& get_instance();
            $token = (string) $CI->input->post('download_token');
            if ($token === '') {
                $token = (string) $CI->input->get('download_token');
            }
        }
        $this->load_progress();
        return Pdf_progress::sanitize_token($token);
    }

    /**
     * Merge PDF parts with qpdf or Ghostscript when FPDI cannot parse Dompdf output.
     *
     * @param array $files
     * @return string|null PDF binary or null
     */
    protected function merge_pdf_files_external(array $files)
    {
        if (empty($files)) {
            return null;
        }

        $out = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'koha_merged_' . uniqid('', true) . '.pdf';
        $args = array();
        foreach ($files as $f) {
            if (is_file($f)) {
                $args[] = escapeshellarg($f);
            }
        }
        if (empty($args)) {
            return null;
        }
        $list = implode(' ', $args);
        $out_esc = escapeshellarg($out);

        $commands = array(
            'qpdf --empty --pages ' . $list . ' -- ' . $out_esc,
            'gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -dPDFSETTINGS=/default -sOutputFile=' . $out_esc . ' ' . $list,
            'gswin64c -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=' . $out_esc . ' ' . $list,
            'gswin32c -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=' . $out_esc . ' ' . $list,
        );

        foreach ($commands as $cmd) {
            $output = array();
            $code = 1;
            @exec($cmd . ' 2>&1', $output, $code);
            if ($code === 0 && is_file($out) && filesize($out) > 0) {
                $bin = file_get_contents($out);
                @unlink($out);
                return $bin !== false ? $bin : null;
            }
            if (is_file($out)) {
                @unlink($out);
            }
        }

        return null;
    }

    /**
     * @param string $html
     * @param string $orientation
     * @param string $paper
     * @param array  $extra
     * @return string PDF binary
     */
    public function render_html_to_string($html, $orientation = 'landscape', $paper = 'A4', $extra = array())
    {
        require_once FCPATH . 'vendor/autoload.php';

        $options = new Options();
        $options->set('isRemoteEnabled', true);
        $options->set('isHtml5ParserEnabled', true);
        $options->set('isPhpEnabled', true);
        // Keep Dompdf footprint smaller between chunks
        $options->set('isFontSubsettingEnabled', true);

        $default_font = 'DejaVu Sans';
        if (!empty($extra['default_font'])) {
            $default_font = $extra['default_font'];
        } else {
            $CI =& get_instance();
            $CI->load->helper('settings');
            $default_font = app_font_pdf();
        }
        $options->set('defaultFont', $default_font);
        $options->set('chroot', array(FCPATH, '/usr/share/fonts'));

        $dompdf = new Dompdf($options);
        $dompdf->setHttpContext(
            stream_context_create(array(
                'ssl' => array(
                    'verify_peer' => false,
                    'verify_peer_name' => false,
                    'allow_self_signed' => true,
                ),
            ))
        );

        $CI =& get_instance();
        $CI->load->helper('settings');
        $pdf_font = !empty($extra['default_font']) ? $extra['default_font'] : app_font_pdf();
        $font_style = '<style id="app-font-override">'
            . 'body { font-family: "' . str_replace('"', '', $pdf_font) . '", DejaVu Sans, sans-serif; }'
            . 'table, td, th, p, div, span, li { font-family: inherit; }'
            . '</style>';

        if (stripos($html, '<!DOCTYPE') === false && stripos($html, '<html') === false) {
            $html = '<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
                . $font_style
                . '</head><body>'
                . $html
                . '</body></html>';
        } elseif (stripos($html, '</head>') !== false) {
            $html = preg_replace('/<\/head>/i', $font_style . '</head>', $html, 1);
        } else {
            $html = $font_style . $html;
        }

        $html = mb_convert_encoding($html, 'UTF-8', 'UTF-8');
        $dompdf->loadHtml($html, 'UTF-8');
        unset($html);
        $dompdf->setPaper($paper, $orientation);
        $dompdf->render();

        // Prefer PDF 1.4 for broader FPDI compatibility when merging
        try {
            $canvas = $dompdf->getCanvas();
            if ($canvas && method_exists($canvas, 'get_cpdf')) {
                $cpdf = $canvas->get_cpdf();
                if (is_object($cpdf)) {
                    if (method_exists($cpdf, 'setPdfVersion')) {
                        $cpdf->setPdfVersion('1.4');
                    } elseif (property_exists($cpdf, 'pdfversion') || isset($cpdf->pdfversion)) {
                        $cpdf->pdfversion = '1.4';
                    }
                }
            }
        } catch (Throwable $e) {
            // ignore version tweak failures
        }

        if (!empty($extra['page_numbers'])) {
            $canvas = $dompdf->getCanvas();
            $font = $dompdf->getFontMetrics()->getFont('Helvetica', 'normal');
            $x = isset($extra['page_x']) ? $extra['page_x'] : 540;
            $y = isset($extra['page_y']) ? $extra['page_y'] : 810;
            $size = isset($extra['page_size']) ? (int) $extra['page_size'] : 10;
            $text = isset($extra['page_text']) ? $extra['page_text'] : '{PAGE_NUM}';
            $canvas->page_text($x, $y, $text, $font, $size, array(0, 0, 0));
        }

        $out = $dompdf->output();
        try {
            $canvas = $dompdf->getCanvas();
            if ($canvas && method_exists($canvas, 'get_cpdf')) {
                $cpdf = $canvas->get_cpdf();
                if (is_object($cpdf) && method_exists($cpdf, 'close')) {
                    $cpdf->close();
                }
            }
        } catch (Throwable $e) {
            // ignore cleanup failures
        }
        unset($dompdf);
        if (function_exists('gc_collect_cycles')) {
            gc_collect_cycles();
        }
        return $out;
    }

    /**
     * @param array  $files
     * @param string $orientation
     * @param string $paper
     * @param array  $extra
     * @return string
     */
    protected function merge_pdf_files(array $files, $orientation, $paper, array $extra)
    {
        $pdf = new \setasign\Fpdi\Fpdi();
        $pdf->SetAutoPageBreak(false);

        foreach ($files as $file) {
            $pageCount = $pdf->setSourceFile($file);
            for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
                $tpl = $pdf->importPage($pageNo);
                $size = $pdf->getTemplateSize($tpl);
                $orient = ($size['width'] > $size['height']) ? 'L' : 'P';
                $pdf->AddPage($orient, array($size['width'], $size['height']));
                $pdf->useTemplate($tpl);
            }
        }

        if (!empty($extra['page_numbers'])) {
            $total = $pdf->PageNo();
            $x = isset($extra['page_x']) ? (float) $extra['page_x'] : 36;
            $y = isset($extra['page_y']) ? (float) $extra['page_y'] : 570;
            $x_mm = $x * 0.352778;
            $y_mm = $y * 0.352778;
            $size = isset($extra['page_size']) ? (int) $extra['page_size'] : 9;
            $template = isset($extra['page_text']) ? $extra['page_text'] : 'Page {PAGE_NUM} of {PAGE_COUNT}';

            for ($i = 1; $i <= $total; $i++) {
                $pdf->setPage($i);
                $label = str_replace(
                    array('{PAGE_NUM}', '{PAGE_COUNT}'),
                    array((string) $i, (string) $total),
                    $template
                );
                $pdf->SetFont('Helvetica', '', $size);
                $pdf->SetTextColor(0, 0, 0);
                $pdf->SetXY($x_mm, $y_mm);
                $pdf->Write(5, $label);
            }
        }

        return $pdf->Output('S');
    }

    /**
     * ZIP already-rendered PDF part files.
     */
    protected function stream_pdf_parts_zip(array $tmp_files, $filename)
    {
        if (!class_exists('ZipArchive') || empty($tmp_files)) {
            throw new RuntimeException('ZIP fallback unavailable.');
        }

        $zip_name = preg_replace('/\.pdf$/i', '', $filename) . '.zip';
        $tmp_zip = tempnam(sys_get_temp_dir(), 'koha_zip_');
        $zip = new ZipArchive();
        if ($zip->open($tmp_zip, ZipArchive::OVERWRITE) !== true) {
            throw new RuntimeException('Unable to create ZIP.');
        }

        foreach ($tmp_files as $i => $path) {
            if (is_file($path)) {
                $zip->addFile($path, sprintf('part_%02d.pdf', $i + 1));
            }
        }
        $zip->close();

        $this->send_download_cookie();
        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="' . $zip_name . '"');
        header('Content-Length: ' . filesize($tmp_zip));
        readfile($tmp_zip);
        @unlink($tmp_zip);
        exit;
    }

    /**
     * Lets the browser JS know the file download started (hide loading overlay).
     */
    public function send_download_cookie()
    {
        $CI =& get_instance();
        $token = (string) $CI->input->post('download_token');
        if ($token === '') {
            $token = (string) $CI->input->get('download_token');
        }
        if ($token === '') {
            return;
        }
        // ASCII-safe token only
        $token = preg_replace('/[^a-zA-Z0-9_\-]/', '', $token);
        if ($token === '') {
            return;
        }
        setcookie('kohaDownloadToken', $token, time() + 3600, '/');
    }

    protected function send_pdf_headers($filename)
    {
        if (stripos($filename, '.pdf') === false) {
            $filename .= '.pdf';
        }
        // Keep filename header-safe
        $filename = str_replace(array('"', "\r", "\n"), '', $filename);
        header('Content-Type: application/pdf');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Cache-Control: private, max-age=0, must-revalidate');
        header('Pragma: public');
    }
}
