<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * PDF text extraction for TNC FlipBook 3D.
 *
 * Extracts text from PDF attachments and stores per-page text in post meta.
 * This is the single source of truth - all addons (SEO, TTS, Chatbot, etc.)
 * read from the stored meta instead of extracting independently.
 *
 * Meta keys:
 *   _tncfb3d_page_texts       - array of per-page text strings (0-indexed)
 *   _tncfb3d_extracted_text   - combined full text (all pages joined)
 *   _tncfb3d_extracted_at     - timestamp of last extraction
 *   _tncfb3d_text_page_count  - number of pages extracted
 *   _tncfb3d_text_char_count  - total character count
 */
class TNC_Flipbook_3D_Text_Extract {

    const MAX_CHARS = 100000;

    public static function init() {
        // Auto-extract on flipbook save
        add_action( 'tncfb3d_after_save_meta', array( __CLASS__, 'maybe_extract_on_save' ), 30, 2 );

        // Background extraction via WP-Cron single event
        add_action( 'tncfb3d_background_extract', array( __CLASS__, 'background_extract' ) );
    }

    /**
     * Extract on save if the PDF has changed or text hasn't been extracted yet.
     */
    public static function maybe_extract_on_save( $post_id, $post ) {
        $p      = TNC_FLIPBOOK_3D_PREFIX;
        $type   = get_post_meta( $post_id, $p . 'source_type', true );

        // Only extract for PDF-based flipbooks
        if ( 'images' === $type ) {
            return;
        }

        $pdf_id = absint( get_post_meta( $post_id, $p . 'pdf_id', true ) );
        if ( ! $pdf_id ) {
            return;
        }

        // Check if already extracted for this PDF
        $last_pdf = get_post_meta( $post_id, '_tncfb3d_text_source_pdf', true );
        if ( (string) $last_pdf === (string) $pdf_id ) {
            // Same PDF - already extracted
            return;
        }

        // Extract
        self::extract( $post_id );
    }

    /**
     * Extract text from the PDF attached to a flipbook post.
     *
     * @param  int $post_id Post ID.
     * @return array Array of per-page text strings, or empty array on failure.
     */
    /** Maximum PDF file size for auto-extraction (50 MB). */
    const MAX_FILE_SIZE = 50 * 1024 * 1024;

    public static function extract( $post_id ) {
        $p      = TNC_FLIPBOOK_3D_PREFIX;
        $pdf_id = absint( get_post_meta( $post_id, $p . 'pdf_id', true ) );

        if ( ! $pdf_id ) {
            return array();
        }

        $file_path = get_attached_file( $pdf_id );
        if ( ! $file_path || ! file_exists( $file_path ) ) {
            return array();
        }

        // Check if we have enough memory for inline extraction.
        // If not, schedule a background cron job instead of crashing.
        $file_size     = filesize( $file_path );
        $memory_limit  = self::get_memory_limit();
        $memory_used   = memory_get_usage( true );
        $memory_free   = $memory_limit - $memory_used;
        $memory_needed = $file_size ? $file_size * 3 : 64 * 1024 * 1024;

        if ( ( $file_size && $file_size > self::MAX_FILE_SIZE ) || $memory_free < $memory_needed ) {
            // Not enough memory right now - schedule background extraction.
            self::schedule_background( $post_id );
            return array();
        }

        // Try PHP pdfparser first, fall back to pdftotext CLI (handles encrypted PDFs)
        $page_texts = self::extract_with_pdfparser( $file_path );

        if ( empty( $page_texts ) || '' === trim( implode( '', $page_texts ) ) ) {
            $page_texts = self::extract_with_pdftotext( $file_path );
        }

        if ( empty( $page_texts ) ) {
            return array();
        }

        // Build full text — if every page is empty, treat as extraction failure
        $full_text = implode( "\n\n", array_filter( $page_texts ) );
        if ( '' === trim( $full_text ) ) {
            update_post_meta( $post_id, '_tncfb3d_text_extract_error', 'PDF text could not be read (encrypted or scanned). Try uploading an unprotected PDF.' );
            return array();
        }
        if ( strlen( $full_text ) > self::MAX_CHARS ) {
            $full_text = substr( $full_text, 0, self::MAX_CHARS );
        }

        /**
         * @hook tncfb3d_extracted_text
         * @param string $full_text  Combined extracted text from all pages.
         * @param int    $post_id    Flipbook post ID.
         * @return string
         */
        $full_text = apply_filters( 'tncfb3d_extracted_text', $full_text, $post_id );

        // Store (and clear any previous extraction error)
        self::save( $post_id, $page_texts, $full_text, $pdf_id );
        delete_post_meta( $post_id, '_tncfb3d_text_extract_error' );

        /**
         * @hook tncfb3d_text_extracted
         * @param int $post_id Flipbook post ID.
         */
        do_action( 'tncfb3d_text_extracted', $post_id );

        return $page_texts;
    }

    /**
     * Save extracted text to post meta.
     */
    private static function save( $post_id, $page_texts, $full_text, $pdf_id ) {
        update_post_meta( $post_id, '_tncfb3d_page_texts', $page_texts );
        update_post_meta( $post_id, '_tncfb3d_extracted_text', $full_text );
        update_post_meta( $post_id, '_tncfb3d_extracted_at', current_time( 'mysql' ) );
        update_post_meta( $post_id, '_tncfb3d_text_page_count', count( $page_texts ) );
        update_post_meta( $post_id, '_tncfb3d_text_char_count', strlen( $full_text ) );
        update_post_meta( $post_id, '_tncfb3d_text_source_pdf', $pdf_id );
    }

