package recordmutationv1

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"sort"
	"unicode/utf8"
)

// CanonicalSourceJSON converts one caller-authored JSON object or array into
// the record-mutation v1 canonical JSON grammar. The grammar is defined here
// rather than delegated to a language's default JSON encoder:
//
//   - decoded object keys sort by ascending UTF-8 bytes;
//   - a duplicate decoded key keeps its last value;
//   - array order and exact number lexical text remain semantic;
//   - strings use UTF-8 directly except for JSON syntax, backslash, and U+0000
//     through U+001F; short control escapes are preferred and other controls
//     use lowercase \u00xx;
//   - HTML characters, U+2028, and U+2029 are not escaped;
//   - invalid UTF-8 and unpaired UTF-16 surrogate escapes are rejected.
//
// This output is no larger than the accepted input: whitespace, duplicate
// members, and non-minimal escapes can only be removed or shortened. That
// property keeps the semantic and encoded-command envelope proof independent
// of a Go-specific encoder. Scalar JSON values use the String, Boolean, Number,
// or Null FieldValue arms instead.
func CanonicalSourceJSON(raw string) (string, error) {
	if !utf8.ValidString(raw) {
		return "", errors.New("record mutation: source JSON must be valid UTF-8")
	}
	if err := validateSourceJSONSurrogates(raw); err != nil {
		return "", err
	}
	decoder := json.NewDecoder(bytes.NewBufferString(raw))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return "", err
	}
	if err := requireSourceJSONEOF(decoder); err != nil {
		return "", err
	}
	switch value.(type) {
	case map[string]any, []any:
	default:
		return "", errors.New("record mutation: source JSON must be an object or array")
	}
	var canonical bytes.Buffer
	if err := writeCanonicalSourceJSON(&canonical, value); err != nil {
		return "", err
	}
	if canonical.Len() > len(raw) {
		return "", errors.New("record mutation: canonical source JSON expanded unexpectedly")
	}
	return canonical.String(), nil
}

func writeCanonicalSourceJSON(out *bytes.Buffer, value any) error {
	switch value := value.(type) {
	case nil:
		out.WriteString("null")
	case bool:
		if value {
			out.WriteString("true")
		} else {
			out.WriteString("false")
		}
	case string:
		writeCanonicalJSONString(out, value)
	case json.Number:
		if !jsonNumberPattern.MatchString(string(value)) {
			return errors.New("record mutation: invalid JSON number")
		}
		out.WriteString(string(value))
	case []any:
		out.WriteByte('[')
		for i, item := range value {
			if i != 0 {
				out.WriteByte(',')
			}
			if err := writeCanonicalSourceJSON(out, item); err != nil {
				return err
			}
		}
		out.WriteByte(']')
	case map[string]any:
		keys := make([]string, 0, len(value))
		for key := range value {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		out.WriteByte('{')
		for i, key := range keys {
			if i != 0 {
				out.WriteByte(',')
			}
			writeCanonicalJSONString(out, key)
			out.WriteByte(':')
			if err := writeCanonicalSourceJSON(out, value[key]); err != nil {
				return err
			}
		}
		out.WriteByte('}')
	default:
		return fmt.Errorf("record mutation: unsupported decoded JSON value %T", value)
	}
	return nil
}

func writeCanonicalJSONString(out *bytes.Buffer, value string) {
	const hex = "0123456789abcdef"
	out.WriteByte('"')
	for _, r := range value {
		switch r {
		case '"', '\\':
			out.WriteByte('\\')
			out.WriteRune(r)
		case '\b':
			out.WriteString(`\b`)
		case '\f':
			out.WriteString(`\f`)
		case '\n':
			out.WriteString(`\n`)
		case '\r':
			out.WriteString(`\r`)
		case '\t':
			out.WriteString(`\t`)
		default:
			if r < 0x20 {
				out.WriteString(`\u00`)
				out.WriteByte(hex[byte(r)>>4])
				out.WriteByte(hex[byte(r)&0x0f])
			} else {
				out.WriteRune(r)
			}
		}
	}
	out.WriteByte('"')
}

func validateSourceJSONSurrogates(raw string) error {
	for i := 0; i < len(raw); i++ {
		if raw[i] != '"' {
			continue
		}
		for i++; i < len(raw); i++ {
			switch raw[i] {
			case '"':
				goto nextToken
			case '\\':
				if i+1 >= len(raw) {
					return errors.New("record mutation: unterminated JSON escape")
				}
				i++
				if raw[i] != 'u' {
					continue
				}
				code, ok := sourceJSONHexCodeUnit(raw, i+1)
				if !ok {
					continue // encoding/json reports the exact syntax error.
				}
				i += 4
				if code >= 0xdc00 && code <= 0xdfff {
					return errors.New("record mutation: unpaired low surrogate escape")
				}
				if code < 0xd800 || code > 0xdbff {
					continue
				}
				if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' {
					return errors.New("record mutation: unpaired high surrogate escape")
				}
				low, ok := sourceJSONHexCodeUnit(raw, i+3)
				if !ok || low < 0xdc00 || low > 0xdfff {
					return errors.New("record mutation: unpaired high surrogate escape")
				}
				i += 6
			}
		}
		return errors.New("record mutation: unterminated JSON string")
	nextToken:
	}
	return nil
}

func sourceJSONHexCodeUnit(raw string, start int) (uint16, bool) {
	if start+4 > len(raw) {
		return 0, false
	}
	var result uint16
	for i := start; i < start+4; i++ {
		result <<= 4
		switch c := raw[i]; {
		case c >= '0' && c <= '9':
			result |= uint16(c - '0')
		case c >= 'a' && c <= 'f':
			result |= uint16(c-'a') + 10
		case c >= 'A' && c <= 'F':
			result |= uint16(c-'A') + 10
		default:
			return 0, false
		}
	}
	return result, true
}

func requireSourceJSONEOF(decoder *json.Decoder) error {
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		if err == nil {
			return errors.New("record mutation: trailing source JSON value")
		}
		return err
	}
	return nil
}
