package parsers

import (
	"encoding/json"
	"fmt"
	"strings"
	"unicode"

	"github.com/ollama/ollama/api"
)

type ministralParserState int

const (
	ministralCollectingContent = iota
	ministralCollectingThinkingContent
	ministralCollectingToolName
	ministralCollectingToolArgs
)

// ministralEvent represents an event emitted during parsing
type ministralEvent interface {
	isMinistralEvent()
}

type ministralEventContent struct {
	content string
}

type ministralEventThinking struct {
	thinking string
}

type ministralEventToolCall struct {
	name string
	args string // raw JSON string
}

func (ministralEventContent) isMinistralEvent()  {}
func (ministralEventThinking) isMinistralEvent() {}
func (ministralEventToolCall) isMinistralEvent() {}

type MinistralParser struct {
	state              ministralParserState
	buffer             strings.Builder
	tools              []api.Tool
	callIndex          int
	hasThinkingSupport bool
	pendingToolName    string // stores tool name while collecting args
}

func (p *MinistralParser) HasToolSupport() bool {
	return true
}

func (p *MinistralParser) HasThinkingSupport() bool {
	return p.hasThinkingSupport
}

func (p *MinistralParser) setInitialState(lastMessage *api.Message) {
	prefill := lastMessage != nil && lastMessage.Role == "assistant"
	if !p.HasThinkingSupport() {
		p.state = ministralCollectingContent
		return
	}

	if prefill && lastMessage.Content != "" {
		p.state = ministralCollectingContent
		return
	}

	p.state = ministralCollectingThinkingContent
}

func (p *MinistralParser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
	p.tools = tools
	p.callIndex = 0
	p.setInitialState(lastMessage)
	return tools
}

func toolByName(tools []api.Tool, n string) (*api.Tool, error) {
	for i := range tools {
		if tools[i].Function.Name == n {
			return &tools[i], nil
		}
	}
	return nil, fmt.Errorf("tool '%s' not found", n)
}

const (
	ministralToolCallsTag = "[TOOL_CALLS]"
	ministralThinkTag     = "[THINK]"
	ministralThinkEndTag  = "[/THINK]"
	ministralArgsTag      = "[ARGS]"
)

// eat consumes the parser's buffer, and returns a list of any unambiguous
// events from the current parser state. The second return value indicates
// whether to keep looping (true when state transitions, false when waiting
// for more data).
func (p *MinistralParser) eat() ([]ministralEvent, bool) {
	var events []ministralEvent

	switch p.state {
	case ministralCollectingContent:
		bufStr := p.buffer.String()

		// Check for [TOOL_CALLS] tag
		if strings.Contains(bufStr, ministralToolCallsTag) {
			split := strings.SplitN(bufStr, ministralToolCallsTag, 2)
			before := strings.TrimRightFunc(split[0], unicode.IsSpace)
			if len(before) > 0 {
				events = append(events, ministralEventContent{content: before})
			}
			after := split[1]
			p.buffer.Reset()
			p.buffer.WriteString(after)
			p.state = ministralCollectingToolName
			return events, true
		}

		// Check for [THINK] tag
		if strings.Contains(bufStr, ministralThinkTag) {
			split := strings.SplitN(bufStr, ministralThinkTag, 2)
			before := strings.TrimRightFunc(split[0], unicode.IsSpace)
			if len(before) > 0 {
				events = append(events, ministralEventContent{content: before})
			}
			after := split[1]
			p.buffer.Reset()
			p.buffer.WriteString(after)
			p.state = ministralCollectingThinkingContent
			return events, true
		}

		// Check for partial tag overlap with [TOOL_CALLS] or [THINK]
		overlapToolCalls := overlap(bufStr, ministralToolCallsTag)
		overlapThink := overlap(bufStr, ministralThinkTag)
		maxOverlap := max(overlapToolCalls, overlapThink)

		if maxOverlap > 0 {
			// Withhold the potential partial tag
			beforePartialTag := bufStr[:len(bufStr)-maxOverlap]
			trailingWS := trailingWhitespaceLen(beforePartialTag)
			ambiguousStart := len(beforePartialTag) - trailingWS
			unambiguous := bufStr[:ambiguousStart]
			ambiguous := bufStr[ambiguousStart:]
			p.buffer.Reset()
			p.buffer.WriteString(ambiguous)
			if len(unambiguous) > 0 {
				events = append(events, ministralEventContent{content: unambiguous})
			}
			return events, false
		}

		// No tag found: emit content but withhold trailing whitespace
		whitespaceLen := trailingWhitespaceLen(bufStr)
		ambiguousStart := len(bufStr) - whitespaceLen
		unambiguous := bufStr[:ambiguousStart]
		ambiguous := bufStr[ambiguousStart:]
		p.buffer.Reset()
		p.buffer.WriteString(ambiguous)
		if len(unambiguous) > 0 {
			events = append(events, ministralEventContent{content: unambiguous})
		}
		return events, false

	case ministralCollectingThinkingContent:
		bufStr := p.buffer.String()

		if strings.Contains(bufStr, ministralThinkEndTag) {
			split := strings.SplitN(bufStr, ministralThinkEndTag, 2)
			thinkingContent := split[0]
			after := strings.TrimLeftFunc(split[1], unicode.IsSpace)
			p.buffer.Reset()
			p.buffer.WriteString(after)
			if len(thinkingContent) > 0 {
				events = append(events, ministralEventThinking{thinking: thinkingContent})
			}
			p.state = ministralCollectingContent
			return events, true
		}

		// Check for partial overlap with [/THINK]
		if overlapLen := overlap(bufStr, ministralThinkEndTag); overlapLen > 0 {
			unambiguous := bufStr[:len(bufStr)-overlapLen]
			ambiguous := bufStr[len(bufStr)-overlapLen:]
			p.buffer.Reset()
			p.buffer.WriteString(ambiguous)
			if len(unambiguous) > 0 {
				events = append(events, ministralEventThinking{thinking: unambiguous})
			}
			return events, false
		}

		// No tag found: emit all thinking content
		p.buffer.Reset()
		if len(bufStr) > 0 {
			events = append(events, ministralEventThinking{thinking: bufStr})
		}
		return events, false

	case ministralCollectingToolName:
		bufStr := p.buffer.String()

		if strings.Contains(bufStr, ministralArgsTag) {
			split := strings.SplitN(bufStr, ministralArgsTag, 2)
			toolName := split[0]
			after := split[1]
			p.pendingToolName = toolName
			p.buffer.Reset()
			p.buffer.WriteString(after)
			p.state = ministralCollectingToolArgs
			return events, true
		}
		// Wait for more data
		return events, false

	case ministralCollectingToolArgs:
		bufStr := p.buffer.String()
		jsonEnd := findJSONEnd(bufStr)

		if jsonEnd != -1 {
			jsonStr := bufStr[:jsonEnd+1]
			remaining := bufStr[jsonEnd+1:]

			events = append(events, ministralEventToolCall{
				name: p.pendingToolName,
				args: jsonStr,
			})

			p.pendingToolName = ""
			p.buffer.Reset()
			p.buffer.WriteString(remaining)
			p.state = ministralCollectingContent
			return events, true
		}
		// Wait for more data
		return events, false

	default:
		panic("unexpected ministral event")
	}
}

