// Regex Tools Go engine bridge.
//
// The WebAssembly module exposes two fixed callbacks. User-controlled values
// enter only through JSON data and are handed to the standard-library regexp
// package; no Go or JavaScript source is evaluated.
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"regexp"
	"runtime"
	"sort"
	"strings"
	"syscall/js"
	"unicode"
	"unicode/utf8"
)

const (
	bridgeABIVersion          = 1
	expectedGoVersion         = "go1.26.5"
	maximumPatternBytes       = 256 * 1024
	maximumSubjectBytes       = 16 * 1024 * 1024
	maximumReplacementBytes   = 256 * 1024
	maximumMatches            = 10_000
	maximumCaptureRows        = 100_000
	maximumCaptureGroups      = 1_000
	maximumOutputBytes        = 64 * 1024 * 1024
	maximumRequestJSONBytes   = 6*(maximumSubjectBytes+maximumPatternBytes+maximumReplacementBytes) + 64*1024
	maximumErrorMessageBytes  = 1_024
	bridgeInstallerGlobalName = "__regexToolsInstallGoBridge"
)

type bridgeRequest struct {
	ABI                int                        `json:"abi"`
	Operation          string                     `json:"operation"`
	Pattern            string                     `json:"pattern"`
	Flags              string                     `json:"flags"`
	Options            map[string]json.RawMessage `json:"options"`
	Subject            string                     `json:"subject"`
	ScanAll            bool                       `json:"scanAll"`
	MaximumMatches     int                        `json:"maximumMatches"`
	MaximumCaptureRows int                        `json:"maximumCaptureRows"`
	Replacement        *string                    `json:"replacement,omitempty"`
	MaximumOutputBytes *int                       `json:"maximumOutputBytes,omitempty"`
}

type bridgeFailure struct {
	ABI      int    `json:"abi"`
	OK       bool   `json:"ok"`
	Phase    string `json:"phase"`
	Code     string `json:"code"`
	Message  string `json:"message"`
	Position *int   `json:"position,omitempty"`
}

type nativeMatch struct {
	Span     [2]int    `json:"span"`
	Captures []*[2]int `json:"captures"`
}

type bridgeSuccess struct {
	ABI              int           `json:"abi"`
	OK               bool          `json:"ok"`
	GroupCount       int           `json:"groupCount"`
	GroupNames       [][2]any      `json:"groupNames"`
	Matches          []nativeMatch `json:"matches"`
	ResultsTruncated bool          `json:"resultsTruncated"`
	OutputBase64     *string       `json:"outputBase64,omitempty"`
	OutputBytes      *int          `json:"outputBytes,omitempty"`
	OutputTruncated  *bool         `json:"outputTruncated,omitempty"`
}

type identityEnvelope struct {
	ABI      int            `json:"abi"`
	OK       bool           `json:"ok"`
	Identity map[string]any `json:"identity"`
}

func boundedMessage(value string) string {
	if len(value) <= maximumErrorMessageBytes {
		return value
	}
	end := maximumErrorMessageBytes - len("…")
	for end > 0 && !utf8.ValidString(value[:end]) {
		end--
	}
	return value[:end] + "…"
}

func marshal(value any) string {
	encoded, err := json.Marshal(value)
	if err == nil {
		return string(encoded)
	}
	fallback, _ := json.Marshal(bridgeFailure{
		ABI:     bridgeABIVersion,
		OK:      false,
		Phase:   "bridge",
		Code:    "serialization-error",
		Message: "The Go bridge could not serialize its bounded result.",
	})
	return string(fallback)
}

func failure(phase, code string, err any) string {
	return marshal(bridgeFailure{
		ABI:     bridgeABIVersion,
		OK:      false,
		Phase:   phase,
		Code:    code,
		Message: boundedMessage(fmt.Sprint(err)),
	})
}

func decodeRequest(encoded string) (bridgeRequest, string) {
	var request bridgeRequest
	if len(encoded) > maximumRequestJSONBytes {
		return request, failure("bridge", "request-limit", "The encoded request exceeds the bridge limit.")
	}
	decoder := json.NewDecoder(strings.NewReader(encoded))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&request); err != nil {
		return request, failure("bridge", "invalid-request", err)
	}
	if decoder.More() {
		return request, failure("bridge", "invalid-request", "The encoded request contains trailing JSON values.")
	}
	return request, ""
}

