package claude

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/yaoapp/kun/log"
	"github.com/yaoapp/yao/agent/output/message"
)

// streamParser is an explicit state machine for Claude CLI stream-json output.
//
// Each tool call gets its own message lifecycle:
//
//	content_block_start  -> message_start(id=exec-N-xxx)  + ChunkExecute{tool, status:running}
//	input_json_delta     -> ChunkExecute{input_delta:...}  (same message group)
//	content_block_stop   -> message_end(exec-N-xxx)         (streaming phase ends, tool kept in buffer)
//	...later...
//	user/tool_result     -> message_start(id=exec-N-xxx, reuse!) + ChunkExecute{status:completed, output:...} + message_end
//
// For parallel tool calls, multiple tools may be in-flight simultaneously.
// The tools buffer keeps each tool's state until its tool_result arrives.
type streamParser struct {
	handler   message.StreamFunc
	completed bool

	textActive   bool
	toolIndex    int
	activeToolID string                // tool currently receiving content_block_delta
	tools        map[string]*toolState // tool_id -> buffered tool state

	toolNames     map[string]string // tool_id -> tool_name
	toolMsgIDs    map[string]string // tool_id -> message_id (for result reuse)
	toolInputs    map[string]string // tool_id -> full input JSON (for result replay)
	toolSummaries map[string]string // tool_id -> summary (for result replay)
}

type toolState struct {
	id        string
	name      string
	msgID     string
	index     int
	inputJSON strings.Builder
}

func newStreamParser(handler message.StreamFunc) *streamParser {
	return &streamParser{
		handler:       handler,
		tools:         make(map[string]*toolState),
		toolNames:     make(map[string]string),
		toolMsgIDs:    make(map[string]string),
		toolInputs:    make(map[string]string),
		toolSummaries: make(map[string]string),
	}
}

func (p *streamParser) activeTool() *toolState {
	if p.activeToolID == "" {
		return nil
	}
	return p.tools[p.activeToolID]
}

func (p *streamParser) parse(ctx context.Context, stdout io.ReadCloser) error {
	doneParsing := make(chan struct{})
	defer close(doneParsing)
	go func() {
		select {
		case <-ctx.Done():
			stdout.Close()
		case <-doneParsing:
		}
	}()

	scanner := bufio.NewScanner(stdout)
	scanner.Buffer(make([]byte, 0, 64*1024), 10*1024*1024)

	startTime := time.Now()
	lineCount := 0
	lastHeartbeat := time.Now()
	lastEventType := ""

	log.Trace("[claude-parse] stream started")

	for scanner.Scan() {
		line := scanner.Text()
		if line == "" {
			continue
		}
		lineCount++

		if time.Since(lastHeartbeat) > 30*time.Second {
			builderLen := 0
			if t := p.activeTool(); t != nil {
				builderLen = t.inputJSON.Len()
			}
			log.Trace("[claude-parse] heartbeat: lines=%d elapsed=%v lastEvent=%s toolBuilderLen=%d",
				lineCount, time.Since(startTime).Round(time.Second), lastEventType, builderLen)
			lastHeartbeat = time.Now()
		}

		var msg map[string]any
		if err := json.Unmarshal([]byte(line), &msg); err != nil {
			if len(line) > 200 {
				log.Trace("[claude-parse] JSON unmarshal error: %v (line len=%d, prefix=%q)", err, len(line), line[:200])
			} else {
				log.Trace("[claude-parse] JSON unmarshal error: %v (line=%q)", err, line)
			}
			continue
		}

		msgType, _ := msg["type"].(string)
		lastEventType = msgType
		var stopped bool

		switch msgType {
		case "system":
			stopped = p.handleSystem(msg)
		case "stream_event":
			stopped = p.handleStreamEvent(msg)
		case "assistant":
			stopped = p.handleAssistant(msg)
		case "user":
			stopped = p.handleUser(msg)
		case "result":
			log.Trace("[claude-parse] stream ended: lines=%d elapsed=%v completed=true", lineCount, time.Since(startTime).Round(time.Second))
			return p.handleResult(msg)
		case "error":
			log.Trace("[claude-parse] stream ended with error: lines=%d elapsed=%v", lineCount, time.Since(startTime).Round(time.Second))
			return p.handleError(msg)
		}

		if stopped {
			log.Trace("[claude-parse] stream stopped by handler: lines=%d elapsed=%v", lineCount, time.Since(startTime).Round(time.Second))
			return nil
		}
	}

	log.Trace("[claude-parse] stream ended: lines=%d elapsed=%v completed=%v scanErr=%v",
		lineCount, time.Since(startTime).Round(time.Second), p.completed, scanner.Err())

	if err := scanner.Err(); err != nil {
		log.Trace("[claude-parse] scanner error: %v (ctx.Err=%v)", err, ctx.Err())
		if ctx.Err() != nil {
			return ctx.Err()
		}
		return err
	}
	return nil
}

