/*
 * Copyright 2025 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package adk

import (
	"bytes"
	"context"
	"encoding/gob"
	"errors"
	"fmt"
	"io"
	"sort"
	"sync"
	"time"

	"github.com/cloudwego/eino/schema"
)

// runSession CheckpointSchema: persisted via serialization.RunCtx (gob).
type runSession struct {
	Values    map[string]any
	valuesMtx *sync.Mutex

	Events     []*agentEventWrapper
	LaneEvents *laneEvents
	mtx        sync.Mutex

	// TypedEvents stores *[]*typedAgentEventWrapper[M] for M != *schema.Message.
	// For M = *schema.Message, the existing Events field is used instead.
	// The any type is required because Go does not support generic fields in non-generic structs.
	TypedEvents any
}

// laneEvents CheckpointSchema: persisted via serialization.RunCtx (gob).
type laneEvents struct {
	Events []*agentEventWrapper
	Parent *laneEvents
}

// agentEventWrapper CheckpointSchema: persisted via serialization.RunCtx (gob).
type agentEventWrapper struct {
	*AgentEvent
	mu                  sync.Mutex
	concatenatedMessage Message
	// TS is the timestamp (in nanoseconds) when this event was created.
	// It is primarily used by the laneEvents mechanism to order events
	// from different agents in a multi-agent flow.
	TS int64
	// StreamErr stores the error message if the MessageStream contained an error.
	// This field guards against multiple calls to getMessageFromWrappedEvent
	// when the stream has already been consumed and errored.
	// Normally when StreamErr happens, the Agent will return with the error,
	// unless retry is configured for the agent generating this stream, in which case
	// this StreamErr will be of type WillRetryError (indicating retry is pending).
	StreamErr error
}

type typedAgentEventWrapper[M MessageType] struct {
	event               *TypedAgentEvent[M]
	mu                  sync.Mutex
	concatenatedMessage M
	TS                  int64
	StreamErr           error
}

// typedAgentEventWrapperForGob is a gob-serializable representation of typedAgentEventWrapper.
// We encode the event and TS separately to avoid the sync.Mutex and non-exported fields.
type typedAgentEventWrapperForGob[M MessageType] struct {
	Event *TypedAgentEvent[M]
	TS    int64
}

func (e *typedAgentEventWrapper[M]) GobEncode() ([]byte, error) {
	if e.event != nil && e.event.Output != nil && e.event.Output.MessageOutput != nil && e.event.Output.MessageOutput.IsStreaming {
		// Materialize the stream before encoding.
		if isNilMessage(e.concatenatedMessage) && e.StreamErr == nil {
			e.consumeStream()
		}
	}

	buf := &bytes.Buffer{}
	err := gob.NewEncoder(buf).Encode(&typedAgentEventWrapperForGob[M]{
		Event: e.event,
		TS:    e.TS,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to gob encode generic agent event wrapper: %w", err)
	}
	return buf.Bytes(), nil
}

func (e *typedAgentEventWrapper[M]) GobDecode(b []byte) error {
	g := &typedAgentEventWrapperForGob[M]{}
	if err := gob.NewDecoder(bytes.NewReader(b)).Decode(g); err != nil {
		return fmt.Errorf("failed to gob decode generic agent event wrapper: %w", err)
	}
	e.event = g.Event
	e.TS = g.TS
	return nil
}

// consumeStream drains the typed message stream, setting concatenatedMessage on success
// or StreamErr on failure. The stream is replaced with a materialized version safe for
// gob encoding.
//
// NOTE: This method parallels agentEventWrapper.consumeStream in utils.go. The two
// implementations exist because agentEventWrapper is non-generic (uses *schema.Message
// directly) while typedAgentEventWrapper[M] is generic. They cannot be unified without
// making the non-generic wrapper generic, which would cascade through the entire
// non-generic event storage layer.
func (e *typedAgentEventWrapper[M]) consumeStream() {
	e.mu.Lock()
	defer e.mu.Unlock()

	if !isNilMessage(e.concatenatedMessage) {
		return
	}

	s := e.event.Output.MessageOutput.MessageStream
	var msgs []M

	defer s.Close()
	for {
		msg, err := s.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
			e.StreamErr = err
			e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs)
			return
		}
		msgs = append(msgs, msg)
	}

	if len(msgs) == 0 {
		e.StreamErr = errors.New("no messages in typedAgentEventWrapper.MessageStream")
		e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs)
		return
	}

	if len(msgs) == 1 {
		e.concatenatedMessage = msgs[0]
	} else {
		var err error
		e.concatenatedMessage, err = concatMessageStream(schema.StreamReaderFromArray(msgs))
		if err != nil {
			e.StreamErr = err
			e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs)
			return
		}
	}

	e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray([]M{e.concatenatedMessage})
}

type otherAgentEventWrapperForEncode agentEventWrapper

func (a *agentEventWrapper) GobEncode() ([]byte, error) {
	if a.Output != nil && a.Output.MessageOutput != nil && a.Output.MessageOutput.IsStreaming {
		// Materialize the stream before encoding. An unconsumed stream that
		// ends with a non-EOF error (WillRetryError, ErrStreamCanceled) would
		// cause MessageVariant.GobEncode to fail. consumeStream replaces the
		// stream with an error-free, materialized version.
		if a.concatenatedMessage == nil && a.StreamErr == nil {
			a.consumeStream()
		}
	}

	buf := &bytes.Buffer{}
	err := gob.NewEncoder(buf).Encode((*otherAgentEventWrapperForEncode)(a))
	if err != nil {
		return nil, fmt.Errorf("failed to gob encode agent event wrapper: %w", err)
	}
	return buf.Bytes(), nil
}

func (a *agentEventWrapper) GobDecode(b []byte) error {
	return gob.NewDecoder(bytes.NewReader(b)).Decode((*otherAgentEventWrapperForEncode)(a))
}

func newRunSession() *runSession {
	return &runSession{
		Values:    make(map[string]any),
		valuesMtx: &sync.Mutex{},
	}
}

// GetSessionValues returns all session key-value pairs for the current run.
func GetSessionValues(ctx context.Context) map[string]any {
	session := getSession(ctx)
	if session == nil {
		return map[string]any{}
	}

	return session.getValues()
}

// AddSessionValue sets a single session key-value pair for the current run.
func AddSessionValue(ctx context.Context, key string, value any) {
	session := getSession(ctx)
	if session == nil {
		return
	}

	session.addValue(key, value)
}

// AddSessionValues sets multiple session key-value pairs for the current run.
func AddSessionValues(ctx context.Context, kvs map[string]any) {
	session := getSession(ctx)
	if session == nil {
		return
	}

	session.addValues(kvs)
}

// GetSessionValue retrieves a session value by key and reports whether it exists.
func GetSessionValue(ctx context.Context, key string) (any, bool) {
	session := getSession(ctx)
	if session == nil {
		return nil, false
	}

	return session.getValue(key)
}

func (rs *runSession) addEvent(event *AgentEvent) {
	wrapper := &agentEventWrapper{AgentEvent: event, TS: time.Now().UnixNano()}
	// If LaneEvents is not nil, we are in a parallel lane.
	// Append to the lane's local event slice (lock-free).
	if rs.LaneEvents != nil {
		rs.LaneEvents.Events = append(rs.LaneEvents.Events, wrapper)
		return
	}

	// Otherwise, we are on the main path. Append to the shared Events slice (with lock).
	rs.mtx.Lock()
	rs.Events = append(rs.Events, wrapper)
	rs.mtx.Unlock()
}

func (rs *runSession) getEvents() []*agentEventWrapper {
	// If there are no in-flight lane events, we can return the main slice directly.
	if rs.LaneEvents == nil {
		rs.mtx.Lock()
		events := rs.Events
		rs.mtx.Unlock()
		return events
	}

	// If there are in-flight events, we must construct the full view.
	// First, get the committed history from the main Events slice.
	rs.mtx.Lock()
	committedEvents := make([]*agentEventWrapper, len(rs.Events))
	copy(committedEvents, rs.Events)
	rs.mtx.Unlock()

	// Then, assemble the in-flight events by traversing the linked list.
	// Reading the .Parent pointer is safe without a lock because the parent of a lane is immutable after creation.
	var laneSlices [][]*agentEventWrapper
	totalLaneSize := 0
	for lane := rs.LaneEvents; lane != nil; lane = lane.Parent {
		if len(lane.Events) > 0 {
			laneSlices = append(laneSlices, lane.Events)
			totalLaneSize += len(lane.Events)
		}
	}

	// Combine committed and in-flight history.
	finalEvents := make([]*agentEventWrapper, 0, len(committedEvents)+totalLaneSize)
	finalEvents = append(finalEvents, committedEvents...)
	for i := len(laneSlices) - 1; i >= 0; i-- {
		finalEvents = append(finalEvents, laneSlices[i]...)
	}

	return finalEvents
}

func addTypedEvent[M MessageType](session *runSession, event *TypedAgentEvent[M]) {
	var zero M
	if _, ok := any(zero).(*schema.Message); ok {
		session.addEvent(any(event).(*AgentEvent))
		return
	}
	session.mtx.Lock()
	defer session.mtx.Unlock()
	wrapper := &typedAgentEventWrapper[M]{event: event, TS: time.Now().UnixNano()}
	store, _ := session.TypedEvents.(*[]*typedAgentEventWrapper[M])
	if store == nil {
		s := make([]*typedAgentEventWrapper[M], 0)
		store = &s
		session.TypedEvents = store
	}
	*store = append(*store, wrapper)
}

func (rs *runSession) getValues() map[string]any {
	rs.valuesMtx.Lock()
	values := make(map[string]any, len(rs.Values))
	for k, v := range rs.Values {
		values[k] = v
	}
	rs.valuesMtx.Unlock()

	return values
}

func (rs *runSession) addValue(key string, value any) {
	rs.valuesMtx.Lock()
	rs.Values[key] = value
	rs.valuesMtx.Unlock()
}

func (rs *runSession) addValues(kvs map[string]any) {
	rs.valuesMtx.Lock()
	for k, v := range kvs {
		rs.Values[k] = v
	}
	rs.valuesMtx.Unlock()
}

func (rs *runSession) getValue(key string) (any, bool) {
	rs.valuesMtx.Lock()
	value, ok := rs.Values[key]
	rs.valuesMtx.Unlock()

	return value, ok
}

type runContext struct {
	RootInput *AgentInput
	RunPath   []RunStep

	AgenticRootInput any

	Session *runSession
}

func (rc *runContext) isRoot() bool {
	return len(rc.RunPath) == 1
}

func (rc *runContext) deepCopy() *runContext {
	copied := &runContext{
		RootInput:        rc.RootInput,
		AgenticRootInput: rc.AgenticRootInput,
		RunPath:          make([]RunStep, len(rc.RunPath)),
		Session:          rc.Session,
	}

	copy(copied.RunPath, rc.RunPath)

	return copied
}

type runCtxKey struct{}

func getRunCtx(ctx context.Context) *runContext {
	runCtx, ok := ctx.Value(runCtxKey{}).(*runContext)
	if !ok {
		return nil
	}
	return runCtx
}

func setRunCtx(ctx context.Context, runCtx *runContext) context.Context {
	return context.WithValue(ctx, runCtxKey{}, runCtx)
}

func initRunCtx(ctx context.Context, agentName string, input *AgentInput) (context.Context, *runContext) {
	runCtx := getRunCtx(ctx)
	if runCtx != nil {
		runCtx = runCtx.deepCopy()
	} else {
		runCtx = &runContext{Session: newRunSession()}
	}

	runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName})
	if runCtx.isRoot() && input != nil {
		runCtx.RootInput = input
	}

	return setRunCtx(ctx, runCtx), runCtx
}

func initTypedRunCtx[M MessageType](ctx context.Context, agentName string, input *TypedAgentInput[M]) (context.Context, *runContext) {
	runCtx := getRunCtx(ctx)
	if runCtx != nil {
		runCtx = runCtx.deepCopy()
	} else {
		runCtx = &runContext{Session: newRunSession()}
	}

	runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName})
	if runCtx.isRoot() && input != nil {
		var zero M
		if _, ok := any(zero).(*schema.Message); ok {
			runCtx.RootInput = any(input).(*AgentInput)
		} else {
			runCtx.AgenticRootInput = input
		}
	}

	return setRunCtx(ctx, runCtx), runCtx
}

func joinRunCtxs(parentCtx context.Context, childCtxs ...context.Context) {
	switch len(childCtxs) {
	case 0:
		return
	case 1:
		// Optimization for the common case of a single branch.
		newEvents := unwindLaneEvents(childCtxs...)
		commitEvents(parentCtx, newEvents)
		return
	}

	// 1. Collect all new events from the leaf nodes of each context's lane.
	newEvents := unwindLaneEvents(childCtxs...)

	// 2. Sort the collected events by their creation timestamp for chronological order.
	sort.Slice(newEvents, func(i, j int) bool {
		return newEvents[i].TS < newEvents[j].TS
	})

	// 3. Commit the sorted events to the parent.
	commitEvents(parentCtx, newEvents)
}

// commitEvents appends a slice of new events to the correct parent lane or main event log.
func commitEvents(ctx context.Context, newEvents []*agentEventWrapper) {
	runCtx := getRunCtx(ctx)
	if runCtx == nil || runCtx.Session == nil {
		// Should not happen, but handle defensively.
		return
	}

	// If the context we are committing to is itself a lane, append to its event slice.
	if runCtx.Session.LaneEvents != nil {
		runCtx.Session.LaneEvents.Events = append(runCtx.Session.LaneEvents.Events, newEvents...)
	} else {
		// Otherwise, commit to the main, shared Events slice with a lock.
		runCtx.Session.mtx.Lock()
		runCtx.Session.Events = append(runCtx.Session.Events, newEvents...)
		runCtx.Session.mtx.Unlock()
	}
}

// unwindLaneEvents traverses the LaneEvents of the given contexts and collects
// all events from the leaf nodes.
func unwindLaneEvents(ctxs ...context.Context) []*agentEventWrapper {
	var allNewEvents []*agentEventWrapper
	for _, ctx := range ctxs {
		runCtx := getRunCtx(ctx)
		if runCtx != nil && runCtx.Session != nil && runCtx.Session.LaneEvents != nil {
			allNewEvents = append(allNewEvents, runCtx.Session.LaneEvents.Events...)
		}
	}
	return allNewEvents
}

func forkRunCtx(ctx context.Context) context.Context {
	parentRunCtx := getRunCtx(ctx)
	if parentRunCtx == nil || parentRunCtx.Session == nil {
		// Should not happen in a parallel workflow, but handle defensively.
		return ctx
	}

	// Create a new session for the child lane by manually copying the parent's session fields.
	// This is crucial to ensure a new mutex is created and that the LaneEvents pointer is unique.
	childSession := &runSession{
		Events:    parentRunCtx.Session.Events, // Share the committed history
		Values:    parentRunCtx.Session.Values, // Share the values map
		valuesMtx: parentRunCtx.Session.valuesMtx,
	}

	// Fork the lane events within the new session struct.
	childSession.LaneEvents = &laneEvents{
		Parent: parentRunCtx.Session.LaneEvents,
		Events: make([]*agentEventWrapper, 0),
	}

	// Create a new runContext for the child lane, pointing to the new session.
	childRunCtx := &runContext{
		RootInput: parentRunCtx.RootInput,
		RunPath:   make([]RunStep, len(parentRunCtx.RunPath)),
		Session:   childSession,
	}
	copy(childRunCtx.RunPath, parentRunCtx.RunPath)

	return setRunCtx(ctx, childRunCtx)
}

// updateRunPathOnly creates a new context with an updated RunPath, but does NOT modify the Address.
// This is used by sequential workflows to accumulate execution history for LLM context,
// without incorrectly chaining the static addresses of peer agents.
func updateRunPathOnly(ctx context.Context, agentNames ...string) context.Context {
	runCtx := getRunCtx(ctx)
	if runCtx == nil {
		// This should not happen in a sequential workflow context, but handle defensively.
		runCtx = &runContext{Session: newRunSession()}
	} else {
		runCtx = runCtx.deepCopy()
	}

	for _, agentName := range agentNames {
		runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName})
	}

	return setRunCtx(ctx, runCtx)
}

// ClearRunCtx clears the run context of the multi-agents. This is particularly useful
// when a customized agent with a multi-agents inside it is set as a subagent of another
// multi-agents. In such cases, it's not expected to pass the outside run context to the
// inside multi-agents, so this function helps isolate the contexts properly.
func ClearRunCtx(ctx context.Context) context.Context {
	return context.WithValue(ctx, runCtxKey{}, nil)
}

func ctxWithNewTypedRunCtx[M MessageType](ctx context.Context, input *TypedAgentInput[M], sharedParentSession bool) context.Context {
	var session *runSession
	if sharedParentSession {
		if parentSession := getSession(ctx); parentSession != nil {
			session = &runSession{
				Values:    parentSession.Values,
				valuesMtx: parentSession.valuesMtx,
			}
		}
	}
	if session == nil {
		session = newRunSession()
	}
	var zero M
	rc := &runContext{Session: session}
	if _, ok := any(zero).(*schema.Message); ok {
		rc.RootInput = any(input).(*AgentInput)
	} else {
		rc.AgenticRootInput = input
	}
	return setRunCtx(ctx, rc)
}

func getSession(ctx context.Context) *runSession {
	runCtx := getRunCtx(ctx)
	if runCtx != nil {
		return runCtx.Session
	}

	return nil
}