func validateRequest(request bridgeRequest) string {
	if request.ABI != bridgeABIVersion {
		return failure("bridge", "abi-mismatch", "The bridge ABI version is unsupported.")
	}
	if request.Operation != "execute" && request.Operation != "replace" {
		return failure("bridge", "invalid-operation", "The bridge operation is unsupported.")
	}
	if len(request.Options) != 0 {
		return failure("bridge", "unsupported-option", "Go regexp does not accept engine options.")
	}
	if !utf8.ValidString(request.Pattern) || len(request.Pattern) > maximumPatternBytes {
		return failure("bridge", "pattern-limit", "The pattern is invalid UTF-8 or exceeds the bridge limit.")
	}
	if !utf8.ValidString(request.Subject) || len(request.Subject) > maximumSubjectBytes {
		return failure("bridge", "subject-limit", "The subject is invalid UTF-8 or exceeds the bridge limit.")
	}
	if request.MaximumMatches < 1 || request.MaximumMatches > maximumMatches ||
		request.MaximumCaptureRows < 1 || request.MaximumCaptureRows > maximumCaptureRows {
		return failure("bridge", "result-limit", "The requested result bounds are outside the bridge contract.")
	}
	seenFlags := make(map[rune]bool)
	for _, flag := range request.Flags {
		if !strings.ContainsRune("gimsU", flag) || seenFlags[flag] {
			return failure("bridge", "unsupported-flag", "The request contains an unsupported or duplicate Go flag.")
		}
		seenFlags[flag] = true
	}
	if request.Operation == "execute" {
		if request.Replacement != nil || request.MaximumOutputBytes != nil {
			return failure("bridge", "invalid-request", "Execution requests cannot contain replacement fields.")
		}
		return ""
	}
	if request.Replacement == nil || request.MaximumOutputBytes == nil {
		return failure("bridge", "invalid-request", "Replacement requests require a template and output bound.")
	}
	if !utf8.ValidString(*request.Replacement) || len(*request.Replacement) > maximumReplacementBytes {
		return failure("bridge", "replacement-limit", "The replacement is invalid UTF-8 or exceeds the bridge limit.")
	}
	if *request.MaximumOutputBytes < 1 || *request.MaximumOutputBytes > maximumOutputBytes {
		return failure("bridge", "output-limit", "The output bound is outside the bridge contract.")
	}
	return ""
}

func compilePattern(pattern, flags string) (*regexp.Regexp, error) {
	var modifiers strings.Builder
	for _, flag := range "imsU" {
		if strings.ContainsRune(flags, flag) {
			modifiers.WriteRune(flag)
		}
	}
	if modifiers.Len() != 0 {
		pattern = "(?" + modifiers.String() + ")" + pattern
	}
	return regexp.Compile(pattern)
}

func materializeMatch(indices []int, groupCount int) nativeMatch {
	record := nativeMatch{
		Span:     [2]int{indices[0], indices[1]},
		Captures: make([]*[2]int, groupCount),
	}
	for group := 0; group < groupCount; group++ {
		start := indices[2+group*2]
		end := indices[3+group*2]
		if start >= 0 {
			span := [2]int{start, end}
			record.Captures[group] = &span
		}
	}
	return record
}

func collectMatches(
	compiled *regexp.Regexp,
	subject string,
	global bool,
	maximumMatchCount int,
	maximumRows int,
) ([][]int, bool) {
	rowsPerMatch := compiled.NumSubexp()
	if rowsPerMatch < 1 {
		rowsPerMatch = 1
	}
	allowed := maximumMatchCount
	if rowBound := maximumRows / rowsPerMatch; rowBound < allowed {
		allowed = rowBound
	}
	probeLimit := allowed + 1
	var indices [][]int
	if global {
		indices = compiled.FindAllStringSubmatchIndex(subject, probeLimit)
	} else if candidate := compiled.FindStringSubmatchIndex(subject); candidate != nil {
		indices = [][]int{candidate}
	}
	if len(indices) <= allowed {
		return indices, false
	}
	return indices[:allowed], true
}

