package orchestrator

import (
	"context"
	"fmt"
	"strings"

	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/envelope"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/event"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/prompt"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/store"
	"github.com/flothus/tmux-xterm-research/server-go/internal/harness/transport"
)

// Master is the user-facing dispatch layer (plan §12 master loop). It owns:
//   - prompt classification (via prompt.Classifier)
//   - routing user prompts to existing-task agents (steering) or new tasks
//   - user-notification queue (per-task "did we tell the user yet?")
//
// Master itself doesn't call an LLM; the prompt classifier is the only
// LLM-touching component, and even that defaults to deterministic rules. The
// master is *protocol*, not LLM behavior.
//
// OrgRunner is optional. When set, the Master delegates per the loaded org's
// delegates_to graph instead of the hardcoded "worker-1" Phase C fallback.
type Master struct {
	Orch    *Orchestrator
	Queue   *transport.Queue
	Classer prompt.Classifier
	RunID   string
	Runner  *OrgRunner // optional; set when an org is loaded
	// MasterRole is the role id used to look up delegates_to. Defaults to
	// "master".
	MasterRole string
	// AutoEvaluator, when set, is invoked on every report received in
	// ProcessInbox to score the completed task. Wired by runlive.Run.
	AutoEvaluator AutoEvaluator
}

// AutoEvaluator is the optional hook the master calls when a task completes
// via a report. Concrete impl lives in runlive to avoid a circular import.
type AutoEvaluator interface {
	OnTaskCompleted(ctx context.Context, runID, taskID, reportIntent string)
}

// NewMaster constructs a master bound to a run.
func NewMaster(orch *Orchestrator, q *transport.Queue, c prompt.Classifier, runID string) *Master {
	return &Master{Orch: orch, Queue: q, Classer: c, RunID: runID, MasterRole: "master"}
}

