package agent

import (
	"sort"
	"strings"

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

// RememberObservedFrontier records that the runtime observed one AgentTeam target
// message. This is separate from delivery ID reconciliation: freshness uses
// normalized targetKey/targetSeq rather than the transport delivery cursor.
func (ap *Process) RememberObservedFrontier(msg types.InboxMessage) {
	targetKey := strings.TrimSpace(msg.TargetKey)
	if targetKey == "" || msg.TargetSeq <= 0 {
		return
	}
	ap.mu.Lock()
	ap.freshness.MarkObserved(targetKey, msg.TargetSeq, msg.MessageID)
	ap.mu.Unlock()
}

func (ap *Process) markObservedFrontierForRuntimeHandoff(msg types.InboxMessage) func() {
	targetKey := strings.TrimSpace(msg.TargetKey)
	if targetKey == "" || msg.TargetSeq <= 0 {
		return func() {}
	}

	ap.mu.Lock()
	previous, hadPrevious := ap.freshness.Snapshot(targetKey)
	ap.freshness.MarkObserved(targetKey, msg.TargetSeq, msg.MessageID)
	ap.mu.Unlock()

	return func() {
		ap.mu.Lock()
		defer ap.mu.Unlock()

		current, ok := ap.freshness.Snapshot(targetKey)
		if !ok {
			return
		}
		previousLatest := int64(0)
		if hadPrevious {
			previousLatest = previous.LatestKnownSeq
		}
		if current.ObservedSeq > msg.TargetSeq || current.LatestKnownSeq > maxInt64(msg.TargetSeq, previousLatest) {
			return
		}
		if hadPrevious {
			if ap.freshness.ByTarget == nil {
				ap.freshness.ByTarget = make(map[string]types.MessageFreshnessFrontier, 1)
			}
			ap.freshness.ByTarget[targetKey] = previous
			return
		}
		delete(ap.freshness.ByTarget, targetKey)
		if len(ap.freshness.ByTarget) == 0 {
			ap.freshness.ByTarget = nil
		}
	}
}

// RememberUnobservedFrontierHint records that a target message is known to the
// daemon but has not necessarily been incorporated into the model's current
// response. It intentionally does not advance ObservedSeq; it only stores a
// refresh hint so a stale send still uses the last truly observed frontier and
// the local/server freshness gate can hold it.
func (ap *Process) RememberUnobservedFrontierHint(msg types.InboxMessage) {
	ap.mu.Lock()
	ap.rememberUnobservedFrontierHintLocked(msg)
	ap.mu.Unlock()
}

func (ap *Process) rememberUnobservedFrontierHintLocked(msg types.InboxMessage) {
	targetKey := strings.TrimSpace(msg.TargetKey)
	if targetKey == "" || msg.TargetSeq <= 0 {
		return
	}
	ap.freshness.MarkHeld(targetKey, 0, msg.TargetSeq)
}

// RememberObservedFrontiers records all target messages included in one
// runtime-visible delivery, such as a daemon-local tool result or wake payload.
func (ap *Process) RememberObservedFrontiers(messages []types.InboxMessage) {
	if len(messages) == 0 {
		return
	}
	ordered := append([]types.InboxMessage(nil), messages...)
	sort.SliceStable(ordered, func(i, j int) bool {
		leftKey := strings.TrimSpace(ordered[i].TargetKey)
		rightKey := strings.TrimSpace(ordered[j].TargetKey)
		if leftKey != rightKey {
			return leftKey < rightKey
		}
		return ordered[i].TargetSeq < ordered[j].TargetSeq
	})
	ap.mu.Lock()
	defer ap.mu.Unlock()
	for _, msg := range ordered {
		ap.freshness.MarkObserved(msg.TargetKey, msg.TargetSeq, msg.MessageID)
	}
}

// MessageFreshnessSnapshot returns the session's observed freshness frontier
// for targetKey.
func (ap *Process) MessageFreshnessSnapshot(targetKey string) (types.MessageFreshnessFrontier, bool) {
	ap.mu.Lock()
	defer ap.mu.Unlock()
	return ap.freshness.Snapshot(targetKey)
}

// RememberFreshnessHeld records the refresh hint from a held send without
// advancing the observed frontier to latestSeq.
func (ap *Process) RememberFreshnessHeld(targetKey string, seenUpToSeq int64, latestSeq int64) {
	ap.mu.Lock()
	ap.freshness.MarkHeld(targetKey, seenUpToSeq, latestSeq)
	ap.mu.Unlock()
}

// PendingRuntimeInputCoversFreshness reports whether an ack-aware active-turn
// input is currently accepted by the runtime transport and can cover throughSeq
// once the runtime emits its correlated acknowledgement. These pending inputs are
// intentionally outside the daemon-local inbox, so held message_send guidance
// must wait for the next input boundary rather than asking the runtime to fetch
// the same in-flight body through a second context delivery.
func (ap *Process) PendingRuntimeInputCoversFreshness(targetKey string, throughSeq int64) bool {
	targetKey = strings.TrimSpace(targetKey)
	if targetKey == "" {
		return false
	}

	ap.mu.Lock()
	defer ap.mu.Unlock()
	if !ap.hasLivePendingRuntimeInputLocked() {
		return false
	}

	frontier, _ := ap.freshness.Snapshot(targetKey)
	maxPendingSeq := int64(0)
	for _, pending := range ap.pendingRuntimeInputs {
		if pending.reclaimed {
			continue
		}
		msg := pending.Message
		if strings.TrimSpace(msg.TargetKey) != targetKey || msg.TargetSeq <= 0 {
			continue
		}
		if frontier.ObservedSeq > 0 && msg.TargetSeq <= frontier.ObservedSeq {
			continue
		}
		if msg.TargetSeq > maxPendingSeq {
			maxPendingSeq = msg.TargetSeq
		}
	}
	if maxPendingSeq <= 0 {
		return false
	}
	return throughSeq <= 0 || maxPendingSeq >= throughSeq
}

func maxInt64(a, b int64) int64 {
	if a > b {
		return a
	}
	return b
}
