package runtime

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"

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

// Embedding is the data the introspect tool returns. It answers the
// anti-failure questions from the original prompt:
//
//   - what do I work on / what do I report
//   - who can I ask for clarification
//   - where do I store plans / data / progress
//   - how am I embedded in the system
type Embedding struct {
	AgentID         string   `json:"agent_id"`
	Role            string   `json:"role"`
	RunID           string   `json:"run_id"`
	Provider        string   `json:"provider"`
	ZoneScope       []string `json:"zone_scope"`
	Tools           []string `json:"tools"`
	Peers           []Peer   `json:"peers"`
	ParentAgentID   string   `json:"parent_agent_id"`
	EscalateTo      string   `json:"escalate_to"`
	PlanPath        string   `json:"plan_path"`
	ReportPath      string   `json:"report_path"`
	OpenTaskIDs     []string `json:"open_task_ids"`
	OrgName         string   `json:"org_name"`
	OrgVersion      int      `json:"org_version"`
}

// Peer is a registered teammate the agent can talk to.
type Peer struct {
	AgentID string `json:"agent_id"`
	Role    string `json:"role"`
	Status  string `json:"status"`
}

// Introspect builds the embedding bundle for the given agent. Reads from the
// store; doesn't talk to the LLM.
//
// EscalateTo is derived from the live org graph (whichever spawned agent has
// a role whose DelegatesTo list contains this agent's role). Without this,
// every agent would escalate straight to "master" by hard-code — skipping
// the actual orchestrator/lead it reports to, breaking the chain of command
// the brief calls for ("how am I embedded into the system").
func (a *Agent) Introspect(ctx context.Context) (*Embedding, error) {
	emb := &Embedding{
		AgentID:    a.ID,
		Role:       a.Role,
		RunID:      a.RunID,
		Provider:   a.Provider,
		ZoneScope:  a.ZoneScope,
		Tools:      a.Tools,
		PlanPath:   fmt.Sprintf(".td/runs/%s/<task-id>/plan.md", a.RunID),
		ReportPath: fmt.Sprintf(".td/runs/%s/<task-id>/report.md", a.RunID),
	}
	// Org info from runs table.
	_ = a.st.DB().QueryRowContext(ctx,
		`SELECT IFNULL(org_id,''), IFNULL(org_version,0) FROM runs WHERE id=?`, a.RunID,
	).Scan(&emb.OrgName, &emb.OrgVersion)

	// Peers in the same run.
	rows, err := a.st.DB().QueryContext(ctx,
		`SELECT id, IFNULL(role,''), status FROM agents WHERE run_id=? AND id<>?`, a.RunID, a.ID,
	)
	if err == nil {
		defer rows.Close()
		for rows.Next() {
			var p Peer
			_ = rows.Scan(&p.AgentID, &p.Role, &p.Status)
			emb.Peers = append(emb.Peers, p)
		}
	}

	// Derive EscalateTo from the durable parent link the spawner set on
	// the agents row (orgrunner populates parent_agent_id based on the
	// org's delegates_to graph). Falls back to "master" when the agent
	// has no recorded parent, which is correct for the master itself
	// and for any top-level role.
	var parent string
	_ = a.st.DB().QueryRowContext(ctx,
		`SELECT IFNULL(parent_agent_id,'') FROM agents WHERE id=?`, a.ID,
	).Scan(&parent)
	emb.ParentAgentID = parent
	if parent != "" {
		emb.EscalateTo = parent
	} else {
		emb.EscalateTo = "master"
	}

	// Open tasks owned by this agent.
	trows, err := a.st.DB().QueryContext(ctx,
		`SELECT id FROM tasks WHERE owner_agent_id=? AND state NOT IN ('completed','failed','abandoned','escalated')`, a.ID,
	)
	if err == nil {
		defer trows.Close()
		for trows.Next() {
			var tid string
			_ = trows.Scan(&tid)
			emb.OpenTaskIDs = append(emb.OpenTaskIDs, tid)
		}
	}
	return emb, nil
}

// RenderSystemPrompt loads the role md template, substitutes {{...}}
// placeholders with introspection data, and returns the full system prompt
// the spawner feeds to the LLM. Empty string + no error if no template found.
func (a *Agent) RenderSystemPrompt(ctx context.Context, templatePath string) (string, error) {
	if templatePath == "" {
		return "", nil
	}
	raw, err := os.ReadFile(templatePath)
	if err != nil {
		return "", nil // missing role md is non-fatal; LLM gets minimal context
	}
	emb, _ := a.Introspect(ctx)
	if emb == nil {
		emb = &Embedding{AgentID: a.ID, Role: a.Role, RunID: a.RunID}
	}
	out := string(raw)
	// Prefer static peers from the org topology when present — they're
	// known at spawn time even before siblings exist. Falls back to the
	// live agents table for older callers that haven't set StaticPeers.
	peerSource := emb.Peers
	if len(a.StaticPeers) > 0 {
		peerSource = a.StaticPeers
	}
	peers := []string{}
	for _, p := range peerSource {
		if p.AgentID != "" {
			peers = append(peers, fmt.Sprintf("%s (%s)", p.AgentID, p.Role))
		} else {
			peers = append(peers, p.Role)
		}
	}
	zoneStr := "[]"
	if len(a.ZoneScope) > 0 {
		zoneStr = strings.Join(a.ZoneScope, ", ")
	}
	toolStr := strings.Join(a.Tools, ", ")
	subs := map[string]string{
		"{{agent_id}}":   a.ID,
		"{{role}}":       a.Role,
		"{{run_id}}":     a.RunID,
		"{{org_name}}":   emb.OrgName,
		"{{org_version}}": fmt.Sprintf("%d", emb.OrgVersion),
		"{{zone_scope}}": zoneStr,
		"{{tools}}":      toolStr,
		"{{peers}}":      strings.Join(peers, ", "),
		"{{delegates_to}}": delegatesString(emb.Peers),
	}
	for k, v := range subs {
		out = strings.ReplaceAll(out, k, v)
	}
	return out, nil
}

func delegatesString(peers []Peer) string {
	roles := []string{}
	seen := map[string]bool{}
	for _, p := range peers {
		if p.Role != "" && !seen[p.Role] && p.Role != "master" {
			roles = append(roles, p.Role)
			seen[p.Role] = true
		}
	}
	return strings.Join(roles, ", ")
}

// IntrospectJSON returns the embedding as JSON for the introspect tool.
func (a *Agent) IntrospectJSON(ctx context.Context) (string, error) {
	emb, err := a.Introspect(ctx)
	if err != nil {
		return "", err
	}
	b, err := json.MarshalIndent(emb, "", "  ")
	if err != nil {
		return "", err
	}
	return string(b), nil
}

// Helper used in tests: returns the store-level Querier handle the agent
// uses internally. (Avoids exposing private fields.)
var _ = store.Now