// Dispatch is the master's main entry point: it classifies the prompt and
// takes a single action (delegate, steer, or notify).
//
// Returns (kind, taskID, error). kind is the classification; taskID is the
// task created or steered (empty for conversational).
// Dispatch handles the user prompt in LLM-driven mode: it creates the
// root task and pushes the prompt into master's inbox as a Delegate
// envelope (from="user"). The master agent then receives the prompt
// through its normal HandleOne path, lets the LLM decide what to do
// (query the team, delegate, ask back, synthesise), and the harness
// provides plumbing only — no hardcoded routing.
//
// For orgs that opt into `dispatch_mode: deterministic`, runlive uses
// `deterministic.Coordinator.Dispatch` instead — the classifier-driven
// workflow runner lives there, not here.
//
// Returns (kind, taskID, error) matching the previous signature so
// runlive doesn't need a special path for the return shape. Kind is
// always "" in LLM mode (the LLM decides; no upfront classification).
func (m *Master) Dispatch(ctx context.Context, userText string) (prompt.Kind, string, error) {
	masterAgentID := "master"
	teamSummary := ""
	if m.Runner != nil {
		if id, ok := m.Runner.AgentIDFor(m.MasterRole); ok {
			masterAgentID = id
		}
		// Build a team summary the master can use to delegate WITHOUT
		// needing a separate query_registry round-trip on turn 1. The
		// LLM sees this as part of the inbox payload via the harness's
		// envelope rendering.
		if m.Runner.Def != nil {
			var lines []string
			for _, role := range m.Runner.Def.Roles {
				if role.ID == m.MasterRole {
					continue
				}
				lines = append(lines, "- "+role.ID+" (provider="+role.Provider+")")
			}
			if len(lines) > 0 {
				teamSummary = "\n\n[harness] Your team (delegate to these role ids):\n" + strings.Join(lines, "\n")
			}
		}
	}

	rootTaskID := genID("task")
	promptID := genID("prompt")
	now := store.FmtTime(store.Now())
	title := trunc(userText, 80)
	msg := &envelope.Envelope{
		ID: genID("msg"), RunID: m.RunID,
		From: "user", To: masterAgentID, Type: envelope.TypeDelegate,
		TaskID: rootTaskID, TTLMs: 0, // 0 = infinite (V72)
		Payload: envelope.Payload{Intent: userText + teamSummary, Expects: envelope.ExpectsReport},
	}

	var rootWon bool
	err := m.Orch.St.Tx(ctx, func(q store.Querier) error {
		if _, err := q.Exec(
			`INSERT INTO prompts(id, run_id, source, text) VALUES(?, ?, 'user', ?)`,
			promptID, m.RunID, userText,
		); err != nil {
			return err
		}
		if _, err := q.Exec(
			`INSERT INTO tasks(id, run_id, owner_agent_id, prompt_id, title, state, attempts, created_at, updated_at)
			 VALUES(?, ?, ?, ?, ?, 'created', 0, ?, ?)`,
			rootTaskID, m.RunID, masterAgentID, promptID, title, now, now,
		); err != nil {
			return err
		}
		if _, err := q.Exec(`UPDATE tasks SET state='assigned', updated_at=? WHERE id=?`, now, rootTaskID); err != nil {
			return err
		}
		if _, err := q.Exec(`UPDATE tasks SET state='in_progress', updated_at=? WHERE id=?`, now, rootTaskID); err != nil {
			return err
		}
		r, err := q.Exec(`UPDATE runs SET root_task_id=? WHERE id=? AND root_task_id IS NULL`, rootTaskID, m.RunID)
		if err != nil {
			return err
		}
		n, _ := r.RowsAffected()
		rootWon = n == 1
		return m.Queue.SendInTx(q, msg)
	})
	if err != nil {
		return "", "", fmt.Errorf("master: dispatch (shovel-to-inbox): %w", err)
	}

	_, _ = m.Orch.Bus.Emit(ctx, event.Event{
		Kind: event.KindTaskCreated, RunID: m.RunID, TaskID: rootTaskID,
		Payload: map[string]any{"title": title, "parent": ""},
	})
	_, _ = m.Orch.Bus.Emit(ctx, event.Event{
		Kind: event.KindTaskAssigned, RunID: m.RunID, TaskID: rootTaskID, AgentID: masterAgentID,
	})
	_, _ = m.Orch.Bus.Emit(ctx, event.Event{
		Kind: event.KindTaskStateChanged, RunID: m.RunID, TaskID: rootTaskID,
		Payload: map[string]any{"from": "created", "to": "assigned"},
	})
	_, _ = m.Orch.Bus.Emit(ctx, event.Event{
		Kind: event.KindTaskStateChanged, RunID: m.RunID, TaskID: rootTaskID,
		Payload: map[string]any{"from": "assigned", "to": "in_progress"},
	})
	if rootWon {
		_, _ = m.Orch.Bus.Emit(ctx, event.Event{
			Kind: "run.root_set", RunID: m.RunID, TaskID: rootTaskID,
		})
	}
	_, _ = m.Orch.Bus.Emit(ctx, event.Event{
		Kind: "master.prompt_shovelled", RunID: m.RunID, TaskID: rootTaskID,
		Payload: map[string]any{"to": masterAgentID, "prompt": trunc(userText, 120)},
	})
	m.Queue.EmitSent(ctx, msg)
	return "", rootTaskID, nil
}

