package scripted

import (
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/runtime"
)

// msgCounter ensures message IDs synthesized by behaviors are unique even
// when emitted in the same nanosecond. Combines wall-clock + counter.
var msgCounter int64

func uniqMsgID(prefix string) string {
	c := atomic.AddInt64(&msgCounter, 1)
	return fmt.Sprintf("%s_%d_%d", prefix, time.Now().UnixNano(), c)
}

// ProductionBehaviors returns the default RoleBehavior map for scripted runs
// of demo scenarios. Each behavior:
//
//   - reads the incoming message
//   - emits a tool_calls list that (a) writes a real artifact under
//     .td/runs/<run>/<task>/ or .td/demo-project/, and (b) sends a report
//     upward to the original sender
//   - returns plausible token counts so cost tracking has signal
//
// This is what makes scripted runs "production-grade" — they go through the
// same code paths a real LLM would (write_file, send_message, zone check,
// artifact registration, auto-evaluator) and produce inspectable outputs.
func ProductionBehaviors() map[string]RoleBehavior {
	return map[string]RoleBehavior{
		"master":              behaviorMaster,
		"orchestrator":        behaviorOrchestrator,
		"debugger":            behaviorDebugger,
		"feature-plan-writer": behaviorPlanWriter,
		"fe-lead":             behaviorLead("fe-worker"),
		"be-lead":             behaviorLead("be-worker"),
		"fe-worker":           behaviorWorker("frontend"),
		"be-worker":           behaviorWorker("backend"),
		"generalist":          behaviorWorker("flat"),
	}
}

// taskPath returns the canonical artifact path for this task.
func taskPath(in *runtime.LLMRequest, suffix string) string {
	if in == nil || in.IncomingMessage == nil {
		return ""
	}
	taskID := in.IncomingMessage.TaskID
	runID := in.IncomingMessage.RunID
	if taskID == "" || runID == "" {
		return ""
	}
	return fmt.Sprintf(".td/runs/%s/%s/%s", runID, taskID, suffix)
}

// reportTo sends a report envelope back to the original sender.
func reportTo(from, runID, taskID, sender, intent string, refs []string) runtime.ToolCall {
	return runtime.ToolCall{
		Name: "send_message",
		Args: map[string]any{
			"to": sender, "type": "report",
			"task_id": taskID, "run_id": runID, "id": uniqMsgID("msg_report"),
			"ttl_ms": 60000,
			"payload": map[string]any{
				"intent":       intent,
				"context_refs": refs,
				"expects":      "none",
			},
		},
	}
}

// delegateTo crafts a delegate envelope to a peer agent. Used by leads /
// orchestrator to forward work downward.
func delegateTo(from, runID, taskID, target, intent string) runtime.ToolCall {
	return runtime.ToolCall{
		Name: "send_message",
		Args: map[string]any{
			"to": target, "type": "delegate",
			"task_id": taskID, "run_id": runID,
			"id":     uniqMsgID("msg_delegate"),
			"ttl_ms": 60000,
			"payload": map[string]any{
				"intent":  intent,
				"expects": "report",
			},
		},
	}
}

// writeFileCall writes content at the given path. Path is zone-checked by
// the agent.executeTool gate.
func writeFileCall(path, content string) runtime.ToolCall {
	return runtime.ToolCall{
		Name: "write_file",
		Args: map[string]any{"path": path, "content": content},
	}
}

// behaviorMaster: master receives delegates (from user via dispatch) and
// reports (from downstream). When a report arrives, it doesn't need to do
// anything — the master.ProcessInbox already transitions the task to
// completed. We respond with an empty turn.
func behaviorMaster(agentID, role string, in *runtime.LLMRequest) *runtime.LLMResponse {
	if in.IncomingMessage == nil {
		return &runtime.LLMResponse{Text: "no-op"}
	}
	// Master in scripted mode passes the message through. Real master logic
	// is in orchestrator.Master.Dispatch / ProcessInbox.
	return &runtime.LLMResponse{
		Text:   "[master] pass-through",
		Tokens: runtime.TokenUsage{Prompt: 80, Completion: 20},
	}
}

