<?php
/**
 * Title/tag tokenizer that produces a per-lot keyword bag.
 *
 * Pipeline: lowercase → Unicode normalize → split on non-alpha →
 * drop tokens shorter than 3 chars → drop pure-digit tokens → drop
 * stop-words. The leftover bag is what gets tallied per user as
 * dimension=keyword.
 *
 * Pre-extraction is cached into wp_af_lot_keywords on save_post_lot
 * (and on the BidSpirit importer's after-insert action) so view-time
 * lookups are a single indexed read.
 */

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

require_once __DIR__ . '/stop-words.php';

class AF_Keyword_Extractor {

    const TABLE = 'af_stats_lot_keywords';

    /**
     * Tokenize a free-text string into a unique bag of keyword slugs.
     * Pure function — easy to unit-test.
     */
    public static function tokenize($text) {
        if (empty($text)) return array();

        $text = (string) $text;

        // Strip HTML if any leaked through.
        $text = wp_strip_all_tags($text);

        // Decode HTML entities so &amp; etc. don't survive as junk.
        $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        // Lowercase + transliterate accents (so "Sèvres" → "sevres").
        $text = mb_strtolower($text, 'UTF-8');
        if (function_exists('iconv')) {
            $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
            if ($ascii !== false) $text = $ascii;
        }

        // Split on anything that isn't a-z 0-9.
        $tokens = preg_split('/[^a-z0-9]+/', $text, -1, PREG_SPLIT_NO_EMPTY);
        if (!$tokens) return array();

        $stop = array_flip(af_stats_default_stop_words());
        $out = array();

        foreach ($tokens as $tok) {
            if (strlen($tok) < 3) continue;
            if (ctype_digit($tok)) continue;
            if (isset($stop[$tok])) continue;
            $out[$tok] = true;
        }

        return array_keys($out);
    }

    /**
     * Pull every relevant text source off a lot post and tokenize.
     * Returns array of [keyword => source].
     *
     * Sources merged into the same bag:
     *   - title (highest signal)
     *   - lot_tags from wp_bp_meta_data.tags (BidSpirit free-form tags)
     *   - lot_author from wp_bp_meta_data.author (maker/artist)
     */
    public static function extract_for_post($post_id) {
        $post_id = (int) $post_id;
        if ($post_id <= 0) return array();

        $post = get_post($post_id);
        if (!$post || $post->post_type !== 'lot') return array();

        $bag = array();

        foreach (self::tokenize($post->post_title) as $kw) {
            if (!isset($bag[$kw])) $bag[$kw] = 'title';
        }

        global $wpdb;
        $bp = $wpdb->prefix . 'bp_meta_data';
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT tags, author FROM $bp WHERE postId = %d LIMIT 1",
            $post_id
        ));

        if ($row) {
            if (!empty($row->tags)) {
                foreach (self::tokenize($row->tags) as $kw) {
                    if (!isset($bag[$kw])) $bag[$kw] = 'tag';
                }
            }
            if (!empty($row->author)) {
                foreach (self::tokenize($row->author) as $kw) {
                    if (!isset($bag[$kw])) $bag[$kw] = 'author';
                }
            }
        }

        return $bag;
    }

    /**
     * Cache the bag for a post into wp_af_stats_lot_keywords.
     * Idempotent: clears existing rows then inserts the current bag.
     */
    public static function cache_for_post($post_id) {
        global $wpdb;

        if (!self::table_exists()) return false;

        $bag = self::extract_for_post($post_id);
        $table = $wpdb->prefix . self::TABLE;

        $wpdb->delete($table, array('post_id' => $post_id), array('%d'));

        if (empty($bag)) return true;

        foreach ($bag as $kw => $source) {
            $wpdb->insert($table, array(
                'post_id' => $post_id,
                'keyword' => $kw,
                'source'  => $source,
                'weight'  => 1,
            ), array('%d', '%s', '%s', '%d'));
        }

        return true;
    }

    /**
     * Read cached keywords for a post. Returns ['keyword' => source].
     * Falls through to live extraction if the cache row is missing.
     */
    public static function get_for_post($post_id) {
        global $wpdb;
        if (!self::table_exists()) return self::extract_for_post($post_id);

        $table = $wpdb->prefix . self::TABLE;
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT keyword, source FROM $table WHERE post_id = %d",
            $post_id
        ));

        if (empty($rows)) {
            self::cache_for_post($post_id);
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT keyword, source FROM $table WHERE post_id = %d",
                $post_id
            ));
        }

        $out = array();
        foreach ($rows as $r) {
            $out[$r->keyword] = $r->source;
        }
        return $out;
    }

    private static function table_exists() {
        static $exists = null;
        if ($exists !== null) return $exists;

        global $wpdb;
        $table = $wpdb->prefix . self::TABLE;
        $exists = ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) === $table);
        return $exists;
    }

    /**
     * save_post_lot hook — re-cache when a lot is created/updated.
     * Skips revisions and autosaves; defers to a shutdown action so the
     * BidSpirit importer's bulk inserts don't pay the per-row cost on the
     * critical path.
     */
    public static function on_save_post($post_id, $post = null, $update = false) {
        if (!$post_id || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)) return;
        if (wp_is_post_revision($post_id)) return;
        if (!$post) $post = get_post($post_id);
        if (!$post || $post->post_type !== 'lot') return;

        // Defer to shutdown so save_post handlers chain cheaply.
        $deferred = wp_cache_get('af_kw_pending', 'af_stats') ?: array();
        $deferred[$post_id] = true;
        wp_cache_set('af_kw_pending', $deferred, 'af_stats', 60);

        if (!has_action('shutdown', array(__CLASS__, 'flush_pending'))) {
            add_action('shutdown', array(__CLASS__, 'flush_pending'));
        }
    }

    public static function flush_pending() {
        $deferred = wp_cache_get('af_kw_pending', 'af_stats') ?: array();
        if (empty($deferred)) return;
        wp_cache_delete('af_kw_pending', 'af_stats');

        foreach (array_keys($deferred) as $post_id) {
            self::cache_for_post((int) $post_id);
        }
    }
}
