package httprc

import (
	"context"
	"net/http"
	"sync"
	"time"

	"github.com/lestrrat-go/httprc/v3/errsink"
	"github.com/lestrrat-go/httprc/v3/proxysink"
	"github.com/lestrrat-go/httprc/v3/tracesink"
)

// setupSink creates and starts a proxy for the given sink if it's not a Nop sink
// Returns the sink to use and a cancel function that should be chained with the original cancel
func setupSink[T any, S proxysink.Backend[T], NopType any](ctx context.Context, sink S, wg *sync.WaitGroup) (S, context.CancelFunc) {
	if _, ok := any(sink).(NopType); ok {
		return sink, func() {}
	}

	proxy := proxysink.New[T](sink)
	wg.Add(1)
	go func(ctx context.Context, wg *sync.WaitGroup, proxy *proxysink.Proxy[T]) {
		defer wg.Done()
		proxy.Run(ctx)
	}(ctx, wg, proxy)

	// proxy can be converted to one of the sink subtypes
	s, ok := any(proxy).(S)
	if !ok {
		panic("type assertion failed: proxy cannot be converted to type S")
	}
	return s, proxy.Close
}

// Client is the main entry point for the httprc package.
type Client struct {
	mu                 sync.Mutex
	httpcl             HTTPClient
	numWorkers         int
	running            bool
	errSink            ErrorSink
	traceSink          TraceSink
	wl                 Whitelist
	defaultMaxInterval time.Duration
	defaultMinInterval time.Duration
}

// NewClient creates a new `httprc.Client` object.
//
// By default ALL urls are allowed. This may not be suitable for you if
// are using this in a production environment. You are encouraged to specify
// a whitelist using the `WithWhitelist` option.
//
// NOTE: In future versions, this function signature should be changed to
// return an error to properly handle option parsing failures.
func NewClient(options ...NewClientOption) *Client {
	//nolint:staticcheck
	var errSink ErrorSink = errsink.NewNop()
	//nolint:staticcheck
	var traceSink TraceSink = tracesink.NewNop()
	var wl Whitelist = InsecureWhitelist{}
	var httpcl HTTPClient = http.DefaultClient

	defaultMinInterval := DefaultMinInterval
	defaultMaxInterval := DefaultMaxInterval

	numWorkers := DefaultWorkers
	for _, option := range options {
		switch option.Ident() {
		case identHTTPClient{}:
			_ = option.Value(&httpcl)
		case identWorkers{}:
			_ = option.Value(&numWorkers)
		case identErrorSink{}:
			_ = option.Value(&errSink)
		case identTraceSink{}:
			_ = option.Value(&traceSink)
		case identWhitelist{}:
			_ = option.Value(&wl)
		}
	}

	if numWorkers <= 0 {
		numWorkers = 1
	}
	return &Client{
		httpcl:     httpcl,
		numWorkers: numWorkers,
		errSink:    errSink,
		traceSink:  traceSink,
		wl:         wl,

		defaultMinInterval: defaultMinInterval,
		defaultMaxInterval: defaultMaxInterval,
	}
}

// Start sets the client into motion. It will start a number of worker goroutines,
// and return a Controller object that you can use to control the execution of
// the client.
//
// If you attempt to call Start more than once, it will return an error.
func (c *Client) Start(octx context.Context) (Controller, error) {
	c.mu.Lock()
	if c.running {
		c.mu.Unlock()
		return nil, errAlreadyRunning
	}
	c.running = true
	c.mu.Unlock()

	// DON'T CANCEL THIS IN THIS METHOD! It's the responsibility of the
	// controller to cancel this context.
	ctx, cancel := context.WithCancel(octx)

	var donewg sync.WaitGroup

	// start proxy goroutines that will accept sink requests
	// and forward them to the appropriate sink
	errSink, errCancel := setupSink[error, ErrorSink, errsink.Nop](ctx, c.errSink, &donewg)
	traceSink, traceCancel := setupSink[string, TraceSink, tracesink.Nop](ctx, c.traceSink, &donewg)

	// Chain the cancel functions
	ocancel := cancel
	cancel = func() {
		ocancel()
		errCancel()
		traceCancel()
	}

	chbuf := c.numWorkers + 1
	incoming := make(chan any, chbuf)
	outgoing := make(chan Resource, chbuf)
	syncoutgoing := make(chan synchronousRequest, chbuf)

	var readywg sync.WaitGroup
	readywg.Add(c.numWorkers)
	donewg.Add(c.numWorkers)
	for range c.numWorkers {
		wrk := worker{
			incoming:  incoming,
			next:      outgoing,
			nextsync:  syncoutgoing,
			errSink:   errSink,
			traceSink: traceSink,
			httpcl:    c.httpcl,
		}
		go wrk.Run(ctx, &readywg, &donewg)
	}

	tickInterval := oneDay
	ctrl := &controller{
		cancel:    cancel,
		incoming:  incoming,
		shutdown:  make(chan struct{}),
		traceSink: traceSink,
		wl:        c.wl,
	}

	backend := &ctrlBackend{
		items:        make(map[string]Resource),
		outgoing:     outgoing,
		syncoutgoing: syncoutgoing,
		incoming:     incoming,
		traceSink:    traceSink,
		tickInterval: tickInterval,
		check:        time.NewTicker(tickInterval),

		defaultMinInterval: c.defaultMinInterval,
		defaultMaxInterval: c.defaultMaxInterval,
	}
	donewg.Add(1)
	readywg.Add(1)
	go backend.loop(ctx, &readywg, &donewg)

	go func(wg *sync.WaitGroup, ch chan struct{}) {
		wg.Wait()
		close(ch)
	}(&donewg, ctrl.shutdown)

	readywg.Wait()

	return ctrl, nil
}