// parseEvents loops calling eat() until it returns false
func (p *MinistralParser) parseEvents() []ministralEvent {
	var all []ministralEvent
	keepLooping := true
	for keepLooping {
		var events []ministralEvent
		events, keepLooping = p.eat()
		all = append(all, events...)
	}
	return all
}

func (p *MinistralParser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
	p.buffer.WriteString(s)

	events := p.parseEvents()

	var contentBuilder, thinkingBuilder strings.Builder
	var toolCalls []api.ToolCall

	for _, event := range events {
		switch e := event.(type) {
		case ministralEventContent:
			contentBuilder.WriteString(e.content)
		case ministralEventThinking:
			thinkingBuilder.WriteString(e.thinking)
		case ministralEventToolCall:
			// Validate tool exists
			tool, toolErr := toolByName(p.tools, e.name)
			if toolErr != nil {
				return contentBuilder.String(), thinkingBuilder.String(), toolCalls, toolErr
			}
			// Parse JSON arguments
			var args api.ToolCallFunctionArguments
			if jsonErr := json.Unmarshal([]byte(e.args), &args); jsonErr != nil {
				return contentBuilder.String(), thinkingBuilder.String(), toolCalls, jsonErr
			}
			toolCalls = append(toolCalls, api.ToolCall{
				Function: api.ToolCallFunction{
					Name:      tool.Function.Name,
					Arguments: args,
				},
			})
		}
	}

	for i := range toolCalls {
		toolCalls[i].Function.Index = p.callIndex
		p.callIndex++
	}

	return contentBuilder.String(), thinkingBuilder.String(), toolCalls, nil
}

// findJSONEnd finds the index of the closing brace that completes a JSON object.
// It properly handles nested objects, arrays, and strings (including escaped characters).
// Returns -1 if the JSON is not yet complete.
func findJSONEnd(s string) int {
	depth := 0
	inString := false
	escaped := false

	for i, r := range s {
		if inString {
			switch {
			case escaped:
				// If the previous character was a backslash, skip this character
				escaped = false
			case r == '\\':
				// Mark the next character as escaped
				escaped = true
			case r == '"':
				// End of string literal
				inString = false
			}
			continue
		}

		switch r {
		case '"':
			// Start of string literal
			inString = true
		case '{', '[':
			// Increase nesting level for objects and arrays
			depth++
		case '}', ']':
			// Decrease nesting level
			depth--
			if depth == 0 {
				// Reached the end of the root JSON structure
				return i
			}
		}
	}

	return -1
}