// OpenTasks returns the in-flight task summaries for this run. Used by
// the deterministic coordinator's classifier and by any other caller
// that needs to reason about the run's open work.
func (m *Master) OpenTasks(ctx context.Context) ([]prompt.TaskSummary, error) {
	// V107: ORDER BY created_at gives the classifier deterministic input.
	rows, err := m.Orch.St.DB().QueryContext(ctx,
		`SELECT id, title, state FROM tasks WHERE run_id=? AND state NOT IN ('completed','failed','abandoned','escalated')
		   ORDER BY created_at ASC LIMIT 50`,
		m.RunID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []prompt.TaskSummary
	for rows.Next() {
		var s prompt.TaskSummary
		if err := rows.Scan(&s.ID, &s.Title, &s.State); err != nil {
			return nil, err
		}
		out = append(out, s)
	}
	return out, rows.Err()
}

// NotifyUser exposes notifyUser similarly.
func (m *Master) NotifyUser(ctx context.Context, taskID, kind, body string) error {
	return m.notifyUser(ctx, taskID, kind, body)
}

// RecordPrompt persists a classified prompt row for audit. Mirrors the
// columns the deterministic delegate path writes so the prompts table
// has uniform shape regardless of dispatch mode.
func (m *Master) RecordPrompt(ctx context.Context, text string, res prompt.Result) error {
	return m.Orch.St.Tx(ctx, func(q store.Querier) error {
		_, err := q.Exec(
			`INSERT INTO prompts(id, run_id, source, text, classified_kind, classified_at,
			                     classified_confidence, classified_rationale, classifier_impl)
			 VALUES(?, ?, 'user', ?, ?, ?, ?, ?, ?)`,
			genID("prompt"), m.RunID, text, string(res.Kind), store.FmtTime(store.Now()),
			res.Confidence, res.Rationale, prompt.ImplName(m.Classer),
		)
		return err
	})
}


func trunc(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n]
}


// ProcessInbox pulls the next message addressed to the master and handles it.
// Master responsibilities (plan §12):
//   - report → transition the linked task to completed, ack
//   - clarify → record as a notification awaiting the user, ack
//   - nack → log; future: re-dispatch or escalate
// Returns (handled, error). handled=false means the inbox was empty.
//
// Run this in a poll loop alongside agent HandleOne calls. It's the
// administrative half of the master that isn't an LLM call.
func (m *Master) ProcessInbox(ctx context.Context) (bool, error) {
	// Master agent id depends on whether the runner spawned us.
	masterID := "master"
	if m.Runner != nil {
		if id, ok := m.Runner.AgentIDFor(m.MasterRole); ok {
			masterID = id
		}
	}
	msg, err := m.Queue.Receive(ctx, masterID)
	if err != nil {
		return false, err
	}
	if msg == nil {
		return false, nil
	}
	// Heartbeat only when actually handling a message. Spamming heartbeat
	// every poll would falsify the dead-agent signal (a hung master would
	// look alive). This way the dead-agent reaper can legitimately notice
	// "master hasn't processed anything in 5 minutes" — though in practice
	// the master is the entry point and not subject to the same crash
	// modes as worker agents.
	_ = m.Orch.St.Tx(ctx, func(q store.Querier) error {
		_, err := q.Exec(`UPDATE agents SET heartbeat_at=? WHERE id=?`,
			store.FmtTime(store.Now()), masterID)
		return err
	})
	switch msg.Type {
	case envelope.TypeReport:
		if msg.TaskID != "" {
			// Only transition if task is in a state that allows it; ignore errors.
			_ = m.Orch.Transition(ctx, msg.TaskID, StateCompleted)
		}
		_ = m.notifyUser(ctx, msg.TaskID, "completed", msg.Payload.Intent)
		// Auto-evaluate synchronously: the evaluation.recorded event must
		// be in the timeline before run.ended so dashboards + tests see a
		// deterministic order. Evaluators are cheap; they don't meaningfully
		// block the master loop.
		if m.AutoEvaluator != nil && msg.TaskID != "" {
			m.AutoEvaluator.OnTaskCompleted(ctx, m.RunID, msg.TaskID, msg.Payload.Intent)
		}
	case envelope.TypeClarify:
		_ = m.notifyUser(ctx, msg.TaskID, "clarification", msg.Payload.Intent)
	case envelope.TypeNack:
		// A nack arriving at the master means a delegate the master sent
		// has died — either the worker rejected it, or the queue exhausted
		// retries and dead-lettered. The brief: "report failures to either
		// the sender or a relevant agent (orchestrator)" — the master IS
		// that relevant agent.
		//
		// Recovery: transition the task to Failed, notify the user, log a
		// recovery event. Without this the task sits in 'in_progress' until
		// the run watchdog times out, the user learns nothing, and the
		// research-data records a stall rather than a delegate failure.
		_, _ = m.Orch.Bus.Emit(ctx, event.Event{
			Kind: "master.nack_received", RunID: m.RunID, TaskID: msg.TaskID,
			Payload: map[string]any{
				"from": msg.From, "reason": msg.Payload.Reason,
				"action": "transition_failed+notify_user",
			},
		})
		if msg.TaskID != "" {
			if err := m.Orch.Transition(ctx, msg.TaskID, StateFailed); err != nil {
				// Already terminal or illegal — record but don't fail the
				// inbox processing. Acking still happens below.
				_, _ = m.Orch.Bus.Emit(ctx, event.Event{
					Kind: "master.nack_transition_skipped", RunID: m.RunID, TaskID: msg.TaskID,
					Payload: map[string]any{"error": err.Error()},
				})
			}
		}
		_ = m.notifyUser(ctx, msg.TaskID, "delegate_failed",
			"A delegation failed: "+msg.Payload.Reason+
				" (from "+msg.From+"). Task "+msg.TaskID+" is now in failed state.",
		)
	}
	_ = m.Queue.Ack(ctx, msg.ID)
	return true, nil
}