type boundedOutput struct {
	buffer    bytes.Buffer
	maximum   int
	truncated bool
}

func (output *boundedOutput) append(value string) {
	if output.truncated || value == "" {
		return
	}
	remaining := output.maximum - output.buffer.Len()
	if len(value) <= remaining {
		output.buffer.WriteString(value)
		return
	}
	end := remaining
	for end > 0 && !utf8.ValidString(value[:end]) {
		end--
	}
	output.buffer.WriteString(value[:end])
	output.truncated = true
}

func extractReplacementReference(value string) (name string, number int, rest string, ok bool) {
	if value == "" {
		return
	}
	braced := false
	if value[0] == '{' {
		braced = true
		value = value[1:]
	}
	index := 0
	for index < len(value) {
		character, size := utf8.DecodeRuneInString(value[index:])
		if !unicode.IsLetter(character) && !unicode.IsDigit(character) && character != '_' {
			break
		}
		index += size
	}
	if index == 0 {
		return
	}
	name = value[:index]
	if braced {
		if index >= len(value) || value[index] != '}' {
			return
		}
		index++
	}

	number = 0
	for position := 0; position < len(name); position++ {
		if name[position] < '0' || name[position] > '9' || number >= 1e8 {
			number = -1
			break
		}
		number = number*10 + int(name[position]) - '0'
	}
	if name[0] == '0' && len(name) > 1 {
		number = -1
	}
	rest = value[index:]
	ok = true
	return
}

func expandReplacementBounded(
	output *boundedOutput,
	compiled *regexp.Regexp,
	template, subject string,
	match []int,
) {
	for len(template) > 0 && !output.truncated {
		before, after, found := strings.Cut(template, "$")
		if !found {
			break
		}
		output.append(before)
		if output.truncated {
			return
		}
		template = after
		if template != "" && template[0] == '$' {
			output.append("$")
			template = template[1:]
			continue
		}
		name, number, rest, ok := extractReplacementReference(template)
		if !ok {
			output.append("$")
			continue
		}
		template = rest
		if number >= 0 {
			if 2*number+1 < len(match) && match[2*number] >= 0 {
				output.append(subject[match[2*number]:match[2*number+1]])
			}
			continue
		}
		for index, candidate := range compiled.SubexpNames() {
			if name == candidate && 2*index+1 < len(match) && match[2*index] >= 0 {
				output.append(subject[match[2*index]:match[2*index+1]])
				break
			}
		}
	}
	output.append(template)
}

func replaceBounded(
	compiled *regexp.Regexp,
	subject, replacement string,
	indices [][]int,
	maximumBytes int,
) (string, int, bool) {
	output := boundedOutput{maximum: maximumBytes}
	cursor := 0
	for _, match := range indices {
		output.append(subject[cursor:match[0]])
		expandReplacementBounded(&output, compiled, replacement, subject, match)
		cursor = match[1]
	}
	output.append(subject[cursor:])
	value := output.buffer.Bytes()
	return base64.StdEncoding.EncodeToString(value), len(value), output.truncated
}

func run(encoded string) string {
	request, requestError := decodeRequest(encoded)
	if requestError != "" {
		return requestError
	}
	if validationError := validateRequest(request); validationError != "" {
		return validationError
	}

	compiled, err := compilePattern(request.Pattern, request.Flags)
	if err != nil {
		return failure("compile", "compile-error", err)
	}
	groupCount := compiled.NumSubexp()
	if groupCount > maximumCaptureGroups {
		return failure(
			"compile",
			"capture-group-limit",
			fmt.Sprintf("The pattern declares %d capture groups; the bridge limit is %d.", groupCount, maximumCaptureGroups),
		)
	}

	names := make([][2]any, 0)
	for number, name := range compiled.SubexpNames() {
		if number > 0 && name != "" {
			names = append(names, [2]any{number, name})
		}
	}
	sort.Slice(names, func(left, right int) bool {
		return names[left][0].(int) < names[right][0].(int)
	})

	global := strings.ContainsRune(request.Flags, 'g')
	indices, resultsTruncated := collectMatches(
		compiled,
		request.Subject,
		global,
		request.MaximumMatches,
		request.MaximumCaptureRows,
	)
	records := make([]nativeMatch, len(indices))
	for index, match := range indices {
		records[index] = materializeMatch(match, groupCount)
	}
	result := bridgeSuccess{
		ABI:              bridgeABIVersion,
		OK:               true,
		GroupCount:       groupCount,
		GroupNames:       names,
		Matches:          records,
		ResultsTruncated: resultsTruncated,
	}
	if request.Operation == "replace" {
		outputBase64, outputBytes, outputTruncated := replaceBounded(
			compiled,
			request.Subject,
			*request.Replacement,
			indices,
			*request.MaximumOutputBytes,
		)
		result.OutputBase64 = &outputBase64
		result.OutputBytes = &outputBytes
		result.OutputTruncated = &outputTruncated
	}
	return marshal(result)
}

