package mcp

import (
	"context"
	"fmt"

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

	"github.com/zzet/gortex/internal/graph"
)

// recommendedCall is one entry in plan_turn's output. Each suggestion carries
// the tool name, pre-filled arguments the agent can pass straight back to the
// MCP client, and a short `why` explaining why this call ranks where it does.
type recommendedCall struct {
	Tool string         `json:"tool"`
	Args map[string]any `json:"args"`
	Why  string         `json:"why"`
}

// handlePlanTurn is a cheap opening-move router. It runs the same keyword
// extraction + BM25 search that smart_context does, then emits a short
// ordered list of recommended tool calls — what to run *next*, not the
// context itself. Agents (and subagents especially) waste the first few
// turns picking the wrong tool; a ranked list of calls with args filled in
// collapses that exploration into one cheap query.
//
// The output is deliberately small (~200 tokens): this is a routing tool,
// not a context tool.
func (s *Server) handlePlanTurn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	task, err := req.RequireString("task")
	if err != nil {
		return mcp.NewToolResultError("task is required"), nil
	}
	maxCalls := req.GetInt("max_calls", 4)
	if maxCalls <= 0 {
		maxCalls = 4
	}

	keywords := extractKeywords(task)

	seen := make(map[string]bool)
	var candidates []*graph.Node

	// Exact-name match first. extractKeywords already prioritises
	// identifier-shape tokens; if the user actually typed the symbol
	// they care about (`renderToolsSearchResult`), the graph's name
	// index has the answer directly. engine.SearchSymbols is plain
	// BM25 — without a rerank pipeline it ranks ts_lex / set_contains
	// above a single exact-name match because parser tables saturate
	// the token corpus. The exact-name shortcut puts the user's
	// identifier at position 0; BM25 still fills the rest.
	for _, kw := range keywords {
		if len(kw) < 3 || !hasIdentifierShape(kw) {
			continue
		}
		for _, m := range s.scopedNodeSlice(ctx, s.graph.FindNodesByName(kw)) {
			if m.Kind == graph.KindFile || m.Kind == graph.KindImport {
				continue
			}
			if !seen[m.ID] {
				seen[m.ID] = true
				candidates = append(candidates, m)
			}
		}
	}

	// BM25 search per keyword; dedup + filter out files/imports.
	for _, kw := range keywords {
		if len(kw) < 3 {
			continue
		}
		for _, m := range s.scopedNodeSlice(ctx, s.engineFor(ctx).SearchSymbols(kw, 10)) {
			if m.Kind == graph.KindFile || m.Kind == graph.KindImport {
				continue
			}
			if !seen[m.ID] {
				seen[m.ID] = true
				candidates = append(candidates, m)
			}
		}
	}

	// Top 5 candidates are enough to derive recommendations — plan_turn is a
	// router, not a context-assembler.
	const maxCandidates = 5
	if len(candidates) > maxCandidates {
		candidates = candidates[:maxCandidates]
	}

	recs := buildRecommendedCalls(task, candidates, maxCalls)

	topCandidateIDs := make([]string, 0, len(candidates))
	for _, c := range candidates {
		topCandidateIDs = append(topCandidateIDs, c.ID)
	}

	return s.respondJSONOrTOON(ctx, req, map[string]any{
		"task":              task,
		"keywords":          keywords,
		"recommended_calls": recs,
		"top_candidates":    topCandidateIDs,
		"candidate_count":   len(candidates),
	})
}

// buildRecommendedCalls composes the ranked suggestion list. The rules here
// are the codified "good first moves":
//  1. smart_context with the task — always a safe opening move for
//     exploratory work.
//  2. get_editing_context on the top candidate's file — the next step
//     once smart_context lands on a file to edit.
//  3. find_usages on the top candidate symbol — only useful if the symbol
//     is callable (function/method/type); for variables/packages it's
//     noise, so the check is explicit.
//  4. search_symbols with the raw task string as fallback when BM25
//     produced no candidates.
//
// Returns at most maxCalls entries. Never returns empty — even with no
// candidates, smart_context is worth suggesting as an opening move.
func buildRecommendedCalls(task string, candidates []*graph.Node, maxCalls int) []recommendedCall {
	recs := make([]recommendedCall, 0, maxCalls)

	recs = append(recs, recommendedCall{
		Tool: "smart_context",
		Args: map[string]any{"task": task},
		Why:  "Opening move — aggregates top symbols, entry file context, related tests in one call",
	})

	if len(candidates) > 0 {
		top := candidates[0]
		if top.FilePath != "" && len(recs) < maxCalls {
			recs = append(recs, recommendedCall{
				Tool: "get_editing_context",
				Args: map[string]any{"path": top.FilePath},
				Why:  fmt.Sprintf("Top-ranked symbol %q lives in %s — load its file context before editing", top.Name, top.FilePath),
			})
		}
		if isCallableKind(top.Kind) && len(recs) < maxCalls {
			recs = append(recs, recommendedCall{
				Tool: "find_usages",
				Args: map[string]any{"id": top.ID},
				Why:  fmt.Sprintf("Top candidate %q is a %s — see callers before changing its signature", top.Name, top.Kind),
			})
		}
	}

	// Raw-string search_symbols as a fallback path. Useful when the task
	// mentions a specific concept that keyword extraction may have split.
	if len(recs) < maxCalls {
		recs = append(recs, recommendedCall{
			Tool: "search_symbols",
			Args: map[string]any{"query": task, "compact": true, "limit": 10},
			Why:  "Broad BM25 search across the raw task text; use when candidates above aren't the right lead",
		})
	}

	if len(recs) > maxCalls {
		recs = recs[:maxCalls]
	}
	return recs
}

// isCallableKind returns true for symbol kinds where find_usages /
// get_callers are meaningful. Packages, imports, and files are excluded —
// usage queries on those return noise.
func isCallableKind(k graph.NodeKind) bool {
	switch k {
	case graph.KindFunction, graph.KindMethod, graph.KindType, graph.KindInterface:
		return true
	}
	return false
}
