package mcp

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
)

// defaultResponseBufferCap is how many recent tool responses a session
// keeps available for post-filter re-cutting.
const defaultResponseBufferCap = 8

// minCapturedResponseBytes is the capture floor: responses smaller
// than this are cheap to re-fetch, so they never displace a ring slot.
const minCapturedResponseBytes = 1024

// postFilterTools never have their own responses captured — re-cutting
// a re-cut would only churn the ring.
var postFilterTools = map[string]bool{
	"ctx_stats":    true,
	"ctx_peek":     true,
	"ctx_slice":    true,
	"ctx_grep":     true,
	"grep_results": true,
	"head_results": true,
}

// bufferedResponse is one captured tool response held for re-cutting.
type bufferedResponse struct {
	ID         string
	Tool       string
	Text       string
	CapturedAt time.Time
}

// responseBuffer is a per-session ring of recent large tool responses.
// It backs the post-filter tools (ctx_grep, ctx_slice, …) so an agent
// can re-cut a prior result without re-issuing the original query.
type responseBuffer struct {
	mu      sync.Mutex
	entries []bufferedResponse
	seq     int
}

// capture stores a response and returns its handle ID. The oldest
// entry is evicted, and its backing memory released, once the ring is
// full.
func (b *responseBuffer) capture(tool, text string) string {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.seq++
	id := fmt.Sprintf("resp_%d", b.seq)
	b.entries = append(b.entries, bufferedResponse{
		ID:         id,
		Tool:       tool,
		Text:       text,
		CapturedAt: time.Now(),
	})
	if len(b.entries) > defaultResponseBufferCap {
		trimmed := make([]bufferedResponse, defaultResponseBufferCap)
		copy(trimmed, b.entries[len(b.entries)-defaultResponseBufferCap:])
		b.entries = trimmed
	}
	return id
}

// get resolves a handle. An empty or "latest" id returns the most
// recent capture; the bool is false when the buffer is empty or the id
// is unknown.
func (b *responseBuffer) get(id string) (bufferedResponse, bool) {
	b.mu.Lock()
	defer b.mu.Unlock()
	if len(b.entries) == 0 {
		return bufferedResponse{}, false
	}
	if id == "" || id == "latest" {
		return b.entries[len(b.entries)-1], true
	}
	for i := len(b.entries) - 1; i >= 0; i-- {
		if b.entries[i].ID == id {
			return b.entries[i], true
		}
	}
	return bufferedResponse{}, false
}

// list returns every buffered response, most recent first.
func (b *responseBuffer) list() []bufferedResponse {
	b.mu.Lock()
	defer b.mu.Unlock()
	out := make([]bufferedResponse, 0, len(b.entries))
	for i := len(b.entries) - 1; i >= 0; i-- {
		out = append(out, b.entries[i])
	}
	return out
}

// responseBufferFor returns the calling session's response buffer,
// allocating it on first use.
func (s *Server) responseBufferFor(ctx context.Context) *responseBuffer {
	sess := s.sessionFor(ctx)
	sess.mu.Lock()
	defer sess.mu.Unlock()
	if sess.responses == nil {
		sess.responses = &responseBuffer{}
	}
	return sess.responses
}

// captureResponse stores a large, successful, non-post-filter tool
// response in the session ring so it can be re-cut later. Called from
// wrapToolHandler for every tool call.
func (s *Server) captureResponse(ctx context.Context, tool string, res *mcp.CallToolResult) {
	if res == nil || res.IsError || postFilterTools[tool] {
		return
	}
	text := normalizeForBuffer(toolResultText(res))
	if len(text) < minCapturedResponseBytes {
		return
	}
	s.responseBufferFor(ctx).capture(tool, text)
}

// normalizeForBuffer renders a compact-JSON response as indented text
// so the post-filter tools' line-based slicing and grep land on
// meaningful lines. Non-JSON text (GCX, TOON) is already
// line-structured and passes through unchanged.
func normalizeForBuffer(text string) string {
	t := strings.TrimSpace(text)
	if t == "" || (t[0] != '{' && t[0] != '[') {
		return text
	}
	var buf bytes.Buffer
	if err := json.Indent(&buf, []byte(text), "", "  "); err != nil {
		return text
	}
	return buf.String()
}

// toolResultText extracts the concatenated text content of a tool
// result. The common single-block case is returned without a copy.
func toolResultText(res *mcp.CallToolResult) string {
	if res == nil || len(res.Content) == 0 {
		return ""
	}
	if len(res.Content) == 1 {
		if tc, ok := res.Content[0].(mcp.TextContent); ok {
			return tc.Text
		}
		return ""
	}
	var b strings.Builder
	for _, c := range res.Content {
		if tc, ok := c.(mcp.TextContent); ok {
			b.WriteString(tc.Text)
		}
	}
	return b.String()
}
