package mcp

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/dtoro/oikos/internal/core/ports"
	"github.com/dtoro/oikos/internal/adapters/postgres"
	"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
	"github.com/dtoro/oikos/internal/observability"
	"github.com/google/uuid"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
	return []toolReg{
				// ── request_execution (legacy fixed enum) retired 2026-07-14 ──
				// All mutations now route through `run`. The handler functions
				// (runRexecRestart, runRexecSystemctl, etc.) are kept as reference
				// for future runbook extraction — especially pct_create DNS/VMID logic.
				// DO NOT re-register this tool. See plans/2026-07-10-general-gated-execution.md.
		
						{tool: &mcp.Tool{Name: "run", Description: "Run ANY shell command against any host, LXC, or VM. This is the general execution primitive — prefer it over asking the operator to run something manually, and don't wait for a matching fixed action to exist. Every command is automatically risk-classified: read-only inspection (cat, systemctl status, docker ps, journalctl, df, git status, ...) runs immediately; anything that changes state requires operator approval (granted by the operator replying \"go ahead\"/\"yes\" in chat, or via the Approve button); commands matching a destructive pattern (rm -rf, dd, mkfs, pct/qm destroy, DROP TABLE, reboot, piping curl into a shell, ...) always require approval regardless of what you declare. You cannot talk your way past the destructive check by declaring a lower risk.\n\nHost-level mutations (apt-get install, dpkg, systemctl enable) always classify as config_mutation — operator approval required.\n\nVM targets: the QEMU guest agent must be running inside the VM. If the entity's qemu_guest_agent attribute is not_running, the run is blocked immediately with a clear error.",
					InputSchema: objSchema(
						prop{"target", "string", "Target entity slug: host:<slug> (e.g. host:strong), lxc:<slug> (e.g. lxc:caddy), or vm:<slug> (e.g. vm:zimaos). LXC commands run via pct exec on their Proxmox host automatically. VM commands run via qm guest exec on their Proxmox host (requires the QEMU guest agent inside the VM — standard for Proxmox VMs)."},
						prop{"command", "string", "The shell command to run. Can be a full script (multi-line, &&-chained). Runs as root."},
						prop{"purpose", "string", "One sentence: why you're running this. Shown to the operator alongside the approval — be specific, this is what they're approving."},
						prop{"declared_risk", "string", "Optional self-assessment: read_only, reversible_low, config_mutation, or destructive. This can only ESCALATE the automatic classification, never lower it — declaring a mutating command as read_only has no effect."},
					),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					targetSlug, _ := args["target"].(string)
					command, _ := args["command"].(string)
					purpose, _ := args["purpose"].(string)
					declaredRisk, _ := args["declared_risk"].(string)
					sessionID, _ := args["_session_id"].(string)
					if targetSlug == "" || command == "" {
						return textResult("error: target and command are required"), nil
					}
		
					var targetID uuid.UUID
					if err := pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", targetSlug).Scan(&targetID); err != nil {
						return textResult(fmt.Sprintf("target not found: %s", targetSlug)), nil
					}
		
					return classifyAndGate(ctx, pool, agentID, targetID, targetSlug, command, purpose, declaredRisk, sessionID), nil
				}},
				// docker_exec wraps a command inside a Docker container on an LXC.
			// Resolves the LXC, looks up the pve_id, and runs via
			// `pct exec <pve_id> -- docker exec <container> sh -c '...'`.
			// Uses the same classifyAndGate safety path as `run`.
			{tool: &mcp.Tool{Name: "docker_exec", Description: "Run a shell command inside a Docker container on an LXC. Use when you need to query an app's REST API inside a docker container (curl, Sonarr/Radarr API calls), read its logs or config from inside the container, or make config changes. This is the ergonomic replacement for `run(target, 'docker exec <container> ...')` — it handles command escaping, container resolution, and classification. Commands that mutate container state (POST/PUT/DELETE /api/, file writes) classify as config_mutation and require approval. Reads (GET, cat, ls, status checks) auto-run.",
				InputSchema: objSchema(
					prop{"lxc_slug", "string", "LXC entity slug (e.g. lxc:arriman, lxc:caddy). The LXC must be active and running Docker."},
					prop{"container", "string", "Docker container name inside the LXC (e.g. prowlarr, sonarr)."},
					prop{"command", "string", "The shell command to run inside the container. Runs via `docker exec <container> sh -c '...'`. Shell escaping is handled for you."},
					prop{"purpose", "string", "One sentence: why you're running this. Shown alongside any approval request."},
					prop{"declared_risk", "string", "Optional self-assessment: read_only, config_mutation. Override the auto-classification."},
				),
			}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
				args := argsMap(req)
				lxcSlug, _ := args["lxc_slug"].(string)
				container, _ := args["container"].(string)
				command, _ := args["command"].(string)
				purpose, _ := args["purpose"].(string)
				declaredRisk, _ := args["declared_risk"].(string)
				sessionID, _ := args["_session_id"].(string)

				if lxcSlug == "" || container == "" || command == "" {
					return textResult("error: lxc_slug, container, and command are required"), nil
				}
				if purpose == "" {
					return textResult("error: purpose is required"), nil
				}

				// Normalize slug prefix
				if !strings.HasPrefix(lxcSlug, "lxc:") {
					lxcSlug = "lxc:" + lxcSlug
				}

				// Look up the LXC entity
				var targetID uuid.UUID
				var pveID string
				if err := pool.QueryRow(ctx,
					`SELECT id, COALESCE(attributes->>'pve_id', '') FROM entities WHERE slug = $1`,
					lxcSlug).Scan(&targetID, &pveID); err != nil {
					return textResult(fmt.Sprintf("LXC not found: %s", lxcSlug)), nil
				}
				if pveID == "" {
					return textResult(fmt.Sprintf("LXC %s has no pve_id attribute — can't resolve Proxmox host", lxcSlug)), nil
				}

				// Escape the command for safe sh -c '...' inside docker exec.
				// The standard trick: replace ' with '\'' then wrap in single quotes.
				noQuote := strings.ReplaceAll(command, "'", "'\\''")
				dockerCmd := fmt.Sprintf("docker exec %s sh -c '%s'", container, noQuote)

				return classifyAndGate(ctx, pool, agentID, targetID, lxcSlug, dockerCmd, purpose, declaredRisk, sessionID), nil
			}},
				// inspect_path is the bulk fact-gathering tool from
				// plans/2026-07-18-session-review-three-sessions.md P1.5.
				// Sessions 1e9c7691 and 55927f0a each spent ~15 `run` calls
				// gathering identical facts (`mount | grep`, `df`, `ls -la`,
				// `stat`) across hosts and LXCs to understand where a path
				// lives, who mounts it, and what permissions it has. This tool
				// collapses that fan-out into one call: pass a path and a list
				// of targets, get back per-target mount/df/ls/stat output as
				// JSON. All commands are read-only, so no approval is needed.
				{tool: &mcp.Tool{Name: "inspect_path", Description: "Bulk fact-gathering: run mount/df/ls/stat for the same path across multiple host/LXC/VM targets in ONE call. Returns a JSON object keyed by target slug, each with the target's view of the path (mount source, filesystem, size, top-level entries with ownership/permissions). Use this instead of N separate `run` calls when you need to understand a path's footprint across the fleet (e.g. tracing where a volume is mounted, checking permissions on the same NFS path from server + client). All commands are read-only — no approval needed.",
					InputSchema: objSchema(
						prop{"path", "string", "Absolute path to inspect on each target (e.g. /mnt/media_local, /media/ludo-library)."},
						prop{"targets", "array", "List of target entity slugs (host:strong, lxc:nfs-export, vm:zimaos, …). Up to 8 per call."},
					),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					path, _ := args["path"].(string)
					if path == "" {
						return textResult("error: path is required"), nil
					}
					rawTargets, _ := args["targets"].([]any)
					if len(rawTargets) == 0 {
						return textResult("error: at least one target is required"), nil
					}
					if len(rawTargets) > 8 {
						return textResult("error: at most 8 targets per inspect_path call (use two calls if you need more)"), nil
					}
					targets := make([]string, 0, len(rawTargets))
					for _, t := range rawTargets {
						if s, ok := t.(string); ok && s != "" {
							targets = append(targets, s)
						}
					}
					results := inspectPathAcrossTargets(ctx, pool, path, targets)
					out, _ := json.MarshalIndent(results, "", "  ")
					return textResult(string(out)), nil
				}},
				{tool: &mcp.Tool{Name: "get_execution_status", Description: "Check the status of a requested execution",
					InputSchema: objSchema(
						prop{"execution_id", "string", "Execution UUID (from request_execution output)"},
					),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					execID, _ := args["execution_id"].(string)
					if execID == "" {
						return textResult("execution_id required"), nil
					}
					eid, err := uuid.Parse(execID)
					if err != nil {
						// Try finding by exec slug prefix
						var found uuid.UUID
						err2 := pool.QueryRow(ctx, "SELECT entity_id FROM executions WHERE entity_id::text LIKE $1 LIMIT 1", execID+"%").Scan(&found)
						if err2 != nil {
							return textResult(fmt.Sprintf("execution not found: %s", execID)), nil
						}
						eid = found
					}
					return queryRows(ctx, pool, `
					SELECT e.entity_id::text, e.action, e.risk_class, e.status,
					       e.result::text, e.duration_ms, e.started_at::text,
					       e.completed_at::text, e.correlation_id
					FROM executions e
					WHERE e.entity_id = $1`, eid), nil
				}},
				{tool: &mcp.Tool{Name: "tail_log", Description: "Get recent log lines from a service via journalctl",
					InputSchema: objSchema(
						prop{"service_slug", "string", "Service entity slug (e.g. lxc:caddy)"},
						prop{"lines", "integer", "Number of lines (default 50)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					slug, _ := args["service_slug"].(string)
					n := int(getFloat(args, "lines", 50))
					if slug == "" {
						return textResult("service_slug is required"), nil
					}
					host, user, err := resolveHost(ctx, pool, slug)
					if err != nil {
						return textResult(fmt.Sprintf("resolve host: %v", err)), nil
					}
					svc := strings.TrimPrefix(slug, "lxc:")
					out, err := sshExec(ctx, host, user, fmt.Sprintf("journalctl -u %s -n %d --no-pager 2>&1 || true", svc, n))
					if err != nil {
						return textResult(fmt.Sprintf("ssh: %v", err)), nil
					}
					return textResult(out), nil
				}},
				{tool: &mcp.Tool{Name: "get_service_status", Description: "Check systemd service status on a host",
					InputSchema: objSchema(
						prop{"service_slug", "string", "Service entity slug (e.g. lxc:caddy)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					slug, _ := args["service_slug"].(string)
					if slug == "" {
						return textResult("service_slug is required"), nil
					}
					host, user, err := resolveHost(ctx, pool, slug)
					if err != nil {
						return textResult(fmt.Sprintf("resolve host: %v", err)), nil
					}
					svc := strings.TrimPrefix(slug, "lxc:")
					out, err := sshExec(ctx, host, user,
						fmt.Sprintf("systemctl is-active %s; systemctl is-enabled %s; systemctl show %s -p ActiveEnterTimestamp -p SubState 2>&1 || true", svc, svc, svc))
					if err != nil {
						return textResult(fmt.Sprintf("ssh: %v", err)), nil
					}
					return textResult(out), nil
				}},
				{tool: &mcp.Tool{Name: "get_lxc_state", Description: "Get LXC container resource state from Proxmox host",
					InputSchema: objSchema(
						prop{"lxc_slug", "string", "LXC entity slug (e.g. lxc:caddy)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					slug, _ := args["lxc_slug"].(string)
					if slug == "" {
						return textResult("lxc_slug is required"), nil
					}
					var pveID string
					err := pool.QueryRow(ctx, "SELECT attributes->>'pve_id' FROM entities WHERE slug = $1", slug).Scan(&pveID)
					if err != nil || pveID == "" {
						return textResult(fmt.Sprintf("LXC not found or missing pve_id: %s", slug)), nil
					}
					// Resolve the Proxmox host — find the host that runs this LXC
					var hostID uuid.UUID
					err = pool.QueryRow(ctx, `
					SELECT t.id FROM entities t
					JOIN relationships r ON r.source_id = t.id
					JOIN entities s ON s.id = r.target_id
					WHERE s.slug = $1 AND r.type = 'hosts' AND r.valid_to IS NULL
					LIMIT 1`, slug).Scan(&hostID)
					if err != nil {
						// Fallback: use the inventory host attribute if no relationship
						var hostSlug string
						err = pool.QueryRow(ctx, "SELECT attributes->>'host' FROM entities WHERE slug = $1", slug).Scan(&hostSlug)
						if err != nil || hostSlug == "" {
							return textResult(fmt.Sprintf("cannot resolve Proxmox host for %s", slug)), nil
						}
						var host, user string
						host, user, err = resolveHost(ctx, pool, "host:"+hostSlug)
						if err != nil {
							return textResult(fmt.Sprintf("resolve: %v", err)), nil
						}
						out, err2 := sshExec(ctx, host, user, fmt.Sprintf("pct status %s --verbose 2>&1 || true", pveID))
						if err2 != nil {
							return textResult(fmt.Sprintf("ssh: %v", err2)), nil
						}
						return textResult(out), nil
					}
					var hostSlug string
					pool.QueryRow(ctx, "SELECT slug FROM entities WHERE id = $1", hostID).Scan(&hostSlug)
					host, user, err := resolveHost(ctx, pool, hostSlug)
					if err != nil {
						return textResult(fmt.Sprintf("resolve host: %v", err)), nil
					}
					out, err := sshExec(ctx, host, user, fmt.Sprintf("pct status %s --verbose 2>&1 || true", pveID))
					if err != nil {
						return textResult(fmt.Sprintf("ssh: %v", err)), nil
					}
					return textResult(out), nil
				}},
				{tool: &mcp.Tool{Name: "ping_service", Description: "Check if a service is reachable via HTTP — returns scheduler health state plus a live HTTP probe",
					InputSchema: objSchema(prop{"service_slug", "string", "Service entity slug"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					slug, _ := args["service_slug"].(string)
					if slug == "" {
						return textResult("service_slug is required"), nil
					}
					rows, err := pool.Query(ctx, `
					SELECT st.health, st.last_check_at,
					  COALESCE(
					    e.attributes->>'url',
					    CASE WHEN e.attributes->>'public_host' IS NOT NULL
					         THEN 'https://' || e.attributes->>'public_host'
					    END
					  ) AS url
					FROM entity_status st
					JOIN entities e ON e.id = st.entity_id
					WHERE e.slug = $1`, slug)
					if err != nil {
						return textResult(fmt.Sprintf("query error: %v", err)), nil
					}
					defer rows.Close()
					if !rows.Next() {
						return textResult(fmt.Sprintf("service not found: %s", slug)), nil
					}
					var health, lastCheck, url string
					rows.Scan(&health, &lastCheck, &url)
					if url == "" {
						return textResult(fmt.Sprintf("health=%s last_check=%s url=no-url (entity has no url or public_host attribute)", health, lastCheck)), nil
					}
					// Live HTTP probe — HEAD request to check current state
					code := "n/a"
					if resp, err := http.Head(url); err == nil {
						resp.Body.Close()
						code = fmt.Sprintf("%d", resp.StatusCode)
					} else {
						code = fmt.Sprintf("err: %v", err)
					}
					return textResult(fmt.Sprintf("health=%s last_check=%s url=%s http=%s", health, lastCheck, url, code)), nil
				}},
				// ─── Phase 5: operational MCP tools ──────────────────────────────
		
				{tool: &mcp.Tool{Name: "list_lxcs", Description: "List all LXC containers with ID, host, IP, state, and last-audited hint. Pass state=\"active\" to exclude destroyed/deprecated containers. The last_audited_at column shows the most recent knowledge entry (investigation or document tagged audit/update) linked via an 'about' edge — use it to skip re-running `run` against LXCs that were already audited recently.",
					InputSchema: objSchema(
						prop{"state", "string", "Optional: filter by entity state (active, destroyed, …)"},
					),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					state, _ := argsMap(req)["state"].(string)
					var statePtr *string
					if state != "" {
						statePtr = &state
					}
					return annotateJSONResult(queryRows(ctx, pool, `
					SELECT e.slug, e.name, e.attributes->>'pve_id' AS pve_id,
					       e.attributes->>'lan_ip' AS lan_ip,
					       e.state,
					       st.health, st.last_check_at,
					       (SELECT MAX(k.created_at)
					        FROM relationships r
					        JOIN knowledge_entities k ON k.entity_id = r.source_id
					        WHERE r.target_id = e.id
					          AND r.type = 'about'
					          AND r.valid_to IS NULL
					          AND (k.tags @> ARRAY['audit']::text[]
					            OR k.tags @> ARRAY['update']::text[]
					            OR k.title ILIKE '%audit%'
					            OR k.title ILIKE '%update%')
					       ) AS last_audited_at
					FROM entities e
					LEFT JOIN entity_status st ON st.entity_id = e.id
					WHERE e.type = 'lxc'
					  AND ($1::text IS NULL OR e.state = $1)
					ORDER BY CASE WHEN e.state = 'active' THEN 0 ELSE 1 END,
					         (e.attributes->>'pve_id')::int`, statePtr), "lxc_list"), nil
				}},
				// ── Stage 2: External agent observe ──────────────────────────
		
				// ── Stage 4: External agent act (mutations) ─────────────────────
		
				{tool: &mcp.Tool{Name: "ack_signal", Description: "Acknowledge an open signal. Use when investigating an alert — marks it as seen and being worked on.",
					InputSchema: objSchema(prop{"signal_id", "string", "Signal entity UUID"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					sid, _ := args["signal_id"].(string)
					id, err := uuid.Parse(sid)
					if err != nil {
						return textResult(fmt.Sprintf("invalid signal_id: %v", err)), nil
					}
					tag, err := pool.Exec(ctx,
						`UPDATE signals SET state = 'acknowledged', updated_at = now()
						 WHERE entity_id = $1 AND state IN ('raised','failed')`, id)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					if tag.RowsAffected() == 0 {
						return textResult(fmt.Sprintf("signal %s not found or not in a state that can be acknowledged", sid)), nil
					}
					return textResult(fmt.Sprintf("Signal %s acknowledged.", sid)), nil
				}},
				{tool: &mcp.Tool{Name: "resolve_signal", Description: "Resolve a signal with an optional resolution note. Use when the underlying issue is fixed — marks the signal as resolved so it stops showing as active.",
					InputSchema: objSchema(
						prop{"signal_id", "string", "Signal entity UUID"},
						prop{"resolution", "string", "Optional note describing what fixed it"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					sid, _ := args["signal_id"].(string)
					id, err := uuid.Parse(sid)
					if err != nil {
						return textResult(fmt.Sprintf("invalid signal_id: %v", err)), nil
					}
					tag, err := pool.Exec(ctx,
						`UPDATE signals SET state = 'resolved', updated_at = now()
						 WHERE entity_id = $1 AND state IN ('raised','acknowledged','acting','failed')`, id)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					if tag.RowsAffected() == 0 {
						return textResult(fmt.Sprintf("signal %s not found or not in a state that can be resolved", sid)), nil
					}
					resolution, _ := args["resolution"].(string)
					if resolution != "" {
						return textResult(fmt.Sprintf("Signal %s resolved: %s", sid, resolution)), nil
					}
					return textResult(fmt.Sprintf("Signal %s resolved.", sid)), nil
				}},
				{tool: &mcp.Tool{Name: "mute_signal", Description: "Temporarily mute a signal. Suppresses it from active views for the given duration. Use for known, non-urgent issues that don't need immediate attention.",
					InputSchema: objSchema(
						prop{"signal_id", "string", "Signal entity UUID"},
						prop{"duration_s", "integer", "Mute duration in seconds (default 3600 = 1 hour)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					sid, _ := args["signal_id"].(string)
					id, err := uuid.Parse(sid)
					if err != nil {
						return textResult(fmt.Sprintf("invalid signal_id: %v", err)), nil
					}
					dur := int64(getFloat(args, "duration_s", 3600))
					muteUntil := time.Now().UTC().Add(time.Duration(dur) * time.Second)
					tag, err := pool.Exec(ctx,
						`UPDATE signals SET state = 'muted', mute_until = $2, updated_at = now()
						 WHERE entity_id = $1 AND state IN ('raised','acknowledged')`, id, muteUntil)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					if tag.RowsAffected() == 0 {
						return textResult(fmt.Sprintf("signal %s not found or not in a state that can be muted", sid)), nil
					}
					return textResult(fmt.Sprintf("Signal %s muted until %s.", sid, muteUntil.Format(time.RFC3339))), nil
				}},
				{tool: &mcp.Tool{Name: "cancel_execution", Description: "Cancel a queued or running execution. Use when you realize the command was wrong, targets the wrong host, or should not proceed. Requires a reason.",
					InputSchema: objSchema(
						prop{"execution_id", "string", "Execution entity UUID"},
						prop{"reason", "string", "Why this execution should be cancelled"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					eid, _ := args["execution_id"].(string)
					id, err := uuid.Parse(eid)
					if err != nil {
						return textResult(fmt.Sprintf("invalid execution_id: %v", err)), nil
					}
					reason, _ := args["reason"].(string)
					result := jsonErr("cancelled by agent: %s", reason)
					tag, err := pool.Exec(ctx,
						`UPDATE executions SET status = 'cancelled', result = $2::jsonb
						 WHERE entity_id = $1 AND status IN ('running','pending_approval','approved','queued')`,
						id, result)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					if tag.RowsAffected() == 0 {
						return textResult(fmt.Sprintf("execution %s not found or already final", eid)), nil
					}
					// Write audit entry.
					_ = observability.Audit(ctx, sqlcgen.New(pool), "agent", "nomos", "cancel",
						&id, "POST", "/mcp", "", nil,
						map[string]any{"reason": reason})
					return textResult(fmt.Sprintf("Execution %s cancelled: %s", eid, reason)), nil
				}},
				{tool: &mcp.Tool{Name: "update_check", Description: "Enable or disable a health check. Disable a noisy probe that's firing false positives; re-enable after fixing the underlying issue.",
					InputSchema: objSchema(
						prop{"check_id", "string", "Check entity UUID"},
						prop{"enabled", "boolean", "true to enable, false to disable"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					cid, _ := args["check_id"].(string)
					id, err := uuid.Parse(cid)
					if err != nil {
						return textResult(fmt.Sprintf("invalid check_id: %v", err)), nil
					}
					enabled, _ := args["enabled"].(bool)
					tag, err := pool.Exec(ctx,
						`UPDATE check_defs SET enabled = $2 WHERE entity_id = $1`, id, enabled)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					if tag.RowsAffected() == 0 {
						return textResult(fmt.Sprintf("check %s not found", cid)), nil
					}
					status := "enabled"
					if !enabled {
						status = "disabled"
					}
					return textResult(fmt.Sprintf("Check %s %s.", cid, status)), nil
				}},
				{tool: &mcp.Tool{Name: "list_checks", Description: "List health checks with verdict, last run time, probe kind, and config. Filter by entity slug or enabled status. Each check's last_health explains which probe is responsible for an entity's overall health.",
					InputSchema: objSchema(
						prop{"entity_slug", "string", "Filter by target entity slug"},
						prop{"enabled", "boolean", "Filter enabled/disabled (optional)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					return annotateJSONResult(queryRows(ctx, pool, `
					SELECT cd.entity_id, e.slug, cd.kind,
					       COALESCE(te.slug, '') AS target_slug, cd.target_type,
					       cd.config::text, cd.interval_s, cd.timeout_s, cd.enabled,
					       e.version, cd.last_health, cd.last_run_at::text
					FROM check_defs cd
					JOIN entities e ON e.id = cd.entity_id
					LEFT JOIN entities te ON te.id = cd.target_id
					WHERE ($1::text IS NULL OR te.slug = $1)
					  AND ($2::bool IS NULL OR cd.enabled = $2)
					ORDER BY e.slug LIMIT 200`,
						nStr(args["entity_slug"]), args["enabled"]), "check_table"), nil
				}},
				{tool: &mcp.Tool{Name: "list_executions", Description: "Cursor-paginated execution history. Filter by entity slug, status, or risk class. Returns newest-first with duration, result, and target info.",
					InputSchema: objSchema(
						prop{"entity_slug", "string", "Filter by target entity slug"},
						prop{"status", "string", "Filter by status (running/completed/failed/pending_approval)"},
						prop{"limit", "integer", "Max rows (default 25)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					limit := int(getFloat(args, "limit", 25))
					return queryRows(ctx, pool, `
					SELECT e.entity_id, te.slug AS target, e.action, e.risk_class,
					       e.status, e.result::text, e.duration_ms,
					       e.correlation_id, e.started_at::text, e.completed_at::text, e.created_at::text,
					       COALESCE(npe.session_id::text, '') AS session_id
					FROM executions e
					JOIN entities te ON te.id = e.target_entity_id
					LEFT JOIN nomos_plan_executions npe ON npe.execution_id = e.entity_id
					WHERE ($1::text IS NULL OR te.slug = $1)
					  AND ($2::text IS NULL OR e.status = $2)
					ORDER BY e.created_at DESC LIMIT $3`,
						nStr(args["entity_slug"]), nStr(args["status"]), limit), nil
				}},
				{tool: &mcp.Tool{Name: "list_entity_sessions", Description: "Active Nomos sessions (tasks) linked to an entity. Shows goal, status, outcome, and when the session was last active. Use to discover what agents are working on related to this entity.",
					InputSchema: objSchema(
						prop{"entity_slug", "string", "Entity slug to find sessions for"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					slug, _ := args["entity_slug"].(string)
					return queryRows(ctx, pool, `
					SELECT DISTINCT as2.id, as2.title, as2.goal, as2.status, as2.outcome,
					       as2.summary, as2.last_active_at::text, as2.closed_at::text
					FROM agent_sessions as2
					JOIN nomos_plan_executions npe ON npe.session_id = as2.id
					JOIN executions ex ON ex.entity_id = npe.execution_id
					JOIN entities te ON te.id = ex.target_entity_id
					WHERE te.slug = $1 AND as2.closed_at IS NULL
					ORDER BY as2.last_active_at DESC LIMIT 20`, slug), nil
				}},
				// ── Stage 2: External agent observe ──────────────────────────
		
				{tool: &mcp.Tool{Name: "get_dashboard_summary", Description: "Fleet overview in one call: entity counts by type and state, health breakdown (healthy/degraded/down/stale/unknown), active signals by severity, pending approval count, execution counts in last 24h, and event rate over last 6h.",
					InputSchema: objSchema(),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					result := map[string]any{}
		
					// Entity counts by type
					result["entities_by_type"] = rowsToMap(ctx, pool,
						`SELECT type, count(*) FROM entities GROUP BY type`)
		
					// Entity counts by state
					result["entities_by_state"] = rowsToMap(ctx, pool,
						`SELECT coalesce(state, 'unknown'), count(*) FROM entities GROUP BY state`)
		
					// Health rollup (excluding check entities)
					result["health"] = rowsToMap(ctx, pool, `
						SELECT COALESCE(st.health, 'unknown') AS health, count(*)
						FROM entity_status st JOIN entities e ON e.id = st.entity_id
						WHERE e.type <> 'check' GROUP BY st.health`)
		
					// Active signals by severity
					result["signals_by_severity"] = rowsToMap(ctx, pool, `
						SELECT severity, count(*) FROM signals
						WHERE state NOT IN ('resolved', 'failed') GROUP BY severity`)
		
					// Pending approvals
					var pending int
					pool.QueryRow(ctx, `SELECT count(*) FROM approvals WHERE status = 'pending'`).Scan(&pending)
					result["approvals_pending"] = pending
		
					// Executions in last 24h
					result["executions_by_state"] = rowsToMap(ctx, pool, `
						SELECT status, count(*) FROM executions
						WHERE created_at > now() - interval '24 hours' GROUP BY status`)
		
					// Event rate (5-min buckets over 6h)
					events := []map[string]any{}
					erows, _ := pool.Query(ctx, `
						SELECT date_trunc('hour', ts) + (extract(minute FROM ts)::int / 5) * interval '5 minutes' AS bucket, count(*)
						FROM events WHERE ts > now() - interval '6 hours'
						GROUP BY bucket ORDER BY bucket`)
					if erows != nil {
						for erows.Next() {
							var bucket time.Time
							var n int
							if erows.Scan(&bucket, &n) == nil {
								events = append(events, map[string]any{"bucket": bucket, "count": n})
							}
						}
						erows.Close()
					}
					result["event_rate"] = events
		
					b, _ := json.MarshalIndent(result, "", "  ")
					return textResult(string(b)), nil
				}},
				{tool: &mcp.Tool{Name: "get_secret", Description: "Retrieve a secret value from the Infisical vault. Returns the secret value. Use for service credentials, tokens, and keys needed to operate the homelab.",
					InputSchema: objSchema(
						prop{"key", "string", "Secret key to retrieve (e.g. 'matrix-token', 'clients/host:hubris/age-key')"},
						prop{"path", "string", "Secret path prefix (default '/')"},
						prop{"environment", "string", "Environment slug (default 'dev')"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					if sec == nil {
						return textResult("error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)"), nil
					}
					args := argsMap(req)
					key, _ := args["key"].(string)
					if key == "" {
						return textResult("error: key is required"), nil
					}
					val, err := sec.Get(ctx, key)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					return textResult(val), nil
				}},
				{tool: &mcp.Tool{Name: "list_secrets", Description: "List secret keys in the Infisical vault. Returns key names only (no values). Filter by path prefix to scope to a client or shared path.",
					InputSchema: objSchema(
						prop{"path_prefix", "string", "Filter to keys matching this prefix (e.g. 'clients/', 'shared/', 'config/')"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					if sec == nil {
						return textResult("error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)"), nil
					}
					args := argsMap(req)
					prefix, _ := args["path_prefix"].(string)
		
					keys, err := sec.List(ctx)
					if err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					if prefix != "" {
						filtered := keys[:0]
						for _, k := range keys {
							if strings.HasPrefix(k, prefix) {
								filtered = append(filtered, k)
							}
						}
						keys = filtered
					}
					data, _ := json.MarshalIndent(keys, "", "  ")
					return textResult(string(data)), nil
				}},
				{tool: &mcp.Tool{Name: "set_secret", Description: "Store or update a secret in the Infisical vault. Use when discovering new credentials that need to be persisted. Requires operator approval (config_mutation).",
					InputSchema: objSchema(
						prop{"key", "string", "Secret key to store"},
						prop{"value", "string", "Secret value to store"},
						prop{"path", "string", "Secret path prefix (default '/')"},
						prop{"environment", "string", "Environment slug (default 'dev')"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					if sec == nil {
						return textResult("error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)"), nil
					}
					args := argsMap(req)
					key, _ := args["key"].(string)
					value, _ := args["value"].(string)
					if key == "" {
						return textResult("error: key is required"), nil
					}
					if value == "" {
						return textResult("error: value is required"), nil
					}
					if err := sec.Set(ctx, key, value); err != nil {
						return textResult(fmt.Sprintf("error: %v", err)), nil
					}
					return textResult(fmt.Sprintf("secret %s stored", key)), nil
				}},
				{tool: &mcp.Tool{Name: "restart_service", Description: "Restart a systemd service on a host or LXC. Wraps `systemctl restart <service>` with correct risk classification (config_mutation — requires operator approval). Prefer this over raw `run` for restarts: it resolves the target, sets the risk class, and records a clean execution row.",
					InputSchema: objSchema(
						prop{"target", "string", "Target entity slug (e.g. lxc:seanime, host:hubris)"},
						prop{"service", "string", "systemd unit name (e.g. seanime, caddy)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					targetSlug := slugArg(args, "target", "service_slug", "slug")
					service := slugArg(args, "service", "unit")
					if targetSlug == "" || service == "" {
						return textResult("error: target and service are required"), nil
					}
					sessionID, _ := args["_session_id"].(string)
					var targetID uuid.UUID
					if err := pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", targetSlug).Scan(&targetID); err != nil {
						return textResult(fmt.Sprintf("target not found: %s", targetSlug)), nil
					}
					command := fmt.Sprintf("systemctl restart %s", service)
					purpose := fmt.Sprintf("restart %s on %s", service, targetSlug)
					return classifyAndGate(ctx, pool, agentID, targetID, targetSlug, command, purpose, "config_mutation", sessionID), nil
				}},
				{tool: &mcp.Tool{Name: "push_file", Description: "Copy a file from a Proxmox host into an LXC container via `pct push`. The source path must already exist on the Proxmox host that owns the LXC (stage it there first via `run` on the host, e.g. with scp/curl/wget). Classified config_mutation — requires operator approval. Prefer this over manual 3-hop SSH piping (`cat | ssh | pct exec tee`), which repeatedly drops into partial-write/text-file-busy states.",
					InputSchema: objSchema(
						prop{"target", "string", "LXC entity slug (e.g. lxc:seanime)"},
						prop{"source_path", "string", "Path on the Proxmox host (e.g. /tmp/seanime)"},
						prop{"dest_path", "string", "Destination path inside the container (e.g. /opt/seanime/bin/seanime)"},
						prop{"backup", "boolean", "If true, copy the existing dest to dest.bak inside the container first (default true)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					targetSlug := slugArg(args, "target", "slug")
					sourcePath := slugArg(args, "source_path")
					destPath := slugArg(args, "dest_path")
					backup := true
					if b, ok := args["backup"].(bool); ok {
						backup = b
					}
					if targetSlug == "" || sourcePath == "" || destPath == "" {
						return textResult("error: target, source_path, and dest_path are required"), nil
					}
					if !strings.HasPrefix(targetSlug, "lxc:") {
						return textResult(fmt.Sprintf("error: push_file only supports lxc: targets for now (got %s) — host/vm push needs scp, not yet wired", targetSlug)), nil
					}
					// Resolve pve_id and the owning Proxmox host (same chain as get_lxc_state).
					var pveID string
					if err := pool.QueryRow(ctx, "SELECT attributes->>'pve_id' FROM entities WHERE slug = $1", targetSlug).Scan(&pveID); err != nil || pveID == "" {
						return textResult(fmt.Sprintf("LXC not found or missing pve_id: %s", targetSlug)), nil
					}
					var hostID uuid.UUID
					err := pool.QueryRow(ctx, `
						SELECT t.id FROM entities t
						JOIN relationships r ON r.source_id = t.id
						JOIN entities s ON s.id = r.target_id
						WHERE s.slug = $1 AND r.type = 'hosts' AND r.valid_to IS NULL
						LIMIT 1`, targetSlug).Scan(&hostID)
					var hostSlug string
					if err == nil {
						pool.QueryRow(ctx, "SELECT slug FROM entities WHERE id = $1", hostID).Scan(&hostSlug)
					} else {
						pool.QueryRow(ctx, "SELECT attributes->>'host' FROM entities WHERE slug = $1", targetSlug).Scan(&hostSlug)
					}
					if hostSlug == "" {
						return textResult(fmt.Sprintf("cannot resolve Proxmox host for %s", targetSlug)), nil
					}
					cmd := fmt.Sprintf("pct push %s %s %s", pveID, sourcePath, destPath)
					if backup {
						cmd = fmt.Sprintf("pct exec %s -- cp -n %s %s.bak 2>/dev/null || true; %s", pveID, destPath, destPath, cmd)
					}
					sessionID, _ := args["_session_id"].(string)
					var hostEntityID uuid.UUID
					if err := pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", "host:"+hostSlug).Scan(&hostEntityID); err != nil {
						// hostSlug may already carry the host: prefix
						if err2 := pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", hostSlug).Scan(&hostEntityID); err2 != nil {
							return textResult(fmt.Sprintf("host entity not found: %s", hostSlug)), nil
						}
					}
					purpose := fmt.Sprintf("push %s into %s at %s", sourcePath, targetSlug, destPath)
					return classifyAndGate(ctx, pool, agentID, hostEntityID, "host:"+hostSlug, cmd, purpose, "config_mutation", sessionID), nil
				}},
				// ── Approval Management (replaces Matrix notifier) ─────────────
				{tool: &mcp.Tool{Name: "list_approvals", Description: "List pending and recent approvals. Returns approval ID, action, risk class, target slug, status, and timing. Filter by status (pending, approved, denied) or entity slug to scope. Use after a `run` returns 'requires approval' to see what's pending so you can present it to the operator for a decision.",
					InputSchema: objSchema(
						prop{"status", "string", "Optional: filter by status (pending, approved, denied, expired, revoked)"},
						prop{"entity_slug", "string", "Optional: filter by target entity slug"},
						prop{"limit", "integer", "Max rows (default 20)"}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					status, _ := args["status"].(string)
					entSlug, _ := args["entity_slug"].(string)
					lim := int(getFloat(args, "limit", 20))
					if lim < 1 {
						lim = 1
					}
					if lim > 100 {
						lim = 100
					}
					var statusPtr, slugPtr *string
					if status != "" {
						statusPtr = &status
					}
					if entSlug != "" {
						slugPtr = &entSlug
					}
					return queryRows(ctx, pool, `
						SELECT a.entity_id, e.slug AS target_slug, a.action, a.risk_class,
					       a.kind, a.status, a.expires_at, a.decided_at, a.created_at
						FROM approvals a
						JOIN entities e ON e.id = COALESCE(a.subject_entity_id, a.entity_id)
						WHERE ($1::text IS NULL OR a.status = $1)
						  AND ($2::text IS NULL OR e.slug = $2)
						ORDER BY a.created_at DESC LIMIT $3`, statusPtr, slugPtr, lim), nil
				}},
				{tool: &mcp.Tool{Name: "decide_approval", Description: "Approve or deny a pending execution approval. Call this after presenting the command details to the operator and getting their explicit authorization (\"go ahead\", \"yes\", \"proceed\", or for destructive actions \"I confirm ...\"). The operator's typed approval in any chat (Hermes desktop, Telegram, Discord, WhatsApp) works — this tool is the agent-side action to record the decision and trigger the execution. Returns the new approval status and execution result once done.",
					InputSchema: objSchema(
						prop{"approval_id", "string", "Approval entity UUID from list_approvals or a prior run result (e.g. 'execution X queued')"},
						prop{"decision", "string", "Decision: 'approve' to authorize the action, 'deny' to reject it. Destructive actions still need explicit typed confirmation from the operator."}),
				}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
					args := argsMap(req)
					appID, _ := args["approval_id"].(string)
					decision, _ := args["decision"].(string)
					if appID == "" || decision == "" {
						return textResult("error: approval_id and decision are required"), nil
					}
					if decision != "approve" && decision != "deny" {
						return textResult("error: decision must be 'approve' or 'deny'"), nil
					}

					// Validate the approval exists and is still pending
					var status string
					if err := pool.QueryRow(ctx, `SELECT status FROM approvals WHERE entity_id = $1`, appID).Scan(&status); err != nil {
						return textResult(fmt.Sprintf("approval not found: %s", appID)), nil
					}
					if status != "pending" {
						return textResult(fmt.Sprintf("approval %s is already %s — cannot decide again", appID, status)), nil
					}

					// Call the HTTP API decision endpoint (same path as the UI Approve button
					// and nomos chat-assent), so all approval paths share one code path for
					// execution dispatch, session management, events, and audit trail.
					apiBase := os.Getenv("OIKOS_API_BASE")
					if apiBase == "" {
						apiBase = "http://api:8090"
					}
					body, _ := json.Marshal(map[string]string{"decision": decision})
					client := &http.Client{Timeout: 30 * time.Second}
					hreq, hreqErr := http.NewRequestWithContext(ctx, http.MethodPost,
						apiBase+"/api/v1/approvals/"+appID+"/decision", bytes.NewReader(body))
					if hreqErr != nil {
						return textResult(fmt.Sprintf("error: %v", hreqErr)), nil
					}
					hreq.Header.Set("Content-Type", "application/json")
					if token := os.Getenv("OIKOS_MCP_BEARER_TOKEN"); token != "" {
						hreq.Header.Set("Authorization", "Bearer "+token)
					}
					resp, reqErr := client.Do(hreq)
					if reqErr != nil {
						return textResult(fmt.Sprintf("error: API call failed: %v", reqErr)), nil
					}
					defer resp.Body.Close()

					var result map[string]any
					if err := json.NewDecoder(resp.Body).Decode(&result); err == nil && len(result) > 0 {
						b, _ := json.MarshalIndent(result, "", "  ")
						return textResult(fmt.Sprintf("%s: approval %s -> %s\nResponse: %s", decision, appID, decision, string(b))), nil
					}
					if resp.StatusCode == http.StatusOK {
						return textResult(fmt.Sprintf("%s: approval %s decided successfully.", decision, appID)), nil
					}
					return textResult(fmt.Sprintf("%s: API returned status %d for approval %s", decision, resp.StatusCode, appID)), nil
				}},
	}
}
