<?php
namespace boru\boruai\Openai\Response;

use boru\boruai\BoruAI;
use boru\boruai\Models\ToolDefinition;
use boru\boruai\Models\ToolParameters;
use boru\boruai\Openai\Models\BaseModel;
use boru\boruai\Openai\Response\Parts\Content\Content;
use boru\boruai\Openai\Response\Parts\FunctionCall;
use boru\boruai\Openai\Response\Parts\InputMessage;
use boru\boruai\Openai\Response\Parts\OutputMessage;
use boru\output\Output;

class ResponseObject extends BaseModel {
    /** @var string */
    private $id;
    /** @var string */
    private $model;
    /** @var string */
    private $object = "response";
    /** @var string */
    private $createdAt;
    /** @var string */
    private $status;
    /** @var string */
    private $error;
    /** @var array */
    private $incompleteDetails;
    /** @var string */
    private $instructions;
    /** @var int */
    private $maxOutputTokens;
    
    /** @var OutputMessage|OutputMessage[] */
    private $output = [];
    /** @var bool */
    private $parallelToolCalls;
    /** @var string */
    private $previousResponseId;
    /** @var array */
    private $reasoning = [];
    /** @var bool */
    private $store;
    /** @var int */
    private $temperature;
    /** @var array */
    private $text = [
        "format" => [
            "type" => "text"
        ]
    ];
    /** @var string */
    private $toolChoice;
    /** @var ToolDefinition[] */
    private $tools = [];
    /** @var int */
    private $topP;
    /** @var string */
    private $truncation;
    /** @var array */
    private $usage = [
        "input_tokens" => 0,
        "input_tokens_details" => [
            "cached_tokens" => 0
        ],
        "output_tokens" => 0,
        "output_tokens_details" => [
            "reasoning_tokens" => 0
        ],
        "total_tokens" => 0
    ];
    /** @var string */
    private $user;
    /** @var array */
    private $metadata = [];

    //input only variables
    /** @var InputMessage|InputMessage[] */
    private $input = [];
    /** @var bool */
    private $stream = false;
    /** @var string */
    private $serviceTier;

    /** @var string|null */
    private $summary = null;


    public function id($id=null) {
        if($id !== null) {
            $this->id = $id;
        }
        return $this->id;
    }
    public function object($object=null) {
        if($object !== null) {
            $this->object = $object;
        }
        return $this->object;
    }
    public function createdAt($createdAt=null) {
        if($createdAt !== null) {
            $this->createdAt = $createdAt;
        }
        return $this->createdAt;
    }
    public function status($status=null) {
        if($status !== null) {
            $this->status = $status;
        }
        return $this->status;
    }
    public function error($error=null) {
        if($error !== null) {
            $this->error = $error;
        }
        return $this->error;
    }
    public function incompleteDetails($incompleteDetails=null) {
        if($incompleteDetails !== null) {
            $this->incompleteDetails = $incompleteDetails;
        }
        return $this->incompleteDetails;
    }
    public function instructions($instructions=null) {
        if($instructions !== null) {
            $this->instructions = $instructions;
        }
        return $this->instructions;
    }
    public function maxOutputTokens($maxOutputTokens=null) {
        if($maxOutputTokens !== null) {
            $this->maxOutputTokens = $maxOutputTokens;
        }
        return $this->maxOutputTokens;
    }
    public function model($model=null) {
        if($model !== null) {
            $this->model = $model;
        }
        return $this->model;
    }
    public function content() {
        $output =  $this->output();
        if($output instanceof Content) {
            return $output->output();
        }
    }
    public function output($output=null) {
        if($output !== null) {
            $this->addOutputObject($output);
        }
        return $this->output;
    }
    public function parallelToolCalls($parallelToolCalls=null) {
        if($parallelToolCalls !== null) {
            $this->parallelToolCalls = $parallelToolCalls ? true : false;
        }
        return $this->parallelToolCalls ? true : false;
    }
    public function previousResponseId($previousResponseId=null) {
        if($previousResponseId !== null) {
            $this->previousResponseId = $previousResponseId;
        }
        return $this->previousResponseId;
    }
    public function reasoning($reasoning=null) {
        if($reasoning !== null) {
            $this->reasoning = $reasoning;
        }
        return $this->reasoning;
    }
    public function reasoningEffort($reasoningEffort=null) {
        if($reasoningEffort !== null) {
            $this->reasoning["effort"] = $reasoningEffort;
        }
        return $this->reasoning["effort"];
    }
    public function reasoningSummary($reasoningSummary=null) {
        if($reasoningSummary !== null) {
            $this->reasoning["summary"] = $reasoningSummary;
        }
        return $this->reasoning["summary"];
    }
    public function store($store=null) {
        if($store !== null) {
            $this->store = $store;
        }
        return $this->store;
    }
    public function temperature($temperature=null) {
        if($temperature !== null) {
            $this->temperature = $temperature;
        }
        return $this->temperature;
    }
    public function toolChoice($toolChoice=null) {
        if($toolChoice !== null) {
            $this->toolChoice = $toolChoice;
        }
        return $this->toolChoice;
    }
    public function tools($tools=null) {
        if($tools !== null) {
            $this->tools = $tools;
        }
        return $this->tools;
    }
    public function topP($topP=null) {
        if($topP !== null) {
            $this->topP = $topP;
        }
        return $this->topP;
    }
    public function truncation($truncation=null) {
        if($truncation !== null) {
            $this->truncation = $truncation;
        }
        return $this->truncation;
    }
    public function usage($usage=null) {
        if($usage !== null) {
            $this->usage = $usage;
        }
        return $this->usage;
    }
    public function user($user=null) {
        if($user !== null) {
            $this->user = $user;
        }
        return $this->user;
    }
    public function metadata($metadata=null) {
        if($metadata !== null) {
            $this->metadata = $metadata;
        }
        return $this->metadata;
    }

