package dns

import (
	"context"
	"maps"
	"slices"
	"sync"

	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing/common"
	E "github.com/sagernet/sing/common/exceptions"
)

type TransportConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.DNSTransport, error)

func RegisterTransport[Options any](registry *TransportRegistry, transportType string, constructor TransportConstructorFunc[Options]) {
	registry.register(transportType, func() any {
		return new(Options)
	}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.DNSTransport, error) {
		var options *Options
		if rawOptions != nil {
			options = rawOptions.(*Options)
		}
		return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
	})
}

var _ adapter.DNSTransportRegistry = (*TransportRegistry)(nil)

type (
	optionsConstructorFunc func() any
	constructorFunc        func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.DNSTransport, error)
)

type TransportRegistry struct {
	access       sync.Mutex
	optionsType  map[string]optionsConstructorFunc
	constructors map[string]constructorFunc
}

func NewTransportRegistry() *TransportRegistry {
	return &TransportRegistry{
		optionsType:  make(map[string]optionsConstructorFunc),
		constructors: make(map[string]constructorFunc),
	}
}

func (r *TransportRegistry) OptionTypes() []string {
	r.access.Lock()
	defer r.access.Unlock()
	return slices.Sorted(maps.Keys(r.optionsType))
}

func (r *TransportRegistry) CreateOptions(transportType string) (any, bool) {
	r.access.Lock()
	defer r.access.Unlock()
	optionsConstructor, loaded := r.optionsType[transportType]
	if !loaded {
		return nil, false
	}
	return optionsConstructor(), true
}

func (r *TransportRegistry) CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (adapter.DNSTransport, error) {
	r.access.Lock()
	defer r.access.Unlock()
	constructor, loaded := r.constructors[transportType]
	if !loaded {
		return nil, E.New("transport type not found: " + transportType)
	}
	return constructor(ctx, logger, tag, options)
}

func (r *TransportRegistry) register(transportType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
	r.access.Lock()
	defer r.access.Unlock()
	r.optionsType[transportType] = optionsConstructor
	r.constructors[transportType] = constructor
}
