// General purpose "utilities" that act as my own "standard library"
package basic

import (
	"fmt"
	"math/rand"
	"os"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"unicode"
)

// Equivalent to Atoi, but returns int32 rather than (int, error)
func Atoi32(s string) int32 {
	i, _ := strconv.ParseInt(s, 10, 32)
	return int32(i)
}

// Equivalent to Atoi, but returns int64 rather than (int, error)
func Atoi64(s string) int64 {
	i, _ := strconv.ParseInt(s, 10, 64)
	return i
}

func GetPathParts(path string) []string {
	trimmed := strings.TrimPrefix(path, "/")
	return strings.Split(trimmed, "/")
}

// Takes a tree pointer and a slice of path segments to insert.
// It looks for an existing child with the current segment name; if none is found,
// it creates a new node on the tree. Then it recurses on the remaining segments.
//
// Ex: Generate tree nodes from url segments
// `/app/examples/webpage`     -> {"app", "examples", "webpage"}
// `/app/examples/hello-world` -> {"app", "examples", "hello-world"
// `/auth/login`               -> {"auth", "login"}
//
// =>
//
//	root {
//	    app {
//	        examples {
//	            webpage
//	            hello-world
//	        }
//	    }
//
//	    auth {
//	        login
//	    }
//	}
//
// This function is used in the code generation process to generate `pageinfo` structs
// from all known application page URLs.
func AddStringPartsToTree(tree *Tree, parts []string) {
	if len(parts) == 0 {
		return
	}

	for i := range parts {
		if parts[i] == "" {
			parts[i] = "index"
		}
	}

	if tree.Children == nil {
		tree.Children = new([]Tree)
	}

	// Search for an existing child with the current part's name.
	var child *Tree
	for i := range *tree.Children {
		if (*tree.Children)[i].Name == parts[0] {
			child = &((*tree.Children)[i])
			break
		}
	}

	// If no child is found, create a new one and append it.
	if child == nil {
		newNode := Tree{Name: parts[0]}
		*tree.Children = append(*tree.Children, newNode)
		child = &((*tree.Children)[len(*tree.Children)-1])
	}

	AddStringPartsToTree(child, parts[1:])
}

type Tree struct {
	Name     string
	Children *[]Tree
}

func CapitalizeFirstLetter(s string) string {
	if s == "" {
		return s
	}
	// Convert the first rune to uppercase
	first := []rune(s)[0]
	return string(unicode.ToUpper(first)) + s[1:]
}

// Known compound surnames where a simple prefix rule would cause false positives
// (e.g. "De" matches Dean/Dennis, "La" matches Laura/Lance). Add entries as needed.
var compoundSurnames = map[string]string{
	// De-
	"deangelo": "DeAngelo", "decarlo": "DeCarlo", "dejesus": "DeJesus",
	"deleon": "DeLeon", "deluca": "DeLuca", "demarco": "DeMarco",
	"depaul": "DePaul", "derosa": "DeRosa", "desantis": "DeSantis",
	"devries": "DeVries", "devris": "DeVris", "dewitt": "DeWitt",
	"deyoung": "DeYoung",
	// Di-
	"dicaprio": "DiCaprio", "dicarlo": "DiCarlo", "dimaggio": "DiMaggio",
	"dinapoli": "DiNapoli", "dipietro": "DiPietro",
	// La-
	"lafleur": "LaFleur", "lafrance": "LaFrance", "lamontagne": "LaMontagne",
	"laporte": "LaPorte", "larocca": "LaRocca", "larue": "LaRue",
	"lasalle": "LaSalle",
	// Le-
	"leblanc": "LeBlanc", "lebron": "LeBron", "legrand": "LeGrand",
	"lemay": "LeMay",
	// Lo-
	"lopresti": "LoPresti",
	// Du-
	"dubois": "DuBois", "dupont": "DuPont", "dupree": "DuPree",
}