    public function input($input=null) {
        if($input !== null) {
            $this->input = $input;
        }
        return $this->input;
    }

    public function text($text=null) {
        if($text !== null) {
            if(is_array($text)) {
                $this->text = $text;
            } else {
                $this->text = ["format" => $text];
            }
        }
        return $this->text;
    }
    public function stream($stream=null) {
        if($stream !== null) {
            $this->stream = $stream ? true : false;
        }
        return $this->stream ? true : false;
    }
    public function serviceTier($serviceTier=null) {
        if($serviceTier !== null) {
            $this->serviceTier = $serviceTier;
        }
        return $this->serviceTier;
    }

    public function toArray() {
        $data = [
            "id" => $this->id(),
            "object" => $this->object(),
            "created_at" => $this->createdAt(),
            "status" => $this->status(),
            "error" => $this->error(),
            "incomplete_details" => $this->incompleteDetails(),
            "instructions" => $this->instructions(),
            "max_output_tokens" => $this->maxOutputTokens(),
            "model" => $this->model(),
            "output" => $this->output(),
            "parallel_tool_calls" => $this->parallelToolCalls(),
            "previous_response_id" => $this->previousResponseId(),
            "reasoning" => $this->reasoning(),
            "store" => $this->store(),
            "temperature" => $this->temperature(),
            "text" => $this->text(),
            "tool_choice" => $this->toolChoice(),
            "tools" => $this->tools(),
            "top_p" => $this->topP(),
            "truncation" => $this->truncation(),
            "usage" => $this->usage(),
            "user" => $this->user(),
            "metadata" => $this->metadata()
        ];
        return array_filter($data);
    }
    public function forInput($sanitize=false) {
        $data = [
            "input" => $this->getInput(),
            "model" => $this->model(),
            "instructions" => $this->instructions(),
            "temperature" => $this->temperature(),
            "reasoning" => $this->reasoning(),
            "top_p" => $this->topP(),
            "max_output_tokens" => $this->maxOutputTokens(),
            "truncation" => $this->truncation(),
            "tool_choice" => $this->toolChoice(),
            "tools" => $this->getTools(),
            "stream" => $this->stream(),
            "service_tier" => $this->serviceTier(),
            "previous_response_id" => $this->previousResponseId(),
            "store" => $this->store(),
            "parallel_tool_calls" => $this->parallelToolCalls(),
            "text" => $this->text(),
            "user" => $this->user(),
            "metadata" => $this->metadata(),
        ];
        if(is_array($data["input"]) && $sanitize) {
            $data["input"] = $this->sanitizeInput($data["input"]);
        }
        return array_filter($data);
    }
    public function sanitizeInput($inputArray) {
        if(!is_array($inputArray)) {
            return $inputArray;
        }
        foreach($inputArray as $key=>$value) {
            if(is_array($value)) {
                $inputArray[$key] = $this->sanitizeInput($value);
            } elseif(is_object($value)) {
                $inputArray[$key] = $this->sanitizeInput($value->toArray());
            } elseif(is_string($value)) {
                if($key == "file_data") {
                    $inputArray[$key] = "<fileData>";
                }
            }
        }
        return $inputArray;
    }
    