    /**
     * Get stored per-page text array.
     *
     * @param  int $post_id
     * @return array
     */
    public static function get_page_texts( $post_id ) {
        $texts = get_post_meta( $post_id, '_tncfb3d_page_texts', true );
        return is_array( $texts ) ? $texts : array();
    }

    /**
     * Get stored full text.
     *
     * @param  int $post_id
     * @return string
     */
    public static function get_text( $post_id ) {
        return (string) get_post_meta( $post_id, '_tncfb3d_extracted_text', true );
    }

    /**
     * Check if text has been extracted for a flipbook.
     *
     * @param  int $post_id
     * @return bool
     */
    public static function has_text( $post_id ) {
        return (bool) get_post_meta( $post_id, '_tncfb3d_extracted_at', true );
    }

    /**
     * Force re-extraction (clears cached data and re-extracts).
     *
     * @param  int $post_id
     * @return array
     */
    public static function re_extract( $post_id ) {
        self::clear( $post_id );
        return self::extract( $post_id );
    }

    /**
     * Clear stored text data.
     */
    public static function clear( $post_id ) {
        delete_post_meta( $post_id, '_tncfb3d_page_texts' );
        delete_post_meta( $post_id, '_tncfb3d_extracted_text' );
        delete_post_meta( $post_id, '_tncfb3d_extracted_at' );
        delete_post_meta( $post_id, '_tncfb3d_text_page_count' );
        delete_post_meta( $post_id, '_tncfb3d_text_char_count' );
        delete_post_meta( $post_id, '_tncfb3d_text_source_pdf' );
    }

    /**
     * Clean extracted text.
     */
    /**
     * Schedule a background cron event to extract text from a large PDF.
     */
    private static function schedule_background( $post_id ) {
        if ( wp_next_scheduled( 'tncfb3d_background_extract', array( $post_id ) ) ) {
            return; // Already scheduled.
        }
        wp_schedule_single_event( time() + 10, 'tncfb3d_background_extract', array( $post_id ) );
    }

    /**
     * Background extraction handler (runs via WP-Cron with full memory available).
     */
    public static function background_extract( $post_id ) {
        // Bump memory limit for background processing.
        if ( function_exists( 'wp_raise_memory_limit' ) ) {
            wp_raise_memory_limit( 'admin' );
        }

        self::extract( $post_id );
    }

    private static function clean_text( $text ) {
        $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $text );
        $text = preg_replace( '/[ \t]+/', ' ', $text );
        $text = preg_replace( '/\n{3,}/', "\n\n", $text );
        return trim( $text );
    }

    /**
     * Get PHP memory limit in bytes.
     */
    private static function get_memory_limit() {
        $limit = ini_get( 'memory_limit' );
        if ( '-1' === $limit ) {
            return PHP_INT_MAX;
        }
        $value = (int) $limit;
        $unit  = strtolower( substr( trim( $limit ), -1 ) );
        switch ( $unit ) {
            case 'g': $value *= 1024;
            case 'm': $value *= 1024;
            case 'k': $value *= 1024;
        }
        return $value;
    }

    /**
     * Extract text using PHP smalot/pdfparser library.
     *
     * @param  string $file_path Absolute path to the PDF file.
     * @return array  Array of per-page text strings, or empty array on failure.
     */
    private static function extract_with_pdfparser( $file_path ) {
        $autoloader = TNC_FLIPBOOK_3D_DIR . 'vendor/autoload.php';
        if ( ! file_exists( $autoloader ) ) {
            return array();
        }
        require_once $autoloader;

        if ( ! class_exists( '\\Smalot\\PdfParser\\Parser' ) ) {
            return array();
        }

        $page_texts = array();

        try {
            $config = new \Smalot\PdfParser\Config();
            $config->setRetainImageContent( false );
            $config->setIgnoreEncryption( true );

            $parser = new \Smalot\PdfParser\Parser( array(), $config );
            $pdf    = $parser->parseFile( $file_path );
            $pages  = $pdf->getPages();

            foreach ( $pages as $page ) {
                $text = $page->getText();
                $text = self::clean_text( $text );
                $page_texts[] = $text;
            }
        } catch ( \Throwable $e ) {
            return array();
        }

        return $page_texts;
    }

    /**
     * Extract text using the pdftotext CLI (poppler-utils).
     * Handles encrypted/secured PDFs that PHP pdfparser cannot read.
     *
     * @param  string $file_path Absolute path to the PDF file.
     * @return array  Array of per-page text strings, or empty array if pdftotext unavailable.
     */
    private static function extract_with_pdftotext( $file_path ) {
        // Check if pdftotext is available
        $which = trim( (string) shell_exec( 'which pdftotext 2>/dev/null' ) ); // phpcs:ignore
        if ( ! $which ) {
            return array();
        }

        // Get page count first
        $count_cmd = sprintf( 'pdfinfo %s 2>/dev/null | grep "^Pages:" | awk \'{print $2}\'', escapeshellarg( $file_path ) );
        $num_pages = (int) trim( (string) shell_exec( $count_cmd ) ); // phpcs:ignore
        if ( $num_pages < 1 ) {
            // Fallback: try extracting all text as a single page
            $cmd    = sprintf( 'pdftotext %s - 2>/dev/null', escapeshellarg( $file_path ) );
            $output = (string) shell_exec( $cmd ); // phpcs:ignore
            $text   = self::clean_text( $output );
            return $text ? array( $text ) : array();
        }

        $page_texts = array();
        for ( $i = 1; $i <= $num_pages; $i++ ) {
            $cmd  = sprintf( 'pdftotext -f %d -l %d %s - 2>/dev/null', $i, $i, escapeshellarg( $file_path ) );
            $text = (string) shell_exec( $cmd ); // phpcs:ignore
            $page_texts[] = self::clean_text( $text );
        }

        return $page_texts;
    }
}
