package main

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
)

// Task tools are nomos-LOCAL, not MCP tools. They are session-scoped, and the
// shared MCP server (api:8090/mcp) has no session id — so these are handled
// in-process by nomos, which knows the session/task and holds the store.
// buildTools appends these to the model's tool list; the agent loop routes a
// call whose name isTaskTool to handleTaskTool instead of the MCP client.
//
// Phase 3 ships complete_task; set_goal / propose_plan / update_plan_step /
// ask_operator land in later phases through the same mechanism.

func taskToolDefs() []toolDef {
	return []toolDef{
		{
			Name: "set_goal",
			Description: "State the goal of this task in one sentence, as early as you " +
				"can. This is what the task is trying to achieve (e.g. 'Deploy TypeType " +
				"as an LXC on strong'); it heads the task on the board and the context " +
				"panel. Call it once you understand what the operator wants.",
			InputSchema: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"goal": map[string]any{"type": "string", "description": "The task's goal, one sentence."},
				},
				"required": []string{"goal"},
			},
		},
		{
			Name: "propose_plan",
			Description: "Lay out ALL the ordered steps you'll take to reach the goal, in ONE " +
				"call, listing every step end-to-end — not just the next one. The operator " +
				"sees the full list in the context panel and watches it progress; a plan " +
				"with only 1 step looks broken to them even if you intend to add more later. " +
				"Your FIRST step should be research (prior knowledge, relations, blast radius " +
				"— not just this target's status) and your LAST step should be writing back " +
				"what you learned (update_entity_attributes / create_relationship / " +
				"upsert_knowledge) BEFORE complete_task — this is what keeps the knowledge " +
				"graph from drifting out of date. " +
				"Call this ONCE, before you start executing (after gathering what you need). " +
				"As you work, call update_plan_step (not propose_plan again) to advance each " +
				"step. Only re-call propose_plan if the plan itself has fundamentally changed " +
				"(e.g. a new approach is needed) — in that case new steps are appended after " +
				"whatever already ran, never erasing completed work.",
			InputSchema: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"steps": map[string]any{
						"type":        "array",
						"description": "Ordered steps, first to last.",
						"items": map[string]any{
							"type": "object",
							"properties": map[string]any{
								"title":       map[string]any{"type": "string", "description": "Short imperative step title (e.g. 'Create the LXC')."},
								"detail":      map[string]any{"type": "string", "description": "Optional one-line detail."},
								"target_slug": map[string]any{"type": "string", "description": "Optional entity slug this step acts on (e.g. lxc:typetype)."},
							},
							"required": []string{"title"},
						},
					},
				},
				"required": []string{"steps"},
			},
		},
		{
			Name: "update_plan_step",
			Description: "Advance a plan step as you work it. Set status to 'running' when " +
				"you start it (pass execution_id if the step queued a gated action, so " +
				"the board can auto-close it when that finishes), then 'done' / 'failed' " +
				"/ 'skipped' / 'blocked' when it resolves. Keeps the operator's progress " +
				"view honest.",
			InputSchema: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"seq":          map[string]any{"type": "integer", "description": "1-based step number from propose_plan."},
					"status":       map[string]any{"type": "string", "enum": []string{"running", "done", "failed", "skipped", "blocked"}, "description": "New status for the step."},
					"execution_id": map[string]any{"type": "string", "description": "Optional execution UUID this step is running, so it auto-closes on completion."},
				},
				"required": []string{"seq", "status"},
			},
		},
		{
			Name: "ask_operator",
			Description: "Ask the operator a question when you hit a real decision only " +
				"they can make — an ambiguous target, a trade-off, missing information, " +
				"or a destructive choice not already approved. This pins a structured " +
				"question card in the context panel (with your options and the entities " +
				"involved) and PAUSES the task until they answer; their answer resumes " +
				"you automatically. Do NOT use it for things you can determine yourself " +
				"with tools — only for genuine decisions.",
			InputSchema: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"prompt": map[string]any{"type": "string", "description": "The question, stated plainly."},
					"why":    map[string]any{"type": "string", "description": "Why you're asking / what's at stake."},
					"options": map[string]any{
						"type": "array", "items": map[string]any{"type": "string"},
						"description": "The choices, if it's a pick-one decision.",
					},
					"context_entities": map[string]any{
						"type": "array", "items": map[string]any{"type": "string"},
						"description": "Entity slugs relevant to the decision (shown as chips).",
					},
				},
				"required": []string{"prompt"},
			},
		},
		{
			Name: "complete_task",
			Description: "Mark the current task finished. Call this once the goal is " +
				"verified done — or when you've genuinely failed or only partially " +
				"succeeded. Sets the task's outcome and a one-line summary shown on the " +
				"task board. Record what you learned with upsert_knowledge BEFORE " +
				"completing, so future tasks on the same entities benefit.",
			InputSchema: map[string]any{
				"type": "object",
				"properties": map[string]any{
					"outcome": map[string]any{
						"type":        "string",
						"enum":        []string{"success", "failure", "partial"},
						"description": "Did the task achieve its goal?",
					},
					"summary": map[string]any{
						"type":        "string",
						"description": "One line describing the result (shown on the task card).",
					},
				},
				"required": []string{"outcome", "summary"},
			},
		},
	}
}

// toInt coerces a JSON tool-arg number (float64 after unmarshal) to int.
func toInt(v any) int {
	switch n := v.(type) {
	case float64:
		return int(n)
	case int:
		return n
	default:
		return 0
	}
}

