package daemon

import (
	"regexp"
	"strings"
	"unicode/utf8"
)

var (
	whitespaceRegex = regexp.MustCompile(`\s+`)
	assignStrRegex  = regexp.MustCompile(`^([a-zA-Z_]\w*)\s*=\s*[rR]?['"]([^'"]*)['"]`)
	assignPathRegex = regexp.MustCompile(`^([a-zA-Z_]\w*)\s*=\s*(?:pathlib\.)?Path\(\s*[rR]?['"]([^'"]*)['"]\s*\)`)
	pathMethodRegex = regexp.MustCompile(`(?:(?:pathlib\.)?Path\(\s*[rR]?['"]([^'"]*)['"]\s*\)|([a-zA-Z_]\w*))\.(write_text|write_bytes|read_text|read_bytes|open)\(\s*([^)]*)\)`)
	funcCallRegex   = regexp.MustCompile(`\b(edit|read|open)\(\s*([^)]*)\)`)
	// run(program, *args) and rem.run(...), but not cells.run(id)
	runCallRegex    = regexp.MustCompile(`(?:^|[^.\w]|\brem\.)run\(([^)]*)`)
	runWordRegex    = regexp.MustCompile(`^\s*[rR]?['"]([^'"]*)['"]\s*(?:,|$)`)
	argExtractRegex = regexp.MustCompile(`(?:(?:path|file)\s*=\s*)?(?:[rR]?['"]([^'"]*)['"]|([a-zA-Z_]\w*))`)
	modeRegex       = regexp.MustCompile(`['"]([rwaxbt+]+)['"]`)
)

func cleanLabel(s string) string {
	s = sanitizeControlRunes(s)
	s = whitespaceRegex.ReplaceAllString(s, " ")
	s = strings.TrimSpace(s)
	if len(s) > 300 {
		return s[:300]
	}
	return s
}

func ParsePythonIntent(code string) *ToolIntent {
	bindings := make(map[string]string)
	var intents []ToolIntent

	for rawLine := range strings.SplitSeq(code, "\n") {
		line := strings.TrimSpace(rawLine)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		if m := assignStrRegex.FindStringSubmatch(line); m != nil {
			bindings[m[1]] = m[2]
			continue
		}

		if m := assignPathRegex.FindStringSubmatch(line); m != nil {
			bindings[m[1]] = m[2]
			continue
		}

		if m := pathMethodRegex.FindStringSubmatch(line); m != nil {
			pathLiteral := m[1]
			pathVariable := m[2]
			method := m[3]
			args := m[4]

			target := pathLiteral
			if target == "" && pathVariable != "" {
				target = bindings[pathVariable]
			}

			if target != "" {
				label := cleanLabel(target)
				switch method {
				case "write_text", "write_bytes":
					intents = append(intents, ToolIntent{Kind: "write", Target: label})
				case "read_text", "read_bytes":
					intents = append(intents, ToolIntent{Kind: "read", Target: label})
				case "open":
					intents = append(intents, ToolIntent{Kind: openIntentKind(args), Target: label})
				}
				continue
			}
		}

		if m := runCallRegex.FindStringSubmatch(line); m != nil {
			if words := runWords(m[1]); len(words) > 0 {
				intents = append(intents, ToolIntent{Kind: "run", Target: cleanLabel(strings.Join(words, " "))})
				continue
			}
		}

		if m := funcCallRegex.FindStringSubmatch(line); m != nil {
			functionName := m[1]
			args := m[2]

			if am := argExtractRegex.FindStringSubmatch(args); am != nil {
				literalValue := am[1]
				variableName := am[2]
				target := literalValue
				if target == "" && variableName != "" {
					target = bindings[variableName]
				}

				if target != "" {
					label := cleanLabel(target)
					switch functionName {
					case "edit":
						intents = append(intents, ToolIntent{Kind: "edit", Target: label})
					case "read":
						intents = append(intents, ToolIntent{Kind: "read", Target: label})
					case "open":
						intents = append(intents, ToolIntent{Kind: openIntentKind(args), Target: label})
					}
				}
			}
		}
	}

	if len(intents) == 0 {
		return nil
	}
	return &intents[len(intents)-1]
}