// NormalizeName intelligently capitalizes a name field (first or last name).
// Handles edge cases like O'Brian, McDonald, MacArthur, hyphenated names,
// compound surnames (DeSantis, DiCaprio, LeBlanc), and suffixes (Jr, III).
func NormalizeName(name string) string {
	name = strings.TrimSpace(name)
	if name == "" {
		return name
	}

	// If the name has mixed capitalization, assume the user typed it intentionally.
	hasUpper, hasLower := false, false
	for _, r := range name {
		if unicode.IsUpper(r) {
			hasUpper = true
		} else if unicode.IsLower(r) {
			hasLower = true
		}
		if hasUpper && hasLower {
			return name
		}
	}

	name = strings.ToLower(name)

	hyphenParts := strings.Split(name, "-")
	for i, hpart := range hyphenParts {
		words := strings.Fields(hpart)
		for j, word := range words {
			if mapped, ok := compoundSurnames[word]; ok {
				words[j] = mapped
				continue
			}

			runes := []rune(word)

			switch {
			case word == "ii" || word == "iii" || word == "iv" || word == "vi" || word == "vii" || word == "viii":
				words[j] = strings.ToUpper(word)
			case word == "jr" || word == "sr":
				words[j] = CapitalizeFirstLetter(word) + "."
			case strings.HasPrefix(word, "mc") && len(runes) >= 3:
				words[j] = "Mc" + string(unicode.ToUpper(runes[2])) + string(runes[3:])
			case strings.HasPrefix(word, "mac") && len(runes) >= 5:
				words[j] = "Mac" + string(unicode.ToUpper(runes[3])) + string(runes[4:])
			case strings.HasPrefix(word, "o'") && len(runes) >= 3:
				words[j] = "O'" + string(unicode.ToUpper(runes[2])) + string(runes[3:])
			default:
				words[j] = string(unicode.ToUpper(runes[0])) + string(runes[1:])
			}
		}
		hyphenParts[i] = strings.Join(words, " ")
	}
	return strings.Join(hyphenParts, "-")
}

var profanityList = map[string]bool{
	// Common profanity — whole-word matches only, so substrings in real names
	// (e.g. "Massimo", "Dickens", "Cockburn") are not flagged.
	"ass": true, "arse": true, "asshole": true,
	"bastard": true, "bitch": true, "bollocks": true,
	"cock": true, "crap": true, "cunt": true,
	"damn": true, "dildo": true,
	"fag": true, "fuck": true, "fucker": true,
	"goddamn":      true,
	"jackass":      true,
	"motherfucker": true,
	"nigger":       true, "nigga": true,
	"piss": true, "prick": true, "pussy": true,
	"shit": true, "slut": true,
	"tit": true, "tits": true, "twat": true,
	"wanker": true, "whore": true,
}

var leetReplacer = strings.NewReplacer(
	"0", "o",
	"1", "i",
	"3", "e",
	"4", "a",
	"5", "s",
	"7", "t",
	"8", "b",
	"@", "a",
	"$", "s",
	"!", "i",
)

// NameContainsProfanity checks whether any whole word in the name matches a known profane term.
// Words are split on spaces, hyphens, and apostrophes so that substrings within legitimate
// names (e.g. "Massimo", "Dickens", "Cockburn") are not flagged.
// Also normalizes leet speak substitutions (e.g. "b1tch", "a$$", "sh!t").
func NameContainsProfanity(name string) bool {
	name = strings.ToLower(name)
	parts := strings.FieldsFunc(name, func(r rune) bool {
		return r == ' ' || r == '-' || r == '\''
	})
	for _, part := range parts {
		if profanityList[part] {
			return true
		}
		// Check leet speak variant
		normalized := leetReplacer.Replace(part)
		if normalized != part && profanityList[normalized] {
			return true
		}
	}
	return false
}

func IntAbs(x int) int {
	if x < 0 {
		return -x
	}

	return x
}

func MakeURLParams(base string, params ...[2]string) string {
	output := base

	for i, v := range params {
		if i == 0 {
			output += "?" + v[0] + "=" + v[1]
		} else {
			output += "&" + v[0] + "=" + v[1]
		}
	}

	return output
}

func ToSnakeCase(s string) string {
	s = strings.ReplaceAll(s, " ", "_")

	return s
}

