<?php
/**
 * Bot-reputation classifier — turns the hub's declared-identity-vs-observed-
 * behaviour signal (`Seoforge_Intel_Client::bot_reputation()`) into a per-UA
 * verdict.
 *
 * READ-TIME ANNOTATION ONLY. Nothing here changes what gets tracked or how a
 * hit is stored: `verdict_for()` is consulted only when rendering a snapshot
 * group or an admin panel, never when a visit row is written. A `suspect`
 * verdict must never drop, reclassify or rewrite a row — the feed is a remote,
 * pooled signal that can itself be wrong or stale, and a write-time effect
 * would be unfixable after the fact. When the feed hasn't been fetched yet
 * (or ext-sodium is unavailable, or the hub simply has nothing to say about a
 * given family), every UA resolves to `unknown` — this class never guesses.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_Bot_Reputation {

    /** Verdicts the hub is allowed to emit; anything else is treated as absent. */
    const ALLOWED_VERDICTS = array( 'good', 'mixed', 'suspect' );

    /** Per-request memoisation, keyed by raw user-agent string. */
    private static $cache = array();

    /** True when the signed feed actually carried a bot_reputation block. */
    public static function available() {
        return null !== self::feed_data();
    }

    /**
     * `good` | `mixed` | `suspect` | `unknown` for a single user-agent string.
     * Resolution order: a matching `ua_patterns` entry (most specific) wins
     * over the declared-family lookup; either can only return `unknown`
     * when the feed has nothing to say, never as a guess.
     */
    public static function verdict_for( $user_agent ) {
        $ua = is_string( $user_agent ) ? $user_agent : '';
        if ( array_key_exists( $ua, self::$cache ) ) {
            return self::$cache[ $ua ];
        }
        $verdict           = self::resolve_verdict( $ua );
        self::$cache[ $ua ] = $verdict;
        return $verdict;
    }

    private static function resolve_verdict( $ua ) {
        $feed = self::feed_data();
        if ( null === $feed || '' === $ua ) {
            return 'unknown';
        }

        $patterns = isset( $feed['ua_patterns'] ) && is_array( $feed['ua_patterns'] ) ? $feed['ua_patterns'] : array();
        foreach ( $patterns as $p ) {
            if ( ! is_array( $p ) || empty( $p['pattern'] ) || ! is_string( $p['pattern'] ) ) {
                continue;
            }
            if ( false !== stripos( $ua, $p['pattern'] ) ) {
                $verdict = self::sanitize_verdict( $p['verdict'] ?? null );
                if ( null !== $verdict ) {
                    return $verdict;
                }
            }
        }

        $families = isset( $feed['families'] ) && is_array( $feed['families'] ) ? $feed['families'] : array();
        foreach ( self::known_tokens() as $token ) {
            if ( '' === $token || false === stripos( $ua, $token ) ) {
                continue;
            }
            // First declared-family match wins (mirrors Seoforge_Intel::classify_bot_rows()).
            if ( isset( $families[ $token ] ) && is_array( $families[ $token ] ) ) {
                $verdict = self::sanitize_verdict( $families[ $token ]['verdict'] ?? null );
                if ( null !== $verdict ) {
                    return $verdict;
                }
            }
            break;
        }

        return 'unknown';
    }

    private static function sanitize_verdict( $v ) {
        $v = is_string( $v ) ? $v : '';
        return in_array( $v, self::ALLOWED_VERDICTS, true ) ? $v : null;
    }

    /** AI-bot tokens (Seoforge_Robots) + mainstream search-engine tokens (Seoforge_Intel) — the one shared list, not duplicated here. */
    private static function known_tokens() {
        $ai   = class_exists( 'Seoforge_Robots' ) ? array_keys( Seoforge_Robots::ai_bots() ) : array();
        $main = class_exists( 'Seoforge_Intel' ) ? Seoforge_Intel::MAINSTREAM_BOTS : array();
        return array_merge( $ai, $main );
    }

    private static function feed_data() {
        if ( ! class_exists( 'Seoforge_Intel_Client' ) ) {
            return null;
        }
        $data = Seoforge_Intel_Client::bot_reputation();
        return is_array( $data ) ? $data : null;
    }
}
