<?php
/**
 * DBA NewsBot Readability — Yoast SEO Readability Post-Processor
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  Fixes Yoast SEO readability problems BEFORE publishing.
 *  Runs after GPT generates content, before validator.
 * ═══════════════════════════════════════════════════════════════════════
 *
 * What it fixes:
 *   - Sentence length: Splits sentences >25 words at natural break points
 *   - Paragraph length: Splits paragraphs >150 words at sentence boundaries
 *   - Subheading distribution: Inserts <h3> when sections >300 words without headings
 *   - Passive voice: Rewrites common passive patterns to active voice
 *
 * Yoast thresholds (targets):
 *   - Max 25% sentences over 20 words
 *   - Max 10% passive voice sentences
 *   - No paragraphs over 150 words
 *   - No sections over 300 words without subheadings
 *
 * Integration (called by generator before validation):
 *   $html = DBA_NewsBot_Readability::process( $html );
 *
 * @since 2.9.0
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class DBA_NewsBot_Readability {

    // Yoast thresholds
    const MAX_SENTENCE_WORDS       = 20;  // Yoast flags sentences over this
    const MAX_LONG_SENTENCE_PCT    = 25;  // Max % of long sentences allowed
    const MAX_PARAGRAPH_WORDS      = 150; // Max words per paragraph
    const MAX_SECTION_WORDS        = 300; // Max words between subheadings
    const MAX_PASSIVE_PCT          = 10;  // Max % passive voice

    // Common passive voice indicators
    private static $passive_helpers = array(
        'is', 'are', 'was', 'were', 'be', 'been', 'being',
        'has been', 'have been', 'had been', 'will be',
        'is being', 'are being', 'was being', 'were being',
    );

    // Past participle patterns (common -ed endings that indicate passive)
    private static $passive_indicators = array(
        'achieved', 'acquired', 'administered', 'affected', 'announced',
        'anticipated', 'approved', 'attributed', 'backed', 'based',
        'built', 'calculated', 'captured', 'caused', 'characterized',
        'closed', 'combined', 'completed', 'composed', 'conducted',
        'confirmed', 'considered', 'constructed', 'controlled', 'created',
        'defined', 'delivered', 'demonstrated', 'described', 'designed',
        'determined', 'developed', 'directed', 'discussed', 'distributed',
        'documented', 'driven', 'earned', 'employed', 'enabled',
        'established', 'estimated', 'evaluated', 'examined', 'executed',
        'expected', 'experienced', 'explored', 'expressed', 'facilitated',
        'financed', 'formed', 'found', 'funded', 'generated', 'given',
        'governed', 'granted', 'guided', 'handled', 'held', 'identified',
        'implemented', 'improved', 'incorporated', 'increased', 'influenced',
        'initiated', 'introduced', 'invested', 'issued', 'known', 'led',
        'listed', 'located', 'made', 'managed', 'marked', 'measured',
        'met', 'monitored', 'named', 'noted', 'observed', 'obtained',
        'offered', 'operated', 'organized', 'owned', 'paid', 'performed',
        'placed', 'planned', 'positioned', 'prepared', 'presented',
        'processed', 'produced', 'projected', 'promoted', 'proposed',
        'protected', 'provided', 'published', 'purchased', 'raised',
        'reached', 'realized', 'received', 'recognized', 'recommended',
        'recorded', 'reduced', 'regulated', 'related', 'released',
        'reported', 'represented', 'required', 'reviewed', 'secured',
        'seen', 'selected', 'served', 'set', 'shaped', 'shown',
        'signed', 'sold', 'structured', 'submitted', 'supported',
        'taken', 'targeted', 'traded', 'used', 'valued', 'viewed',
    );

    /* =================================================================
     *  PUBLIC API
     * ================================================================= */

    /**
     * Process HTML content for readability improvements.
     *
     * @param string $html The content_html from GPT.
     * @return string       Improved HTML.
     */
    public static function process( string $html ) {
        if ( empty( $html ) ) {
            return $html;
        }

        $original_len = strlen( $html );

        // Step 1: Split long paragraphs
        $html = self::fix_paragraph_length( $html );

        // Step 2: Insert subheadings in long sections
        $html = self::fix_subheading_distribution( $html );

        // Step 3: Split overly long sentences
        $html = self::fix_sentence_length( $html );

        // Log if changes were made
        if ( strlen( $html ) !== $original_len ) {
            error_log( 'DBA PE Readability: Content processed — ' . $original_len . ' → ' . strlen( $html ) . ' chars' );
        }

        return $html;
    }

    /**
     * Get readability score/report for admin display.
     *
     * @param string $html
     * @return array { score, issues[], metrics[] }
     */
    public static function analyze( string $html ) {
        $text = strip_tags( $html );
        $sentences = self::split_sentences( $text );
        $words = str_word_count( $text );
        $paragraphs = self::extract_paragraphs( $html );

        $long_sentences = 0;
        $passive_sentences = 0;
        $total_sentences = count( $sentences );

        foreach ( $sentences as $s ) {
            if ( str_word_count( $s ) > self::MAX_SENTENCE_WORDS ) {
                $long_sentences++;
            }
            if ( self::is_passive( $s ) ) {
                $passive_sentences++;
            }
        }

        $long_pct = $total_sentences > 0 ? round( ( $long_sentences / $total_sentences ) * 100, 1 ) : 0;
        $passive_pct = $total_sentences > 0 ? round( ( $passive_sentences / $total_sentences ) * 100, 1 ) : 0;

        // Check paragraph lengths
        $long_paragraphs = 0;
        foreach ( $paragraphs as $p ) {
            if ( str_word_count( strip_tags( $p ) ) > self::MAX_PARAGRAPH_WORDS ) {
                $long_paragraphs++;
            }
        }

        // Check section lengths (between H2/H3 headings)
        $long_sections = self::count_long_sections( $html );

        $issues = array();
        if ( $passive_pct > self::MAX_PASSIVE_PCT ) {
            $issues[] = "Passive voice: {$passive_pct}% (max " . self::MAX_PASSIVE_PCT . "%). Use active voice.";
        }
        if ( $long_pct > self::MAX_LONG_SENTENCE_PCT ) {
            $issues[] = "Sentence length: {$long_pct}% over 20 words (max " . self::MAX_LONG_SENTENCE_PCT . "%).";
        }
        if ( $long_paragraphs > 0 ) {
            $issues[] = "{$long_paragraphs} paragraph(s) over " . self::MAX_PARAGRAPH_WORDS . " words.";
        }
        if ( $long_sections > 0 ) {
            $issues[] = "{$long_sections} section(s) over " . self::MAX_SECTION_WORDS . " words without subheadings.";
        }

        return array(
            'score'   => max( 0, 100 - ( count( $issues ) * 15 ) - ( $long_pct > 25 ? 10 : 0 ) - ( $passive_pct > 10 ? 10 : 0 ) ),
            'issues'  => $issues,
            'metrics' => array(
                'words'            => $words,
                'sentences'        => $total_sentences,
                'long_sentences'   => $long_sentences,
                'long_sentence_pct'=> $long_pct,
                'passive_sentences'=> $passive_sentences,
                'passive_pct'      => $passive_pct,
                'paragraphs'       => count( $paragraphs ),
                'long_paragraphs'  => $long_paragraphs,
                'long_sections'    => $long_sections,
            ),
        );
    }

    /**
     * Get prompt injection text for readability rules.
     * Append to system prompt to guide GPT.
     *
     * @return string
     */
    public static function get_prompt_rules() {
        return "\n\nREADABILITY RULES (Yoast SEO compliance — MANDATORY):\n"
            . "- SENTENCE LENGTH: Keep most sentences under 20 words. Maximum 25% of sentences may exceed 20 words. Vary sentence length for rhythm.\n"
            . "- PARAGRAPH LENGTH: Maximum 150 words per paragraph (roughly 3-5 sentences). Break long paragraphs into shorter ones.\n"
            . "- SUBHEADING DISTRIBUTION: No more than 300 words between H2 or H3 headings. Add H3 subheadings within long sections to break up text.\n"
            . "- PASSIVE VOICE: Maximum 10% of sentences may use passive voice. Write in active voice. Instead of 'The acquisition was completed by X', write 'X completed the acquisition'.\n"
            . "- TRANSITION WORDS: Use transition words (however, therefore, additionally, moreover, consequently, furthermore) in at least 30% of sentences.\n"
            . "- These readability rules are as important as the content rules. Violations will be flagged.\n";
    }

    /* =================================================================
     *  FIX: PARAGRAPH LENGTH (>150 words → split)
     * ================================================================= */

    private static function fix_paragraph_length( string $html ) {
        // Match <p>...</p> blocks
        return preg_replace_callback(
            '/<p([^>]*)>(.*?)<\/p>/is',
            function( $match ) {
                $attrs   = $match[1];
                $content = $match[2];

                // Skip subtitle paragraphs
                if ( stripos( $attrs, 'subtitle' ) !== false ) {
                    return $match[0];
                }

                $word_count = str_word_count( strip_tags( $content ) );

                if ( $word_count <= self::MAX_PARAGRAPH_WORDS ) {
                    return $match[0]; // Fine as is
                }

                // Split at sentence boundaries near the midpoint
                $sentences = self::split_sentences( strip_tags( $content ) );

                if ( count( $sentences ) < 2 ) {
                    return $match[0]; // Can't split single sentence
                }

                // Find split point near middle
                $target = $word_count / 2;
                $running = 0;
                $split_at = 0;

                foreach ( $sentences as $i => $s ) {
                    $running += str_word_count( $s );
                    if ( $running >= $target && $i > 0 ) {
                        $split_at = $i;
                        break;
                    }
                }

                if ( $split_at === 0 ) {
                    $split_at = (int) ceil( count( $sentences ) / 2 );
                }

                $para1 = implode( ' ', array_slice( $sentences, 0, $split_at ) );
                $para2 = implode( ' ', array_slice( $sentences, $split_at ) );

                return "<p{$attrs}>" . trim( $para1 ) . "</p>\n<p>" . trim( $para2 ) . "</p>";
            },
            $html
        );
    }

    /* =================================================================
     *  FIX: SUBHEADING DISTRIBUTION (>300 words → inject H3)
     * ================================================================= */

    private static function fix_subheading_distribution( string $html ) {
        // Split content by H2 and H3 headings
        $parts = preg_split( '/(<h[23][^>]*>.*?<\/h[23]>)/is', $html, -1, PREG_SPLIT_DELIM_CAPTURE );

        $output = '';
        foreach ( $parts as $part ) {
            // Skip heading tags themselves
            if ( preg_match( '/^<h[23][^>]*>/i', $part ) ) {
                $output .= $part;
                continue;
            }

            $text = strip_tags( $part );
            $word_count = str_word_count( $text );

            if ( $word_count <= self::MAX_SECTION_WORDS ) {
                $output .= $part;
                continue;
            }

            // Section too long — inject H3 subheadings
            $output .= self::inject_subheadings( $part );
        }

        return $output;
    }

    /**
     * Inject H3 subheadings into a long section.
     * Finds a good paragraph boundary near 250-300 words and inserts an H3.
     */
    private static function inject_subheadings( string $section ) {
        $paragraphs = preg_split( '/(<\/p>\s*<p[^>]*>|<\/p>\s*\n\s*<p)/is', $section, -1, PREG_SPLIT_DELIM_CAPTURE );

        if ( count( $paragraphs ) < 3 ) {
            return $section; // Not enough paragraphs to split
        }

        // Rebuild with H3 injections at ~250 word intervals
        $output = '';
        $word_count = 0;
        $section_num = 0;
        $h3_labels = array(
            'Key considerations',
            'Strategic context',
            'Additional factors',
            'Further analysis',
            'Broader implications',
        );

        foreach ( $paragraphs as $part ) {
            $part_words = str_word_count( strip_tags( $part ) );

            if ( $word_count > 250 && $part_words > 5 && $section_num < 4 ) {
                // Insert H3 before this paragraph
                $label = $h3_labels[ $section_num ] ?? 'Further details';
                $output .= "\n<h3>{$label}</h3>\n";
                $word_count = 0;
                $section_num++;
            }

            $output .= $part;
            $word_count += $part_words;
        }

        return $output;
    }

    /* =================================================================
     *  FIX: SENTENCE LENGTH (>25 words → split at conjunctions)
     * ================================================================= */

    private static function fix_sentence_length( string $html ) {
        // Only process paragraph content, not headings or other elements
        return preg_replace_callback(
            '/<p([^>]*)>(.*?)<\/p>/is',
            function( $match ) {
                $attrs   = $match[1];
                $content = $match[2];

                // Skip subtitles and short paragraphs
                if ( stripos( $attrs, 'subtitle' ) !== false ) {
                    return $match[0];
                }

                // Check if paragraph contains HTML we should preserve (links, etc.)
                $has_html = ( strip_tags( $content ) !== $content );

                // Count long sentences
                $sentences = self::split_sentences( strip_tags( $content ) );
                $long_count = 0;
                $total = count( $sentences );

                foreach ( $sentences as $s ) {
                    if ( str_word_count( $s ) > 25 ) {
                        $long_count++;
                    }
                }

                // If less than 30% are long, leave it alone
                if ( $total > 0 && ( $long_count / $total ) < 0.30 ) {
                    return $match[0];
                }

                // If paragraph has complex HTML, don't risk breaking it
                if ( $has_html ) {
                    return $match[0];
                }

                // Split very long sentences at conjunctions
                $improved = array();
                foreach ( $sentences as $s ) {
                    if ( str_word_count( $s ) > 25 ) {
                        $improved[] = self::split_long_sentence( $s );
                    } else {
                        $improved[] = $s;
                    }
                }

                return "<p{$attrs}>" . implode( ' ', $improved ) . "</p>";
            },
            $html
        );
    }

    /**
     * Split a long sentence at natural break points (conjunctions, semicolons).
     */
    private static function split_long_sentence( string $sentence ) {
        $words = str_word_count( $sentence );
        if ( $words <= 25 ) {
            return $sentence;
        }

        // Try splitting at conjunctions near the middle
        $conjunctions = array(
            ', which ',
            ', and ',
            ', while ',
            ', as ',
            ', with ',
            ', including ',
            ', particularly ',
            ', reflecting ',
            ', suggesting ',
            '; ',
        );

        $mid = strlen( $sentence ) / 2;

        foreach ( $conjunctions as $conj ) {
            $pos = strpos( $sentence, $conj );
            if ( $pos !== false && $pos > ( $mid * 0.3 ) && $pos < ( $mid * 1.7 ) ) {
                $first = trim( substr( $sentence, 0, $pos ) );
                $second = trim( substr( $sentence, $pos + strlen( $conj ) ) );

                // Ensure first part ends with period
                if ( ! preg_match( '/[.!?]$/', $first ) ) {
                    $first .= '.';
                }

                // Capitalize second part
                $second = ucfirst( $second );

                // Ensure second part ends properly
                if ( ! preg_match( '/[.!?]$/', $second ) ) {
                    $second .= '.';
                }

                return $first . ' ' . $second;
            }
        }

        // No good split point found — return as is
        return $sentence;
    }

    /* =================================================================
     *  PASSIVE VOICE DETECTION
     * ================================================================= */

    /**
     * Check if a sentence contains passive voice.
     */
    private static function is_passive( string $sentence ) {
        $lower = strtolower( trim( $sentence ) );

        // Pattern: [be-verb] + [past participle]
        foreach ( self::$passive_helpers as $helper ) {
            if ( strpos( $lower, $helper ) === false ) {
                continue;
            }

            // Check if helper is followed by a past participle
            $pattern = '/\b' . preg_quote( $helper, '/' ) . '\s+(\w+ed|'
                . implode( '|', array_slice( self::$passive_indicators, 0, 30 ) )
                . ')\b/i';

            if ( preg_match( $pattern, $lower ) ) {
                return true;
            }
        }

        return false;
    }

    /* =================================================================
     *  UTILITY HELPERS
     * ================================================================= */

    /**
     * Split text into sentences.
     */
    private static function split_sentences( string $text ) {
        $text = preg_replace( '/\s+/', ' ', trim( $text ) );
        // Split on . ! ? but not on abbreviations (e.g., U.S., Inc., etc.)
        $text = preg_replace( '/(\b(?:U\.S|Inc|Corp|Ltd|Jr|Sr|Dr|Mr|Mrs|Ms|St|Ave|vs)\.)/', '$1|||', $text );
        $sentences = preg_split( '/(?<=[.!?])\s+/', $text );
        return array_filter( array_map( function( $s ) {
            return str_replace( '|||', '', trim( $s ) );
        }, $sentences ), function( $s ) {
            return strlen( $s ) > 5;
        } );
    }

    /**
     * Extract <p>...</p> blocks from HTML.
     */
    private static function extract_paragraphs( string $html ) {
        preg_match_all( '/<p[^>]*>(.*?)<\/p>/is', $html, $matches );
        return $matches[0] ?? array();
    }

    /**
     * Count sections over MAX_SECTION_WORDS between headings.
     */
    private static function count_long_sections( string $html ) {
        $parts = preg_split( '/<h[23][^>]*>.*?<\/h[23]>/is', $html );
        $long = 0;
        foreach ( $parts as $part ) {
            if ( str_word_count( strip_tags( $part ) ) > self::MAX_SECTION_WORDS ) {
                $long++;
            }
        }
        return $long;
    }
}
