package agent

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const codexAppServerRequestTimeout = 300 * time.Second

func (d *CodexAppServerDriver) executeServerRequest(ctx context.Context, method string, params json.RawMessage) (any, *appServerResponseError) {
	return d.executeServerRequestWithActivityForGeneration(ctx, 0, "", method, params)
}

func (d *CodexAppServerDriver) executeServerRequestWithActivity(ctx context.Context, requestKey string, method string, params json.RawMessage) (any, *appServerResponseError) {
	return d.executeServerRequestWithActivityForGeneration(ctx, 0, requestKey, method, params)
}

func (d *CodexAppServerDriver) executeServerRequestWithActivityForGeneration(ctx context.Context, generation uint64, requestKey string, method string, params json.RawMessage) (any, *appServerResponseError) {
	switch method {
	case "mcpServer/tool/call":
		var request appServerMCPServerToolCallParams
		if err := json.Unmarshal(params, &request); err != nil {
			return nil, &appServerResponseError{
				Code:    -32602,
				Message: "invalid mcpServer/tool/call params",
			}
		}
		request.Server = strings.TrimSpace(request.Server)
		request.Tool = strings.TrimSpace(request.Tool)
		request.ThreadID = strings.TrimSpace(request.ThreadID)
		request.CallID = strings.TrimSpace(request.CallID)
		request.TurnID = strings.TrimSpace(request.TurnID)
		if request.Server == "" || request.Tool == "" {
			return nil, &appServerResponseError{
				Code:    -32602,
				Message: "mcpServer/tool/call requires server and tool",
			}
		}
		toolEventName := d.mcpToolEventName(request.Server, request.Tool)
		var activityRef toolActivityRef
		var rpcErr *appServerResponseError
		if !d.withLaunchGeneration(generation, func() {
			activityRef, rpcErr = d.resolveToolOutputActivity("mcpServer/tool/call", request.CallID, requestKey, request.ThreadID, request.TurnID, toolEventName)
		}) {
			return nil, staleCodexServerRequestError()
		}
		if rpcErr != nil {
			return nil, rpcErr
		}
		request.ThreadID = activityRef.threadID
		request.CallID = activityRef.callID
		request.TurnID = activityRef.turnID
		toolEventName = activityRef.toolName
		toolResult, err := d.callMCPToolForGeneration(ctx, generation, request.Server, request.Tool, request.Arguments, request.ThreadID)
		if err != nil {
			if !d.withLaunchGeneration(generation, func() {
				d.emitMCPToolOutputTextForTurn(request.ThreadID, request.TurnID, request.CallID, toolEventName, err.Error())
				d.forgetToolActivity(request.CallID, requestKey)
			}) {
				return nil, staleCodexServerRequestError()
			}
			return nil, &appServerResponseError{
				Code:    -32000,
				Message: err.Error(),
			}
		}
		if !d.withLaunchGeneration(generation, func() {
			d.emitMCPToolOutputForTurn(request.ThreadID, request.TurnID, request.CallID, toolEventName, mcpToolResultMessage(toolResult), toolResultStructuredMetadata(toolResult))
			d.forgetToolActivity(request.CallID, requestKey)
		}) {
			return nil, staleCodexServerRequestError()
		}
		if toolResult.IsError {
			return nil, &appServerResponseError{
				Code:    -32000,
				Message: mcpToolResultMessage(toolResult),
			}
		}
		return appServerMCPToolCallResult{
			Content:                 toolResult.Content,
			StructuredContent:       toolResult.StructuredContent,
			runtimeDeliveryReceipts: types.RuntimeDeliveryReceipts(toolResult),
			runtimeDeliverySession:  request.ThreadID,
		}, nil
	case "item/tool/call":
		var request appServerDynamicToolCallParams
		if err := json.Unmarshal(params, &request); err != nil {
			return nil, &appServerResponseError{
				Code:    -32602,
				Message: "invalid item/tool/call params",
			}
		}
		request.ThreadID = strings.TrimSpace(request.ThreadID)
		request.CallID = strings.TrimSpace(request.CallID)
		request.TurnID = strings.TrimSpace(request.TurnID)
		toolEventName := appServerDynamicToolName(request.Namespace, request.Tool)
		var activityRef toolActivityRef
		var rpcErr *appServerResponseError
		if !d.withLaunchGeneration(generation, func() {
			activityRef, rpcErr = d.resolveToolOutputActivity("item/tool/call", request.CallID, requestKey, request.ThreadID, request.TurnID, toolEventName)
		}) {
			return nil, staleCodexServerRequestError()
		}
		if rpcErr != nil {
			return nil, rpcErr
		}
		request.ThreadID = activityRef.threadID
		request.CallID = activityRef.callID
		request.TurnID = activityRef.turnID
		toolEventName = activityRef.toolName
		var serverName, toolName string
		var ok bool
		if !d.withLaunchGeneration(generation, func() {
			serverName, toolName, ok = d.resolveDynamicToolCallTarget(toolEventName)
		}) {
			return nil, staleCodexServerRequestError()
		}
		if !ok {
			d.withLaunchGeneration(generation, func() { d.forgetToolActivity(request.CallID, requestKey) })
			return nil, &appServerResponseError{
				Code:    -32601,
				Message: fmt.Sprintf("unsupported dynamic tool call %q", toolEventName),
			}
		}
		toolResult, err := d.callMCPToolForGeneration(ctx, generation, serverName, toolName, request.Arguments, request.ThreadID)
		if err != nil {
			if !d.withLaunchGeneration(generation, func() {
				d.emitMCPToolOutputTextForTurn(request.ThreadID, request.TurnID, request.CallID, toolEventName, err.Error())
				d.forgetToolActivity(request.CallID, requestKey)
			}) {
				return nil, staleCodexServerRequestError()
			}
			return appServerDynamicToolCallResponse{
				ContentItems: []appServerDynamicToolCallOutputContentItem{{Type: "inputText", Text: err.Error()}},
				Success:      false,
			}, nil
		}
		if !d.withLaunchGeneration(generation, func() {
			d.emitMCPToolOutputForTurn(request.ThreadID, request.TurnID, request.CallID, toolEventName, mcpToolResultMessage(toolResult), toolResultStructuredMetadata(toolResult))
			d.forgetToolActivity(request.CallID, requestKey)
		}) {
			return nil, staleCodexServerRequestError()
		}
		response := dynamicToolCallResponseFromMCPToolResult(toolResult)
		response.runtimeDeliverySession = request.ThreadID
		return response, nil
	case "mcpServer/elicitation/request":
		var request appServerMCPServerElicitationRequestParams
		if err := json.Unmarshal(params, &request); err != nil {
			return nil, &appServerResponseError{
				Code:    -32602,
				Message: "invalid mcpServer/elicitation/request params",
			}
		}
		if response, ok := autoApproveMCPToolApprovalElicitation(request); ok {
			return response, nil
		}
		return appServerMCPServerElicitationRequestResponse{Action: "cancel"}, nil
	case "item/permissions/requestApproval":
		var request appServerPermissionsRequestApprovalParams
		if err := json.Unmarshal(params, &request); err != nil {
			return nil, &appServerResponseError{
				Code:    -32602,
				Message: "invalid item/permissions/requestApproval params",
			}
		}
		mode, err := d.currentLaunchConfig().EffectivePermissionMode()
		if err != nil {
			return nil, &appServerResponseError{
				Code:    -32000,
				Message: err.Error(),
			}
		}
		if mode == types.AgentPermissionModeWorkspaceWrite {
			return nil, &appServerResponseError{
				Code:    -32000,
				Message: "permission request denied by agent permission mode agent_workspace_write",
			}
		}
		return appServerPermissionsRequestApprovalResponse{
			Permissions: request.Permissions,
			Scope:       "turn",
		}, nil
	default:
		return nil, &appServerResponseError{
			Code:    -32601,
			Message: fmt.Sprintf("unsupported codex-app-server server request %q", method),
		}
	}
}

