package bfcodegen

import (
	"bytes"
	_ "embed"
	"encoding/json"
	"errors"
	"fmt"
	"go/format"
	"gogs.mikescher.com/BlackForestBytes/goext"
	"gogs.mikescher.com/BlackForestBytes/goext/cryptext"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"gogs.mikescher.com/BlackForestBytes/goext/rext"
	"io"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"text/template"
)

type EnumDefVal struct {
	VarName     string
	Value       string
	Description *string
	Data        *map[string]any
	RawComment  *string
}

type EnumDef struct {
	File         string
	FileRelative string
	EnumTypeName string
	Type         string
	Values       []EnumDefVal
}

type EnumGenOptions struct {
	DebugOutput *bool
	GoFormat    *bool
}

var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]+)\s*$`))

var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))

var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]*"|[0-9]+))\s*(//(?P<comm>.*))?.*$`))

var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))

//go:embed enum-generate.template
var templateEnumGenerateText string

func GenerateEnumSpecs(sourceDir string, destFile string, opt EnumGenOptions) error {

	oldChecksum := "N/A"
	if _, err := os.Stat(destFile); !os.IsNotExist(err) {
		content, err := os.ReadFile(destFile)
		if err != nil {
			return err
		}
		if m, ok := rexEnumChecksumConst.MatchFirst(string(content)); ok {
			oldChecksum = m.GroupByName("cs").Value()
		}
	}

	gocode, _, changed, err := _generateEnumSpecs(sourceDir, destFile, oldChecksum, langext.Coalesce(opt.GoFormat, true), langext.Coalesce(opt.DebugOutput, false))
	if err != nil {
		return err
	}

	if !changed {
		return nil
	}

	err = os.WriteFile(destFile, []byte(gocode), 0o755)
	if err != nil {
		return err
	}

	return nil
}

func _generateEnumSpecs(sourceDir string, destFile string, oldChecksum string, gofmt bool, debugOutput bool) (string, string, bool, error) {

	files, err := os.ReadDir(sourceDir)
	if err != nil {
		return "", "", false, err
	}

	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) })
	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") })
	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") })
	langext.SortBy(files, func(v os.DirEntry) string { return v.Name() })

	newChecksumStr := goext.GoextVersion
	for _, f := range files {
		content, err := os.ReadFile(path.Join(sourceDir, f.Name()))
		if err != nil {
			return "", "", false, err
		}
		newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content)
	}

	newChecksum := cryptext.BytesSha256([]byte(newChecksumStr))

	if newChecksum != oldChecksum {
		fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum)
	} else {
		fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum)
		return "", oldChecksum, false, nil
	}

	allEnums := make([]EnumDef, 0)

	pkgname := ""

	for _, f := range files {
		if debugOutput {
			fmt.Printf("========= %s =========\n\n", f.Name())
		}

		fileEnums, pn, err := processEnumFile(sourceDir, path.Join(sourceDir, f.Name()), debugOutput)
		if err != nil {
			return "", "", false, err
		}

		if debugOutput {
			fmt.Printf("\n")
		}

		allEnums = append(allEnums, fileEnums...)

		if pn != "" {
			pkgname = pn
		}
	}

	if pkgname == "" {
		return "", "", false, errors.New("no package name found in any file")
	}

	rdata := fmtEnumOutput(newChecksum, allEnums, pkgname)

	if !gofmt {
		return rdata, newChecksum, true, nil
	}

	fdata, err := format.Source([]byte(rdata))
	if err != nil {
		return "", "", false, err
	}

	return string(fdata), newChecksum, true, nil
}