// behaviorOrchestrator: classifies the inbound intent and forwards to the
// appropriate specialist. Keyword routing matches the role md.
func behaviorOrchestrator(agentID, role string, in *runtime.LLMRequest) *runtime.LLMResponse {
	if in.IncomingMessage == nil {
		return &runtime.LLMResponse{Text: "no-op"}
	}
	m := in.IncomingMessage
	if m.Type != "delegate" {
		// Likely a report flowing back up from a specialist; forward to
		// master regardless of which specialist sent it.
		return &runtime.LLMResponse{
			Text:   "[orchestrator] forwarding report up to master",
			Tokens: runtime.TokenUsage{Prompt: 100, Completion: 30},
			ToolCalls: []runtime.ToolCall{
				reportTo(agentID, m.RunID, m.TaskID, "master-AGENTID",
					"specialist reported back: "+m.Payload.Intent,
					m.Payload.ContextRefs),
			},
		}
	}
	intent := strings.ToLower(m.Payload.Intent)
	target := ""
	rationale := ""
	if containsAny(intent, "bug", "broken", "error", "crash", "fails", "regression", "weird", "doesn't work", "why is", "investigate") {
		target = "debugger"
		rationale = "bug keyword detected → debugger"
	} else {
		target = "feature-plan-writer"
		rationale = "default → feature-plan-writer (no debug keywords)"
	}
	// Use query_registry to find the actual spawned id (the real LLM does
	// the same; we simulate by using the role name suffix the orchestrator
	// would have looked up).
	return &runtime.LLMResponse{
		Text:   "[orchestrator] " + rationale,
		Tokens: runtime.TokenUsage{Prompt: 200, Completion: 80},
		ToolCalls: []runtime.ToolCall{
			{Name: "query_registry", Args: map[string]any{
				"kind":   "agents",
				"filter": map[string]any{"role": target},
			}},
			// We can't dynamically look up the agent id in scripted-auto, but
			// the runlive layer wires the role behavior with the actual ids
			// via the agent-resolver below. For the scripted-auto fallback we
			// use the role name and the harness will resolve at delegate time.
			delegateTo(agentID, m.RunID, m.TaskID, target+"-AGENTID", "[paraphrased] "+m.Payload.Intent),
		},
	}
}

// behaviorDebugger: writes a fake diagnosis to disk, reports back.
func behaviorDebugger(agentID, role string, in *runtime.LLMRequest) *runtime.LLMResponse {
	if in.IncomingMessage == nil {
		return &runtime.LLMResponse{Text: "no-op"}
	}
	m := in.IncomingMessage
	reportPath := taskPath(in, "report.md")
	report := `# Diagnosis report

**Symptom**: ` + m.Payload.Intent + `

**Evidence**: traced the issue to .td/demo-project/frontend/components/Counter.tsx
where the reset() handler decrements instead of resetting to initial.

**Root cause**: typo in the reset handler — uses ` + "`count - 1`" + ` instead of ` + "`initial`" + `.

**Suggested fix**: change ` + "`setCount(count - 1)`" + ` to ` + "`setCount(initial)`" + ` on the reset handler.

(Diagnosis only — implementation is out of scope for the debugger role.)
`
	return &runtime.LLMResponse{
		Text:   "[debugger] diagnosed root cause, report written",
		Tokens: runtime.TokenUsage{Prompt: 400, Completion: 220},
		ToolCalls: []runtime.ToolCall{
			writeFileCall(reportPath, report),
			reportTo(agentID, m.RunID, m.TaskID, m.From,
				"diagnosis complete — see report.md",
				[]string{reportPath}),
		},
	}
}

// behaviorPlanWriter: writes a structured plan, reports back.
func behaviorPlanWriter(agentID, role string, in *runtime.LLMRequest) *runtime.LLMResponse {
	if in.IncomingMessage == nil {
		return &runtime.LLMResponse{Text: "no-op"}
	}
	m := in.IncomingMessage
	planPath := taskPath(in, "plan.md")
	plan := `# Plan

## Goal

` + m.Payload.Intent + `

## Scope

- In: minimal changes inside .td/demo-project/ to deliver the requested feature.
- Out: production code (client/, server-go/) — fenced by zone enforcement.

## Decomposition

1. Update .td/demo-project/shared/types.ts to add any new shared fields.
2. Update .td/demo-project/frontend/components/Counter.tsx for FE changes.
3. Update .td/demo-project/backend/handlers/counter.go for BE changes.

## Open questions

- Should we keep backward compatibility with the existing /counter API shape?

## Risks / tradeoffs

Refactoring multiple files at once requires the connector to coordinate
fe-lead and be-lead. Low risk in the demo project sandbox.
`
	return &runtime.LLMResponse{
		Text:   "[plan-writer] structured plan written",
		Tokens: runtime.TokenUsage{Prompt: 350, Completion: 280},
		ToolCalls: []runtime.ToolCall{
			writeFileCall(planPath, plan),
			reportTo(agentID, m.RunID, m.TaskID, m.From,
				"plan written — see plan.md",
				[]string{planPath}),
		},
	}
}