func (d *CodexAppServerDriver) emitMCPToolOutputText(threadID string, toolCallID string, toolName string, text string) {
	d.emitMCPToolOutput(threadID, toolCallID, toolName, text, nil)
}

// emitMCPToolOutputTextForTurn is the async server-request completion path.
// Unlike notification handling, it runs outside the app-server read agentteam, so it
// takes the same turn event fence before publishing root activity.
func (d *CodexAppServerDriver) emitMCPToolOutputTextForTurn(threadID string, turnID string, toolCallID string, toolName string, text string) {
	d.emitMCPToolOutputForTurn(threadID, turnID, toolCallID, toolName, text, nil)
}

func (d *CodexAppServerDriver) emitMCPToolOutputForTurn(threadID string, turnID string, toolCallID string, toolName string, text string, metadata map[string]any) {
	d.turnEventMu.Lock()
	defer d.turnEventMu.Unlock()
	if !d.isCurrentPrimaryTurnActivity(threadID, turnID) {
		return
	}
	d.emitMCPToolOutput(threadID, toolCallID, toolName, text, metadata)
}

func (d *CodexAppServerDriver) emitMCPToolOutput(threadID string, toolCallID string, toolName string, text string, metadata map[string]any) {
	trimmedText := strings.TrimSpace(text)
	if trimmedText == "" {
		return
	}
	sessionID := strings.TrimSpace(threadID)
	if sessionID == "" {
		return
	}
	// Child and detached-review threads share the app-server connection and
	// must still execute their server requests, but their translated tool
	// activity belongs to that child thread, not to the primary AgentTeam Process.
	if !d.isPrimaryThreadEvent(sessionID) {
		return
	}
	d.enqueueTranslatedEvent(translatedEvent{
		Kind:       consts.ParsedEventKindToolOutput,
		Text:       trimmedText,
		ToolName:   strings.TrimSpace(toolName),
		ToolCallID: strings.TrimSpace(toolCallID),
		SessionID:  sessionID,
		Metadata:   cloneToolMetadata(metadata),
	})
}

