<?php

declare(strict_types=1);

namespace ContextPaging;

/**
 * Parses tool calls from LLM responses.
 *
 * Supports two modes:
 * - NATIVE: OpenAI-style tool_calls array in the response
 * - RAW: Tool calls embedded in content as markers
 */
class ToolCallParser
{
    private ToolCallMode $mode;

    /**
     * Regex patterns for raw tool calls.
     * SmolLM3 and similar models output tool calls as markers in content.
     */
    private const RAW_PATTERNS = [
        // Primary pattern: markers with JSON
        '/<tool_call>\s*(\{.*?\})\s*<\/tool_call>/s',
        // Alternative: code block format
        '/```tool_call\s*\n?\s*(\{.*?\})\s*\n?\s*```/s',
        // Alternative: just JSON with tool_call context
        '/\{"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:\s*\{.*?\}\s*\}/s',
    ];

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

    /**
     * Extract tool calls from a response.
     *
     * @param array $response The full API response (decoded JSON)
     * @return array|null Array of tool calls, or null if none found
     */
    public function extract(array $response): ?array
    {
        $message = $response['choices'][0]['message'] ?? null;

        if (!$message) {
            return null;
        }

        // Try native tool_calls first (if mode is NATIVE or AUTO)
        if ($this->mode === ToolCallMode::NATIVE || $this->mode === ToolCallMode::AUTO) {
            $nativeCalls = $this->extractNative($message);
            if (!empty($nativeCalls)) {
                return $nativeCalls;
            }
        }

        // Try raw tool calls in content (if mode is RAW or AUTO)
        if ($this->mode === ToolCallMode::RAW || $this->mode === ToolCallMode::AUTO) {
            $rawCalls = $this->extractRaw($message);
            if (!empty($rawCalls)) {
                return $rawCalls;
            }
        }

        return null;
    }

    /**
     * Check if a response contains tool calls.
     */
    public function hasToolCalls(array $response): bool
    {
        return $this->extract($response) !== null;
    }

    /**
     * Get the detected mode from a response.
     *
     * @param array $response The API response
     * @return ToolCallMode The detected mode, or current mode if set explicitly
     */
    public function detectMode(array $response): ToolCallMode
    {
        if ($this->mode !== ToolCallMode::AUTO) {
            return $this->mode;
        }

        $message = $response['choices'][0]['message'] ?? null;

        if (!$message) {
            return ToolCallMode::RAW; // Default fallback
        }

        // Check for native tool_calls
        if (!empty($message['tool_calls'])) {
            return ToolCallMode::NATIVE;
        }

        // Check for raw tool calls in content
        $content = $message['content'] ?? '';
        if ($this->contentHasRawToolCall($content)) {
            return ToolCallMode::RAW;
        }

        return ToolCallMode::RAW; // No tool calls found
    }

    /**
     * Check if content contains raw tool call markers.
     */
    private function contentHasRawToolCall(string $content): bool
    {
        foreach (self::RAW_PATTERNS as $pattern) {
            if (preg_match($pattern, $content)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Extract native OpenAI-style tool calls.
     */
    private function extractNative(array $message): ?array
    {
        $toolCalls = $message['tool_calls'] ?? [];

        if (empty($toolCalls)) {
            return null;
        }

        $result = [];
        foreach ($toolCalls as $call) {
            $fn = $call['function'] ?? [];
            $args = $fn['arguments'] ?? '{}';

            // Parse JSON arguments
            if (is_string($args)) {
                $args = json_decode($args, true) ?? [];
            }

            $result[] = [
                'id' => $call['id'] ?? null,
                'name' => $fn['name'] ?? null,
                'arguments' => $args,
            ];
        }

        return $result;
    }

    /**
     * Extract raw tool calls from message content.
     */
    private function extractRaw(array $message): ?array
    {
        $content = $message['content'] ?? '';

        if (empty($content)) {
            return null;
        }

        $result = [];
        $seen = []; // Track MD5 of JSON to deduplicate

        foreach (self::RAW_PATTERNS as $pattern) {
            if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $json = $match[1] ?? $match[0];
                    
                    // Deduplicate by content hash
                    $hash = md5($json);
                    if (isset($seen[$hash])) {
                        continue;
                    }
                    $seen[$hash] = true;

                    $decoded = json_decode($json, true);

                    if ($decoded && isset($decoded['name'])) {
                        $result[] = [
                            'id' => null,
                            'name' => $decoded['name'],
                            'arguments' => $decoded['arguments'] ?? [],
                        ];
                    }
                }
            }
        }

        return empty($result) ? null : $result;
    }

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

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