package agent

import (
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/messagesend"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// Message-send scope sources name where one message-send scope target came
// from. They are guardrail vocabulary only — routing or security wording
// is out of scope per the #3850 boundary.
const (
	sendScopeSourceConversation = "conversation"
	sendScopeSourceWorkflow     = "workflow"
)

// sendScopeState is the turn message-send scope for one
// Process: the exact sendable targets of the messages the runtime actually
// delivered in the current authoritative request. It is deliberately
// independent from delivery admission, message freshness, and ackEpoch
// fencing: admission only proves daemon ownership, queued or transport-pending
// bodies are not model-visible, and same-session continuations keep the turn.
// It is produced only at trusted delivery-evidence points and consumed only by
// the message_send guard.
type sendScopeState struct {
	targets []messagesend.Target
	seen    map[string]struct{}
	// generation fences late delivery acknowledgements from a retired
	// authoritative request. Every retirement increments it; pending runtime
	// inputs stamp it at admission so a late final-hop ack can only settle
	// durability and never re-authorize the retired turn's targets.
	generation uint64
	// handover is the owner-scoped CAS transaction between a runtime
	// delivery RPC and its exact acknowledgement. ownerID binds it to the
	// exact input/message that began it; commit and abort must name the same
	// owner, so a late valid ack for an older pending input can neither end
	// another delivery's handover nor roll a committed scope back.
	handover *sendScopeHandover
	// invalidDelivery marks that a trusted delivery carried a conversation
	// target that failed normalization. An empty scope with live=true means
	// "no delivery context" (proactive allowed); an invalid delivery must
	// instead fail closed.
	invalidDelivery bool
}

// sendScopeHandover is one in-flight delivery's scope transaction.
type sendScopeHandover struct {
	ownerID   string
	mode      string // replace | append
	saved     savedSendScope
	committed bool
}

// addSendTargetLocked adds one exact target with its source, keeping
// first-seen order and dropping duplicates. Must be called with ap.mu held.
// maxTurnSendScopeTargets bounds the turn scope. Reaching the cap
// stops appending but never fails sends globally: known exact hits keep
// working and unknown targets flow through the cross-target branch.
const maxTurnSendScopeTargets = 64

func (ap *Process) addSendTargetLocked(target string, source string) {
	// Normalize with the same parser the message_send sender uses so alias or
	// mixed-case delivery targets and their normalized copies compare equal.
	target = messagesend.NormalizeTarget(target)
	if target == "" {
		return
	}
	_ = target // appended below; handover state is owner-scoped, never cleared here
	if len(ap.sendScope.targets) >= maxTurnSendScopeTargets {
		return
	}
	// The scope is keyed by the canonical target only: the same target
	// arriving via multiple sources (conversation and task_scope) authorizes
	// once, and the first-seen source is kept.
	if _, duplicate := ap.sendScope.seen[target]; duplicate {
		return
	}
	if ap.sendScope.seen == nil {
		ap.sendScope.seen = make(map[string]struct{})
	}
	ap.sendScope.seen[target] = struct{}{}
	ap.sendScope.targets = append(ap.sendScope.targets, messagesend.Target{
		Target: target,
		Source: source,
	})
}

// addSendTargetsLocked records the exact sendable target one delivered
// message carries: its conversation target (tagged workflow when the delivery
// carries trusted workflow context). Task discussion targets are never
// derived from deliveries; sending to them requires the explicit
// allow_cross_target opt-in.
// Must be called with ap.mu held after the delivery-evidence fence checks.
func (ap *Process) addSendTargetsLocked(msg types.InboxMessage) {
	source := sendScopeSourceConversation
	if msg.ResolvedWorkflowContext().Valid() {
		source = sendScopeSourceWorkflow
	}
	if normalized := messagesend.NormalizeTarget(msg.Target); normalized == "" && strings.TrimSpace(msg.Target) != "" {
		// A trusted delivery whose conversation target cannot be normalized
		// invalidates the whole delivery for scope purposes because the turn's
		// primary destination is unresolvable. It must never degrade into a
		// proactive allowance, and it must invalidate any earlier snapshot:
		// bump the generation so a guard that snapshotted an empty-but-live
		// scope before this poison fails its final validation.
		ap.sendScope.invalidDelivery = true
		ap.sendScope.generation++
		return
	}
	ap.addSendTargetLocked(msg.Target, source)
}

// resetSendScopeLocked clears the turn scope. Called when the
// authoritative request ends: ordinary turn drain, runtime idle drain, stop,
// exit, and a successful InterruptAndDeliver starting the next authoritative
// request. Must be called with ap.mu held.
func (ap *Process) resetSendScopeLocked() {
	ap.sendScope.targets = nil
	clear(ap.sendScope.seen)
	ap.sendScope.generation++
	ap.sendScope.handover = nil
	ap.sendScope.invalidDelivery = false
}

// savedSendScope is the snapshot used to abort a handover transaction.
type savedSendScope struct {
	targets []messagesend.Target
	seen    map[string]struct{}
	gen     uint64
}

// beginSendScopeHandoverLocked enters a REPLACE handover before an interrupt
// or new-turn delivery RPC: the previous turn's targets are set aside
// (returned for rollback) and SendScope reports unavailable until the new
// authoritative request's first target lands, so a tool call racing inside
// the RPC can neither read the previous turn's scope nor mistake an empty
// scope for a proactive allowance. Must be called with ap.mu held.
func (ap *Process) beginSendScopeHandoverLocked() savedSendScope {
	saved := ap.beginSendScopeHandoverOwnerLocked("")
	return saved
}

// beginSendScopeHandoverOwnerLocked starts a REPLACE handover owned by the
// given input/message owner. The zero owner is only used by tests; production
// paths pass the exact input ID so a late ack for another pending input can
// never finish this transaction.
func (ap *Process) beginSendScopeHandoverOwnerLocked(ownerID string) savedSendScope {
	saved := savedSendScope{
		targets: ap.sendScope.targets,
		seen:    ap.sendScope.seen,
		gen:     ap.sendScope.generation,
	}
	ap.sendScope.targets = nil
	ap.sendScope.seen = nil
	ap.sendScope.generation++
	ap.sendScope.handover = &sendScopeHandover{ownerID: strings.TrimSpace(ownerID), mode: "replace", saved: saved}
	return saved
}

// beginSendScopeAppendLocked enters an APPEND handover before a busy
// same-turn delivery RPC: the turn keeps its already-delivered targets
// readable throughout, and the new message's target joins them at ack time.
// Same-turn deliveries extend the turn, they never replace it, so unlike a
// REPLACE handover there is no unavailable window at all. Must be called
// with ap.mu held.
func (ap *Process) beginSendScopeAppendLocked() {}

// beginSendScopeAppendOwnerLocked records an APPEND handover owner without
// touching the live targets.
func (ap *Process) beginSendScopeAppendOwnerLocked(ownerID string) {
	ap.sendScope.handover = &sendScopeHandover{ownerID: strings.TrimSpace(ownerID), mode: "append"}
}

// commitSendScopeHandoverLocked atomically installs the delivered message's
// targets and ends the handover window. For a REPLACE handover the window
// cleared the previous targets; for an APPEND handover they were kept and
// the new target simply joins them. Must be called with ap.mu held.
func (ap *Process) commitSendScopeHandoverLocked(msg types.InboxMessage) {
	ap.addSendTargetsLocked(msg)
	ap.sendScope.handover = nil
}

// commitAckedSendScopeLocked is the single trusted entry point an exact
// runtime acknowledgement calls to update the send scope, independent of
// delivery-RPC state. It applies the same fences everywhere: stopped
// processes never authorize, a stale generation (retired turn or late
// final-hop) only settles durability, a replace handover commits its
// replacement, and an ordinary same-turn ack appends. Must be called with
// ap.mu held after the session-correlation check passed.
func (ap *Process) commitAckedSendScopeLocked(pending pendingRuntimeInput) {
	if ap.stopping || ap.state == processStateStopped {
		return
	}
	// Restored or reclaimed acknowledgements settle durability and frontiers
	// only; they are not fresh delivery evidence and never produce scope.
	if pending.Restored || pending.reclaimed {
		return
	}
	// Final-hop tool receipts carry the generation they were stamped with:
	// an ack from a retired generation settles durability only. Ordinary
	// pending runtime inputs are different — the delivery contract lets them
	// queue in an earlier turn and become the CURRENT runtime-visible input
	// at ack time, so their exact ack installs into the live generation.
	if pending.FinalHopDeferred && pending.sendScopeGen != ap.sendScope.generation {
		return
	}
	owner := strings.TrimSpace(pending.Input.ID)
	if h := ap.sendScope.handover; h != nil && h.ownerID != "" && owner != "" && h.ownerID != owner {
		// A late valid ack for a DIFFERENT pending input may append its target
		// but never finishes another delivery's handover transaction.
		ap.addSendTargetsLocked(pending.Message)
		return
	}
	if h := ap.sendScope.handover; h != nil && h.mode == "replace" {
		h.committed = true
		ap.commitSendScopeHandoverLocked(pending.Message)
		return
	}
	ap.addSendTargetsLocked(pending.Message)
}

// abortSendScopeAppendLocked ends an append handover after a failed busy
// same-turn delivery: the turn's targets were never set aside, so there is
// nothing to restore. Must be called with ap.mu held.
func (ap *Process) abortSendScopeAppendLocked() {}

// abortSendScopeHandoverLocked restores the saved scope after a failed
// delivery RPC. Must be called with ap.mu held.
func (ap *Process) abortSendScopeHandoverLocked(saved savedSendScope) {
	if h := ap.sendScope.handover; h != nil {
		if h.committed {
			// A valid exact ack already committed inside the delivery RPC;
			// the RPC's later transport error must not roll the committed
			// scope back.
			return
		}
		ap.sendScope.targets = saved.targets
		ap.sendScope.seen = saved.seen
		ap.sendScope.generation = saved.gen
		ap.sendScope.handover = nil
	}
}

// abortSendScopeHandoverAmbiguousLocked handles an ambiguous transport
// error: the replacement input may already be inside the runtime, so the
// conservative outcome stays UNAVAILABLE — the old turn's targets must not
// be re-authorized, and no empty-scope proactive allowance may appear.
func (ap *Process) abortSendScopeHandoverAmbiguousLocked() {
	if h := ap.sendScope.handover; h != nil {
		if h.committed {
			return
		}
		// Keep the handover open with no targets: SendScope reports
		// unavailable until a later authoritative event resolves the turn.
		ap.sendScope.targets = nil
		clear(ap.sendScope.seen)
	}
}

// SendScope returns a copy of the current turn scope and
// whether the process is live. live=false covers a nil, stopping, or stopped
// process: the caller must fail closed instead of treating the empty scope as
// a proactive allowance. An empty scope with live=true means the turn has no
// delivered context.
func (ap *Process) SendScope() (messagesend.Scope, bool) {
	scope, live, _ := ap.SendScopeWithGeneration()
	return scope, live
}

// SendScopeWithGeneration additionally returns the scope generation the
// snapshot was taken at. The caller passes it to ValidateSendScope as the
// final atomic linearization check: any stop, exit, retirement, or handover
// that lands between the snapshot and the validation fails closed.
func (ap *Process) SendScopeWithGeneration() (messagesend.Scope, bool, uint64) {
	if ap == nil {
		return messagesend.Scope{}, false, 0
	}
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if ap.stopping || ap.state == processStateStopped {
		return messagesend.Scope{}, false, ap.sendScope.generation
	}
	if h := ap.sendScope.handover; h != nil && h.mode == "replace" {
		return messagesend.Scope{}, false, ap.sendScope.generation
	}
	generation := ap.sendScope.generation
	if ap.sendScope.invalidDelivery {
		return messagesend.Scope{}, false, generation
	}
	if len(ap.sendScope.targets) == 0 {
		return messagesend.Scope{}, true, generation
	}
	return messagesend.Scope{
		Targets: append([]messagesend.Target(nil), ap.sendScope.targets...),
	}, true, generation
}

// ValidateSendScope reports whether the given scope generation is still the
// live, unchanged scope of a running process. It is the guard's final
// linearization point: a stop, exit, retirement, or handover that landed
// after the snapshot makes it false.
func (ap *Process) ValidateSendScope(generation uint64) bool {
	if ap == nil {
		return false
	}
	ap.mu.Lock()
	defer ap.mu.Unlock()
	if ap.stopping || ap.state == processStateStopped {
		return false
	}
	if ap.sendScope.invalidDelivery {
		return false
	}
	return ap.sendScope.generation == generation
}

// seedInitialDeliveryScope seeds the turn message-send scope from the launch's
// initial delivery. The launch prompt surfaces the initial wake message before
// any model tool call can happen, so the scope must already resolve the wake
// target inside Spawn.
func (ap *Process) seedInitialDeliveryScope(delivery *types.InboxMessage) {
	if delivery == nil {
		return
	}
	ap.mu.Lock()
	defer ap.mu.Unlock()
	ap.addSendTargetsLocked(*delivery)
}

// seedInitialTurnDeliveryLocked mirrors seedInitialDeliveryScope for the
// turn-delivery target set: the launch prompt already carries the initial
// delivery, so its conversation target is trusted evidence from turn one.
func (ap *Process) seedInitialTurnDeliveryLocked(delivery *types.InboxMessage) {
	if delivery == nil {
		return
	}
	ap.mu.Lock()
	defer ap.mu.Unlock()
	ap.addTurnDeliveryLocked(delivery.Target)
}