func (d *CodexAppServerDriver) resolveToolOutputActivity(method string, callID string, requestKey string, threadID string, turnID string, toolName string) (toolActivityRef, *appServerResponseError) {
	method = strings.TrimSpace(method)
	callID = strings.TrimSpace(callID)
	requestKey = strings.TrimSpace(requestKey)
	threadID = strings.TrimSpace(threadID)
	turnID = strings.TrimSpace(turnID)
	toolName = strings.TrimSpace(toolName)

	activityKey := firstNonEmptyString(callID, requestKey)
	if activityKey == "" {
		return toolActivityRef{}, &appServerResponseError{
			Code:    -32602,
			Message: method + " requires toolCallId",
		}
	}

	if ref, ok := d.resolveToolActivity(activityKey); ok {
		ref.threadID = strings.TrimSpace(ref.threadID)
		ref.turnID = firstNonEmptyString(ref.turnID, turnID)
		ref.callID = firstNonEmptyString(ref.callID, activityKey)
		ref.toolName = firstNonEmptyString(ref.toolName, toolName)
		if ref.threadID == "" {
			return toolActivityRef{}, &appServerResponseError{
				Code:    -32602,
				Message: method + " tool activity is unknown",
			}
		}
		if threadID != "" && threadID != ref.threadID {
			return toolActivityRef{}, &appServerResponseError{
				Code:    -32602,
				Message: method + " tool activity threadId mismatch",
			}
		}
		return ref, nil
	}

	if threadID == "" {
		return toolActivityRef{}, &appServerResponseError{
			Code:    -32602,
			Message: method + " requires threadId for toolCallId",
		}
	}
	ref := toolActivityRef{
		threadID: threadID,
		turnID:   turnID,
		callID:   activityKey,
		toolName: toolName,
	}
	d.registerToolActivity(ref, activityKey)
	return ref, nil
}

func (d *CodexAppServerDriver) registerToolActivity(ref toolActivityRef, keys ...string) {
	normalizedKeys := normalizeToolActivityKeys(keys...)
	if len(normalizedKeys) == 0 {
		return
	}
	ref.threadID = strings.TrimSpace(ref.threadID)
	ref.turnID = strings.TrimSpace(ref.turnID)
	ref.callID = strings.TrimSpace(ref.callID)
	ref.toolName = strings.TrimSpace(ref.toolName)
	d.mu.Lock()
	if d.toolActivities == nil {
		d.toolActivities = make(map[string]toolActivityRef)
	}
	for _, key := range normalizedKeys {
		d.toolActivities[key] = ref
	}
	d.mu.Unlock()
}

