<?php
namespace boru\boruai\Models\Functions;

use boru\boruai\Models\Tool;
use boru\boruai\Models\ToolDefinition;
use boru\boruai\Tools\CodeManager;
use boru\boruai\Tools\Util;
use boru\output\Output;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use InvalidArgumentException;
use RuntimeException;

class Browse extends FunctionBase {
    private static $CodeManager;

    public static function init($rootDirectory=null) {
        if(!static::$CodeManager) {
            if($rootDirectory) {
                static::$CodeManager = new CodeManager($rootDirectory);
            } else {
                static::$CodeManager = new CodeManager(getcwd());
            }

        }
        if(static::debug()) {
            Output::outLine(static::class."::init()");
        }
    }
    
    public static function functionDefinitions() {
        $return = [];

        $toolDefinition = new ToolDefinition();
        $toolDefinition->name("genericHttpRequest");
        $toolDefinition->description("Perform a generic HTTP request with a specified method, URL, and optional headers/body.
        Supported methods: GET, POST, PUT, PATCH, DELETE.
        If utilizing an API, ensure you have the necessary permissions to access the resource.. for example if the resource requires an API key, ensure you have it before making the request.");
        $toolDefinition->addString("method", "HTTP method (GET, POST, PUT, PATCH, DELETE).");
        $toolDefinition->addString("url", "The request URL.");
        $toolDefinition->addString("headers", "Optional: JSON-encoded headers array. Example: {\"Content-Type\":\"application/json\"}");
        $toolDefinition->addString("body", "Optional: The raw request body to send.");
        $toolDefinition->register(function($args) {
            return static::callback("genericHttpRequest",$args);
        });
        $return[] = $toolDefinition;

        $toolDefinition = new ToolDefinition();
        $toolDefinition->name("genericJsonRequest");
        $toolDefinition->description("Perform an HTTP request with JSON data. Supported methods: GET, POST, PUT, PATCH, DELETE. The jsonBody parameter will be sent as JSON.
        If utilizing an API, ensure you have the necessary permissions to access the resource.. for example if the resource requires an API key, ensure you have it before making the request.");
        $toolDefinition->addString("method", "HTTP method (GET, POST, PUT, PATCH, DELETE).");
        $toolDefinition->addString("url", "The request URL.");
        $toolDefinition->addString("headers", "Optional: JSON-encoded headers array. Example: {\"Authorization\":\"Bearer token\"}");
        $toolDefinition->addString("jsonBody", "Optional: JSON-encoded request body. Example: {\"key\":\"value\"}");
        $toolDefinition->register(function($args) {
            return static::callback("genericJsonRequest",$args);
        });
        $return[] = $toolDefinition;
        
        return $return;
    }

    /**
     * Perform a generic HTTP request using Guzzle and return the response body as a string.
     *
     * @param Tool $tool
     *   'method'  => (string) The HTTP method (GET, POST, PUT, PATCH, DELETE).
     *   'url'     => (string) The request URL.
     *   'headers' => (optional string) JSON-encoded headers.
     *   'body'    => (optional string) The raw request body.
     *
     * @return string The response body.
     */
    public static function genericHttpRequest($tool) {
        $args = $tool->get();
        if (empty($args['method']) || empty($args['url'])) {
            return static::handleException(new InvalidArgumentException("Missing required parameters: method and url"));
        }

        $method = strtoupper($args['method']);
        $url = $args['url'];
        $headers = [];
        if (!empty($args['headers'])) {
            $headersData = json_decode($args['headers'], true);
            if (is_array($headersData)) {
                $headers = $headersData;
            }
        }

        $client = null;
        if (method_exists($tool, 'context')) {
            // Optionally merge in headers from context (e.g., auth tokens)
            $contextHeaders = $tool->context('default_http_headers', []);
            if (is_array($contextHeaders) && !empty($contextHeaders)) {
                $headers = array_merge($contextHeaders, $headers);
            }

            // Allow a shared Guzzle client via context, falling back to a new one
            $sharedClient = $tool->context('http_client');
            if ($sharedClient instanceof Client) {
                $client = $sharedClient;
            }
        }

        $body = isset($args['body']) ? $args['body'] : null;

        if ($client === null) {
            $client = new Client(['timeout' => 30, 'connect_timeout' => 10]);
        }

        $options = [
            'headers' => $headers,
            // We only set body if it's not a GET request, and if body is provided
            'body'    => ($method !== 'GET' && $body !== null) ? $body : null
        ];

        try {
            $response = $client->request($method, $url, $options);
        } catch (RequestException $e) {
            return static::handleException(new RuntimeException("HTTP request failed: " . $e->getMessage()));
        }

        return static::parseResponse((string) $response->getBody());
    }

    /**
     * Perform an HTTP request with a JSON request body using Guzzle.
     * This function encodes the given jsonBody and sets appropriate headers.
     *
     * @param Tool $tool
     *   'method'   => (string) The HTTP method.
     *   'url'      => (string) The request URL.
     *   'headers'  => (optional string) JSON-encoded headers.
     *   'jsonBody' => (optional string) JSON-encoded data to send as the request body.
     *
     * @return string The response body.
     */
    public static function genericJsonRequest($tool) {
        $args = $tool->get();
        if (empty($args['method']) || empty($args['url'])) {
            return static::handleException(new InvalidArgumentException("Missing required parameters: method and url"));
        }

        $method = strtoupper($args['method']);
        $url = $args['url'];
        $headers = [];
        if (!empty($args['headers'])) {
            $headersData = json_decode($args['headers'], true);
            if (is_array($headersData)) {
                $headers = $headersData;
            }
        }

        $jsonBody = [];
        if (!empty($args['jsonBody'])) {
            $decodedJson = json_decode($args['jsonBody'], true);
            if (json_last_error() === JSON_ERROR_NONE) {
                $jsonBody = $decodedJson;
            } else {
                return static::handleException(new InvalidArgumentException("Invalid JSON provided in jsonBody parameter."));
            }
        }

        $client = new Client(['timeout' => 30, 'connect_timeout' => 10]);

        $options = [
            'headers' => $headers,
            'json'    => ($method !== 'GET' && !empty($jsonBody)) ? $jsonBody : null
        ];

        try {
            $response = $client->request($method, $url, $options);
        } catch (RequestException $e) {
            return static::handleException(new RuntimeException("HTTP request failed: " . $e->getMessage()));
        }

        return static::parseResponse((string) $response->getBody());
    }


    ////////////////////////////////////////////
    // Helper Functions (Not Tools)
    // For GPT to parse or extract information
    ////////////////////////////////////////////

    /**
     * Attempt to parse response data. If it's valid JSON, return a nicely formatted JSON string.
     * Otherwise, return the original response.
     *
     * @param string $responseData The response data to parse.
     * @return string A readable format (pretty JSON if original is JSON, else original).
     */
    public static function parseResponse(string $responseData): string
    {
        $decoded = json_decode($responseData, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            // It's valid JSON, return a pretty-printed JSON string
            return json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        }

        // Not JSON, just return the raw response data
        return $responseData;
    }

    /**
     * Extract all links (href attributes) from an HTML document.
     *
     * @param string $html The HTML content.
     * @return array An array of extracted links.
     */
    public static function extractLinksFromHTML(string $html): array
    {
        $links = [];
        // Simple regex to find href attributes. For production, consider DOM parsing for robustness.
        if (preg_match_all('/<a[^>]+href="([^"]+)"[^>]*>/i', $html, $matches)) {
            $links = $matches[1];
        }
        return $links;
    }

    private static function handleException($e) {
        $message = $e->getMessage();
        if(static::debug()) {
            Output::outLine("Exception: ".$message);
        }
        return $message;
    }
}