func processEnumFile(basedir string, fn string, debugOutput bool) ([]EnumDef, string, error) {
	file, err := os.Open(fn)
	if err != nil {
		return nil, "", err
	}

	defer func() { _ = file.Close() }()

	bin, err := io.ReadAll(file)
	if err != nil {
		return nil, "", err
	}

	lines := strings.Split(string(bin), "\n")

	enums := make([]EnumDef, 0)

	pkgname := ""

	for i, line := range lines {
		if i == 0 && strings.HasPrefix(line, "// Code generated by") {
			break
		}

		if match, ok := rexEnumPackage.MatchFirst(line); i == 0 && ok {
			pkgname = match.GroupByName("name").Value()
			continue
		}

		if match, ok := rexEnumDef.MatchFirst(line); ok {

			rfp, err := filepath.Rel(basedir, fn)
			if err != nil {
				return nil, "", err
			}

			def := EnumDef{
				File:         fn,
				FileRelative: rfp,
				EnumTypeName: match.GroupByName("name").Value(),
				Type:         match.GroupByName("type").Value(),
				Values:       make([]EnumDefVal, 0),
			}
			enums = append(enums, def)

			if debugOutput {
				fmt.Printf("Found enum definition { '%s' -> '%s' }\n", def.EnumTypeName, def.Type)
			}
		}

		if match, ok := rexEnumValueDef.MatchFirst(line); ok {
			typename := match.GroupByName("type").Value()

			comment := match.GroupByNameOrEmpty("comm").ValueOrNil()
			var descr *string = nil
			var data *map[string]any = nil
			if comment != nil {
				comment = langext.Ptr(strings.TrimSpace(*comment))
				if strings.HasPrefix(*comment, "{") {
					if v, ok := tryParseDataComment(*comment); ok {
						data = &v
						if anyDataDescr, ok := v["description"]; ok {
							if dataDescr, ok := anyDataDescr.(string); ok {
								descr = &dataDescr
							}
						}
					} else {
						descr = comment
					}
				} else {
					descr = comment
				}
			}

			def := EnumDefVal{
				VarName:     match.GroupByName("name").Value(),
				Value:       match.GroupByName("value").Value(),
				RawComment:  comment,
				Description: descr,
				Data:        data,
			}

			found := false
			for i, v := range enums {
				if v.EnumTypeName == typename {
					enums[i].Values = append(enums[i].Values, def)
					found = true

					if debugOutput {
						if def.Description != nil {
							fmt.Printf("Found enum value [%s] for '%s'  ('%s')\n", def.Value, def.VarName, *def.Description)
						} else {
							fmt.Printf("Found enum value [%s] for '%s'\n", def.Value, def.VarName)
						}
					}
					break
				}
			}
			if !found {
				if debugOutput {
					fmt.Printf("Found non-enum value [%s] for '%s' ( looks like enum value, but no matching @enum:type )\n", def.Value, def.VarName)
				}
			}
		}
	}

	return enums, pkgname, nil
}

func tryParseDataComment(s string) (map[string]any, bool) {

	r := make(map[string]any)

	err := json.Unmarshal([]byte(s), &r)
	if err != nil {
		return nil, false
	}

	for _, v := range r {

		rv := reflect.ValueOf(v)

		if rv.Kind() == reflect.Ptr && rv.IsNil() {
			continue
		}
		if rv.Kind() == reflect.Bool {
			continue
		}
		if rv.Kind() == reflect.String {
			continue
		}
		if rv.Kind() == reflect.Int64 {
			continue
		}
		if rv.Kind() == reflect.Float64 {
			continue
		}

		return nil, false
	}

	return r, true
}

func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string {

	templ := template.New("enum-generate")

	templ = templ.Funcs(template.FuncMap{
		"boolToStr": func(b bool) string { return langext.Conditional(b, "true", "false") },
		"deref":     func(v *string) string { return *v },
		"trimSpace": func(str string) string { return strings.TrimSpace(str) },
		"hasStr":    func(v EnumDef) bool { return v.Type == "string" },
		"hasDescr": func(v EnumDef) bool {
			return langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Description != nil })
		},
		"hasData": func(v EnumDef) bool {
			return len(v.Values) > 0 && langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Data != nil })
		},
		"gostr": func(v any) string {
			return fmt.Sprintf("%#+v", v)
		},
		"goobj": func(name string, v any) string {
			return fmt.Sprintf("%#+v", v)
		},
		"godatakey": func(v string) string {
			return strings.ToUpper(v[0:1]) + v[1:]
		},
		"godatavalue": func(v any) string {
			return fmt.Sprintf("%#+v", v)
		},
		"godatatype": func(v any) string {
			return fmt.Sprintf("%T", v)
		},
		"mapindex": func(v map[string]any, k string) any {
			return v[k]
		},
		"generalDataKeys": func(v EnumDef) map[string]string {
			r0 := make(map[string]int)

			for _, eval := range v.Values {
				for k := range *eval.Data {
					if ctr, ok := r0[k]; ok {
						r0[k] = ctr + 1
					} else {
						r0[k] = 1
					}
				}
			}

			r1 := langext.MapToArr(r0)
			r2 := langext.ArrFilter(r1, func(p langext.MapEntry[string, int]) bool { return p.Value == len(v.Values) })
			r3 := langext.ArrMap(r2, func(p langext.MapEntry[string, int]) string { return p.Key })
			r4 := langext.ArrToKVMap(r3, func(p string) string { return p }, func(p string) string { return fmt.Sprintf("%T", (*v.Values[0].Data)[p]) })

			return r4
		},
	})

	templ = template.Must(templ.Parse(templateEnumGenerateText))

	buffer := bytes.Buffer{}

	err := templ.Execute(&buffer, langext.H{
		"PkgName":      pkgname,
		"Checksum":     cs,
		"GoextVersion": goext.GoextVersion,
		"Enums":        enums,
	})
	if err != nil {
		panic(err)
	}

	return buffer.String()
}