package middleware

import (
	"reflect"
	"regexp"
	"strings"

	"github.com/gofiber/fiber/v2"
	"github.com/microcosm-cc/bluemonday"
)

var (
	// Policy for bluemonday (Strict: Strip all HTML tags, just text)
	strictPolicy = bluemonday.StrictPolicy()

	// Path traversal patterns
	pathTraversalPatterns = []*regexp.Regexp{
		regexp.MustCompile(`\.\.\/`),
		regexp.MustCompile(`\.\.\\`),
	}
)

// SanitizeInputMiddleware sanitizes all input data
func SanitizeInputMiddleware() fiber.Handler {
	return func(c *fiber.Ctx) error {
		// Sanitize query parameters
		c.Request().URI().QueryArgs().VisitAll(func(key, value []byte) {
			sanitized := sanitizeString(string(value))
			c.Request().URI().QueryArgs().Set(string(key), sanitized)
		})

		// Sanitize form values (for multipart/form-data and application/x-www-form-urlencoded)
		if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
			contentType := string(c.Request().Header.ContentType())

			if strings.Contains(contentType, "application/x-www-form-urlencoded") ||
				strings.Contains(contentType, "multipart/form-data") {
				c.Request().PostArgs().VisitAll(func(key, value []byte) {
					sanitized := sanitizeString(string(value))
					c.Request().PostArgs().Set(string(key), sanitized)
				})
			}
		}

		return c.Next()
	}
}

// sanitizeString removes potentially dangerous characters and patterns using bluemonday
func sanitizeString(input string) string {
	// Trim whitespace
	input = strings.TrimSpace(input)

	// Use Bluemonday strict policy to remove any HTML / JS tags
	input = strictPolicy.Sanitize(input)

	// Check for path traversal attempts and strip them
	for _, pattern := range pathTraversalPatterns {
		if pattern.MatchString(input) {
			input = pattern.ReplaceAllString(input, "")
		}
	}

	return input
}

// SanitizeJSON sanitizes a map of JSON data
func SanitizeJSON(data map[string]interface{}) map[string]interface{} {
	sanitized := make(map[string]interface{})

	for key, value := range data {
		switch v := value.(type) {
		case string:
			sanitized[key] = sanitizeString(v)
		case map[string]interface{}:
			sanitized[key] = SanitizeJSON(v)
		case []interface{}:
			sanitized[key] = sanitizeArray(v)
		default:
			sanitized[key] = value
		}
	}

	return sanitized
}

// sanitizeArray sanitizes an array of values
func sanitizeArray(arr []interface{}) []interface{} {
	sanitized := make([]interface{}, len(arr))

	for i, value := range arr {
		switch v := value.(type) {
		case string:
			sanitized[i] = sanitizeString(v)
		case map[string]interface{}:
			sanitized[i] = SanitizeJSON(v)
		case []interface{}:
			sanitized[i] = sanitizeArray(v)
		default:
			sanitized[i] = value
		}
	}

	return sanitized
}

// SanitizeStruct automatically sanitizes all string fields in a struct using reflection
func SanitizeStruct(s interface{}) {
	v := reflect.ValueOf(s)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return
	}

	for i := 0; i < v.NumField(); i++ {
		field := v.Field(i)
		if field.Kind() == reflect.String && field.CanSet() {
			val := field.String()
			field.SetString(sanitizeString(val))
		} else if field.Kind() == reflect.Struct {
			SanitizeStruct(field.Addr().Interface())
		} else if field.Kind() == reflect.Slice {
			for j := 0; j < field.Len(); j++ {
				sliceVal := field.Index(j)
				if sliceVal.Kind() == reflect.Ptr {
					SanitizeStruct(sliceVal.Interface())
				} else if sliceVal.Kind() == reflect.Struct {
					SanitizeStruct(sliceVal.Addr().Interface())
				}
			}
		}
	}
}

// ValidateInput checks if input contains dangerous patterns
// This is used for early rejection of malicious attempts
func ValidateInput(input string) bool {
	// If bluemonday strips out something, it means it had malicious HTML/JS syntax
	sanitized := strictPolicy.Sanitize(input)
	if len(sanitized) < len(input) && strings.ContainsAny(input, "<>") {
		// Strict check: if input had HTML tags that were removed
		return false
	}

	// Check path traversal
	for _, pattern := range pathTraversalPatterns {
		if pattern.MatchString(input) {
			return false
		}
	}

	return true
}
