/*
Tool for generating helper functions for [new-storage.Role] structs inside of the storage package
It generates the following functions:
  - CollapseRolesIntoOne: Collapse a list of roles into one singular role. Each value will be set to the
    value of the role with the highest priority
  - RoleDeepCopy: Copy a role, including all arrays. Every value will be copied too
  - CompareRoles: Compare two roles. Returns true only if all fields are equal
    (if one of the fields is nil, that field is seen as equal)
*/
package main

import (
	"flag"
	"fmt"
	"io"
	"os"
	"regexp"
	"strings"

	"git.mstar.dev/mstar/goutils/sliceutils"
)

var findRoleStructRegex = regexp.MustCompile(`type Role struct \{([\s\S]+)\}\n\n/\*`)

var (
	flagInputFile  = flag.String("input", "", "Specify the input file. If empty, read from stdin")
	flagOutputFile = flag.String(
		"output",
		"",
		"Specify the output file. If empty, writes to stdout",
	)
	flagOutputModule = flag.String(
		"mod",
		"",
		"Module for the output file. If empty, use mod of input file",
	)
)

func main() {
	flag.Parse()
	var input io.Reader
	var output io.Writer

	if *flagInputFile == "" {
		input = os.Stdin
	} else {
		file, err := os.Open(*flagInputFile)
		if err != nil {
			panic(err)
		}
		defer file.Close()
		input = file
	}
	if *flagOutputFile == "" {
		output = os.Stdout
	} else {
		file, err := os.Create(*flagOutputFile)
		if err != nil {
			panic(err)
		}
		defer file.Close()
		output = file
	}

	data, err := io.ReadAll(input)
	if err != nil {
		panic(err)
	}
	if !findRoleStructRegex.Match(data) {
		panic("Input doesn't contain role struct")
	}
	content := findRoleStructRegex.FindStringSubmatch(string(data))[1]
	lines := strings.Split(content, "\n")
	lines = sliceutils.Map(lines, func(t string) string { return strings.TrimSpace(t) })
	importantLines := sliceutils.Filter(lines, func(t string) bool {
		if strings.HasPrefix(t, "//") {
			return false
		}
		if strings.Contains(t, "gorm.Model") {
			return false
		}
		data := sliceutils.Filter(strings.Split(t, " "), func(t string) bool { return t != "" })
		if len(data) < 2 {
			return false
		}
		if !strings.HasPrefix(data[1], "*") && !strings.HasPrefix(data[1], "[]") {
			return false
		}
		return true
	})
	nameTypeMap := map[string]string{}
	for _, line := range importantLines {
		parts := sliceutils.Filter(strings.Split(line, " "), func(t string) bool { return t != "" })
		nameTypeMap[parts[0]] = parts[1]
	}
	pkgString, _, _ := strings.Cut(string(data), "\n")

	outBuilder := strings.Builder{}
	outBuilder.WriteString(`// Code generated by cmd/RolesGenerator DO NOT EDIT.
// If you need to refresh the content, run go generate again
`)
	if *flagOutputModule == "" {
		outBuilder.WriteString(pkgString + "\n\n")
	} else {
		outBuilder.WriteString("package " + *flagOutputModule + "\n\n")
	}
	outBuilder.WriteString(
		`import (
  "slices"
  "git.mstar.dev/mstar/goutils/sliceutils"
    "git.mstar.dev/mstar/linstrom/storage-new/models"
)

`,
	)

	// Build role collapse function
	outBuilder.WriteString(
		`// CollapseRolesIntoOne takes a list of roles and collapses them down into one.
// It ensures to follow the priority of each role.
// All results will use [models.DefaultUserRole] as the baseline.
// The resulting role will have each entry filled with the value of the highest priority.
// If multiple roles have the same priority, the order in which they are applied is not stable.
func CollapseRolesIntoOne(roles ...models.Role) models.Role {
  startingRole := RoleDeepCopy(models.DefaultUserRole)
  slices.SortFunc(roles, func(a, b models.Role) int { return int(int64(a.Priority)-int64(b.Priority)) })
  for _, role := range roles {
`)
	// Write all the stupid conditions here
	for valName, valType := range nameTypeMap {
		if strings.HasPrefix(valType, "[]") {
			outBuilder.WriteString(fmt.Sprintf(`    if role.%s != nil {
      startingRole.%s = append(startingRole.%s, role.%s...)
    }
`, valName, valName, valName, valName))
		} else {
			outBuilder.WriteString(fmt.Sprintf(`    if role.%s != nil {
      *startingRole.%s = *role.%s
    }
`, valName, valName, valName))
		}
	}
	// Then finish up with the end of the function
	outBuilder.WriteString(
		`  }
  return startingRole
}
`)

	// Then build the deep copy function
	outBuilder.WriteString(`
// RoleDeepCopy performs a deep copy of a given role.
// Each element will point to a newly stored value.
// The new and old role will contain identical information.
func RoleDeepCopy(o models.Role) models.Role {
`)
	outBuilder.WriteString(`  n := models.Role{}
  n.Model = o.Model
  n.Name = o.Name
  n.Priority = o.Priority
  n.IsUserRole = o.IsUserRole
  n.IsBuiltIn = o.IsBuiltIn
`)
	for valName, valType := range nameTypeMap {
		if strings.HasPrefix(valType, "[]") {
			outBuilder.WriteString(fmt.Sprintf("  n.%s = slices.Clone(o.%s)\n", valName, valName))
		} else {
			outBuilder.WriteString(fmt.Sprintf(`  if o.%s == nil { n.%s = nil } else {
      t := *o.%s
      n.%s = &t
    }
`, valName, valName, valName, valName))
		}
	}
	outBuilder.WriteString("  return n\n}\n\n")

	// Build compare function
	outBuilder.WriteString(`// Compare two roles for equality.
// If a permission is nil in one of the roles, that permission is ignored.
func CompareRoles(a, b *models.Role) bool {
`)
	outBuilder.WriteString("  return ")
	lastName, lastType := "", ""
	for valName, valType := range nameTypeMap {
		lastName = valName
		lastType = valType
		outBuilder.WriteString(fmt.Sprintf("(a.%s == nil || b.%s == nil || ", valName, valName))
		if strings.HasPrefix(valType, "[]") {
			outBuilder.WriteString(
				fmt.Sprintf("sliceutils.CompareUnordered(a.%s,b.%s)) && ", valName, valName),
			)
		} else {
			outBuilder.WriteString(fmt.Sprintf("a.%s == b.%s) && ", valName, valName))
		}
	}
	outBuilder.WriteString("(a == nil || b == nil || ")
	if strings.HasPrefix(lastType, "[]") {
		outBuilder.WriteString(
			fmt.Sprintf("sliceutils.CompareUnordered(a.%s,b.%s))", lastName, lastName),
		)
	} else {
		outBuilder.WriteString(fmt.Sprintf("a.%s == b.%s)", lastName, lastName))
	}
	outBuilder.WriteString("\n}")

	// And write the entire thing to the output
	fmt.Fprint(output, outBuilder.String())
}