func (d *CodexAppServerDriver) resolveToolActivity(keys ...string) (toolActivityRef, bool) {
	normalizedKeys := normalizeToolActivityKeys(keys...)
	if len(normalizedKeys) == 0 {
		return toolActivityRef{}, false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	for _, key := range normalizedKeys {
		ref, ok := d.toolActivities[key]
		if ok {
			return ref, true
		}
	}
	return toolActivityRef{}, false
}

func (d *CodexAppServerDriver) forgetToolActivity(keys ...string) {
	normalizedKeys := normalizeToolActivityKeys(keys...)
	if len(normalizedKeys) == 0 {
		return
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	for _, key := range normalizedKeys {
		delete(d.toolActivities, key)
	}
}

func (d *CodexAppServerDriver) forgetToolActivitiesForTurn(threadID string, turnID string) {
	threadID = strings.TrimSpace(threadID)
	turnID = strings.TrimSpace(turnID)
	if threadID == "" && turnID == "" {
		return
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	for key, ref := range d.toolActivities {
		if threadID != "" && strings.TrimSpace(ref.threadID) != threadID {
			continue
		}
		if turnID != "" && strings.TrimSpace(ref.turnID) != turnID {
			continue
		}
		delete(d.toolActivities, key)
	}
}

func normalizeToolActivityKeys(keys ...string) []string {
	seen := make(map[string]struct{}, len(keys))
	out := make([]string, 0, len(keys))
	for _, key := range keys {
		trimmed := strings.TrimSpace(key)
		if trimmed == "" {
			continue
		}
		if _, exists := seen[trimmed]; exists {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

func (d *CodexAppServerDriver) mcpToolEventName(server string, tool string) string {
	server = strings.TrimSpace(server)
	tool = strings.TrimSpace(tool)
	if tool == "" {
		return ""
	}
	if server == "" {
		return tool
	}
	return fmt.Sprintf("mcp__%s__%s", server, tool)
}

func (d *CodexAppServerDriver) mcpDynamicToolName(server string, tool string) string {
	server = strings.TrimSpace(server)
	tool = strings.TrimSpace(tool)
	if tool == "" {
		return ""
	}
	if server == "" {
		return tool
	}
	return fmt.Sprintf("mcp_%s_%s", server, tool)
}

func appServerDynamicToolName(namespace string, tool string) string {
	namespace = strings.TrimSpace(namespace)
	tool = strings.TrimSpace(tool)
	if namespace == "" {
		return tool
	}
	if tool == "" {
		return namespace
	}
	if strings.HasSuffix(namespace, "__") || strings.HasSuffix(namespace, "_") {
		return namespace + tool
	}
	if strings.HasPrefix(namespace, "mcp__") {
		return namespace + "__" + tool
	}
	if strings.HasPrefix(namespace, "mcp_") {
		return namespace + "_" + tool
	}
	return namespace + tool
}

func appServerApprovalPolicy(cfg drivercontract.LaunchConfig) (map[string]any, error) {
	mode, err := cfg.EffectivePermissionMode()
	if err != nil {
		return nil, err
	}
	return map[string]any{
		"granular": map[string]any{
			"mcp_elicitations":    true,
			"request_permissions": mode == types.AgentPermissionModeFullAccess,
			"rules":               false,
			"sandbox_approval":    false,
			"skill_approval":      false,
		},
	}, nil
}

func autoApproveMCPToolApprovalElicitation(request appServerMCPServerElicitationRequestParams) (appServerMCPServerElicitationRequestResponse, bool) {
	if strings.TrimSpace(request.Mode) != "form" {
		return appServerMCPServerElicitationRequestResponse{}, false
	}
	if !isMCPToolApprovalElicitation(request) {
		return appServerMCPServerElicitationRequestResponse{}, false
	}

	content, ok := buildMCPToolApprovalElicitationContent(request.RequestedSchema)
	if !ok {
		return appServerMCPServerElicitationRequestResponse{}, false
	}
	return appServerMCPServerElicitationRequestResponse{
		Action:  "accept",
		Content: content,
	}, true
}

func isMCPToolApprovalElicitation(request appServerMCPServerElicitationRequestParams) bool {
	if kind := trimmedAnyString(request.Meta["codex_approval_kind"]); kind == "mcp_tool_call" {
		return true
	}
	if property, ok := request.RequestedSchema.Properties["codex_approval_kind"]; ok {
		if value, ok := elicitationPropertyValue("codex_approval_kind", property); ok {
			if kind, ok := value.(string); ok && strings.TrimSpace(kind) == "mcp_tool_call" {
				return true
			}
		}
	}
	_, hasApprovalMode := request.RequestedSchema.Properties["approval_mode"]
	return hasApprovalMode
}

func buildMCPToolApprovalElicitationContent(schema appServerMCPServerElicitationRequestedSchema) (map[string]any, bool) {
	if strings.TrimSpace(schema.Type) != "object" {
		return nil, false
	}
	if len(schema.Properties) == 0 {
		return map[string]any{}, len(schema.Required) == 0
	}

	content := make(map[string]any)
	for _, fieldName := range []string{"approval_mode", "codex_approval_kind"} {
		property, ok := schema.Properties[fieldName]
		if !ok {
			continue
		}
		value, ok := elicitationPropertyValue(fieldName, property)
		if !ok {
			return nil, false
		}
		content[fieldName] = value
	}

	for _, fieldName := range schema.Required {
		property, ok := schema.Properties[fieldName]
		if !ok {
			return nil, false
		}
		if _, exists := content[fieldName]; exists {
			continue
		}
		value, ok := elicitationPropertyValue(fieldName, property)
		if !ok {
			return nil, false
		}
		content[fieldName] = value
	}

	if _, exists := content["approval_mode"]; !exists {
		return nil, false
	}
	return content, true
}

func elicitationPropertyValue(fieldName string, property appServerMCPServerElicitationProperty) (any, bool) {
	if len(bytes.TrimSpace(property.Default)) != 0 && !bytes.Equal(bytes.TrimSpace(property.Default), []byte("null")) {
		var decoded any
		if err := json.Unmarshal(property.Default, &decoded); err == nil {
			return decoded, true
		}
	}

	options := elicitationPropertyOptions(property)
	switch fieldName {
	case "approval_mode":
		for _, candidate := range []string{"approve", "accept"} {
			if containsString(options, candidate) {
				return candidate, true
			}
		}
		for _, option := range options {
			trimmed := strings.TrimSpace(option)
			if trimmed != "" && trimmed != "cancel" && trimmed != "decline" {
				return trimmed, true
			}
		}
	case "persist":
		if containsString(options, "session") {
			return "session", true
		}
		if containsString(options, "always") {
			return "always", true
		}
	case "codex_approval_kind":
		if containsString(options, "mcp_tool_call") {
			return "mcp_tool_call", true
		}
	}

	if len(options) == 1 {
		return options[0], true
	}
	return nil, false
}

func elicitationPropertyOptions(property appServerMCPServerElicitationProperty) []string {
	options := make([]string, 0, len(property.Enum)+len(property.OneOf)+len(property.AnyOf))
	for _, option := range property.Enum {
		appendUniqueString(&options, option)
	}
	for _, option := range property.OneOf {
		appendUniqueString(&options, option.Const)
	}
	for _, option := range property.AnyOf {
		appendUniqueString(&options, option.Const)
	}
	return options
}

func staleCodexServerRequestError() *appServerResponseError {
	return &appServerResponseError{Code: -32000, Message: "codex app-server server request generation was superseded"}
}

func (d *CodexAppServerDriver) callMCPTool(ctx context.Context, serverName string, toolName string, arguments json.RawMessage) (*types.MCPToolResult, error) {
	mcpCfg, ok := d.mcpConfig(strings.TrimSpace(serverName))
	if !ok {
		return nil, fmt.Errorf("unknown MCP server %q", strings.TrimSpace(serverName))
	}
	d.mu.Lock()
	runtimeSessionID := strings.TrimSpace(d.threadID)
	d.mu.Unlock()
	return d.callMCPToolWithBinding(ctx, d.httpClientForServerRequests(), mcpCfg, serverName, toolName, arguments, runtimeSessionID)
}

func (d *CodexAppServerDriver) callMCPToolForGeneration(ctx context.Context, generation uint64, serverName string, toolName string, arguments json.RawMessage, runtimeSessionID string) (*types.MCPToolResult, error) {
	if generation == 0 {
		return d.callMCPTool(ctx, serverName, toolName, arguments)
	}
	var (
		mcpCfg types.NamedMCPConfig
		client *http.Client
		ok     bool
	)
	if !d.withLaunchGeneration(generation, func() {
		mcpCfg, ok = d.mcpConfig(strings.TrimSpace(serverName))
		client = d.httpClientForServerRequests()
	}) {
		return nil, context.Canceled
	}
	if !ok {
		return nil, fmt.Errorf("unknown MCP server %q", strings.TrimSpace(serverName))
	}
	return d.callMCPToolWithBinding(ctx, client, mcpCfg, serverName, toolName, arguments, runtimeSessionID)
}

func (d *CodexAppServerDriver) callMCPToolWithBinding(ctx context.Context, client *http.Client, mcpCfg types.NamedMCPConfig, serverName string, toolName string, arguments json.RawMessage, runtimeSessionID string) (*types.MCPToolResult, error) {
	if strings.TrimSpace(mcpCfg.URL) == "" {
		return nil, fmt.Errorf("MCP server %q has no configured URL", strings.TrimSpace(serverName))
	}
	if err := d.authorizeMCPToolEffect(serverName, toolName, runtimeSessionID); err != nil {
		return nil, err
	}

	trimmedArgs := bytes.TrimSpace(arguments)
	if len(trimmedArgs) == 0 || bytes.Equal(trimmedArgs, []byte("null")) {
		trimmedArgs = []byte(`{}`)
	}

	requestBody := map[string]any{
		"jsonrpc": "2.0",
		"id":      1,
		"method":  "tools/call",
		"params": map[string]any{
			"name":      strings.TrimSpace(toolName),
			"arguments": json.RawMessage(trimmedArgs),
		},
	}
	data, err := json.Marshal(requestBody)
	if err != nil {
		return nil, fmt.Errorf("marshal MCP tool call: %w", err)
	}

	if ctx == nil {
		ctx = context.Background()
	}
	ctx, cancel := context.WithTimeout(ctx, codexAppServerRequestTimeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(mcpCfg.URL), bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("build MCP tool call request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	for key, value := range mcpCfg.Headers {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		req.Header.Set(key, value)
	}
	// This HTTP response is only the first hop. The server-request handler owns
	// the authoritative commit after the complete JSON-RPC response is written
	// to the generation-scoped Codex child stdin.
	req.Header.Set(types.MCPDeferredToolDeliveryHeader, "true")
	if runtimeSessionID = strings.TrimSpace(runtimeSessionID); runtimeSessionID != "" {
		req.Header.Set(types.MCPRuntimeSessionHeader, runtimeSessionID)
	}

	if client == nil {
		client = &http.Client{Timeout: codexAppServerRequestTimeout}
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("call MCP tool %s/%s: %w", strings.TrimSpace(serverName), strings.TrimSpace(toolName), err)
	}
	defer func() {
		// Best-effort drain/close after the request has completed; failures only
		// affect connection reuse and should not mask the tool-call result.
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read MCP tool %s/%s response: %w", strings.TrimSpace(serverName), strings.TrimSpace(toolName), err)
	}
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("MCP tool %s/%s HTTP %d: %s", strings.TrimSpace(serverName), strings.TrimSpace(toolName), resp.StatusCode, strings.TrimSpace(string(body)))
	}

	var rpcResponse appServerEnvelope
	if err := json.Unmarshal(body, &rpcResponse); err != nil {
		return nil, fmt.Errorf("decode MCP tool %s/%s response: %w", strings.TrimSpace(serverName), strings.TrimSpace(toolName), err)
	}
	if rpcResponse.Error != nil {
		message := strings.TrimSpace(rpcResponse.Error.Message)
		if message == "" {
			message = fmt.Sprintf("JSON-RPC error %d", rpcResponse.Error.Code)
		}
		return nil, fmt.Errorf("MCP tool %s/%s: %s", strings.TrimSpace(serverName), strings.TrimSpace(toolName), message)
	}
	if len(bytes.TrimSpace(rpcResponse.Result)) == 0 {
		return nil, fmt.Errorf("MCP tool %s/%s returned an empty result", strings.TrimSpace(serverName), strings.TrimSpace(toolName))
	}

	var toolResult types.MCPToolResult
	if err := json.Unmarshal(rpcResponse.Result, &toolResult); err != nil {
		return nil, fmt.Errorf("decode MCP tool %s/%s result: %w", strings.TrimSpace(serverName), strings.TrimSpace(toolName), err)
	}
	return &toolResult, nil
}

func (d *CodexAppServerDriver) dynamicToolsForMCPServers(ctx context.Context) ([]appServerDynamicTool, error) {
	bindings := d.mcpInventoryBindingsByLengthDesc()
	if len(bindings) == 0 {
		return nil, nil
	}

	var dynamicTools []appServerDynamicTool
	for _, binding := range bindings {
		if strings.TrimSpace(binding.config.URL) == "" {
			continue
		}
		toolDefs, err := listMCPToolsWithBinding(ctx, binding)
		if err != nil {
			if codexAppServerMCPDiscoveryRequired(binding.serverName) {
				return nil, err
			}
			slog.Debug("skip optional MCP dynamic tools after tools/list failure",
				"server", binding.serverName,
				"error", err)
			continue
		}
		if !d.replaceTrustedMCPToolEffects(binding.generation, binding.serverName, toolDefs) {
			return nil, fmt.Errorf("MCP tools/list %s generation was superseded", binding.serverName)
		}
		for _, def := range toolDefs {
			toolName := strings.TrimSpace(def.Name)
			if toolName == "" {
				continue
			}
			dynamicTools = append(dynamicTools, appServerDynamicTool{
				Name:        d.mcpDynamicToolName(binding.serverName, toolName),
				Description: strings.TrimSpace(def.Description),
				InputSchema: dynamicToolInputSchema(def.InputSchema),
			})
		}
	}
	return dynamicTools, nil
}

func trustedMCPToolEffectKey(serverName, toolName string) string {
	return strings.TrimSpace(serverName) + "\x00" + strings.TrimSpace(toolName)
}

// replaceTrustedMCPToolEffects atomically publishes one server's complete
// daemon-fetched inventory for the captured launch generation. Model arguments
// never participate in the read/write classification: missing, false, removed,
// and unknown tools all remain effectful.
func (d *CodexAppServerDriver) replaceTrustedMCPToolEffects(generation uint64, serverName string, defs []appServerMCPToolDef) bool {
	serverName = strings.TrimSpace(serverName)
	if serverName == "" {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.launchGeneration != generation {
		return false
	}
	if d.trustedMCPToolReadOnly == nil {
		d.trustedMCPToolReadOnly = make(map[string]bool)
	}
	serverPrefix := serverName + "\x00"
	for key := range d.trustedMCPToolReadOnly {
		if strings.HasPrefix(key, serverPrefix) {
			delete(d.trustedMCPToolReadOnly, key)
		}
	}
	for _, def := range defs {
		toolName := strings.TrimSpace(def.Name)
		if toolName == "" {
			continue
		}
		readOnly := def.Annotations != nil && def.Annotations.ReadOnlyHint
		d.trustedMCPToolReadOnly[trustedMCPToolEffectKey(serverName, toolName)] = readOnly
	}
	return true
}

// authorizeMCPToolEffect enforces the first daemon-owned delegated-child
// boundary before any AgentTeam MCP HTTP side effect. Primary calls proceed. A
// positively identified child may call only tools marked readOnlyHint=true in
// the trusted AgentTeam inventory; missing, false, and unknown fail closed.
func (d *CodexAppServerDriver) authorizeMCPToolEffect(serverName, toolName, providerThreadID string) error {
	serverName = strings.TrimSpace(serverName)
	if serverName != "agentteam" {
		return nil
	}

	d.mu.Lock()
	authority := runtimeCallerAuthority{
		processGeneration: fmt.Sprintf("%d", d.launchGeneration),
		runtime:           d.Name(),
		providerThreadID:  strings.TrimSpace(providerThreadID),
		primaryThreadID:   strings.TrimSpace(d.threadID),
	}
	readOnly, known := d.trustedMCPToolReadOnly[trustedMCPToolEffectKey(serverName, toolName)]
	d.mu.Unlock()

	if authority.isPrimary() {
		return nil
	}
	if known && readOnly {
		return nil
	}
	slog.Warn(
		"denied delegated Codex child AgentTeam MCP effect",
		"process_generation", authority.processGeneration,
		"provider_thread_id", authority.providerThreadID,
		"primary_thread_id", authority.primaryThreadID,
		"tool", strings.TrimSpace(toolName),
		"inventory_known", known,
		"read_only_hint", readOnly,
		"reason", "delegated_child_effect_denied",
	)
	return fmt.Errorf(
		"delegated child cannot call AgentTeam write tool %q; return result to leader",
		strings.TrimSpace(toolName),
	)
}

func codexAppServerMCPDiscoveryRequired(serverName string) bool {
	return strings.TrimSpace(serverName) == "agentteam"
}

func dynamicToolInputSchema(schema json.RawMessage) json.RawMessage {
	trimmed := bytes.TrimSpace(schema)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
		return json.RawMessage(`{"type":"object","properties":{}}`)
	}
	return append(json.RawMessage(nil), trimmed...)
}

type codexMCPInventoryBinding struct {
	generation uint64
	serverName string
	config     types.NamedMCPConfig
	client     *http.Client
}

func listMCPToolsWithBinding(ctx context.Context, binding codexMCPInventoryBinding) ([]appServerMCPToolDef, error) {
	serverName := strings.TrimSpace(binding.serverName)
	mcpCfg := binding.config
	if strings.TrimSpace(mcpCfg.URL) == "" {
		return nil, fmt.Errorf("MCP server %q has no configured URL", serverName)
	}

	requestBody := map[string]any{
		"jsonrpc": "2.0",
		"id":      1,
		"method":  "tools/list",
		"params":  map[string]any{},
	}
	data, err := json.Marshal(requestBody)
	if err != nil {
		return nil, fmt.Errorf("marshal MCP tools/list: %w", err)
	}

	if ctx == nil {
		ctx = context.Background()
	}
	ctx, cancel := context.WithTimeout(ctx, codexAppServerRequestTimeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(mcpCfg.URL), bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("build MCP tools/list request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	for key, value := range mcpCfg.Headers {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		req.Header.Set(key, value)
	}

	client := binding.client
	if client == nil {
		client = &http.Client{Timeout: codexAppServerRequestTimeout}
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("list MCP tools for %s: %w", strings.TrimSpace(serverName), err)
	}
	defer func() {
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read MCP tools/list %s response: %w", strings.TrimSpace(serverName), err)
	}
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("MCP tools/list %s HTTP %d: %s", strings.TrimSpace(serverName), resp.StatusCode, strings.TrimSpace(string(body)))
	}

	var rpcResponse appServerEnvelope
	if err := json.Unmarshal(body, &rpcResponse); err != nil {
		return nil, fmt.Errorf("decode MCP tools/list %s response: %w", strings.TrimSpace(serverName), err)
	}
	if rpcResponse.Error != nil {
		message := strings.TrimSpace(rpcResponse.Error.Message)
		if message == "" {
			message = fmt.Sprintf("JSON-RPC error %d", rpcResponse.Error.Code)
		}
		return nil, fmt.Errorf("MCP tools/list %s: %s", strings.TrimSpace(serverName), message)
	}

	var result appServerMCPToolsListResult
	if err := json.Unmarshal(rpcResponse.Result, &result); err != nil {
		return nil, fmt.Errorf("decode MCP tools/list %s result: %w", strings.TrimSpace(serverName), err)
	}
	return result.Tools, nil
}

func (d *CodexAppServerDriver) resolveDynamicToolCallTarget(toolName string) (string, string, bool) {
	name := strings.TrimSpace(toolName)
	if server, tool, ok := d.resolveNamespacedMCPToolCallTarget(name); ok {
		return server, tool, true
	}
	if server, tool, ok := d.resolveFlatMCPToolCallTarget(name); ok {
		return server, tool, true
	}
	return "", "", false
}

func (d *CodexAppServerDriver) resolveNamespacedMCPToolCallTarget(name string) (string, string, bool) {
	const prefix = "mcp__"
	if !strings.HasPrefix(name, prefix) {
		return "", "", false
	}
	for _, server := range d.mcpConfigNamesByLengthDesc() {
		server = strings.TrimSpace(server)
		if server == "" {
			continue
		}
		serverPrefix := prefix + server + "__"
		if strings.HasPrefix(name, serverPrefix) {
			tool := strings.TrimSpace(strings.TrimPrefix(name, serverPrefix))
			if tool != "" {
				return server, tool, true
			}
		}
	}
	return "", "", false
}

func (d *CodexAppServerDriver) resolveFlatMCPToolCallTarget(name string) (string, string, bool) {
	const prefix = "mcp_"
	if !strings.HasPrefix(name, prefix) {
		return "", "", false
	}
	for _, server := range []string{"agentteam"} {
		if server, tool, ok := d.resolveFlatMCPToolCallTargetForServer(name, server); ok {
			return server, tool, true
		}
	}
	for _, cfg := range d.mcpConfigNamesByLengthDesc() {
		server := strings.TrimSpace(cfg)
		if server == "" || server == "agentteam" {
			continue
		}
		if server, tool, ok := d.resolveFlatMCPToolCallTargetForServer(name, server); ok {
			return server, tool, true
		}
	}
	return "", "", false
}

func (d *CodexAppServerDriver) resolveFlatMCPToolCallTargetForServer(name string, server string) (string, string, bool) {
	server = strings.TrimSpace(server)
	if server == "" {
		return "", "", false
	}
	if _, ok := d.mcpConfig(server); !ok {
		return "", "", false
	}
	serverPrefix := "mcp_" + server + "_"
	if !strings.HasPrefix(name, serverPrefix) {
		return "", "", false
	}
	tool := strings.TrimSpace(strings.TrimPrefix(name, serverPrefix))
	if tool == "" {
		return "", "", false
	}
	return server, tool, true
}

func (d *CodexAppServerDriver) mcpConfigNamesByLengthDesc() []string {
	d.mu.Lock()
	defer d.mu.Unlock()
	names := make([]string, 0, len(d.mcpConfigs))
	for name := range d.mcpConfigs {
		name = strings.TrimSpace(name)
		if name != "" {
			names = append(names, name)
		}
	}
	sort.Slice(names, func(i, j int) bool {
		if len(names[i]) == len(names[j]) {
			return names[i] < names[j]
		}
		return len(names[i]) > len(names[j])
	})
	return names
}

func (d *CodexAppServerDriver) mcpInventoryBindingsByLengthDesc() []codexMCPInventoryBinding {
	d.mu.Lock()
	defer d.mu.Unlock()
	if len(d.mcpConfigs) == 0 {
		return nil
	}
	client := d.httpClient
	if client == nil {
		client = &http.Client{Timeout: codexAppServerRequestTimeout}
	}
	bindings := make([]codexMCPInventoryBinding, 0, len(d.mcpConfigs))
	for name, config := range d.mcpConfigs {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		bindings = append(bindings, codexMCPInventoryBinding{
			generation: d.launchGeneration,
			serverName: name,
			config:     config,
			client:     client,
		})
	}
	sort.Slice(bindings, func(i, j int) bool {
		if len(bindings[i].serverName) == len(bindings[j].serverName) {
			return bindings[i].serverName < bindings[j].serverName
		}
		return len(bindings[i].serverName) > len(bindings[j].serverName)
	})
	return bindings
}

func (d *CodexAppServerDriver) mcpConfig(name string) (types.NamedMCPConfig, bool) {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.mcpConfigs == nil {
		return types.NamedMCPConfig{}, false
	}
	cfg, ok := d.mcpConfigs[strings.TrimSpace(name)]
	return cfg, ok
}

func (d *CodexAppServerDriver) httpClientForServerRequests() *http.Client {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.httpClient != nil {
		return d.httpClient
	}
	return &http.Client{Timeout: codexAppServerRequestTimeout}
}

func dynamicToolCallResponseFromMCPToolResult(result *types.MCPToolResult) appServerDynamicToolCallResponse {
	if result == nil {
		return appServerDynamicToolCallResponse{
			ContentItems: []appServerDynamicToolCallOutputContentItem{},
			Success:      false,
		}
	}

	items := make([]appServerDynamicToolCallOutputContentItem, 0, len(result.Content))
	for _, content := range result.Content {
		items = append(items, appServerDynamicToolCallOutputContentItem{
			Text: strings.TrimSpace(content.Text),
			Type: "inputText",
		})
	}
	return appServerDynamicToolCallResponse{
		ContentItems:            items,
		Success:                 !result.IsError,
		runtimeDeliveryReceipts: types.RuntimeDeliveryReceipts(result),
	}
}

func toolResultStructuredMetadata(result *types.MCPToolResult) map[string]any {
	if result == nil {
		return nil
	}
	metadata := map[string]any{}
	if result.IsError {
		metadata["toolError"] = true
	}
	if result.StructuredContent == nil {
		return metadata
	}
	if metadata, ok := result.StructuredContent.(map[string]any); ok {
		cloned := cloneToolMetadata(metadata)
		if result.IsError {
			cloned["toolError"] = true
		}
		return cloned
	}
	raw, err := json.Marshal(result.StructuredContent)
	if err != nil {
		return nil
	}
	var structured map[string]any
	if err := json.Unmarshal(raw, &structured); err != nil {
		return metadata
	}
	for key, value := range structured {
		metadata[key] = value
	}
	return metadata
}

func cloneToolMetadata(metadata map[string]any) map[string]any {
	if len(metadata) == 0 {
		return nil
	}
	out := make(map[string]any, len(metadata))
	for key, value := range metadata {
		out[key] = value
	}
	return out
}

func mcpToolResultMessage(result *types.MCPToolResult) string {
	if result == nil {
		return "tool call failed"
	}
	parts := make([]string, 0, len(result.Content))
	for _, content := range result.Content {
		text := strings.TrimSpace(content.Text)
		if text != "" {
			parts = append(parts, text)
		}
	}
	if len(parts) == 0 {
		return "tool call failed"
	}
	return strings.Join(parts, "\n")
}

func namedMCPConfigsByName(configs []types.NamedMCPConfig) map[string]types.NamedMCPConfig {
	if len(configs) == 0 {
		return nil
	}
	result := make(map[string]types.NamedMCPConfig, len(configs))
	for _, cfg := range configs {
		name := strings.TrimSpace(cfg.Name)
		if name == "" {
			continue
		}
		result[name] = cfg
	}
	return result
}
