package provider

import (
	"context"
	"maps"
	"reflect"
	"slices"
	"strings"

	"github.com/huandu/xstrings"
	"github.com/rs/zerolog/log"
	"github.com/traefik/traefik/v3/pkg/config/dynamic"
	"github.com/traefik/traefik/v3/pkg/observability/logs"
	"github.com/traefik/traefik/v3/pkg/tls"
)

type resourceMeta struct {
	logField    string
	displayName string
}

var resourceLogFields = map[reflect.Type]resourceMeta{
	reflect.TypeFor[dynamic.Router]():              {logs.RouterName, "HTTP router"},
	reflect.TypeFor[dynamic.Service]():             {logs.ServiceName, "HTTP service"},
	reflect.TypeFor[dynamic.Middleware]():          {logs.MiddlewareName, "HTTP middleware"},
	reflect.TypeFor[dynamic.ServersTransport]():    {logs.ServersTransportName, "HTTP servers transport"},
	reflect.TypeFor[dynamic.TCPRouter]():           {logs.RouterName, "TCP router"},
	reflect.TypeFor[dynamic.TCPService]():          {logs.ServiceName, "TCP service"},
	reflect.TypeFor[dynamic.TCPMiddleware]():       {logs.MiddlewareName, "TCP middleware"},
	reflect.TypeFor[dynamic.TCPServersTransport](): {logs.ServersTransportName, "TCP servers transport"},
	reflect.TypeFor[dynamic.UDPRouter]():           {logs.RouterName, "UDP router"},
	reflect.TypeFor[dynamic.UDPService]():          {logs.ServiceName, "UDP service"},
}

// ResourceStrategy defines how the merge should handle resources.
type ResourceStrategy int

const (
	// ResourceStrategyMerge tries to call the Merge method on the resource.
	ResourceStrategyMerge ResourceStrategy = iota
	// ResourceStrategySkipDuplicates skips duplicate resources.
	ResourceStrategySkipDuplicates
)

// NamedConfiguration is a configuration with its name.
type NamedConfiguration struct {
	Name          string
	Configuration *dynamic.Configuration
}

// NameSortedConfigurations returns the configurations sorted by name.
func NameSortedConfigurations(configurations map[string]*dynamic.Configuration) []NamedConfiguration {
	origins := slices.Sorted(maps.Keys(configurations))

	sorted := make([]NamedConfiguration, 0, len(origins))
	for _, origin := range origins {
		sorted = append(sorted, NamedConfiguration{Name: origin, Configuration: configurations[origin]})
	}

	return sorted
}

// Merge merges multiple configurations.
func Merge(ctx context.Context, configurations []NamedConfiguration, strategy ResourceStrategy) *dynamic.Configuration {
	merged := &dynamic.Configuration{
		HTTP: &dynamic.HTTPConfiguration{
			Routers:           make(map[string]*dynamic.Router),
			Middlewares:       make(map[string]*dynamic.Middleware),
			Services:          make(map[string]*dynamic.Service),
			ServersTransports: make(map[string]*dynamic.ServersTransport),
		},
		TCP: &dynamic.TCPConfiguration{
			Routers:           make(map[string]*dynamic.TCPRouter),
			Services:          make(map[string]*dynamic.TCPService),
			Middlewares:       make(map[string]*dynamic.TCPMiddleware),
			ServersTransports: make(map[string]*dynamic.TCPServersTransport),
		},
		UDP: &dynamic.UDPConfiguration{
			Routers:  make(map[string]*dynamic.UDPRouter),
			Services: make(map[string]*dynamic.UDPService),
		},
		TLS: &dynamic.TLSConfiguration{
			Stores: make(map[string]tls.Store),
		},
	}

	tracker := newMergeTracker()

	for _, c := range configurations {
		if c.Configuration.HTTP != nil {
			mergeResourceMaps(ctx, reflect.ValueOf(merged.HTTP).Elem(), reflect.ValueOf(c.Configuration.HTTP).Elem(), c.Name, tracker, strategy)
		}
		if c.Configuration.TCP != nil {
			mergeResourceMaps(ctx, reflect.ValueOf(merged.TCP).Elem(), reflect.ValueOf(c.Configuration.TCP).Elem(), c.Name, tracker, strategy)
		}
		if c.Configuration.UDP != nil {
			mergeResourceMaps(ctx, reflect.ValueOf(merged.UDP).Elem(), reflect.ValueOf(c.Configuration.UDP).Elem(), c.Name, tracker, strategy)
		}
		if c.Configuration.TLS != nil {
			mergeResourceMaps(ctx, reflect.ValueOf(merged.TLS).Elem(), reflect.ValueOf(c.Configuration.TLS).Elem(), c.Name, tracker, strategy)

			merged.TLS.Certificates = mergeCertificates(ctx, merged.TLS.Certificates, c.Configuration.TLS.Certificates, c.Name, strategy)
		}
	}

	deleteConflicts(ctx, tracker)

	return merged
}

