package swarm

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"
)

// Agent is one supervised task. Public fields are immutable after
// Spawn; mutable state (status, activity, transcript) lives behind
// the embedded mutex.
type Agent struct {
	ID      string
	Task    string
	Dir     string // always the host's RepoRoot; agents share its cwd.
	Started time.Time

	// Model and Provider, when non-empty, override the child
	// subprocess's model resolution. Empty means "inherit whatever
	// the child resolves on its own from config / env / flags" —
	// the historical behaviour. Persisted in meta.json so Resume
	// keeps using the same model across zot restarts.
	Model    string
	Provider string

	// SessionID, when non-empty, scopes the agent to a particular
	// host zot session: the dashboard only surfaces agents whose
	// SessionID matches the active session. Empty means "unscoped"
	// (legacy meta files from before the field existed, or agents
	// spawned without a session context such as in tests). Set at
	// Spawn time from Swarm.activeSession and persisted in
	// meta.json so the scope survives restarts.
	SessionID string

	// Resuming is true when this Agent struct was built by Resume
	// rather than Spawn. The runner consults it to decide whether
	// to pass the original Task as a positional argv to the child:
	// on first Spawn we want the child to run the task immediately;
	// on Resume the task was already run last time and replaying it
	// would produce a second turn that collides ("agent busy; send
	// 'cancel' first") with whatever the user types next. Not
	// persisted — every Resume sets it explicitly.
	Resuming bool

	// InboxPath is the unix socket the child agent listens on for
	// follow-up prompts and control messages. The supervisor opens
	// an Inbox at this path; the child opens a Listener.
	InboxPath string

	// EventLogPath is the durable JSONL event log for this agent.
	// The runner appends every well-formed event from the child
	// (plus lifecycle events of its own) here. /swarm open in any
	// zot process reads from this file to replay the full history.
	EventLogPath string

	// SessionPath is the child's persistent session file. Surfaced
	// so the dashboard / /swarm open can resume the agent later.
	SessionPath string

	// inbox is the supervisor-side socket handle. Populated by
	// Swarm.Spawn so SendInput can route messages without each
	// caller redialing.
	inbox *Inbox

	mu         sync.Mutex
	status     Status
	activity   string
	transcript []string
	finished   time.Time
	lastErr    error

	// OnTurnEnd, if set, fires once per turn_end event the runner
	// observes from the child daemon. Used by auto-swarm watchers
	// to detect that a sub-agent's first (or n-th) task has
	// finished without waiting for the long-lived daemon itself to
	// exit — sub-agents keep running on the inbox even after the
	// initial task completes, so Wait() never unblocks for them.
	OnTurnEnd func(step int, errMsg string)

	ctx    context.Context
	cancel context.CancelFunc
	runner Runner

	// done closes when the run goroutine finalises the agent's
	// status (done / failed / killed). Wait blocks on this so
	// callers don't have to poll.
	done chan struct{}
}

// Inbox exposes the supervisor-side socket handle. Returns nil for
// agents without inboxes (e.g. tests using a custom Runner that
// doesn't speak the daemon protocol).
func (a *Agent) Inbox() *Inbox { return a.inbox }

// Status returns the current high-level status. Cheap; safe from any goroutine.
func (a *Agent) Status() Status {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.status
}

// Activity returns the current one-line activity string.
func (a *Agent) Activity() string {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.activity
}

// Transcript returns a copy of the running transcript.
func (a *Agent) Transcript() []string {
	a.mu.Lock()
	defer a.mu.Unlock()
	out := make([]string, len(a.transcript))
	copy(out, a.transcript)
	return out
}

// Err returns the runner's terminal error, if any.
func (a *Agent) Err() error {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.lastErr
}

// Wait blocks until the agent reaches a terminal state. Used by tests
// and by /swarm wait <id>.
func (a *Agent) Wait() { <-a.done }

// SetOnTurnEnd installs (or clears, with nil) the per-turn callback
// fired from the runner when the child daemon emits a turn_end
// event. Safe to call from any goroutine: the runner reads the
// callback under the same mutex.
func (a *Agent) SetOnTurnEnd(fn func(step int, errMsg string)) {
	a.mu.Lock()
	a.OnTurnEnd = fn
	a.mu.Unlock()
}

func (a *Agent) setStatus(s Status) {
	a.mu.Lock()
	a.status = s
	a.mu.Unlock()
}

func (a *Agent) setActivity(msg string) {
	a.mu.Lock()
	a.activity = strings.TrimSpace(msg)
	a.mu.Unlock()
}

func (a *Agent) appendTranscript(chunk string) {
	chunk = strings.TrimRight(chunk, "\n")
	if chunk == "" {
		return
	}
	a.mu.Lock()
	for _, line := range strings.Split(chunk, "\n") {
		a.transcript = append(a.transcript, line)
	}
	// Bound the transcript so dashboards don't hold gigabytes for
	// long-running agents. 2000 lines is plenty for inspection;
	// the durable record lives in the agent's session file.
	const cap = 2000
	if len(a.transcript) > cap {
		a.transcript = a.transcript[len(a.transcript)-cap:]
	}
	a.mu.Unlock()
}

// newAgentID returns a short, mostly-collision-free identifier of the
// form "<slug>-<nano>". The slug is derived from the task text so
// dashboards stay readable; the nano suffix guarantees uniqueness
// even when two agents are spawned in the same millisecond.
func newAgentID(task string, now time.Time) string {
	slug := taskSlug(task)
	return fmt.Sprintf("%s-%d", slug, now.UnixNano()%1_000_000)
}

func taskSlug(task string) string {
	task = strings.ToLower(task)
	var b strings.Builder
	dash := false
	for _, r := range task {
		switch {
		case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
			b.WriteRune(r)
			dash = false
		default:
			if !dash && b.Len() > 0 {
				b.WriteByte('-')
				dash = true
			}
		}
		if b.Len() >= 24 {
			break
		}
	}
	out := strings.Trim(b.String(), "-")
	if out == "" {
		out = "agent"
	}
	return out
}