// --- Message lifecycle helpers ---

func (p *streamParser) beginMessageWithID(id, msgType string) (stopped bool) {
	startData := message.EventMessageStartData{
		MessageID: id,
		Type:      msgType,
		Timestamp: time.Now().UnixMilli(),
	}
	sd, _ := json.Marshal(startData)
	return p.handler != nil && p.handler(message.ChunkMessageStart, sd) != 0
}

func (p *streamParser) beginMessage(msgType string) (messageID string, stopped bool) {
	id := fmt.Sprintf("sandbox-%s-%s", msgType, message.GenerateNanoID())
	return id, p.beginMessageWithID(id, msgType)
}

func (p *streamParser) endMessage() {
	if p.handler != nil {
		p.handler(message.ChunkMessageEnd, nil)
	}
}

func (p *streamParser) closeTextMessage() {
	if p.textActive {
		p.endMessage()
		p.textActive = false
	}
}

// closeStreamingTool closes the currently streaming tool's message group,
// flushing accumulated input and emitting message_end. The tool remains
// in p.tools so handleUser can later reuse its msgID for the completed phase.
func (p *streamParser) closeStreamingTool() {
	t := p.activeTool()
	if t == nil {
		return
	}
	inputStr := t.inputJSON.String()
	if inputStr != "" {
		p.toolInputs[t.id] = inputStr
		summary := extractSummary(t.name, inputStr)
		if summary != "" {
			p.toolSummaries[t.id] = summary
			p.emitExecute(map[string]any{
				"summary": summary,
			})
		}
	}
	p.endMessage()
	p.activeToolID = ""
}

// suspendStreamingTool temporarily closes the active tool's message group
// (emits message_end) so another message group can be opened. The tool
// remains in p.tools and p.activeToolID is cleared. Call resumeStreamingTool
// to reopen it.
func (p *streamParser) suspendStreamingTool() {
	t := p.activeTool()
	if t == nil {
		return
	}
	p.endMessage()
	p.activeToolID = ""
}

// resumeStreamingTool reopens a previously suspended tool's message group
// by emitting a new message_start with the same msgID, and restores it as
// the active streaming tool.
func (p *streamParser) resumeStreamingTool(toolID string) {
	t, ok := p.tools[toolID]
	if !ok {
		return
	}
	p.beginMessageWithID(t.msgID, "execute")
	p.activeToolID = toolID
}

func (p *streamParser) ensureTextMessage() (stopped bool) {
	if !p.textActive {
		_, stopped = p.beginMessage("text")
		if stopped {
			return true
		}
		p.textActive = true
	}
	return false
}

func (p *streamParser) emitText(text string) (stopped bool) {
	text = strings.ReplaceAll(text, "\r\n", "\n")
	text = strings.ReplaceAll(text, "\r", "\n")
	return p.handler != nil && p.handler(message.ChunkText, []byte(text)) != 0
}

func (p *streamParser) emitExecute(props map[string]any) (stopped bool) {
	data, _ := json.Marshal(props)
	return p.handler != nil && p.handler(message.ChunkExecute, data) != 0
}

func (p *streamParser) emitMetadata(data map[string]any) {
	if p.handler == nil {
		return
	}
	encoded, _ := json.Marshal(data)
	p.handler(message.ChunkMetadata, encoded)
}

// extractSummary builds a short human-readable summary from the tool input JSON.
func extractSummary(toolName string, inputJSON string) string {
	if inputJSON == "" {
		return ""
	}
	var obj map[string]any
	if err := json.Unmarshal([]byte(inputJSON), &obj); err != nil {
		return ""
	}

	switch strings.ToLower(toolName) {
	case "bash", "execute":
		if cmd, ok := obj["command"].(string); ok {
			return truncate(cmd, 80)
		}
	case "write", "create":
		if fp, ok := obj["file_path"].(string); ok {
			return fp
		}
	case "read":
		if fp, ok := obj["file_path"].(string); ok {
			return fp
		}
	case "edit":
		if fp, ok := obj["file_path"].(string); ok {
			return fp
		}
	}

	for _, key := range []string{"path", "file_path", "command", "url", "query"} {
		if v, ok := obj[key].(string); ok {
			return truncate(v, 80)
		}
	}
	return ""
}