    public function getResult($asString=true) {
        $output = $this->output();
        if($output instanceof OutputMessage) {
            return $output->output();
        } elseif(is_array($output)) {
            $result = [];
            foreach($output as $item) {
                if($item instanceof OutputMessage) {
                    $result[] = $item->output();
                } else {
                    $result[] = $item;
                }
            }
            if($asString) {
                return implode("\n",$result);
            }
            return $result;
        }
        return false;
    }
    public function enableWebSearch() {
        //ensure we're not already enabled..
        foreach($this->tools as $tool) {
            if(is_array($tool) && isset($tool["type"]) && $tool["type"] == "web_search") {
                return $this;
            }
        }
        $this->tools[] = ["type" => "web_search"];
        return $this;
    }
    public function addTools($arrayOfTools=[]) {
        foreach($arrayOfTools as $tool) {
            $this->addTool($tool);
        }
        return $this;
    }
    /**
     * Generic entrypoint to add a tool to this Response.
     *
     * Supported usages:
     *   - addTool('Browse');                // class name, resolved via BoruAI::loadTool()
     *   - addTool($toolDefinition);         // ToolDefinition instance
     *   - addTool([$def1, $def2]);          // array of ToolDefinition instances
     *   - addTool(['name' => ..., 'parameters' => ...]); // raw schema array
     *   - addTool([
     *         'name'        => 'get_weather',
     *         'description' => '...',
     *         'schema'      => $toolParams,      // ToolParameters or array
     *         'callback'    => $closure,         // callable
     *     ]);
     *
     * @param mixed $tool
     * @return $this
     * @throws \Exception
     */
    public function addTool($tool)
    {
        // 1) String or something BoruAI::loadTool can handle: keep old behavior
        if (is_string($tool)) {
            $loaded = BoruAI::loadTool($tool);
            if ($loaded && method_exists($loaded, "forResponse")) {
                $this->tools[] = $loaded;
            }
            return $this;
        }

        // 2) Already a ToolDefinition instance
        if ($tool instanceof ToolDefinition) {
            // No callback known here -> just register via existing path
            // (Caller can separately register callbacks using ToolDefinition::register
            //  or you can add an optional 'callback' entry on the definition in future.)
            $loaded = BoruAI::loadTool($tool);
            if ($loaded && method_exists($loaded, "forResponse")) {
                $this->tools[] = $loaded;
            }
            return $this;
        }

        // 3) Array of tools / definitions
        if (is_array($tool) && isset($tool[0]) && !isset($tool['name'])) {
            foreach ($tool as $t) {
                $this->addTool($t);
            }
            return $this;
        }

        // 4) Associative array – interpret as inline definition
        if (is_array($tool) && isset($tool['name'])) {
            $name        = $tool['name'];
            $description = isset($tool['description']) ? $tool['description'] : '';
            $schema      = isset($tool['schema'])      ? $tool['schema']      : (isset($tool['parameters']) ? $tool['parameters'] : []);
            $callback    = isset($tool['callback'])    ? $tool['callback']    : null;
            $strict      = array_key_exists('strict', $tool) ? (bool)$tool['strict'] : true;

            // If we have a callback + schema, treat this as a closure tool
            if ($callback && is_callable($callback)) {
                $this->addClosureTool($name, $description, $schema, $callback, $strict);
                return $this;
            }

            // Otherwise, treat as plain ToolDefinition schema with no callback
            $def = new ToolDefinition([
                'name'        => $name,
                'description' => $description,
                'strict'      => $strict,
            ]);

            // Normalize schema into ToolParameters like addClosureTool does
            if ($schema instanceof ToolParameters) {
                $params = $schema;
            } elseif (is_array($schema)) {
                if (isset($schema['type']) || isset($schema['properties'])) {
                    $params = new ToolParameters($schema);
                } else {
                    $params = new ToolParameters(['properties' => $schema]);
                }
            } else {
                $params = new ToolParameters(['properties' => []]);
            }
            $def->parameters($params);

            // Attach via existing addToolDefinition path, but no callback
            $loaded = BoruAI::loadTool($def);
            if ($loaded && method_exists($loaded, "forResponse")) {
                $this->tools[] = $loaded;
            }
            return $this;
        }

        // Fallback: do nothing
        return $this;
    }
    public function getTools($forInput=true) {
        $this->tools = ToolDefinition::merge($this->tools);
        if($forInput) {
            $tools = [];
            foreach($this->tools as $tool) {
                if($tool instanceof ToolDefinition) {
                    $toolEntry = $tool->forResponse();
                    $tools[] = $toolEntry;
                    continue;
                }
                $tools[] = $tool;
            }
            return $tools;
        } else {
            return $this->tools;
        }
    }
    /**
     * Attach a single tool definition + callback to this Response.
     *
     * $callback can be:
     *   - any callable (closure, [$obj, 'method'], 'Class::method', etc.)
     *   - a FunctionBase subclass name with a handle() method (string),
     *     which will be wrapped as Class::callback('handle', $tool).
     *
     * @param ToolDefinition $definition
     * @param mixed          $callback
     * @return $this
     * @throws \Exception
     */
    public function addToolDefinition(ToolDefinition $definition, $callback)
    {
        // Register the callback in the global Tool registry
        $definition->register($callback);
        $tool = BoruAI::loadTool($definition);
        if($tool && method_exists($tool,"forResponse")) {
            $this->tools[] = $tool;
        }
        return $this;
    }

