package domain

import (
	"log/slog"
	"net/mail"
	"strings"
	"unicode"
	"unicode/utf8"

	"golang.org/x/text/unicode/norm"
)

// LogSanitizeChange applies sanitizeFn to raw, logs when the value changes, and writes
// the sanitized value to dest. Raw and sanitized values are intentionally omitted.
func LogSanitizeChange(
	providerType string,
	providerName string,
	field string,
	raw string,
	sanitizeFn func() string,
	dest *string,
) {
	sanitized := sanitizeFn()
	if sanitized != raw {
		message := "sanitization modified field value from external provider"
		if sanitized == "" {
			message = "sanitization cleared field value from external provider"
		}
		slog.Warn(message,
			"provider_type", SanitizeString(providerType, 64),
			"provider", SanitizeString(providerName, 128),
			"field", SanitizeString(field, 64),
		)
	}
	*dest = sanitized
}

var reservedUserIdentifiers = map[string]struct{}{
	"all":               {},
	"new":               {},
	"id":                {},
	CtxSystemAdminId:    {},
	CtxUnknownUserId:    {},
	CtxSystemLdapSyncer: {},
	CtxSystemWgImporter: {},
	CtxSystemV1Migrator: {},
	CtxSystemDBMigrator: {},
}

// SanitizeString normalizes to NFC, trims leading and trailing whitespace, strips Unicode
// control and format characters, drops invalid UTF-8 bytes, and truncates the result to
// maxLen runes. If maxLen <= 0, returns "".
func SanitizeString(s string, maxLen int) string {
	if maxLen <= 0 {
		return ""
	}

	s = norm.NFC.String(strings.TrimSpace(s))

	var b strings.Builder
	b.Grow(len(s))
	for len(s) > 0 {
		r, size := utf8.DecodeRuneInString(s)
		s = s[size:]
		if r == utf8.RuneError && size == 1 {
			continue
		}
		if !unicode.IsControl(r) && !unicode.Is(unicode.Cf, r) {
			b.WriteRune(r)
		}
	}
	s = b.String()

	if utf8.RuneCountInString(s) > maxLen {
		runes := []rune(s)
		s = string(runes[:maxLen])
	}

	return strings.TrimSpace(s)
}

// SanitizeEmail applies SanitizeString first, then returns "" if the original s
// contains CR/LF or if the sanitized result is not a plain email address.
func SanitizeEmail(s string, maxLen int) string {
	if strings.ContainsRune(s, '\r') || strings.ContainsRune(s, '\n') {
		return ""
	}

	sanitized := SanitizeString(s, maxLen)

	if sanitized == "" || strings.Count(sanitized, "@") != 1 {
		return ""
	}
	addr, err := mail.ParseAddress(sanitized)
	if err != nil || addr.Name != "" || addr.Address != sanitized {
		return ""
	}

	return sanitized
}

// SanitizePhone applies SanitizeString first, then removes all characters not in the
// set [0-9+\-() .]. Returns "" if the result after filtering is empty.
func SanitizePhone(s string, maxLen int) string {
	sanitized := SanitizeString(s, maxLen)

	// Remove all characters not in [0-9+\-() .]
	var b strings.Builder
	b.Grow(len(sanitized))
	for _, r := range sanitized {
		if isAllowedPhoneRune(r) {
			b.WriteRune(r)
		}
	}
	result := strings.TrimSpace(b.String())

	if result == "" {
		return ""
	}

	return result
}

// isAllowedPhoneRune reports whether r is in the allowed phone character set [0-9+\-() .].
func isAllowedPhoneRune(r rune) bool {
	switch {
	case r >= '0' && r <= '9':
		return true
	case r == '+', r == '-', r == '(', r == ')', r == ' ', r == '.':
		return true
	default:
		return false
	}
}

// SanitizeIdentifier applies SanitizeString first, then returns "" if the result equals
// a reserved user identifier (case-sensitive, exact match).
func SanitizeIdentifier(s string, maxLen int) string {
	sanitized := SanitizeString(s, maxLen)

	if IsReservedUserIdentifier(UserIdentifier(sanitized)) {
		return ""
	}

	return sanitized
}

func IsReservedUserIdentifier(identifier UserIdentifier) bool {
	_, reserved := reservedUserIdentifiers[string(identifier)]
	return reserved
}
