package agent

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"strings"
	"sync"

	"github.com/google/uuid"

	"go-micro.dev/v6/ai"
)

// StreamEventType identifies an event emitted by a tool-aware agent stream.
type StreamEventType string

const (
	// StreamEventToolStart is emitted immediately before a tool call runs.
	StreamEventToolStart StreamEventType = "tool_start"
	// StreamEventToolEnd is emitted after a tool call returns or is refused.
	StreamEventToolEnd StreamEventType = "tool_end"
	// StreamEventToken carries a chunk of the final answer.
	StreamEventToken StreamEventType = "token"
	// StreamEventDone carries the completed agent response.
	StreamEventDone StreamEventType = "done"
)

// StreamEvent is one event from StreamAsk.
type StreamEvent struct {
	Type     StreamEventType
	Token    string
	ToolCall ai.ToolCall
	Result   ai.ToolResult
	Response *Response
}

// AgentStream is a stream of tool execution events followed by final-answer chunks.
type AgentStream interface {
	Recv() (*StreamEvent, error)
	Close() error
}

// StreamAsk runs an agent Ask turn with tool start/end events and streams the final answer.
// It is additive for callers that hold the public Agent interface; concrete agents also
// expose the same method directly.
func StreamAsk(ctx context.Context, ag Agent, message string) (AgentStream, error) {
	streamer, ok := ag.(interface {
		StreamAsk(context.Context, string) (AgentStream, error)
	})
	if !ok {
		return nil, errors.New("agent: StreamAsk unsupported by implementation")
	}
	return streamer.StreamAsk(ctx, message)
}

// ResumeStreamAsk resumes a checkpointed agent run and emits the same event
// shape as StreamAsk. Completed runs are streamed from the persisted response;
// unfinished runs continue from their checkpoint and emit tool events for any
// work that still needs to run. Tool calls already recorded as done in the
// checkpoint are reused by the agent checkpoint wrapper and are not re-executed.
func ResumeStreamAsk(ctx context.Context, ag Agent, runID string) (AgentStream, error) {
	a, ok := ag.(*agentImpl)
	if !ok {
		return nil, errors.New("agent: ResumeStreamAsk unsupported by implementation")
	}
	return a.resumeStreamAsk(ctx, runID)
}

// StreamAsk runs tools like Ask, emits ToolStart/ToolEnd events as they execute,
// then emits chunks of the final answer followed by a Done event.
func (a *agentImpl) StreamAsk(ctx context.Context, message string) (AgentStream, error) {
	streamCtx, cancel := context.WithCancel(ctx)
	events := make(chan *StreamEvent, 16)
	done := make(chan struct{})
	s := &agentStream{events: events, done: done, cancel: cancel}

	go func() {
		defer close(events)
		defer close(done)
		resp, err := a.askWithStreamEvents(streamCtx, message, events)
		if err != nil {
			s.setErr(err)
			return
		}
		for _, tok := range splitStreamTokens(resp.Reply) {
			if !sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventToken, Token: tok}) {
				return
			}
		}
		_ = sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventDone, Response: resp})
	}()
	return s, nil
}

func (a *agentImpl) resumeStreamAsk(ctx context.Context, runID string) (AgentStream, error) {
	streamCtx, cancel := context.WithCancel(ctx)
	events := make(chan *StreamEvent, 16)
	done := make(chan struct{})
	s := &agentStream{events: events, done: done, cancel: cancel}

	go func() {
		defer close(events)
		defer close(done)
		resp, err := a.resumeWithStreamEvents(streamCtx, runID, events)
		if err != nil {
			s.setErr(err)
			return
		}
		for _, tok := range splitStreamTokens(resp.Reply) {
			if !sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventToken, Token: tok}) {
				return
			}
		}
		_ = sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventDone, Response: resp})
	}()
	return s, nil
}

func (a *agentImpl) askWithStreamEvents(ctx context.Context, message string, events chan<- *StreamEvent) (*Response, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	if a.tools == nil {
		a.tools = ai.NewTools(a.opts.Registry, ai.ToolClient(a.opts.Client))
	}
	base := a.toolHandler()
	handler := func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
		_ = sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventToolStart, ToolCall: call})
		result := base(ctx, call)
		_ = sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventToolEnd, ToolCall: call, Result: result})
		return result
	}
	a.setupWithToolHandler(handler)
	defer a.setupWithToolHandler(nil)
	return a.askLocked(ctx, uuid.New().String(), message, a.parentRunID, nil, true)
}