func SnakeCaseToTitleCase(s string) string {
	parts := strings.Split(s, "_")

	for i, part := range parts {
		parts[i] = CapitalizeFirstLetter(part)
	}

	return strings.Join(parts, " ")
}

func NullableToString[T any](i *T) string {
	if i == nil {
		return ""
	}

	return ToString(*i)
}

func ToString(i any) string {
	v := reflect.ValueOf(i)

	output := ""

	switch v.Kind() {
	case reflect.String:
		output = v.String()
	case reflect.Int:
		output = fmt.Sprintf("%d", v.Int())
	case reflect.Float64:
		output = fmt.Sprintf("%f", v.Float())
	case reflect.Bool:
		output = fmt.Sprintf("%t", v.Bool())
	default:
		output = fmt.Sprintf("%v", i)
	}

	return output
}

func Int64ToStringWithCommas(i int64) string {
	str := strconv.Itoa(int(i))

	negative := false
	if strings.HasPrefix(str, "-") {
		negative = true
		str = str[1:]
	}

	result := ""
	for i, char := range str {
		if i > 0 && (len(str)-i)%3 == 0 {
			result += ","
		}
		result += string(char)
	}

	if negative {
		result = "-" + result
	}

	return result
}

// Strips non-numeric characters from a string, then converts digits in string to int
func StringToInt(input string) int {
	i, _ := strconv.Atoi(SanitizeNum(input))
	return i
}

// Strips non-numeric characters from a string, then converts digits in string to int32
func StringToInt32(input string) int32 {
	return Atoi32(SanitizeNum(input))
}

// Strips non-numeric characters from a string, then converts digits in string to int64
func StringToInt64(input string) int64 {
	return Atoi64(SanitizeNum(input))
}

// ABC
func SafeIndex[T any](index int, arr []T) T {
	var temp T

	if index > len(arr)-1 {
		return temp
	}

	return arr[index]
}

func SafeDereference[T any](value *T, optionalDefault ...T) T {
	if value != nil {
		return *value
	}

	if len(optionalDefault) > 0 {
		return optionalDefault[0]
	}

	return *new(T)
}

func MakePtr[T any](value T) *T {
	output := new(T)
	*output = value
	return output
}

func Reverse[T comparable](s []T) {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
}

// Given two arrays, make sure that elements in array 1 are present in array 2
// Return false if the requirements are not met
func ContainsAll[T comparable](arr1, arr2 []T) bool {
	elements := make(map[T]bool)

	for _, num := range arr1 {
		elements[num] = true
	}

	for _, num := range arr2 {
		if !elements[num] {
			return false
		}
	}

	return true
}

func IndexOf[T comparable](collection []T, el T) int {
	for i, x := range collection {
		if x == el {
			return i
		}
	}
	return -1
}

func Remove[T comparable](arr []T, s T) []T {
	return append(arr[:IndexOf(arr, s)], arr[IndexOf(arr, s)+1:]...)
}

func RemoveDuplicates[T comparable](sliceList []T) []T {
	allKeys := make(map[T]bool)
	list := []T{}
	for _, item := range sliceList {
		if _, value := allKeys[item]; !value {
			allKeys[item] = true
			list = append(list, item)
		}
	}
	return list
}

// Return elements in slice 1 minus elements in slice2
func RemoveMany[T comparable](slice1, slice2 []T) []T {
	removeMap := make(map[T]bool)
	for _, item := range slice2 {
		removeMap[item] = true
	}

	result := make([]T, 0)

	for _, item := range slice1 {
		if !removeMap[item] {
			result = append(result, item)
		}
	}

	return result
}

func GetFirstNChars(s string, n int) string {
	i := 0
	for j := range s {
		if i == n {
			return s[:j]
		}
		i++
	}
	return s
}

func PrintStatus(b bool) {
	var status string

	if b {
		status = "SUCCESS"
	} else {
		status = "FAILED"
	}

	fmt.Printf("... %s\n", status)
}

func DirExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