    /**
     * Convenience: define a tool inline with name/description/schema and a closure.
     *
     * Example:
     *   $response->addClosureTool('weather_lookup', 'Look up weather', [
     *       'location' => ['type' => 'string', 'description' => 'City name'],
     *   ], function (Tool $tool, ToolContext $ctx = null) {
     *       $args = $tool->get();
     *       // ...
     *   });
     *
     * @param string   $name
     * @param string   $description
     * @param array|ToolParameters   $schema     Optional schema / parameters
     * @param callable $callback
     * @param bool     $strict
     * @return $this
     * @throws \Exception
     */
    public function addClosureTool($name, $description, $properties, $callback, $strict = true)
    {
        // Normalize schema to ToolParameters
        if ($properties instanceof ToolParameters) {
            $params = $properties;
        } elseif (is_array($properties)) {
            // Allow user to pass either whole parameters array or just ["properties" => ...]
            if (isset($properties['type']) || isset($properties['properties'])) {
                $params = new ToolParameters($properties);
            } else {
                // treat as properties-only
                $params = new ToolParameters(['properties' => $properties]);
            }
        } else {
            // no parameters
            $params = new ToolParameters(['properties' => []]);
        }
        
        $def = new ToolDefinition([
            'name'        => $name,
            'description' => $description,
            'strict'      => $strict,
        ]);
        $def->parameters($params);

        return $this->addToolDefinition($def, $callback);
    }

