<?php
/**
 * On-page SEO analyzer — produces a 0..100 score per post from 9 weighted
 * checks, each ok/warn/fail:
 *   - title length (30–65 chars OK, 20–75 warn)
 *   - meta description length (120–165 chars OK, 70–200 warn)
 *   - focus keyword in title
 *   - focus keyword in URL slug
 *   - focus keyword in first 1500 chars of body
 *   - internal-link count
 *   - image-alt coverage
 *   - word count
 *   - readability (Flesch DE — Amstad)
 *
 * Runs on save_post (debounced) and on demand from the SEO meta box / list table.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_Analyzer {

    public static function register( Seoforge_Loader $loader ) {
        $loader->add_action( 'save_post', __CLASS__, 'on_save', 20, 2 );
    }

    public static function on_save( $post_id, $post ) {
        if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
            return;
        }
        if ( 'publish' !== $post->post_status && 'draft' !== $post->post_status ) {
            return;
        }
        $public_types = get_post_types( array( 'public' => true ) );
        if ( ! in_array( $post->post_type, $public_types, true ) ) {
            return;
        }
        self::analyze_and_save( $post_id );
    }

    public static function analyze_and_save( $post_id ) {
        $result = self::analyze( $post_id );
        $row    = Seoforge_Meta::get_post_meta( $post_id );
        Seoforge_Meta::save_post_meta( $post_id, array_merge(
            $row,
            array(
                'score'         => $result['score'],
                'score_payload' => $result['checks'],
            )
        ) );
        return $result;
    }

    /**
     * Returns ['score' => int 0..100, 'checks' => array of named results].
     */
    public static function analyze( $post_id ) {
        $post = get_post( $post_id );
        if ( ! $post ) {
            return array( 'score' => 0, 'checks' => array() );
        }
        $meta    = Seoforge_Meta::get_post_meta( $post_id );
        $title   = ! empty( $meta['seo_title'] ) ? $meta['seo_title'] : get_the_title( $post_id );
        $desc    = ! empty( $meta['seo_description'] ) ? $meta['seo_description'] : '';
        $focus   = ! empty( $meta['focus_keyword'] ) ? mb_strtolower( $meta['focus_keyword'] ) : '';
        $content = wp_strip_all_tags( strip_shortcodes( $post->post_content ) );
        $html    = $post->post_content;
        $slug    = $post->post_name;

        $checks = array();

        // 1. Title length
        $title_len = mb_strlen( $title );
        $checks['title_length'] = self::ok_warn_fail(
            ( $title_len >= 30 && $title_len <= 65 ),
            ( $title_len >= 20 && $title_len <= 75 ),
            sprintf(
                /* translators: %d: SEO title length in characters */
                __( 'Title is %d chars (50–60 ideal).', 'brainwerk-seo-suite' ),
                $title_len
            ),
            __( 'Good length — keep it around 50–60 characters so it displays fully in search results.', 'brainwerk-seo-suite' ),
            $title_len < 30
                ? __( 'Title is too short. Add a few more descriptive words to reach 50–60 characters.', 'brainwerk-seo-suite' )
                : __( 'Title is too long and risks being truncated in search results. Trim it to about 50–60 characters.', 'brainwerk-seo-suite' )
        );

        // 2. Meta description length
        $desc_len = mb_strlen( $desc );
        $checks['meta_description'] = self::ok_warn_fail(
            ( $desc_len >= 120 && $desc_len <= 165 ),
            ( $desc_len >= 70 && $desc_len <= 200 ),
            $desc_len === 0
                ? __( 'No meta description set.', 'brainwerk-seo-suite' )
                : sprintf(
                    /* translators: %d: meta description length in characters */
                    __( 'Description is %d chars (120–160 ideal).', 'brainwerk-seo-suite' ),
                    $desc_len
                ),
            __( 'Good length — keep it around 120–160 characters.', 'brainwerk-seo-suite' ),
            $desc_len === 0
                ? __( 'Write a compelling 120–160 character meta description that summarizes the page and includes the focus keyword.', 'brainwerk-seo-suite' )
                : ( $desc_len < 120
                    ? __( 'Description is a bit short. Expand it to 120–160 characters to fill the space search engines show.', 'brainwerk-seo-suite' )
                    : __( 'Description is too long and may get cut off in search results. Trim it to about 120–160 characters.', 'brainwerk-seo-suite' ) )
        );

        // 3. Focus keyword in title
        $checks['focus_in_title'] = $focus === ''
            ? array(
                'status'  => 'warn',
                'message' => __( 'No focus keyword set.', 'brainwerk-seo-suite' ),
                'tip'     => __( 'Set a focus keyword for this post so the analyzer can check whether it appears in the title.', 'brainwerk-seo-suite' ),
            )
            : self::tokens_in_haystack(
                $focus,
                $title,
                __( 'Focus keyword (or all of its words) appears in the title.', 'brainwerk-seo-suite' ),
                __( 'Some of the focus-keyword words are missing from the title.', 'brainwerk-seo-suite' ),
                __( 'Focus keyword is missing from the title.', 'brainwerk-seo-suite' ),
                __( 'Good — the focus keyword is clearly represented in the title.', 'brainwerk-seo-suite' ),
                __( 'Work the focus keyword — or all of its important words — into the title, ideally near the beginning.', 'brainwerk-seo-suite' )
            );

        // 4. Focus keyword in slug
        $checks['focus_in_slug'] = $focus === ''
            ? array(
                'status'  => 'warn',
                'message' => __( 'Set a focus keyword to enable slug check.', 'brainwerk-seo-suite' ),
                'tip'     => __( 'Set a focus keyword for this post so the analyzer can check the URL slug.', 'brainwerk-seo-suite' ),
            )
            : self::tokens_in_haystack(
                $focus,
                str_replace( '-', ' ', $slug ),
                __( 'Focus-keyword words appear in the URL slug.', 'brainwerk-seo-suite' ),
                __( 'Some focus-keyword words are missing from the URL slug.', 'brainwerk-seo-suite' ),
                __( 'Focus keyword is missing from the URL slug.', 'brainwerk-seo-suite' ),
                __( 'Good — the URL slug reflects the focus keyword.', 'brainwerk-seo-suite' ),
                __( 'Edit the permalink slug to include the focus keyword; short, keyword-rich URLs tend to perform better.', 'brainwerk-seo-suite' )
            );

        // 5. Focus keyword in first paragraph (first 150 words of body text)
        $checks['focus_in_intro'] = $focus === ''
            ? array(
                'status'  => 'warn',
                'message' => __( 'Set a focus keyword to enable intro check.', 'brainwerk-seo-suite' ),
                'tip'     => __( 'Set a focus keyword for this post so the analyzer can check the opening paragraph.', 'brainwerk-seo-suite' ),
            )
            : self::tokens_in_haystack(
                $focus,
                mb_substr( $content, 0, 1500 ),
                __( 'Focus-keyword words appear in the intro.', 'brainwerk-seo-suite' ),
                __( 'Some focus-keyword words are missing from the intro.', 'brainwerk-seo-suite' ),
                __( 'Focus keyword does not appear in the first paragraph.', 'brainwerk-seo-suite' ),
                __( 'Good — the focus keyword appears early in the content.', 'brainwerk-seo-suite' ),
                __( 'Mention the focus keyword within the first paragraph so readers and search engines see it right away.', 'brainwerk-seo-suite' )
            );

        // 6. Internal links
        $own_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
        preg_match_all( '/<a\b[^>]+href=[\'"]([^\'"]+)[\'"]/i', $html, $links );
        $internal = 0;
        foreach ( $links[1] as $href ) {
            $h = wp_parse_url( $href, PHP_URL_HOST );
            if ( ! $h || $h === $own_host ) {
                $internal++;
            }
        }
        $checks['internal_links'] = self::ok_warn_fail(
            ( $internal >= 2 ),
            ( $internal >= 1 ),
            sprintf(
                /* translators: %d: number of internal links found in the post body */
                __( '%d internal link(s) found (≥2 recommended).', 'brainwerk-seo-suite' ),
                $internal
            ),
            __( 'Good internal linking.', 'brainwerk-seo-suite' ),
            __( 'Add links to at least 2 related posts or pages on your own site — internal links help visitors and search engines discover more content.', 'brainwerk-seo-suite' )
        );

        // 7. Image alt coverage
        preg_match_all( '/<img\b([^>]*)>/i', $html, $imgs );
        $img_total = count( $imgs[0] );
        $img_alt   = 0;
        foreach ( $imgs[1] as $attrs ) {
            if ( preg_match( '/\balt\s*=\s*[\'"][^\'"]+[\'"]/i', $attrs ) ) {
                $img_alt++;
            }
        }
        if ( $img_total === 0 ) {
            $checks['image_alt'] = array(
                'status'  => 'warn',
                'message' => __( 'No images in this post.', 'brainwerk-seo-suite' ),
                'tip'     => __( 'If you add images later, remember to give each one descriptive alt text.', 'brainwerk-seo-suite' ),
            );
        } else {
            $coverage = $img_total > 0 ? ( $img_alt / $img_total ) : 1;
            $checks['image_alt'] = self::ok_warn_fail(
                ( $coverage >= 0.95 ),
                ( $coverage >= 0.5 ),
                sprintf(
                    /* translators: 1: number of images with alt text, 2: total number of images in the post */
                    __( '%1$d of %2$d images have alt text.', 'brainwerk-seo-suite' ),
                    $img_alt,
                    $img_total
                ),
                __( 'Good — most or all images have alt text.', 'brainwerk-seo-suite' ),
                __( 'Add descriptive alt text to the remaining images; it helps accessibility and image-search rankings.', 'brainwerk-seo-suite' )
            );
        }

        // 8. Word count
        $words = preg_match_all( '/\S+/u', $content, $m );
        $checks['word_count'] = self::ok_warn_fail(
            ( $words >= 300 ),
            ( $words >= 150 ),
            sprintf(
                /* translators: %d: word count of post content */
                __( '%d words (≥300 recommended).', 'brainwerk-seo-suite' ),
                $words
            ),
            __( 'Good length — this content has enough substance for search engines to work with.', 'brainwerk-seo-suite' ),
            __( 'Expand the content to at least 300 words. More in-depth, useful content tends to rank and convert better.', 'brainwerk-seo-suite' )
        );

        // 9. Readability — Flesch German (Amstad)
        $flesch = self::flesch_de( $content );
        $checks['readability'] = self::ok_warn_fail(
            ( $flesch >= 60 ),
            ( $flesch >= 40 ),
            sprintf(
                /* translators: %.0f: Flesch readability score (0–100, higher = easier) */
                __( 'Readability %.0f / 100 (≥60 = easy).', 'brainwerk-seo-suite' ),
                $flesch
            ),
            __( 'Good — the text is easy to read.', 'brainwerk-seo-suite' ),
            __( 'Shorten your sentences and prefer simpler, everyday words to make the text easier to read.', 'brainwerk-seo-suite' )
        );

        // Aggregate score: weighted sum.
        $weights = array(
            'title_length'    => 12,
            'meta_description'=> 12,
            'focus_in_title'  => 12,
            'focus_in_slug'   => 6,
            'focus_in_intro'  => 10,
            'internal_links'  => 10,
            'image_alt'       => 10,
            'word_count'      => 10,
            'readability'     => 18,
        );
        $score = 0;
        foreach ( $weights as $key => $w ) {
            $st = $checks[ $key ]['status'];
            $score += ( 'ok' === $st ? 1 : ( 'warn' === $st ? 0.5 : 0 ) ) * $w;
        }
        $score = (int) round( $score );

        return array(
            'score'  => max( 0, min( 100, $score ) ),
            'checks' => $checks,
        );
    }

    private static function ok_warn_fail( $ok, $warn, $message, $tip_ok = '', $tip_needs_work = '' ) {
        if ( $ok ) {
            return array( 'status' => 'ok', 'message' => $message, 'tip' => $tip_ok );
        }
        if ( $warn ) {
            return array( 'status' => 'warn', 'message' => $message, 'tip' => $tip_needs_work );
        }
        return array( 'status' => 'fail', 'message' => $message, 'tip' => $tip_needs_work );
    }

    /**
     * Token-aware substring check. Multi-word focus keywords rarely appear in
     * the same exact order in titles ("Flag Football Schule" vs "Flag Football
     * in der Schule"), so we tokenize and report:
     *   ok    — exact match OR every token present
     *   warn  — at least half of tokens present
     *   fail  — fewer than half
     * Tokens shorter than 3 chars are ignored (filler words).
     */
    private static function tokens_in_haystack( $needle, $haystack, $msg_ok, $msg_warn, $msg_fail, $tip_ok = '', $tip_needs_work = '' ) {
        $needle   = trim( (string) $needle );
        $haystack = (string) $haystack;
        if ( '' === $needle ) {
            return array( 'status' => 'warn', 'message' => $msg_fail, 'tip' => $tip_needs_work );
        }
        // Exact phrase wins immediately.
        if ( false !== mb_stripos( $haystack, $needle ) ) {
            return array( 'status' => 'ok', 'message' => $msg_ok, 'tip' => $tip_ok );
        }
        $tokens = preg_split( '/\s+/u', mb_strtolower( $needle ) );
        $tokens = array_filter( (array) $tokens, function ( $t ) {
            return mb_strlen( $t ) >= 3;
        } );
        if ( empty( $tokens ) ) {
            return array( 'status' => 'fail', 'message' => $msg_fail, 'tip' => $tip_needs_work );
        }
        $haystack_lc = mb_strtolower( $haystack );
        $hits = 0;
        foreach ( $tokens as $t ) {
            if ( false !== mb_stripos( $haystack_lc, $t ) ) {
                $hits++;
            }
        }
        $ratio = $hits / count( $tokens );
        if ( $ratio >= 1.0 ) {
            return array( 'status' => 'ok', 'message' => $msg_ok, 'tip' => $tip_ok );
        }
        if ( $ratio >= 0.5 ) {
            return array( 'status' => 'warn', 'message' => $msg_warn, 'tip' => $tip_needs_work );
        }
        return array( 'status' => 'fail', 'message' => $msg_fail, 'tip' => $tip_needs_work );
    }

    /**
     * Approximate Flesch reading ease for German (Amstad formula).
     *   FRE = 180 − ASL − 58.5 × ASW
     *   ASL = avg sentence length (words/sentence)
     *   ASW = avg syllables per word (heuristic: count vowel groups)
     */
    public static function flesch_de( $text ) {
        $text = trim( preg_replace( '/\s+/u', ' ', $text ) );
        if ( '' === $text ) {
            return 0.0;
        }
        $sentences = preg_split( '/[.!?]+/u', $text, -1, PREG_SPLIT_NO_EMPTY );
        $sentence_count = max( 1, count( $sentences ) );

        preg_match_all( '/\p{L}+/u', $text, $word_matches );
        $words = $word_matches[0];
        $word_count = max( 1, count( $words ) );

        $syllables = 0;
        foreach ( $words as $w ) {
            $w = mb_strtolower( $w );
            // Treat each vowel group as one syllable.
            preg_match_all( '/[aeiouäöü]+/u', $w, $vm );
            $count = max( 1, count( $vm[0] ) );
            $syllables += $count;
        }
        $asl = $word_count / $sentence_count;
        $asw = $syllables / $word_count;
        $fre = 180 - $asl - ( 58.5 * $asw );
        return max( 0, min( 100, round( $fre, 1 ) ) );
    }
}