func MapToSortedArray(m map[string]int64) [][2]int64 {
	pairs := make([][2]int64, 0, len(m))

	for k, v := range m {
		numKey, err := strconv.ParseInt(k, 10, 64)
		if err != nil {
			continue
		}
		pairs = append(pairs, [2]int64{numKey, v})
	}

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

	return pairs
}

func RandomSortInt32(n int) []int32 {
	result := make([]int32, n)
	for i := 0; i < n; i++ {
		result[i] = int32(i + 1)
	}

	// Shuffle the list using Fisher-Yates algorithm
	for i := n - 1; i > 0; i-- {
		j := rand.Intn(i + 1)
		result[i], result[j] = result[j], result[i]
	}
	return result
}

func SlicesEqual[T comparable](a []T, b []T) bool {
	if len(a) != len(b) {
		return false
	}

	for i := range a {
		if b[i] != a[i] {
			return false
		}
	}

	return true
}

type StructDiff struct {
	FieldName string `json:"field_name"`
	OldValue  any    `json:"old_value"`
	NewValue  any    `json:"new_value"`
}

type StructComparison struct {
	Changes []StructDiff `json:"changes"`
	Summary string       `json:"summary"`
}

func CompareStructs(oldStruct, newStruct any) StructComparison {
	result := StructComparison{
		Changes: []StructDiff{},
	}

	if oldStruct == nil && newStruct == nil {
		result.Summary = "Both structs are nil"
		return result
	}

	if oldStruct == nil {
		result.Summary = "Old struct is nil, new struct has values"
		return result
	}

	if newStruct == nil {
		result.Summary = "New struct is nil, old struct had values"
		return result
	}

	oldVal := reflect.ValueOf(oldStruct)
	newVal := reflect.ValueOf(newStruct)

	if oldVal.Type() != newVal.Type() {
		result.Summary = "Struct types do not match"
		return result
	}

	if oldVal.Kind() == reflect.Ptr {
		oldVal = oldVal.Elem()
	}
	if newVal.Kind() == reflect.Ptr {
		newVal = newVal.Elem()
	}

	if oldVal.Kind() != reflect.Struct || newVal.Kind() != reflect.Struct {
		result.Summary = "Both values must be structs"
		return result
	}

	oldType := oldVal.Type()
	changeCount := 0

	for i := 0; i < oldVal.NumField(); i++ {
		field := oldType.Field(i)

		if !field.IsExported() {
			continue
		}

		oldFieldVal := oldVal.Field(i)
		newFieldVal := newVal.Field(i)

		if !oldFieldVal.CanInterface() || !newFieldVal.CanInterface() {
			continue
		}

		oldInterface := oldFieldVal.Interface()
		newInterface := newFieldVal.Interface()

		if !reflect.DeepEqual(oldInterface, newInterface) {
			result.Changes = append(result.Changes, StructDiff{
				FieldName: field.Name,
				OldValue:  oldInterface,
				NewValue:  newInterface,
			})
			changeCount++
		}
	}

	if changeCount == 0 {
		result.Summary = "No changes detected"
	} else if changeCount == 1 {
		result.Summary = "1 field changed"
	} else {
		result.Summary = fmt.Sprintf("%d fields changed", changeCount)
	}

	return result
}

// Strips non-numeric characters from a string including spaces.
func SanitizeNum(input string) string {
	re := regexp.MustCompile(`[^\d]+`)
	return re.ReplaceAllString(input, "")
}

// Strips non-alphanumeric, non-space characters from a string.
func SanitizeAlphaNum(input string) string {
	re := regexp.MustCompile(`[^a-zA-Z0-9 ]+`)
	return re.ReplaceAllString(input, "")
}

// Strips non-alphanumeric characters from a string including spaces.
func SanitizeAlphaNumStrict(input string) string {
	re := regexp.MustCompile(`[^a-zA-Z0-9]+`)
	return re.ReplaceAllString(input, "")
}

// MapMerge merges two maps, with values from m2 overriding values from m1.
func MapMerge[K comparable, V any](m1, m2 map[K]V) map[K]V {
	result := make(map[K]V, len(m1)+len(m2))
	for k, v := range m1 {
		result[k] = v
	}
	for k, v := range m2 {
		result[k] = v
	}
	return result
}