// mergeResourceMaps merges all the resource maps defined in the provided struct.
// Conflicts are recorded in the given merge tracker.
func mergeResourceMaps(ctx context.Context, dst, src reflect.Value, origin string, tracker *mergeTracker, strategy ResourceStrategy) {
	dstType := dst.Type()

	for i := range dstType.NumField() {
		field := dstType.Field(i)
		if !field.IsExported() {
			continue
		}

		dstField := dst.Field(i)
		srcField := src.Field(i)

		// Merge the resource maps of embedded structs.
		if field.Anonymous {
			mergeResourceMaps(ctx, dstField, srcField, origin, tracker, strategy)
			continue
		}

		if dstField.Kind() == reflect.Map {
			mergeResourceMap(ctx, dstField, srcField, origin, tracker, strategy)
		}
	}
}

// mergeResourceMap merges a resource map src into dst.
// New keys from src are added to dst.
// Duplicate keys are merged if the resource type implements a Merge method, otherwise
// the values must be identical. Conflicts are recorded in the given merge tracker.
func mergeResourceMap(ctx context.Context, dst, src reflect.Value, origin string, tracker *mergeTracker, strategy ResourceStrategy) {
	if src.IsNil() {
		return
	}

	if dst.IsNil() {
		dst.Set(reflect.MakeMap(dst.Type()))
	}

	for _, resourceKey := range src.MapKeys() {
		resourceKeyStr := resourceKey.String()
		tracker.recordOrigin(dst, resourceKeyStr, origin)

		srcValue := src.MapIndex(resourceKey)
		dstValue := dst.MapIndex(resourceKey)

		// Key doesn't exist in dst, add it.
		if !dstValue.IsValid() {
			dst.SetMapIndex(resourceKey, srcValue)
			continue
		}

		// Key exists, need to merge or detect conflict.
		switch strategy {
		case ResourceStrategyMerge:
			if !tryMerge(dstValue, srcValue) {
				tracker.markForDeletion(dst, resourceKeyStr, dst.Type().Elem())
			}
		case ResourceStrategySkipDuplicates:
			logSkippedDuplicate(ctx, dst.Type().Elem(), resourceKeyStr, origin)
		}
	}
}

// tryMerge attempts to merge two resources.
// Returns true if the merge succeeds, false if values conflict.
func tryMerge(dst, src reflect.Value) bool {
	if dst.Kind() != reflect.Ptr {
		return reflect.DeepEqual(dst.Interface(), src.Interface())
	}

	if dst.IsNil() || src.IsNil() {
		return reflect.DeepEqual(dst.Interface(), src.Interface())
	}

	// Check if the struct has the method `func (* T) Merge(other T) bool`.
	// We use reflection to detect this method because Go's type system doesn't allow type assertions
	// on generic interfaces (Mergeable[T]) practically.
	mergeMethod := dst.MethodByName("Merge")
	if mergeMethod.IsValid() {
		methodType := mergeMethod.Type()
		if methodType.NumIn() == 1 && methodType.NumOut() == 1 && methodType.Out(0).Kind() == reflect.Bool {
			// Make sure the parameter type matches the type holding the method.
			if methodType.In(0).AssignableTo(src.Type()) {
				results := mergeMethod.Call([]reflect.Value{src})
				return results[0].Bool()
			}
		}
	}

	// When Merge is not implemented, merge is not allowed; the values must be the same.
	return reflect.DeepEqual(dst.Elem().Interface(), src.Elem().Interface())
}