func openIntentKind(args string) string {
	mode := "r"
	if match := modeRegex.FindStringSubmatch(args); match != nil {
		mode = match[1]
	}
	if strings.ContainsAny(mode, "wax+") {
		return "write"
	}
	return "read"
}

// runWords is the leading string literals of run()'s arguments: the program
// and its arguments, up to the first expression or keyword.
func runWords(args string) []string {
	var words []string
	for {
		m := runWordRegex.FindStringSubmatchIndex(args)
		if m == nil {
			return words
		}
		words = append(words, args[m[2]:m[3]])
		args = args[m[1]:]
	}
}

func ExtractPartialJSONCode(s string) string {
	codeKeyIndex := strings.Index(s, `"code"`)
	if codeKeyIndex == -1 {
		return ""
	}
	colon := strings.Index(s[codeKeyIndex+6:], ":")
	if colon == -1 {
		return ""
	}
	colon += codeKeyIndex + 6

	quote := strings.Index(s[colon+1:], `"`)
	if quote == -1 {
		return ""
	}
	start := colon + 1 + quote + 1

	var decodedCode strings.Builder
	var decoder JSONStringDecoder
	decoder.Append(s[start:], func(text string) { decodedCode.WriteString(text) })
	return decodedCode.String()
}

type ToolCallAssembly struct {
	ID       string
	Function struct {
		Name      string
		Arguments string
	}
}

type ToolProgressReporter struct {
	publish  func(*ToolProgress) error
	previous *ToolProgress
	raw      string
	code     string
}

func NewToolProgressReporter(publish func(*ToolProgress) error) *ToolProgressReporter {
	return &ToolProgressReporter{
		publish: publish,
	}
}

func (r *ToolProgressReporter) Reset() {
	r.raw = ""
	r.code = ""
}

func (r *ToolProgressReporter) Report(call *ToolCallAssembly, phase string) error {
	if call == nil {
		if r.previous != nil {
			err := r.publish(nil)
			r.previous = nil
			r.Reset()
			return err
		}
		r.Reset()
		return nil
	}

	name := cleanLabel(call.Function.Name)
	if len(name) > 100 {
		name = name[:100]
	}
	if name == "" {
		return nil
	}

	callID := call.ID
	if len(callID) > 200 {
		callID = callID[:200]
	}

	args := call.Function.Arguments
	sameCall := r.previous != nil && r.previous.CallID == callID && r.previous.Name == name
	continued := sameCall && strings.HasPrefix(args, r.raw)

	if !continued {
		r.Reset()
	}

	if name == "python" && len(args) > len(r.raw) {
		r.code = ExtractPartialJSONCode(args)
		r.raw = args
	}

	var intent *ToolIntent
	if continued && r.previous != nil {
		intent = r.previous.Intent
	}
	if parsed := ParsePythonIntent(r.code); parsed != nil {
		intent = parsed
	}

	var preview *ToolCodePreview
	if phase == "generating" && len(r.code) > 0 {
		runeCount := utf8.RuneCountInString(r.code)
		offset := max(0, runeCount-512)
		start := 0
		if offset > 0 {
			start = len(r.code)
			for range 512 {
				_, width := utf8.DecodeLastRuneInString(r.code[:start])
				start -= width
			}
		}
		text := sanitizeControlRunes(r.code[start:])
		preview = &ToolCodePreview{
			Offset: offset,
			Text:   text,
		}
	}

	next := &ToolProgress{
		CallID: callID,
		Name:   name,
		Phase:  phase,
		Intent: intent,
		Code:   preview,
	}

	if !toolProgressEqual(next, r.previous) {
		if err := r.publish(next); err != nil {
			return err
		}
		r.previous = next
	}
	return nil
}

func toolProgressEqual(a, b *ToolProgress) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	if a.CallID != b.CallID || a.Name != b.Name || a.Phase != b.Phase {
		return false
	}
	if (a.Intent == nil) != (b.Intent == nil) {
		return false
	}
	if a.Intent != nil && *a.Intent != *b.Intent {
		return false
	}
	if (a.Code == nil) != (b.Code == nil) {
		return false
	}
	if a.Code != nil && *a.Code != *b.Code {
		return false
	}
	return true
}