func selfTest() bool {
	if runtime.Version() != expectedGoVersion {
		return false
	}
	compiled, err := regexp.Compile(`(?P<word>😀+)`)
	if err != nil {
		return false
	}
	match := compiled.FindStringSubmatchIndex("x😀😀")
	if len(match) != 4 || match[0] != 1 || match[1] != 9 ||
		match[2] != 1 || match[3] != 9 || compiled.SubexpNames()[1] != "word" {
		return false
	}
	for _, template := range []string{
		`${word}-$1-$$`,
		`$1x|${1}x`,
		`${missing}|$|${word`,
		`$01|${01}|$1234567890`,
	} {
		expected := string(compiled.ExpandString(nil, template, "x😀😀", match))
		bounded := boundedOutput{maximum: 4 * 1024}
		expandReplacementBounded(&bounded, compiled, template, "x😀😀", match)
		if bounded.truncated || bounded.buffer.String() != expected {
			return false
		}
	}
	adversarial := boundedOutput{maximum: 5}
	expandReplacementBounded(
		&adversarial,
		compiled,
		strings.Repeat("$word", 32*1024),
		"x😀😀",
		match,
	)
	if !adversarial.truncated || adversarial.buffer.String() != "😀" {
		return false
	}
	empty := regexp.MustCompile("a*").FindAllStringIndex("baaab", -1)
	return len(empty) == 3 &&
		empty[0][0] == 0 && empty[0][1] == 0 &&
		empty[1][0] == 1 && empty[1][1] == 4 &&
		empty[2][0] == 5 && empty[2][1] == 5
}

func identity() string {
	return marshal(identityEnvelope{
		ABI: bridgeABIVersion,
		OK:  true,
		Identity: map[string]any{
			"bridge":        "regex-tools-go-json",
			"bridgeVersion": bridgeABIVersion,
			"engine":        "Go standard-library regexp",
			"engineVersion": "1.26.5",
			"goVersion":     runtime.Version(),
			"offsetUnit":    "utf8-byte",
			"package":       "regexp",
			"selfTest":      selfTest(),
		},
	})
}

var retainedCallbacks []js.Func

func main() {
	identityCallback := js.FuncOf(func(_ js.Value, arguments []js.Value) any {
		if len(arguments) != 0 {
			return failure("bridge", "invalid-request", "The identity callback does not accept arguments.")
		}
		return identity()
	})
	runCallback := js.FuncOf(func(_ js.Value, arguments []js.Value) (result any) {
		defer func() {
			if recovered := recover(); recovered != nil {
				result = failure("bridge", "panic", "The Go bridge stopped an unexpected internal panic.")
			}
		}()
		if len(arguments) != 1 || arguments[0].Type() != js.TypeString {
			return failure("bridge", "invalid-request", "The run callback requires one JSON string.")
		}
		return run(arguments[0].String())
	})
	retainedCallbacks = []js.Func{identityCallback, runCallback}

	installer := js.Global().Get(bridgeInstallerGlobalName)
	if installer.Type() != js.TypeFunction {
		panic("Regex Tools Go bridge installer is unavailable")
	}
	installer.Invoke(identityCallback, runCallback)
	select {}
}
