// Package llmjson tolerates the small JSON artifacts that LLMs sometimes emit
// (e.g. `...` placeholders, trailing or duplicated commas) so structured
// outputs can still be parsed instead of failing the whole job.
package llmjson

import (
	"encoding/json"
	"strings"
)

// Unmarshal parses payload into v, retrying once on a sanitized copy when the
// raw bytes are not strictly valid JSON. The original error is returned when
// the sanitized retry also fails, so callers keep a meaningful message.
func Unmarshal(payload []byte, v any) error {
	err := json.Unmarshal(payload, v)
	if err == nil {
		return nil
	}
	if cleaned := Sanitize(payload); cleaned != nil {
		if retryErr := json.Unmarshal(cleaned, v); retryErr == nil {
			return nil
		}
	}
	return err
}

// Sanitize removes ellipsis placeholders and normalizes stray commas that sit
// outside of string literals. It returns nil when nothing changed so callers
// can avoid a redundant re-parse. String/URL contents are never altered.
func Sanitize(in []byte) []byte {
	normalized := normalizeCommas(stripEllipsis(string(in)))
	if normalized == string(in) {
		return nil
	}
	return []byte(normalized)
}

func stripEllipsis(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	inString := false
	escaped := false
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if inString {
			b.WriteByte(ch)
			if escaped {
				escaped = false
			} else if ch == '\\' {
				escaped = true
			} else if ch == '"' {
				inString = false
			}
			continue
		}
		if ch == '"' {
			inString = true
			b.WriteByte(ch)
			continue
		}
		if ch == '.' {
			j := i
			for j < len(s) && s[j] == '.' {
				j++
			}
			if j-i >= 2 {
				i = j - 1
				continue
			}
		}
		b.WriteByte(ch)
	}
	return b.String()
}

func normalizeCommas(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	inString := false
	escaped := false
	var lastMeaningful byte
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if inString {
			b.WriteByte(ch)
			if escaped {
				escaped = false
			} else if ch == '\\' {
				escaped = true
			} else if ch == '"' {
				inString = false
			}
			continue
		}
		if ch == '"' {
			inString = true
			b.WriteByte(ch)
			lastMeaningful = ch
			continue
		}
		if ch == ',' {
			if lastMeaningful == '[' || lastMeaningful == '{' || lastMeaningful == ',' || lastMeaningful == 0 {
				continue
			}
			if next := nextNonSpaceByte(s, i+1); next == ']' || next == '}' {
				continue
			}
			b.WriteByte(ch)
			lastMeaningful = ch
			continue
		}
		b.WriteByte(ch)
		if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' {
			lastMeaningful = ch
		}
	}
	return b.String()
}

func nextNonSpaceByte(s string, from int) byte {
	for i := from; i < len(s); i++ {
		switch s[i] {
		case ' ', '\t', '\n', '\r':
			continue
		default:
			return s[i]
		}
	}
	return 0
}
