<?php

declare(strict_types=1);

namespace ContextPaging;

/**
 * Formats tools for LLM requests.
 *
 * Supports two modes:
 * - NATIVE: OpenAI-style tools array in the request
 * - RAW: Tools described in system prompt with raw output format
 */
class ToolFormatter
{
    private ToolCallMode $mode;

    /**
     * Tools available for the context paging dereference operation.
     */
    public const FETCH_MESSAGE_TOOL = [
        'name' => 'fetch_message',
        'description' => 'Retrieve the full content of a summarized message when you need complete details to answer the current request.',
        'parameters' => [
            'type' => 'object',
            'properties' => [
                'md5' => [
                    'type' => 'string',
                    'description' => 'The MD5 hash of the message to retrieve (from the [md5:...] pointer)',
                ],
            ],
            'required' => ['md5'],
        ],
    ];

    public function __construct(ToolCallMode $mode = ToolCallMode::AUTO)
    {
        $this->mode = $mode;
    }

    /**
     * Format tools for the request payload.
     *
     * @param array $tools Array of tool definitions
     * @param ToolCallMode|null $mode Override mode
     * @return array For NATIVE: ['tools' => [...], 'tool_choice' => 'auto']. For RAW: empty array (use buildPayload instead)
     */
    public function formatForRequest(array $tools, ?ToolCallMode $mode = null): array
    {
        $useMode = $mode ?? $this->mode;

        if ($useMode === ToolCallMode::NATIVE) {
            return $this->formatNative($tools);
        }

        // RAW mode: tools are injected into system prompt, not the payload
        // Return empty array - caller should use buildPayload() or injectToolsIntoSystemPrompt()
        return [];
    }

    /**
     * Format tools in OpenAI native format.
     *
     * Returns an array to be merged into the request payload:
     * ['tools' => [...], 'tool_choice' => 'auto']
     */
    public function formatNative(array $tools): array
    {
        $formatted = [];

        foreach ($tools as $tool) {
            $formatted[] = [
                'type' => 'function',
                'function' => [
                    'name' => $tool['name'],
                    'description' => $tool['description'] ?? '',
                    'parameters' => $tool['parameters'] ?? ['type' => 'object', 'properties' => new \stdClass()],
                ],
            ];
        }

        return [
            'tools' => $formatted,
            'tool_choice' => 'auto',
        ];
    }

    /**
     * Format tools as raw text for system prompt.
     *
     * Returns a string to be appended to the system prompt.
     */
    public function formatRaw(array $tools): string
    {
        $lines = [
            '## Tools',
            'You have access to the following tools:',
            '',
        ];

        foreach ($tools as $tool) {
            $params = $tool['parameters'] ?? ['properties' => []];
            $paramsJson = json_encode($params, JSON_PRETTY_PRINT);

            $lines[] = "<tools>";
            $lines[] = json_encode([
                'name' => $tool['name'],
                'description' => $tool['description'] ?? '',
                'parameters' => $params,
            ], JSON_UNESCAPED_SLASHES);
            $lines[] = "</tools>";
            $lines[] = '';
        }

        $lines[] = 'When you need to use a tool, you MUST respond with ONLY a tool call in this exact format:';
        $lines[] = '`<tool_call>{"name": "tool_name", "arguments": {"arg": "value"}}</tool_call>`';
        $lines[] = '';
        $lines[] = 'Do not explain how to use tools. Actually call them. The tool will be executed and you will receive the result.';

        return implode("\n", $lines);
    }

    /**
     * Build a system prompt with tools injected.
     *
     * @param string $existingSystemPrompt The existing system prompt
     * @param array $tools Tools to inject
     * @param ToolCallMode|null $mode Override mode
     * @return string Modified system prompt
     */
    public function injectToolsIntoSystemPrompt(
        string $existingSystemPrompt,
        array $tools,
        ?ToolCallMode $mode = null
    ): string {
        $useMode = $mode ?? $this->mode;

        // Native mode doesn't modify the system prompt
        if ($useMode === ToolCallMode::NATIVE) {
            return $existingSystemPrompt;
        }

        // Raw mode appends tool definitions to the system prompt
        $toolText = $this->formatRaw($tools);

        // Append with separator
        return rtrim($existingSystemPrompt) . "\n\n" . $toolText;
    }

    /**
     * Build the full request payload with tools.
     *
     * @param array $messages The messages array
     * @param array $options Additional options (model, temperature, etc.)
     * @param array $tools Tools to include
     * @param ToolCallMode|null $mode Override mode
     * @return array Complete request payload
     */
    public function buildPayload(
        array $messages,
        array $options = [],
        array $tools = [],
        ?ToolCallMode $mode = null
    ): array {
        $useMode = $mode ?? $this->mode;

        $payload = $options;
        $payload['messages'] = $messages;

        if (empty($tools)) {
            return $payload;
        }

        if ($useMode === ToolCallMode::NATIVE) {
            // Add tools to payload
            $nativeTools = $this->formatNative($tools);
            $payload['tools'] = $nativeTools['tools'];
            $payload['tool_choice'] = $nativeTools['tool_choice'];
        } else {
            // Inject tools into system prompt (no tools key in payload)
            $payload['messages'] = $this->injectToolsIntoMessages($messages, $tools);
        }

        return $payload;
    }

    /**
     * Inject tools into the messages array by modifying the system prompt.
     */
    public function injectToolsIntoMessages(array $messages, array $tools): array
    {
        $result = [];
        $injected = false;
        $toolText = $this->formatRaw($tools);

        foreach ($messages as $message) {
            if (!$injected && ($message['role'] ?? null) === 'system') {
                // Inject tools into the first system message
                $result[] = [
                    'role' => 'system',
                    'content' => rtrim($message['content'] ?? '') . "\n\n" . $toolText,
                ];
                $injected = true;
            } else {
                $result[] = $message;
            }
        }

        // If no system message, prepend one
        if (!$injected) {
            array_unshift($result, [
                'role' => 'system',
                'content' => $toolText,
            ]);
        }

        return $result;
    }

    /**
     * Set the formatting mode.
     */
    public function setMode(ToolCallMode $mode): self
    {
        $this->mode = $mode;
        return $this;
    }

    /**
     * Get the current mode.
     */
    public function getMode(): ToolCallMode
    {
        return $this->mode;
    }
}