func (a *agentImpl) resumeWithStreamEvents(ctx context.Context, runID string, events chan<- *StreamEvent) (*Response, error) {
	if a.opts.Checkpoint == nil {
		return nil, errors.New("agent: ResumeStreamAsk requires a checkpoint")
	}
	run, ok, err := a.opts.Checkpoint.Load(ctx, runID)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, errors.New("agent: checkpointed run not found")
	}
	if run.Status == "done" {
		var resp Response
		if err := json.Unmarshal(run.State.Data, &resp); err != nil {
			return nil, err
		}
		return &resp, nil
	}
	if terminalAgentRunStatus(run.Status) {
		return nil, errors.New("agent: checkpointed run is terminal with status " + run.Status)
	}

	a.mu.Lock()
	defer a.mu.Unlock()
	if a.tools == nil {
		a.tools = ai.NewTools(a.opts.Registry, ai.ToolClient(a.opts.Client))
	}
	base := a.toolHandler()
	handler := func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
		_ = sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventToolStart, ToolCall: call})
		result := base(ctx, call)
		_ = sendStreamEvent(ctx, events, &StreamEvent{Type: StreamEventToolEnd, ToolCall: call, Result: result})
		return result
	}
	a.setupWithToolHandler(handler)
	defer a.setupWithToolHandler(nil)
	if run.Status == "paused" {
		if run.State.Stage == agentInputStep {
			return nil, errors.New("agent: checkpointed run is input-required; resume with ResumeInput")
		}
		run.Status = "running"
		run.State.Stage = agentAskStep
	}
	return a.askLocked(ctx, run.ID, string(run.State.Data), run.ParentID, &run, false)
}

type agentStreamAdapter struct {
	stream AgentStream
}

type memoryRecordingStream struct {
	stream ai.Stream
	memory Memory

	mu     sync.Mutex
	chunks []string
	closed bool
}

func (s *memoryRecordingStream) Recv() (*ai.Response, error) {
	resp, err := s.stream.Recv()
	if resp != nil && resp.Reply != "" {
		s.mu.Lock()
		s.chunks = append(s.chunks, resp.Reply)
		s.mu.Unlock()
	}
	if errors.Is(err, io.EOF) {
		s.recordAssistant()
	}
	return resp, err
}

func (s *memoryRecordingStream) Close() error {
	s.recordAssistant()
	return s.stream.Close()
}

func (s *memoryRecordingStream) recordAssistant() {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return
	}
	s.closed = true
	if reply := strings.Join(s.chunks, ""); reply != "" {
		s.memory.Add("assistant", reply)
	}
}

func (s *agentStreamAdapter) Recv() (*ai.Response, error) {
	for {
		event, err := s.stream.Recv()
		if err != nil {
			return nil, err
		}
		if event == nil {
			continue
		}
		switch event.Type {
		case StreamEventToken:
			if event.Token == "" {
				continue
			}
			return &ai.Response{Reply: event.Token}, nil
		case StreamEventDone:
			return nil, io.EOF
		}
	}
}

func (s *agentStreamAdapter) Close() error {
	return s.stream.Close()
}

func (a *agentImpl) streamAskAI(ctx context.Context, message string) (ai.Stream, error) {
	stream, err := a.StreamAsk(ctx, message)
	if err != nil {
		return nil, err
	}
	return &agentStreamAdapter{stream: stream}, nil
}

type agentStream struct {
	events <-chan *StreamEvent
	done   <-chan struct{}
	cancel context.CancelFunc
	mu     sync.Mutex
	err    error
}

func (s *agentStream) Recv() (*StreamEvent, error) {
	ev, ok := <-s.events
	if ok {
		return ev, nil
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.err != nil {
		return nil, s.err
	}
	return nil, io.EOF
}

func (s *agentStream) Close() error {
	if s.cancel != nil {
		s.cancel()
	}
	<-s.done
	return nil
}

func (s *agentStream) setErr(err error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.err = err
}

func sendStreamEvent(ctx context.Context, events chan<- *StreamEvent, ev *StreamEvent) bool {
	select {
	case events <- ev:
		return true
	case <-ctx.Done():
		return false
	}
}

func splitStreamTokens(reply string) []string {
	if reply == "" {
		return nil
	}
	parts := strings.Fields(reply)
	if len(parts) == 0 {
		return []string{reply}
	}
	out := make([]string, 0, len(parts))
	for i, part := range parts {
		if i > 0 {
			part = " " + part
		}
		out = append(out, part)
	}
	return out
}