    public function getInput() {
        $input = [];
        foreach($this->input as $message) {
            if($message instanceof InputMessage || method_exists($message,"toArray")) {
                $input[] = $message->toArray();
            } else {
                $input[] = $message;
            }
        }
        return $input;
    }
    public function resetOutput() {
        $this->output = [];
        return $this;
    }
    private function addOutputObject($outputObjectOrArray) {
        //shorthand
        $object = $outputObjectOrArray;
        if(!is_array($object) && !is_object($object)) {
            $object = json_decode($outputObjectOrArray,true);
            if(!$object) {
               return  $this->addOutputObject($object);
            }
        }
        if(is_object($object)) {
            $this->output[] = $object;
            return true;
        }
        
        $isAssociative = array_keys($object) !== range(0, count($object) - 1);
        if(!$isAssociative) {
            foreach($outputObjectOrArray as $object) {
                $this->addOutputObject($object);
            }
            return true;
        }
        if(isset($object["type"]) && $object["type"] == "function_call") {
            $object = new FunctionCall($object);
            $this->output[] = $object;
            return true;
        }
        $this->output[] = new OutputMessage($object);
        return true;

    }
    public function addToInput($message) {
        if(is_array($message)) {

            //Multi-item array
            $isAssociative = array_keys($message) !== range(0, count($message) - 1);
            if(!$isAssociative) {
                foreach($message as $item) {
                    $this->addToInput($item);
                }
                return true;
            }

            // 🔑 REASONING BYPASS
            /*if (isset($message['type']) && $message['type'] === 'reasoning') {
                $this->input[] = $message;
                return true;
            }*/
        }
        if(!is_array($this->input)) {
            if(!empty($this->input)) {
                $inputObject = $this->input;
                $this->input = [];
                $this->input[] = $inputObject;
            }
        }
        if(empty($this->input)) {
            $this->input = [];
        }
        if(is_object($message)) {
            $this->input[] = $message;
            return true;
        }
        $object = Content::fromArray($message,false);
        if($object) {
            $this->input[] = $object;
            return true;
        } else {
            throw new \Exception("Invalid input message".
                " - ".json_encode($message,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
        }
    }
    public function addMessage($message,$newMessage=false,$prependFirstMessage=false) {
        if(is_object($message) && $message instanceof InputMessage) {
            if($prependFirstMessage) {
                $newInput = [];
                $newInput[] = $message;
                if(!is_array($this->input)) {
                    if(empty($this->input)) {
                        $this->input = [];
                    } else {
                        $this->input = [$this->input];
                    }
                }
                foreach($this->input as $input) {
                    $newInput[] = $input;
                }
                $this->input = $newInput;
                return true;
            }
            $this->input[] = $message;
            return true;
        }
        if(is_array($message)) {
            $isAssociative = array_keys($message) !== range(0, count($message) - 1);
            if(!$isAssociative) {
                foreach($message as $item) {
                    $this->addMessage($item,$newMessage,$prependFirstMessage);
                }
                return true;
            }
        }
        $object = $this->getCurrentMessage();
        if($object && !$newMessage) {
            $object->addMessage($message,$prependFirstMessage);
            return true;
        } else {
            $object = new InputMessage();
            $object->addMessage($message,$prependFirstMessage);
            $this->input[] = $object;
            return true;
        }
        return false;
    }
    public function addImage($imageOrUrl,$detail=null) {
        $message = $this->getCurrentMessage();
        if($message && $message instanceof InputMessage) {
            $message->addImage($imageOrUrl,$detail);
            return true;
        }
        $object = new InputMessage();
        $object->addImage($imageOrUrl,$detail);
        $this->input[] = $object;
        return true;
    }
    public function addFile($fileNameOrId) {
        $message = $this->getCurrentMessage();
        if($message && $message instanceof InputMessage) {
            $message->addFile($fileNameOrId);
            return true;
        }
        $object = new InputMessage();
        $object->addFile($fileNameOrId);
        $this->input[] = $object;
        return true;
    }
    public function getCurrentMessage($create=true) {
        if(!is_array($this->input) && !empty($this->input)) {
            $object = $this->input;
            $this->input = [];
            $this->input[] = $object;
        }
        if(!empty($this->input)) {
            return $this->input[count($this->input)-1];
        }
        if($create) {
            $object = new InputMessage();
            $this->input[] = $object;
            return $object;
        }
        return false;
    }
}