func truncate(s string, max int) string {
	s = strings.TrimSpace(s)
	s = strings.ReplaceAll(s, "\n", " ")
	if len(s) > max {
		return s[:max] + "..."
	}
	return s
}

// --- Event handlers ---

func (p *streamParser) handleSystem(msg map[string]any) (stopped bool) {
	if p.handler != nil {
		data, _ := json.Marshal(msg)
		return p.handler(message.ChunkMetadata, data) != 0
	}
	return false
}

func (p *streamParser) handleStreamEvent(msg map[string]any) (stopped bool) {
	event, _ := msg["event"].(map[string]any)
	if event == nil {
		return false
	}
	eventType, _ := event["type"].(string)

	switch eventType {
	case "content_block_start":
		return p.onContentBlockStart(event)
	case "content_block_delta":
		return p.onContentBlockDelta(event)
	case "content_block_stop":
		return p.onContentBlockStop()
	}
	return false
}

func (p *streamParser) onContentBlockStart(event map[string]any) (stopped bool) {
	cb, ok := event["content_block"].(map[string]any)
	if !ok {
		return false
	}
	blockType, _ := cb["type"].(string)
	if blockType != "tool_use" {
		return false
	}

	p.closeTextMessage()

	toolName, _ := cb["name"].(string)
	toolID, _ := cb["id"].(string)
	if toolID == "" {
		toolID = fmt.Sprintf("tool_%d_%d", p.toolIndex, time.Now().UnixNano())
	}

	msgID, stopped := p.beginMessage("execute")
	if stopped {
		return true
	}

	ts := &toolState{id: toolID, name: toolName, msgID: msgID, index: p.toolIndex}
	p.tools[toolID] = ts
	p.activeToolID = toolID
	p.toolIndex++
	p.toolNames[toolID] = toolName
	p.toolMsgIDs[toolID] = msgID

	if p.handler == nil {
		return false
	}

	return p.emitExecute(map[string]any{
		"tool":    toolName,
		"tool_id": toolID,
		"status":  "running",
		"runner":  "claude-cli",
	})
}

func (p *streamParser) onContentBlockStop() (stopped bool) {
	p.closeStreamingTool()
	return false
}

func (p *streamParser) onContentBlockDelta(event map[string]any) (stopped bool) {
	delta, ok := event["delta"].(map[string]any)
	if !ok {
		return false
	}

	deltaType, _ := delta["type"].(string)

	switch deltaType {
	case "text_delta":
		text, _ := delta["text"].(string)
		if text == "" {
			return false
		}
		if !p.textActive && strings.TrimSpace(text) == "" {
			return false
		}
		if p.ensureTextMessage() {
			return true
		}
		return p.emitText(text)

	case "input_json_delta":
		t := p.activeTool()
		if t == nil {
			return false
		}
		partial, _ := delta["partial_json"].(string)
		if partial == "" {
			return false
		}
		t.inputJSON.WriteString(partial)
		builderLen := t.inputJSON.Len()
		if builderLen > 0 && builderLen%100000 < len(partial) {
			log.Trace("[claude-parse] WARN: tool %s inputJSON growing: %d bytes", t.name, builderLen)
		}
		if p.handler != nil {
			return p.emitExecute(map[string]any{
				"input_delta": t.inputJSON.String(),
			})
		}
	}
	return false
}

func (p *streamParser) handleAssistant(msg map[string]any) (stopped bool) {
	msgData, _ := msg["message"].(map[string]any)
	if msgData == nil {
		return false
	}

	if usage, ok := msgData["usage"].(map[string]any); ok {
		p.emitMetadata(map[string]any{
			"usage": usage,
		})
	}

	stopReason, _ := msgData["stop_reason"].(string)
	if stopReason == "" {
		return false
	}

	contentArr, _ := msgData["content"].([]any)
	for _, item := range contentArr {
		ci, ok := item.(map[string]any)
		if !ok {
			continue
		}
		itemType, _ := ci["type"].(string)

		if itemType == "tool_use" && p.handler != nil {
			toolID, _ := ci["id"].(string)

			if _, alreadyStreamed := p.toolNames[toolID]; alreadyStreamed && toolID != "" {
				continue
			}

			p.closeTextMessage()
			p.closeStreamingTool()

			toolName, _ := ci["name"].(string)
			if toolID == "" {
				toolID = fmt.Sprintf("tool_%d_%d", p.toolIndex, time.Now().UnixNano())
			}

			msgID, stopped := p.beginMessage("execute")
			if stopped {
				return true
			}

			p.toolIndex++
			p.toolNames[toolID] = toolName
			p.toolMsgIDs[toolID] = msgID

			inputRaw, _ := json.Marshal(ci["input"])
			inputStr := string(inputRaw)
			p.toolInputs[toolID] = inputStr
			summary := extractSummary(toolName, inputStr)
			p.toolSummaries[toolID] = summary

			if p.emitExecute(map[string]any{
				"tool":    toolName,
				"tool_id": toolID,
				"input":   json.RawMessage(inputRaw),
				"summary": summary,
				"status":  "running",
				"runner":  "claude-cli",
			}) {
				p.endMessage()
				return true
			}
			p.endMessage()
		}

		if itemType == "text" {
			text, _ := ci["text"].(string)
			if text == "" || p.handler == nil {
				continue
			}
			if p.ensureTextMessage() {
				return true
			}
			if p.emitText(text) {
				return true
			}
		}
	}

	p.closeTextMessage()
	return false
}