// deleteConflicts removes conflicting items and logs errors.
func deleteConflicts(ctx context.Context, tracker *mergeTracker) {
	logger := log.Ctx(ctx)

	for ck, info := range tracker.toDelete {
		resourceNameField, resourceTypeWords := resourceLogMeta(info.resourceType)
		logger.Error().
			Str(resourceNameField, ck.resourceKey).
			Interface("configuration", tracker.origins[ck]).
			Msgf("%s defined multiple times with different configurations", resourceTypeWords)

		info.resourceMap.SetMapIndex(reflect.ValueOf(ck.resourceKey), reflect.Value{})
	}
}

// mergeCertificates merges multiple certificates.
func mergeCertificates(ctx context.Context, certificates []*tls.CertAndStores, newCertificates []*tls.CertAndStores, origin string, strategy ResourceStrategy) []*tls.CertAndStores {
	for _, certificate := range newCertificates {
		var found bool
		for _, existingCertificate := range certificates {
			if existingCertificate.Certificate == certificate.Certificate {
				found = true

				switch strategy {
				case ResourceStrategyMerge:
					existingCertificate.Stores = mergeStores(existingCertificate.Stores, certificate.Stores)
				case ResourceStrategySkipDuplicates:
					log.Ctx(ctx).Warn().
						Str("origin", origin).
						Msgf("TLS certificate %v already configured, skipping", certificate.Certificate)
				}

				break
			}
		}

		if !found {
			certificates = append(certificates, certificate)
		}
	}

	return certificates
}

// mergeStores merges two store slices, deduplicating entries while. Order is preserved.
func mergeStores(existing, other []string) []string {
	seen := make(map[string]struct{}, len(existing))
	for _, s := range existing {
		seen[s] = struct{}{}
	}

	for _, s := range other {
		if _, ok := seen[s]; !ok {
			existing = append(existing, s)
			seen[s] = struct{}{}
		}
	}

	return existing
}

// logSkippedDuplicate logs a warning when a duplicate resource is skipped.
func logSkippedDuplicate(ctx context.Context, resourceType reflect.Type, resourceKey, origin string) {
	resourceNameField, resourceTypeWords := resourceLogMeta(resourceType)

	log.Ctx(ctx).Warn().
		Str("origin", origin).
		Str(resourceNameField, resourceKey).
		Msgf("%s already configured, skipping", resourceTypeWords)
}

// resourceLogMeta returns the log field name and human-readable type description for the given resource element type.
func resourceLogMeta(resourceType reflect.Type) (resourceNameField, resourceTypeWords string) {
	if resourceType.Kind() == reflect.Ptr {
		resourceType = resourceType.Elem()
	}

	meta, ok := resourceLogFields[resourceType]
	if ok {
		return meta.logField, meta.displayName
	}

	resourceTypeName := resourceType.Name()
	resourceNameField = xstrings.ToCamelCase(resourceTypeName) + "Name"
	resourceTypeWords = strings.ReplaceAll(xstrings.ToKebabCase(resourceTypeName), "-", " ")

	return resourceNameField, resourceTypeWords
}

// mergeTracker tracks item origins and items marked for deletion during merge.
type mergeTracker struct {
	toDelete map[conflictKey]conflictInfo
	origins  map[conflictKey][]string
}

// conflictKey uniquely identifies an entry in a map.
type conflictKey struct {
	mapPtr      uintptr
	resourceKey string
}

// conflictInfo stores information about a merge conflict.
type conflictInfo struct {
	resourceMap  reflect.Value // The map to delete from.
	resourceType reflect.Type
}

func newMergeTracker() *mergeTracker {
	return &mergeTracker{
		toDelete: make(map[conflictKey]conflictInfo),
		origins:  make(map[conflictKey][]string),
	}
}

func (t *mergeTracker) recordOrigin(resourceMap reflect.Value, resourceKey, origin string) {
	ck := conflictKey{mapPtr: resourceMap.Pointer(), resourceKey: resourceKey}
	t.origins[ck] = append(t.origins[ck], origin)
}

func (t *mergeTracker) markForDeletion(resourceMap reflect.Value, resourceKey string, resourceType reflect.Type) {
	ck := conflictKey{mapPtr: resourceMap.Pointer(), resourceKey: resourceKey}
	t.toDelete[ck] = conflictInfo{
		resourceMap:  resourceMap,
		resourceType: resourceType,
	}
}
