<?php

/**
 * LolaCore - PlaybookInjector
 *
 * Delivers playbook knowledge via two channels:
 *  - Ability usage: attaches the domain manual to tool responses (once per request).
 *  - Model-adaptive hints: appends a PLAYBOOKS block to the system prompt when
 *    trigger keywords match (priority 21, after Skills at 20).
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Playbooks;

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

class PlaybookInjector
{
    public const FILTER_HOOK           = 'lolacore_system_prompt';
    public const FILTER_PRIORITY         = 21;
    public const ACTION_RESPONSE_FILTER  = 'lolacore_action_response';

    public const TURN_CHAR_BUDGET = 6000;

    /**
     * Substrings that identify capable models — always get a short read_playbook hint (D3).
     * Checked before weak patterns. Order within the list does not matter.
     *
     * @var array<int,string>
     */
    private const STRONG_MODEL_SUBSTRINGS = [
        // OpenAI — GPT-5.6 family (Jun 2026) and reasoning tiers
        'gpt-5.6-sol',
        'gpt-5.6-terra',
        'gpt-5-sol',
        'gpt-5-terra',
        '-sol',
        'o3-pro',
        'o3-mini',
        'o4-mini',
        'o1-pro',
        'o1-preview',
        // Anthropic — Opus / Sonnet / Fable (Jul 2026)
        'claude-opus',
        'claude-sonnet',
        'claude-fable',
        'claude-mythos',
        // Google — Pro and agentic Flash (3.5 Flash rivals Pro on coding/agents)
        'gemini-3.1-pro',
        'gemini-3.5-pro',
        'gemini-3.5-flash',
        'gemini-2.5-pro',
        // DeepSeek — V4 Pro and legacy reasoning alias
        'deepseek-v4-pro',
        'deepseek-reasoner',
    ];

    /**
     * Substrings that identify lightweight models — inline playbook body in the prompt (D3).
     *
     * @var array<int,string>
     */
    private const WEAK_MODEL_SUBSTRINGS = [
        // OpenAI — budget / nano tiers
        'gpt-5.6-luna',
        'gpt-5-luna',
        '-luna',
        'gpt-4.1-nano',
        'gpt-4o-mini',
        'gpt-3.5',
        'nano',
        'mini',
        // Anthropic — Haiku
        'haiku',
        // Google — Flash-Lite and legacy Flash
        'flash-lite',
        'flash_lite',
        'gemini-3.1-flash-lite',
        'gemini-2.5-flash-lite',
        'gemini-1.5-flash',
        'gemini-2.0-flash',
        // DeepSeek — V4 Flash and legacy chat alias
        'deepseek-v4-flash',
        'deepseek-chat',
    ];

    private static string $currentMessage = '';
    private static string $currentIntent  = '';
    private static string $currentProvider = '';
    private static string $currentModel    = '';
    private static bool $attachedThisRequest = false;

    private PlaybookRegistry $registry;

    public function __construct(PlaybookRegistry $registry)
    {
        $this->registry = $registry;
    }

    public function register(): void
    {
        if (!function_exists('add_filter')) {
            return;
        }
        add_filter(self::FILTER_HOOK, [$this, 'inject'], self::FILTER_PRIORITY, 1);
        add_filter(self::ACTION_RESPONSE_FILTER, [$this, 'enrichActionResponse'], 10, 2);
    }

    public static function setCurrentMessage(string $message, string $intent = ''): void
    {
        self::$currentMessage = mb_strtolower(trim($message), 'UTF-8');
        self::$currentIntent  = $intent;
    }

    public static function setCurrentModel(string $provider, string $model): void
    {
        self::$currentProvider = strtolower(trim($provider));
        self::$currentModel    = strtolower(trim($model));
    }

    public static function resetContext(): void
    {
        self::$currentMessage        = '';
        self::$currentIntent         = '';
        self::$currentProvider       = '';
        self::$currentModel          = '';
        self::$attachedThisRequest   = false;
    }

    public static function getCurrentMessage(): string
    {
        return self::$currentMessage;
    }

    public static function getCurrentModel(): string
    {
        return self::$currentModel;
    }

    public static function isWeakModel(?string $modelId = null): bool
    {
        $modelId = strtolower($modelId ?? self::$currentModel);
        if ($modelId === '') {
            return false;
        }

        if (self::matchesAnySubstring($modelId, self::STRONG_MODEL_SUBSTRINGS)) {
            return false;
        }

        return self::matchesAnySubstring($modelId, self::WEAK_MODEL_SUBSTRINGS);
    }

    /**
     * @param array<int,string> $needles
     */
    private static function matchesAnySubstring(string $haystack, array $needles): bool
    {
        foreach ($needles as $needle) {
            if ($needle !== '' && str_contains($haystack, $needle)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Filter callback — append model-adaptive playbook hints.
     */
    public function inject(string $prompt): string
    {
        $message = self::$currentMessage;
        if ($message === '') {
            return $prompt;
        }

        $matched = $this->matchHintPlaybooks($message);
        if ($matched === []) {
            return $prompt;
        }

        $lines   = [];
        $used    = 0;
        $touched = [];

        foreach ($matched as $playbook) {
            $chunk = $this->formatHintChunk($playbook);
            if ($chunk === '') {
                continue;
            }
            $len = mb_strlen($chunk, 'UTF-8');
            if ($used + $len > self::TURN_CHAR_BUDGET) {
                break;
            }
            $lines[] = $chunk;
            $used   += $len;
            $touched[] = $playbook;
        }

        if ($lines === []) {
            return $prompt;
        }

        $this->touchUsage($touched);

        return $prompt . "\n\nPLAYBOOKS:\n" . implode("\n\n", $lines);
    }

    /**
     * Attach playbook body to a tool response when the action matches a registered prefix.
     *
     * @param array<string,mixed> $response
     * @return array<string,mixed>
     */
    public function enrichActionResponse(array $response, string $actionId): array
    {
        if (self::$attachedThisRequest) {
            return $response;
        }

        $playbook = $this->findPlaybookForAbility($actionId);
        if ($playbook === null) {
            return $response;
        }

        $body = trim($playbook->getBody());
        if ($body === '') {
            return $response;
        }

        self::$attachedThisRequest = true;
        $this->touchUsage([$playbook]);

        $response['playbook_attachment'] = [
            'slug'        => $playbook->getSlug(),
            'domain'      => $playbook->getDomain(),
            'body'        => $body,
            'action_id'   => $actionId,
        ];

        return $response;
    }

    public function findPlaybookForAbility(string $actionId): ?Playbook
    {
        $canonical = $this->canonicalAbilityId($actionId);

        foreach ($this->registry->all() as $playbook) {
            if (!$playbook->isActivatable()) {
                continue;
            }
            foreach ($playbook->getAbilityPrefixes() as $prefix) {
                if ($prefix === '') {
                    continue;
                }
                $canonicalPrefix = $this->canonicalAbilityId($prefix);
                if (str_starts_with($canonical, $canonicalPrefix)) {
                    return $playbook;
                }
            }
        }

        return null;
    }

    /**
     * @return array<int, Playbook>
     */
    public function matchHintPlaybooks(string $messageLower): array
    {
        $matched = [];

        foreach ($this->registry->all() as $playbook) {
            if (!$playbook->isActivatable()) {
                continue;
            }
            $keywords = $playbook->getTriggers()['keywords'] ?? [];
            if (!is_array($keywords) || $keywords === []) {
                continue;
            }
            foreach ($keywords as $keyword) {
                if (!is_string($keyword) || $keyword === '') {
                    continue;
                }
                if (str_contains($messageLower, mb_strtolower($keyword, 'UTF-8'))) {
                    $matched[] = $playbook;
                    break;
                }
            }
        }

        // Max one playbook hint per turn (strong/weak inline budget applies separately).
        if (count($matched) > 1) {
            $matched = [ $matched[0] ];
        }

        return $matched;
    }

    private function formatHintChunk(Playbook $playbook): string
    {
        if (self::isWeakModel()) {
            $body = trim($playbook->getBody());
            if (mb_strlen($body, 'UTF-8') > PlaybookValidator::BODY_MAX_CHARS) {
                $body = mb_substr($body, 0, PlaybookValidator::BODY_MAX_CHARS, 'UTF-8');
            }
            return sprintf(
                "[%s]\n%s",
                $playbook->getSlug(),
                $body
            );
        }

        return sprintf(
            "Domain playbook available: read_playbook('%s') — %s",
            $playbook->getSlug(),
            $playbook->getDescription()
        );
    }

    /**
     * Normalize ability IDs for prefix matching (lolacore_fse/foo ↔ lolacore-fse/foo).
     */
    public function canonicalAbilityId(string $abilityId): string
    {
        return strtolower(str_replace('_', '-', $abilityId));
    }

    /**
     * @param array<int, Playbook> $playbooks
     */
    private function touchUsage(array $playbooks): void
    {
        if (!isset($GLOBALS['wpdb']) || !is_object($GLOBALS['wpdb'])) {
            return;
        }
        $wpdb = $GLOBALS['wpdb'];
        if (!method_exists($wpdb, 'query') || !method_exists($wpdb, 'prepare')) {
            return;
        }
        $prefix = property_exists($wpdb, 'prefix') ? $wpdb->prefix : '';
        $table  = $prefix . 'lolacore_playbooks';
        $now    = function_exists('current_time') ? (string) current_time('mysql') : gmdate('Y-m-d H:i:s');

        foreach ($playbooks as $playbook) {
            if ($playbook->getOrigin() !== Playbook::ORIGIN_DB) {
                continue;
            }
            @$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
                $wpdb->prepare(
                    "UPDATE {$table} SET load_count = load_count + 1, last_used_at = %s WHERE playbook_slug = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    $now,
                    $playbook->getSlug()
                )
            );
        }
    }
}
