<?php

/**
 * LolaCore - SkillMatcher
 *
 * Activation cascade for the Skills.MD engine. Reproduces the layered
 * pattern of WPAIClientAdapter::routeIntent() — short-circuit at the
 * earliest deterministic match, fall through to context conditions,
 * then enforce the three anti-bloat caps.
 *
 * Layers:
 *   A — always-on (skill->always === true)
 *   B — deterministic triggers (keywords / regex / intents)
 *   C — context conditions (user_capability / locale)
 *   D — user override (handled via Skill::withAlways() in DB rows)
 *
 * Caps applied AFTER matching:
 *   - max_skills_per_prompt (default 5)
 *   - skill_char_budget     (default 6000)
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Skills;

class SkillMatcher
{
    public const CONFIG_MAX_PER_PROMPT = 'skills.max_per_prompt';
    public const CONFIG_CHAR_BUDGET    = 'skills.char_budget';
    public const DEFAULT_MAX_PER_PROMPT = 5;
    public const DEFAULT_CHAR_BUDGET    = 6000;

    private SkillRegistry $registry;

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

    /**
     * Match active skills against the current message and intent context.
     *
     * @param string              $messageLower Lowercased user message.
     * @param string              $intentLabel  Router intent label (optional).
     * @param array<string,mixed> $context      Hook context (locale, capability, etc.).
     * @return array<int, Skill>                Matched skills, priority DESC.
     */
    public function match(string $messageLower, string $intentLabel = '', array $context = []): array
    {
        $candidates = $this->registry->getActive();
        $matched    = [];

        foreach ($candidates as $skill) {
            if ($this->matchesAnyLayer($skill, $messageLower, $intentLabel)) {
                if (!$this->passesContextConditions($skill, $context)) {
                    continue;
                }
                $matched[] = $skill;
            }
        }

        usort($matched, static function (Skill $a, Skill $b): int {
            $pri = $b->getPriority() <=> $a->getPriority();
            if ($pri !== 0) {
                return $pri;
            }
            return strcmp($a->getName(), $b->getName());
        });

        $matched = $this->applyCapMaxPerPrompt($matched);
        $matched = $this->applyCapCharBudget($matched);

        return $matched;
    }

    /**
     * Layer A + B: always-on or deterministic trigger match.
     */
    private function matchesAnyLayer(Skill $skill, string $messageLower, string $intentLabel): bool
    {
        // Layer A — always-on.
        if ($skill->isAlways()) {
            return true;
        }

        // Layer B — deterministic triggers.
        $triggers = $skill->getTriggers();

        if (!empty($triggers['keywords']) && $messageLower !== '') {
            foreach ($triggers['keywords'] as $kw) {
                if (!is_string($kw) || $kw === '') {
                    continue;
                }
                if (str_contains($messageLower, mb_strtolower($kw))) {
                    return true;
                }
            }
        }

        if (!empty($triggers['regex']) && $messageLower !== '') {
            foreach ($triggers['regex'] as $pattern) {
                if (!is_string($pattern) || $pattern === '') {
                    continue;
                }
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions
                set_error_handler(static function (): bool {
                    return true;
                });
                $result = @preg_match($pattern, $messageLower);
                restore_error_handler();
                if ($result === 1) {
                    return true;
                }
            }
        }

        if (!empty($triggers['intents']) && $intentLabel !== '') {
            foreach ($triggers['intents'] as $intent) {
                if (is_string($intent) && $intent === $intentLabel) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Layer C — context conditions (user_capability, locale).
     *
     * @param array<string,mixed> $context
     */
    private function passesContextConditions(Skill $skill, array $context): bool
    {
        $conditions = $skill->getConditions();

        $capability = (string) ($conditions['user_capability'] ?? '');
        if ($capability !== '') {
            $userCan = $this->checkCapability($capability, $context);
            if (!$userCan) {
                return false;
            }
        }

        $localeList = $conditions['locale'] ?? [];
        if (is_array($localeList) && !empty($localeList)) {
            $currentLocale = $this->currentLocale($context);
            if (!in_array($currentLocale, $localeList, true)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param array<string,mixed> $context
     */
    private function checkCapability(string $capability, array $context): bool
    {
        if (array_key_exists('user_capability_override', $context)) {
            return (bool) $context['user_capability_override'];
        }
        if (function_exists('current_user_can')) {
            return (bool) current_user_can($capability);
        }
        return true;
    }

    /**
     * @param array<string,mixed> $context
     */
    private function currentLocale(array $context): string
    {
        if (isset($context['locale']) && is_string($context['locale'])) {
            return $context['locale'];
        }
        if (function_exists('get_locale')) {
            return (string) get_locale();
        }
        return '';
    }

    /**
     * Cap 1: keep only the top-N skills by priority.
     *
     * @param array<int, Skill> $skills Pre-sorted DESC by priority.
     * @return array<int, Skill>
     */
    private function applyCapMaxPerPrompt(array $skills): array
    {
        $cap = (int) $this->readConfigInt(self::CONFIG_MAX_PER_PROMPT, self::DEFAULT_MAX_PER_PROMPT);
        if ($cap <= 0 || count($skills) <= $cap) {
            return $skills;
        }
        $dropped = array_slice($skills, $cap);
        if ($dropped !== [] && defined('WP_DEBUG') && WP_DEBUG && function_exists('error_log')) {
            $names = array_map(static fn(Skill $s) => $s->getName(), $dropped);
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[LolaCore Skills] Cap max_per_prompt dropped: ' . implode(', ', $names));
        }
        return array_slice($skills, 0, $cap);
    }

    /**
     * Cap 2: drop skills that would overflow the char budget.
     * Skills enter whole or not at all — bodies are never truncated.
     *
     * @param array<int, Skill> $skills
     * @return array<int, Skill>
     */
    private function applyCapCharBudget(array $skills): array
    {
        $budget = (int) $this->readConfigInt(self::CONFIG_CHAR_BUDGET, self::DEFAULT_CHAR_BUDGET);
        if ($budget <= 0) {
            return $skills;
        }
        $kept    = [];
        $used    = 0;
        $dropped = [];
        foreach ($skills as $skill) {
            $bodyLen = mb_strlen($skill->getBody(), 'UTF-8');
            if ($used + $bodyLen > $budget) {
                $dropped[] = $skill->getName();
                continue;
            }
            $kept[] = $skill;
            $used  += $bodyLen;
        }
        if ($dropped !== [] && defined('WP_DEBUG') && WP_DEBUG && function_exists('error_log')) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('[LolaCore Skills] Cap char_budget dropped: ' . implode(', ', $dropped));
        }
        return $kept;
    }

    private function readConfigInt(string $key, int $default): int
    {
        if (!isset($GLOBALS['wpdb']) || !is_object($GLOBALS['wpdb'])) {
            return $default;
        }
        $wpdb = $GLOBALS['wpdb'];
        if (!method_exists($wpdb, 'get_var') || !method_exists($wpdb, 'prepare')) {
            return $default;
        }
        $prefix = property_exists($wpdb, 'prefix') ? $wpdb->prefix : '';
        $table  = sanitize_key($prefix . 'lolacore_config');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $value = @$wpdb->get_var($wpdb->prepare("SELECT config_value FROM {$table} WHERE config_key = %s", $key));
        if ($value === null || $value === '') {
            return $default;
        }
        $intValue = (int) $value;
        return $intValue;
    }
}