func (m *Master) notifyUser(ctx context.Context, taskID, kind, payload string) error {
	return m.Orch.St.Tx(ctx, func(q store.Querier) error {
		_, err := q.Exec(
			`INSERT INTO user_notifications(run_id, task_id, kind, payload, created_at) VALUES(?, ?, ?, ?, ?)`,
			m.RunID, nullable(taskID), kind, payload, store.FmtTime(store.Now()),
		)
		return err
	})
}

// genID is a small id helper. Uses store.FmtTime + a random suffix for
// readability in event logs. Not cryptographically anything.
var idCounter = int64(0)

func genID(prefix string) string {
	idCounter++
	return fmt.Sprintf("%s_%d_%d", prefix, store.Now().UnixNano(), idCounter)
}

// RespondToClarification closes the half-built clarification round-trip
// (worker emitted TypeClarify → user_notifications row → user answers
// here → TypeAnswer to worker). Atomic so the notification flip and
// the answer envelope land together or not at all.
func (m *Master) RespondToClarification(ctx context.Context, taskID, userReply string) error {
	if taskID == "" {
		return fmt.Errorf("master: RespondToClarification requires task id")
	}
	var owner, state string
	if err := m.Orch.St.DB().QueryRowContext(ctx,
		`SELECT IFNULL(owner_agent_id,''), state FROM tasks WHERE id=?`, taskID,
	).Scan(&owner, &state); err != nil {
		return err
	}
	if owner == "" {
		return fmt.Errorf("master: task %s has no owner", taskID)
	}
	if TaskState(state) != StateAwaitingClarif {
		return fmt.Errorf("master: task %s is in state %s, not awaiting_clarification", taskID, state)
	}
	msg := &envelope.Envelope{
		ID: genID("msg"), RunID: m.RunID,
		From: "master", To: owner, Type: envelope.TypeAnswer,
		TaskID: taskID, TTLMs: 10000,
		Payload: envelope.Payload{Intent: userReply, Expects: envelope.ExpectsAck},
	}
	err := m.Orch.St.Tx(ctx, func(q store.Querier) error {
		if err := m.Queue.SendInTx(q, msg); err != nil {
			return err
		}
		if _, err := q.Exec(
			`UPDATE user_notifications SET delivered=1, delivered_at=?
			   WHERE run_id=? AND task_id=? AND kind='clarification' AND delivered=0`,
			store.FmtTime(store.Now()), m.RunID, taskID,
		); err != nil {
			return err
		}
		if _, err := q.Exec(
			`INSERT INTO steering(id, target_task_id, forwarded_to, ack_at, created_at) VALUES(?, ?, ?, NULL, ?)`,
			genID("st"), taskID, owner, store.FmtTime(store.Now()),
		); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("master: respond_clarification: %w", err)
	}
	m.Queue.EmitSent(ctx, msg)
	_, _ = m.Orch.Bus.Emit(ctx, event.Event{
		Kind: "master.clarification_answered", RunID: m.RunID, TaskID: taskID,
		Payload: map[string]any{"to": owner, "reply_chars": len(userReply)},
	})
	return nil
}
