<?php

namespace Modules\AiCore\Entities\Services;

use Modules\AiCore\Entities\AiProvider;
use Modules\AiCore\Entities\Contracts\AiTool;

/**
 * Provider-agnostic function-calling loop.
 *
 * Because providers differ (Anthropic/OpenAI native tools vs Gemini), we use a
 * universal TEXT protocol that works everywhere — the same one proven in
 * WhatsApp:
 *
 *   1. The system prompt (built by PromptBuilder) lists the available tools and
 *      instructs the model to reply with EXACTLY one line
 *      `[[ACTION:<name>]]{json-args}` when it wants to use one.
 *   2. We call AiClient::chat().
 *   3. We parse that line; if the name is in the supplied whitelist we decode
 *      the args and run $tool->handle() — the tool validates everything in PHP.
 *   4. Otherwise we return the model text as a normal reply.
 *
 * The model can NEVER run anything except a whitelisted, PHP-validated tool.
 */
class ToolRunner
{
    private AiClient $client;

    public function __construct(?AiClient $client = null)
    {
        $this->client = $client ?: new AiClient();
    }

    /**
     * @param AiProvider $settings provider config
     * @param string     $system   system prompt (should already include the tool protocol — see PromptBuilder::system)
     * @param string     $user     user prompt (wrap untrusted input via PromptBuilder::userEnvelope)
     * @param AiTool[]   $tools    whitelist of tools the model may invoke this turn
     * @param array      $context  trusted caller context passed to a tool's handle()
     * @param array      $opts     ['module'=>?, 'feature'=>?] for usage logging
     *
     * @return array{
     *   ok:bool, reply:string, tool:?string, meta:array, error:?string
     * }
     */
    public function run(AiProvider $settings, string $system, string $user, array $tools = [], array $context = [], array $opts = []): array
    {
        $result = $this->client->chat($settings, $system, $user, $opts);

        if (empty($result['ok']) || empty($result['text'])) {
            return [
                'ok'    => false,
                'reply' => '',
                'tool'  => null,
                'meta'  => [],
                'error' => $result['error'] ?? 'unknown',
            ];
        }

        $text  = (string) $result['text'];
        $byName = $this->index($tools);
        $action = $this->parseAction($text, array_keys($byName));

        if ($action) {
            $tool = $byName[$action['name']];
            try {
                $res = $tool->handle((int) $settings->business_id, $action['args'], $context);
            } catch (\Throwable $e) {
                return [
                    'ok'    => false,
                    'reply' => '',
                    'tool'  => $action['name'],
                    'meta'  => [],
                    'error' => 'tool_failed: ' . $e->getMessage(),
                ];
            }

            return [
                'ok'    => (bool) ($res['ok'] ?? false),
                'reply' => (string) ($res['reply'] ?? ''),
                'tool'  => $action['name'],
                'meta'  => (array) ($res['meta'] ?? []),
                'error' => $res['error'] ?? null,
            ];
        }

        // No (valid) action requested → plain reply.
        return [
            'ok'    => true,
            'reply' => $text,
            'tool'  => null,
            'meta'  => [],
            'error' => null,
        ];
    }

    /**
     * Parse a whitelisted action from model output. Only names present in
     * $whitelist are accepted; anything else is ignored (treated as plain text).
     *
     * @param string[] $whitelist
     * @return array{name:string, args:array}|null
     */
    public function parseAction(string $text, array $whitelist): ?array
    {
        $tag = preg_quote((string) config('aicore.tool_protocol_tag', 'ACTION'), '/');
        if (!preg_match('/\[\[' . $tag . ':([a-z_]+)\]\]\s*(\{.*\})/s', $text, $m)) {
            return null;
        }
        if (!in_array($m[1], $whitelist, true)) {
            return null;
        }
        $args = json_decode($m[2], true);
        return is_array($args) ? ['name' => $m[1], 'args' => $args] : null;
    }

    /**
     * @param AiTool[] $tools
     * @return array<string,AiTool>
     */
    private function index(array $tools): array
    {
        $out = [];
        foreach ($tools as $tool) {
            if ($tool instanceof AiTool) {
                $out[$tool->name()] = $tool;
            }
        }
        return $out;
    }
}