func (p *streamParser) handleUser(msg map[string]any) (stopped bool) {
	msgData, _ := msg["message"].(map[string]any)
	if msgData == nil {
		return false
	}

	contentArr, _ := msgData["content"].([]interface{})
	for _, item := range contentArr {
		ci, ok := item.(map[string]any)
		if !ok {
			continue
		}

		ciType, _ := ci["type"].(string)
		if ciType != "tool_result" {
			continue
		}

		toolUseID, _ := ci["tool_use_id"].(string)

		// If the result belongs to the actively streaming tool, close its
		// streaming phase (emits message_end for the running group).
		if p.activeToolID == toolUseID {
			p.closeStreamingTool()
		}

		// If a DIFFERENT tool is currently streaming, we must suspend its
		// message group before opening the completed-result group, because
		// the downstream handler only tracks one currentGroupID at a time.
		suspendedToolID := ""
		if p.activeToolID != "" && p.activeToolID != toolUseID {
			suspendedToolID = p.activeToolID
			p.suspendStreamingTool()
		}

		p.closeTextMessage()

		content := ci["content"]
		isError, _ := ci["is_error"].(bool)

		status := "completed"
		if isError {
			status = "error"
		}

		execProps := map[string]any{
			"tool_id":  toolUseID,
			"output":   content,
			"status":   status,
			"is_error": isError,
		}
		if name, ok := p.toolNames[toolUseID]; ok {
			execProps["tool"] = name
		}
		if input, ok := p.toolInputs[toolUseID]; ok {
			execProps["input"] = json.RawMessage(input)
		}
		if summary, ok := p.toolSummaries[toolUseID]; ok {
			execProps["summary"] = summary
		}

		if reuseMsgID, ok := p.toolMsgIDs[toolUseID]; ok {
			if p.beginMessageWithID(reuseMsgID, "execute") {
				return true
			}
		} else {
			if _, stopped := p.beginMessage("execute"); stopped {
				return true
			}
		}

		if p.emitExecute(execProps) {
			p.endMessage()
			return true
		}
		p.endMessage()

		delete(p.tools, toolUseID)

		// Resume the suspended tool's message group so subsequent
		// content_block_delta events land in the correct group.
		if suspendedToolID != "" {
			p.resumeStreamingTool(suspendedToolID)
		}
	}
	return false
}

func (p *streamParser) handleResult(msg map[string]any) error {
	isError, _ := msg["is_error"].(bool)
	if isError {
		if result, ok := msg["result"].(string); ok {
			if p.handler != nil {
				p.handler(message.ChunkError, []byte(result))
			}
			return fmt.Errorf("Claude CLI error: %s", result)
		}
	}

	p.closeTextMessage()

	if p.handler != nil {
		p.emitMetadata(map[string]any{
			"result_summary": map[string]any{
				"total_cost_usd": msg["total_cost_usd"],
				"duration_ms":    msg["duration_ms"],
				"num_turns":      msg["num_turns"],
				"usage":          msg["usage"],
			},
		})
	}

	p.completed = true
	return nil
}

func (p *streamParser) handleError(msg map[string]any) error {
	var errMsg string
	switch e := msg["error"].(type) {
	case string:
		errMsg = e
	case map[string]any:
		errMsg, _ = e["message"].(string)
	}
	if errMsg != "" {
		if p.handler != nil {
			p.handler(message.ChunkError, []byte(errMsg))
		}
		return fmt.Errorf("Claude CLI error: %s", errMsg)
	}
	return nil
}