// toStringSlice coerces a JSON tool-arg array to a non-empty []string.
func toStringSlice(v any) []string {
	arr, ok := v.([]any)
	if !ok {
		return nil
	}
	out := make([]string, 0, len(arr))
	for _, e := range arr {
		if s, ok := e.(string); ok && strings.TrimSpace(s) != "" {
			out = append(out, s)
		}
	}
	return out
}

// handleTaskTool executes a nomos-local task tool. Returns (result, true) if it
// handled the call, or (nil, false) if name is not a local task tool (so the
// caller forwards it to the MCP client).
func (a *agent) handleTaskTool(ctx context.Context, sessionID, name string, args map[string]any) (any, bool) {
	switch name {
	case "set_goal":
		goal, _ := args["goal"].(string)
		if strings.TrimSpace(goal) == "" {
			return "error: set_goal needs a goal", true
		}
		if err := a.store.setGoal(ctx, sessionID, goal); err != nil {
			return fmt.Sprintf("error setting goal: %v", err), true
		}
		return "Goal set: " + goal, true

	case "propose_plan":
		raw, _ := args["steps"].([]any)
		var steps []planStepInput
		for _, r := range raw {
			m, ok := r.(map[string]any)
			if !ok {
				continue
			}
			title, _ := m["title"].(string)
			if strings.TrimSpace(title) == "" {
				continue
			}
			detail, _ := m["detail"].(string)
			target, _ := m["target_slug"].(string)
			steps = append(steps, planStepInput{Title: title, Detail: detail, TargetSlug: target})
		}
		if len(steps) == 0 {
			return "error: propose_plan needs at least one step with a title", true
		}
		persisted, err := a.store.proposePlan(ctx, sessionID, steps)
		if err != nil {
			return fmt.Sprintf("error proposing plan: %v", err), true
		}
		return fmt.Sprintf("Plan set: %d step(s). Execute them now, marking each with update_plan_step as you go.", len(persisted)), true

	case "update_plan_step":
		seq := toInt(args["seq"])
		status, _ := args["status"].(string)
		execID, _ := args["execution_id"].(string)
		if seq <= 0 || status == "" {
			return "error: update_plan_step needs seq (>=1) and status", true
		}
		if err := a.store.updatePlanStep(ctx, sessionID, seq, status, execID); err != nil {
			return fmt.Sprintf("error updating step %d: %v", seq, err), true
		}
		return fmt.Sprintf("Step %d → %s", seq, status), true

	case "ask_operator":
		prompt, _ := args["prompt"].(string)
		if strings.TrimSpace(prompt) == "" {
			return "error: ask_operator needs a prompt", true
		}
		qctx := map[string]any{}
		if why, _ := args["why"].(string); strings.TrimSpace(why) != "" {
			qctx["why"] = why
		}
		if opts := toStringSlice(args["options"]); len(opts) > 0 {
			qctx["options"] = opts
		}
		if ents := toStringSlice(args["context_entities"]); len(ents) > 0 {
			qctx["entities"] = ents
		}
		if _, err := a.store.askOperator(ctx, sessionID, prompt, qctx); err != nil {
			return fmt.Sprintf("error posting question: %v", err), true
		}
		return "Question posted to the operator; the task is paused until they answer. " +
			"Do not continue or call more tools — end your turn now and wait for their answer.", true

	case "complete_task":
		outcome, _ := args["outcome"].(string)
		summary, _ := args["summary"].(string)
		switch outcome {
		case "":
			outcome = "success" // no outcome given at all — assume success, the common case
		case "success", "failure", "partial":
			// valid, use as-is
		default:
			// The tool schema declares an enum, but a weaker model (or a
			// typo) can still send anything — an unrecognized value used to
			// persist as-is, silently, with only "failure" special-cased
			// (store.completeTask derives status='failed' from it; anything
			// else became status='done' regardless of what the value
			// actually said). Default to "partial" rather than silently
			// treating an unrecognized value as "success" — safer to
			// under-claim than over-claim a task's outcome.
			slog.Warn("nomos: complete_task got an unrecognized outcome, defaulting to partial",
				"session", sessionID, "outcome", outcome)
			outcome = "partial"
		}
		if err := a.store.completeTask(ctx, sessionID, outcome, summary); err != nil {
			return fmt.Sprintf("error completing task: %v", err), true
		}
		return fmt.Sprintf("Task marked %s: %s", outcome, summary), true
	default:
		return nil, false
	}
}

// autoCompleteTrivialTask is the case-1 fix from
// plans/2026-07-11-task-completion-safety-net.md: a session that never
// called set_goal never framed itself as a structured task, so a turn that
// ends with a plain-text answer and no further tool calls IS the task
// ending — but the model consistently skips complete_task for exactly this
// case (confirmed live: 43/50 production sessions were a single trivial
// Q&A exchange, none of which ever reached a terminal status). Rather than
// leave agent_sessions.status stuck at its creation-time default forever,
// close it out mechanically here: no judgment call needed, since SOUL.md
// already treats a one-shot answered question as done by definition.
func (a *agent) autoCompleteTrivialTask(ctx context.Context, sessionID, responseText string) {
	summary := strings.TrimSpace(responseText)
	summary = strings.SplitN(summary, "\n", 2)[0] // first line only — the board shows one line
	const maxLen = 120
	if len(summary) > maxLen {
		summary = summary[:maxLen] + "…"
	}
	if summary == "" {
		summary = "Answered without further action needed."
	}
	if err := a.store.completeTask(ctx, sessionID, "success", summary); err != nil {
		slog.Error("nomos: auto-complete trivial task failed", "session", sessionID, "error", err)
	}
}
