// Package protoutil contains protobuf helpers that are independent of any
// application module.
package protoutil

import (
	"encoding/json"
	"strings"

	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

// MergeProtoJSON applies a JSON object as a partial protobuf update while
// retaining fields omitted by the caller.
func MergeProtoJSON(target proto.Message, patch json.RawMessage, options protojson.UnmarshalOptions) error {
	currentRaw, err := protojson.MarshalOptions{UseProtoNames: false}.Marshal(target)
	if err != nil {
		return err
	}
	var current map[string]any
	if err = json.Unmarshal(currentRaw, &current); err != nil {
		return err
	}
	var incoming map[string]any
	if err = json.Unmarshal(patch, &incoming); err != nil {
		return err
	}
	incoming = normalizeJSONKeys(incoming).(map[string]any)
	mergeJSONObjects(current, incoming)
	merged, err := json.Marshal(current)
	if err != nil {
		return err
	}
	return options.Unmarshal(merged, target)
}

func normalizeJSONKeys(value any) any {
	switch item := value.(type) {
	case map[string]any:
		result := make(map[string]any, len(item))
		for key, nested := range item {
			result[snakeToLowerCamel(key)] = normalizeJSONKeys(nested)
		}
		return result
	case []any:
		result := make([]any, len(item))
		for index, nested := range item {
			result[index] = normalizeJSONKeys(nested)
		}
		return result
	default:
		return value
	}
}

func snakeToLowerCamel(value string) string {
	if !strings.Contains(value, "_") {
		return value
	}
	parts := strings.Split(value, "_")
	result := parts[0]
	for _, part := range parts[1:] {
		if part != "" {
			result += strings.ToUpper(part[:1]) + part[1:]
		}
	}
	return result
}

func mergeJSONObjects(target, patch map[string]any) {
	for key, value := range patch {
		if incoming, ok := value.(map[string]any); ok {
			if existing, exists := target[key].(map[string]any); exists {
				mergeJSONObjects(existing, incoming)
				continue
			}
		}
		target[key] = value
	}
}
