//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

// Package tool — MCP (Model Context Protocol) wrapper.
//
// Wraps a single MCP-server-discovered tool (utility/mcpclient.Tool) as
// an eino BaseTool so it can be invoked from inside the Agent's
// ReAct loop. The MCP tool list is fetched via utility/mcpclient
// (which currently only implements tools/list discovery; tools/call
// invocation is the next step on the MCP client).
package tool

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/schema"

	mcpclient "ragflow/internal/utility"
)

// MCPToolAdapter wraps a single MCP-discovered tool descriptor as an
// eino InvokableTool. The wire format matches what eino's react.Agent
// expects: a ToolInfo with name/description/params, and an
// InvokableRun that accepts a JSON arguments string and returns a
// string result.
//
// InvokableRun dispatches through mcpclient.CallTool
// (streamable-HTTP transport). The MCP server URL + headers
// are captured on construction so the adapter has everything it
// needs to call back into the server. Adapters built without
// a URL (legacy callers) fall back to the "not yet wired"
// sentinel so existing call sites don't break.
type MCPToolAdapter struct {
	mcpTool    mcpclient.Tool
	serverURL  string
	headers    map[string]string
	timeout    time.Duration
	httpClient *http.Client
}

// NewMCPToolAdapter constructs a wrapper for a single MCP tool.
// The returned adapter has no MCP server URL and so cannot be
// invoked — use NewMCPToolAdapterWithServer for adapters that
// need to call back into the server.
func NewMCPToolAdapter(t mcpclient.Tool) *MCPToolAdapter {
	return &MCPToolAdapter{mcpTool: t}
}

// NewMCPToolAdapterWithServer constructs a wrapper that knows
// the MCP server URL + transport headers. InvokableRun uses this
// to route InvokableRun into mcpclient.CallTool.
func NewMCPToolAdapterWithServer(t mcpclient.Tool, serverURL string, headers map[string]string, timeout time.Duration) *MCPToolAdapter {
	return &MCPToolAdapter{
		mcpTool:   t,
		serverURL: serverURL,
		headers:   headers,
		timeout:   timeout,
	}
}

// NewMCPToolAdapterFull is the most-configurable constructor;
// callers can also pass an *http.Client (e.g. an httptest server's
// Client, or a custom transport with mTLS) so the underlying
// CallTool call doesn't have to fall back to a pinned client.
func NewMCPToolAdapterFull(t mcpclient.Tool, serverURL string, headers map[string]string, timeout time.Duration, client *http.Client) *MCPToolAdapter {
	return &MCPToolAdapter{
		mcpTool:    t,
		serverURL:  serverURL,
		headers:    headers,
		timeout:    timeout,
		httpClient: client,
	}
}

// Name returns the underlying MCP tool name.
func (m *MCPToolAdapter) Name() string { return m.mcpTool.Name }

// Info returns eino-compatible tool metadata. InputSchema is
// translated from the MCP tool's JSON Schema.
func (m *MCPToolAdapter) Info(_ context.Context) (*schema.ToolInfo, error) {
	// eino's schema.ParameterInfo shape: name → description.
	// We translate the MCP tool's inputSchema.properties into a
	// best-effort ParameterInfo map. For tools without a JSON schema
	// the params map is empty — eino falls back to free-form args.
	params := make(map[string]*schema.ParameterInfo, len(m.mcpTool.InputSchema))
	for name := range m.mcpTool.InputSchema {
		params[name] = &schema.ParameterInfo{
			Type:     schema.String, // conservative default
			Desc:     fmt.Sprintf("MCP tool parameter: %s", name),
			Required: false, // MCP doesn't surface required; we err permissive
		}
	}
	return &schema.ToolInfo{
		Name:        m.mcpTool.Name,
		Desc:        m.mcpTool.Description,
		ParamsOneOf: schema.NewParamsOneOfByParams(params),
	}, nil
}

// InvokableRun is the eino entry point. When the adapter was
// built with a server URL, dispatch through mcpclient.CallTool.
// Legacy adapters (no URL) keep the "not yet wired" sentinel
// so existing tests that pin the error message don't break.
func (m *MCPToolAdapter) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
	if m.serverURL == "" {
		return "", fmt.Errorf("mcp tool %q: tools/call not yet implemented in mcpclient; arguments were: %s",
			m.mcpTool.Name, argumentsInJSON)
	}
	argsJSON, mErr := marshalArguments(argumentsInJSON)
	if mErr != nil {
		return "", mErr
	}
	res, err := mcpclient.CallTool(ctx, mcpclient.CallOptions{
		URL:        m.serverURL,
		ServerType: mcpclient.TransportStreamableHTTP,
		Headers:    m.headers,
		ToolName:   m.mcpTool.Name,
		Arguments:  argsJSON,
		Timeout:    m.timeout,
		HTTPClient: m.httpClient,
	})
	if err != nil {
		return "", err
	}
	if res == nil {
		return "", nil
	}
	if res.IsError {
		// Surface the structured tool error under a known prefix
		// so the ReAct loop can route it as a tool-level error
		// rather than a transport failure.
		return "", fmt.Errorf("mcp tool %q returned isError: %s", m.mcpTool.Name, res.Text)
	}
	return res.Text, nil
}

// Close releases resources held by the adapter. In Go's architecture
// MCP sessions are per-invocation (created and torn down within each
// InvokableRun call), so there are no persistent connections to drain.
// The primary resource is the http.Client's idle-connection pool;
// calling Close explicitly drops those idle connections so they don't
// accumulate across many adapter instances over long-running processes.
// Mirrors Python's close_sync() in common/mcp_tool_call_conn.py.
func (m *MCPToolAdapter) Close() {
	if m.httpClient != nil {
		m.httpClient.CloseIdleConnections()
	}
}

// BuildMCPToolAdapters wraps a slice of mcpclient.Tool descriptors as
// eino InvokableTool. Returned slice is suitable for handing to
// agenttool.NewRetrieverTool / NewMCPToolAdapter paths or directly to
// the Agent's tool list.
func BuildMCPToolAdapters(tools []mcpclient.Tool) []tool.InvokableTool {
	out := make([]tool.InvokableTool, 0, len(tools))
	for _, t := range tools {
		out = append(out, NewMCPToolAdapter(t))
	}
	return out
}

// marshalArguments is a helper for the future tools/call
// implementation. The argumentsInJSON string from eino is
// round-tripped through json.RawMessage before being passed to the
// MCP server so the server's expected payload structure is preserved.
func marshalArguments(argumentsInJSON string) (json.RawMessage, error) {
	if argumentsInJSON == "" || argumentsInJSON == "{}" {
		return json.RawMessage("{}"), nil
	}
	if !json.Valid([]byte(argumentsInJSON)) {
		return nil, fmt.Errorf("mcp tool: arguments are not valid JSON: %q", argumentsInJSON)
	}
	return json.RawMessage(argumentsInJSON), nil
}