// behaviorLead: forwards work to the worker, then aggregates on the
// worker's report.
//
// Upward target is `master-AGENTID` (resolved by AgentResolver) regardless
// of who delivered the report to us — the lead remembers structurally that
// it answers to the master, not to whichever child sent the latest message.
// Without this the report would bounce back to the worker → loop.
func behaviorLead(workerRole string) RoleBehavior {
	return func(agentID, role string, in *runtime.LLMRequest) *runtime.LLMResponse {
		if in.IncomingMessage == nil {
			return &runtime.LLMResponse{Text: "no-op"}
		}
		m := in.IncomingMessage
		if m.Type == "delegate" {
			return &runtime.LLMResponse{
				Text:   "[" + role + "] delegating to " + workerRole,
				Tokens: runtime.TokenUsage{Prompt: 200, Completion: 60},
				ToolCalls: []runtime.ToolCall{
					delegateTo(agentID, m.RunID, m.TaskID, workerRole+"-AGENTID", "[lead-relay] "+m.Payload.Intent),
				},
			}
		}
		// Report from worker — aggregate upward to master.
		return &runtime.LLMResponse{
			Text:   "[" + role + "] worker reported, forwarding up to master",
			Tokens: runtime.TokenUsage{Prompt: 150, Completion: 50},
			ToolCalls: []runtime.ToolCall{
				reportTo(agentID, m.RunID, m.TaskID, "master-AGENTID",
					m.Payload.Intent,
					m.Payload.ContextRefs),
			},
		}
	}
}

// behaviorWorker: writes a real file in the demo project + reports.
func behaviorWorker(zone string) RoleBehavior {
	return func(agentID, role string, in *runtime.LLMRequest) *runtime.LLMResponse {
		if in.IncomingMessage == nil {
			return &runtime.LLMResponse{Text: "no-op"}
		}
		m := in.IncomingMessage
		var outPath string
		var content string
		switch zone {
		case "frontend":
			outPath = ".td/demo-project/frontend/scratch-" + m.TaskID + ".tsx"
			content = "// Generated by " + agentID + " for task " + m.TaskID + "\n" +
				"// Intent: " + m.Payload.Intent + "\n" +
				"export const note = '" + m.Payload.Intent + "';\n"
		case "backend":
			outPath = ".td/demo-project/backend/scratch_" + m.TaskID + ".go"
			content = "// Generated by " + agentID + " for task " + m.TaskID + "\n" +
				"// Intent: " + m.Payload.Intent + "\n" +
				"package handlers\n\nvar Note_" + safeIdent(m.TaskID) + " = \"" + m.Payload.Intent + "\"\n"
		default:
			outPath = ".td/demo-project/scratch-" + m.TaskID + ".txt"
			content = m.Payload.Intent + "\n"
		}
		reportPath := taskPath(in, "report.md")
		report := "# Worker report\n\nAgent: " + agentID + "\nRole: " + role +
			"\nIntent: " + m.Payload.Intent + "\n\nWrote: " + outPath + "\n"
		return &runtime.LLMResponse{
			Text:   "[" + role + "] wrote " + outPath,
			Tokens: runtime.TokenUsage{Prompt: 250, Completion: 120},
			ToolCalls: []runtime.ToolCall{
				writeFileCall(outPath, content),
				writeFileCall(reportPath, report),
				reportTo(agentID, m.RunID, m.TaskID, m.From,
					"work done — see "+outPath,
					[]string{outPath, reportPath}),
			},
		}
	}
}

func containsAny(s string, needles ...string) bool {
	for _, n := range needles {
		if strings.Contains(s, n) {
			return true
		}
	}
	return false
}

func safeIdent(s string) string {
	out := make([]byte, 0, len(s))
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
			out = append(out, c)
		} else {
			out = append(out, '_')
		}
	}
	if len(out) == 0 || (out[0] >= '0' && out[0] <= '9') {
		out = append([]byte{'X'}, out...)
	}
	return string(out)
}
