//go:build ignore

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"regexp"
	"sort"
	"strings"
	"text/template"

	"github.com/nats-io/nats-server/v2/server"
)

var tagRe = regexp.MustCompile("\\{(.+?)}")

var templ = `
// Generated code, do not edit. See errors.json and run go generate to update

package server

import "strings"

const (
{{- range $i, $error := . }}
{{- if .Comment }}
	// {{ .Constant }} {{ .Comment }} ({{ .Description | print }})
{{- else }}
	// {{ .Constant }} {{ .Description | print }}
{{- end }}
	{{ .Constant }} ErrorIdentifier = {{ .ErrCode }}
{{ end }}
)

var (
	ApiErrors = map[ErrorIdentifier]*ApiError{
{{- range $i, $error := . }}
		{{ .Constant }}: {Code: {{ .Code }},ErrCode: {{ .ErrCode }},Description: {{ .Description | printf "%q" }}},{{- end }}
	}

{{- range $i, $error := . }}
{{- if .Deprecates }}
// {{ .Deprecates }} Deprecated by {{ .Constant }} ApiError, use IsNatsError() for comparisons
{{ .Deprecates }} = ApiErrors[{{ .Constant }}]
{{- end }}
{{- end }}
)

{{- range $i, $error := . }}
// {{ .Constant | funcNameForConstant }} creates a new {{ .Constant }} error: {{ .Description | printf "%q" }}
func {{ .Constant | funcNameForConstant }}({{ .Description | funcArgsForTags }}) *ApiError {
    eopts := parseOpts(opts)
	if ae, ok := eopts.err.(*ApiError); ok {
		return ae
 	}
{{ if .Description | hasTags }}
	e:=ApiErrors[{{.Constant}}]
	args:=e.toReplacerArgs([]interface{}{ {{.Description | replacerArgsForTags }} })
	return &ApiError{
		Code:        e.Code,
		ErrCode:     e.ErrCode,
		Description: strings.NewReplacer(args...).Replace(e.Description),
	}
{{- else }}
	return ApiErrors[{{.Constant}}]
{{- end }}
}

{{- end }}
`

func panicIfErr(err error) {
	if err == nil {
		return
	}
	panic(err)
}

func goFmt(file string) error {
	c := exec.Command("go", "fmt", file)
	out, err := c.CombinedOutput()
	if err != nil {
		log.Printf("go fmt failed: %s", string(out))
	}

	return err
}

func checkIncrements(errs []server.ErrorsData) error {
	sort.Slice(errs, func(i, j int) bool {
		return errs[i].ErrCode < errs[j].ErrCode
	})

	last := errs[0].ErrCode
	gaps := []uint16{}

	for i := 1; i < len(errs); i++ {
		if errs[i].ErrCode != last+1 {
			gaps = append(gaps, last)
		}
		last = errs[i].ErrCode
	}

	if len(gaps) > 0 {
		return fmt.Errorf("gaps found in sequences: %v", gaps)
	}

	return nil
}

func checkDupes(errs []server.ErrorsData) error {
	codes := []uint16{}
	highest := uint16(0)
	for _, err := range errs {
		codes = append(codes, err.ErrCode)
		if highest < err.ErrCode {
			highest = err.ErrCode
		}
	}

	codeKeys := make(map[uint16]bool)
	constKeys := make(map[string]bool)

	for _, entry := range errs {
		if _, found := codeKeys[entry.ErrCode]; found {
			return fmt.Errorf("duplicate error code %+v, highest code is %d", entry, highest)
		}

		if _, found := constKeys[entry.Constant]; found {
			return fmt.Errorf("duplicate error constant %+v", entry)
		}

		codeKeys[entry.ErrCode] = true
		constKeys[entry.Constant] = true
	}

	return nil
}

func findTags(d string) []string {
	tags := []string{}
	for _, tag := range tagRe.FindAllStringSubmatch(d, -1) {
		if len(tag) != 2 {
			continue
		}

		tags = append(tags, tag[1])
	}

	sort.Strings(tags)

	return tags
}

func main() {
	ej, err := os.ReadFile("server/errors.json")
	panicIfErr(err)

	errs := []server.ErrorsData{}
	panicIfErr(json.Unmarshal(ej, &errs))

	panicIfErr(checkDupes(errs))
	panicIfErr(checkIncrements(errs))

	sort.Slice(errs, func(i, j int) bool {
		return errs[i].Constant < errs[j].Constant
	})

	t := template.New("errors").Funcs(
		template.FuncMap{
			"inc": func(i int) int { return i + 1 },
			"hasTags": func(d string) bool {
				return strings.Contains(d, "{") && strings.Contains(d, "}")
			},
			"replacerArgsForTags": func(d string) string {
				res := []string{}
				for _, tag := range findTags(d) {
					res = append(res, `"{`+tag+`}"`)
					res = append(res, tag)
				}

				return strings.Join(res, ", ")
			},
			"funcArgsForTags": func(d string) string {
				res := []string{}
				for _, tag := range findTags(d) {
					if tag == "err" {
						res = append(res, "err error")
					} else if tag == "seq" {
						res = append(res, "seq uint64")
					} else {
						res = append(res, fmt.Sprintf("%s interface{}", tag))
					}
				}

				res = append(res, "opts ...ErrorOption")

				return strings.Join(res, ", ")
			},
			"funcNameForConstant": func(c string) string {
				res := ""

				switch {
				case strings.HasSuffix(c, "ErrF"):
					res = fmt.Sprintf("New%sError", strings.TrimSuffix(c, "ErrF"))
				case strings.HasSuffix(c, "Err"):
					res = fmt.Sprintf("New%sError", strings.TrimSuffix(c, "Err"))
				case strings.HasSuffix(c, "ErrorF"):
					res = fmt.Sprintf("New%s", strings.TrimSuffix(c, "F"))
				case strings.HasSuffix(c, "F"):
					res = fmt.Sprintf("New%sError", strings.TrimSuffix(c, "F"))
				default:
					res = fmt.Sprintf("New%s", c)
				}

				if !strings.HasSuffix(res, "Error") {
					res = fmt.Sprintf("%sError", res)
				}

				return res
			},
		})
	p, err := t.Parse(templ)
	panicIfErr(err)

	tf, err := os.CreateTemp("", "")
	panicIfErr(err)
	defer tf.Close()

	panicIfErr(p.Execute(tf, errs))

	panicIfErr(os.Rename(tf.Name(), "server/jetstream_errors_generated.go"))
	panicIfErr(goFmt("server/jetstream_errors_generated.go"))
}
