package resolver

import (
	"context"
	"sync"
	"time"

	"github.com/cockroachdb/errors"

	"github.com/milvus-io/milvus/internal/util/streamingutil/service/discoverer"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/util/syncutil"
	"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)

var _ Resolver = (*resolverWithDiscoverer)(nil)

// newResolverWithDiscoverer creates a new resolver with discoverer.
func newResolverWithDiscoverer(d discoverer.Discoverer, retryInterval time.Duration, logger *mlog.Logger) *resolverWithDiscoverer {
	r := &resolverWithDiscoverer{
		taskNotifier:    syncutil.NewAsyncTaskNotifier[struct{}](),
		registerCh:      make(chan *watchBasedGRPCResolver),
		discoverer:      d,
		retryInterval:   retryInterval,
		latestStateCond: syncutil.NewContextCond(&sync.Mutex{}),
		latestState:     nil,
	}
	r.SetLogger(logger)
	go r.doDiscover()
	return r
}

// versionStateWithError is the versionedState with error.
type versionStateWithError struct {
	state VersionedState
	err   error
}

// resolverWithDiscoverer is the resolver for bkproxy service.
type resolverWithDiscoverer struct {
	taskNotifier *syncutil.AsyncTaskNotifier[struct{}]
	mlog.Binder

	registerCh chan *watchBasedGRPCResolver

	discoverer    discoverer.Discoverer // the discoverer method for the bkproxy service
	retryInterval time.Duration

	latestStateCond *syncutil.ContextCond
	latestState     *discoverer.VersionedState
}

// GetLatestState returns the latest state of the resolver.
func (r *resolverWithDiscoverer) GetLatestState(ctx context.Context) (VersionedState, error) {
	r.latestStateCond.L.Lock()
	for r.latestState == nil {
		if err := r.latestStateCond.Wait(ctx); err != nil {
			return discoverer.VersionedState{}, err
		}
	}
	state := r.latestState
	r.latestStateCond.L.Unlock()
	return *state, nil
}

// getLatestState returns the latest state of the resolver.
func (r *resolverWithDiscoverer) getLatestState() *VersionedState {
	r.latestStateCond.L.Lock()
	defer r.latestStateCond.L.Unlock()
	return r.latestState
}

// mustGetLastestState returns the latest state of the resolver.
func (r *resolverWithDiscoverer) mustGetLastestState() VersionedState {
	state := r.getLatestState()
	if state == nil {
		panic("latest state is nil")
	}
	return *state
}

// Watch watch the state change of the resolver.
func (r *resolverWithDiscoverer) Watch(ctx context.Context, cb func(VersionedState) error) error {
	state, err := r.GetLatestState(ctx)
	if err != nil {
		return errors.Mark(err, ErrCanceled)
	}
	if err := cb(state); err != nil {
		return errors.Mark(err, ErrInterrupted)
	}
	version := state.Version
	for {
		if err := r.watchStateChange(ctx, version); err != nil {
			return errors.Mark(err, ErrCanceled)
		}
		state := r.mustGetLastestState()
		if err := cb(state); err != nil {
			return errors.Mark(err, ErrInterrupted)
		}
		version = state.Version
	}
}

// Close closes the resolver.
func (r *resolverWithDiscoverer) Close() {
	// Cancel underlying task and close the discovery service.
	r.taskNotifier.Cancel()
	r.taskNotifier.BlockUntilFinish()
}

// watchStateChange block util the state is changed.
func (r *resolverWithDiscoverer) watchStateChange(ctx context.Context, version typeutil.Version) error {
	r.latestStateCond.L.Lock()
	for version.EQ(r.latestState.Version) {
		if err := r.latestStateCond.Wait(ctx); err != nil {
			return err
		}
	}
	r.latestStateCond.L.Unlock()
	return nil
}

// RegisterNewWatcher registers a new grpc resolver.
// RegisterNewWatcher should always be call before Close.
func (r *resolverWithDiscoverer) RegisterNewWatcher(grpcResolver *watchBasedGRPCResolver) error {
	select {
	case <-r.taskNotifier.Context().Done():
		return errors.Mark(r.taskNotifier.Context().Err(), ErrCanceled)
	case r.registerCh <- grpcResolver:
		return nil
	}
}

// doDiscover do the discovery on background.
func (r *resolverWithDiscoverer) doDiscover() {
	grpcResolvers := make(map[*watchBasedGRPCResolver]struct{}, 0)
	defer func() {
		// Check if all grpc resolver is stopped.
		for r := range grpcResolvers {
			if r.State() == typeutil.LifetimeStateWorking {
				r.Logger().Warn(context.TODO(), "resolver is stopped before grpc watcher exist, maybe bug here")
				break
			}
		}
		r.Logger().Info(context.TODO(), "resolver stopped")
		r.taskNotifier.Finish(struct{}{})
	}()

	for {
		ch := r.asyncDiscover(r.taskNotifier.Context())
		r.Logger().Info(context.TODO(), "service discover task started, listening...")
	L:
		for {
			select {
			case watcher := <-r.registerCh:
				watcher.Logger().Info(context.TODO(), "new grpc resolver registered")
				// New grpc resolver registered.
				// Trigger the latest state to the new grpc resolver.
				grpcResolvers[watcher] = struct{}{}
				state := r.getLatestState()
				if state == nil {
					continue
				}
				if err := watcher.Update(*state); err != nil {
					r.Logger().Info(context.TODO(), "resolver is closed, ignore the new grpc resolver", mlog.Err(err))
					delete(grpcResolvers, watcher)
				}
			case stateWithError := <-ch:
				if stateWithError.err != nil {
					if r.taskNotifier.Context().Err() != nil {
						// resolver stopped.
						return
					}
					r.Logger().Warn(context.TODO(), "service discover break down", mlog.Err(stateWithError.err), mlog.Duration("retryInterval", r.retryInterval))
					time.Sleep(r.retryInterval)
					break L
				}

				// Check if the state is the newer.
				state := stateWithError.state
				latestState := r.getLatestState()
				if latestState != nil && !state.Version.GT(latestState.Version) {
					// Ignore the old version.
					r.Logger().Info(context.TODO(), "service discover update, ignore old version", mlog.Stringer("state", state))
					continue
				}
				// Update all grpc resolver.
				r.Logger().Info(context.TODO(),
					"service discover update, update resolver", mlog.Stringer("state", state), mlog.Int("resolver_count", len(grpcResolvers)))
				for watcher := range grpcResolvers {
					// Update operation do not block.
					// Only return error if the resolver is closed, so just print a info log and delete the resolver.
					if err := watcher.Update(state); err != nil {
						// updateError is always context.Canceled.
						r.Logger().Info(context.TODO(), "resolver is closed, unregister the resolver", mlog.NamedError("updateError", err))
						delete(grpcResolvers, watcher)
					}
				}
				r.Logger().Info(context.TODO(), "update resolver done")
				// Update the latest state and notify all resolver watcher should be executed after the all grpc watcher updated.
				r.latestStateCond.LockAndBroadcast()
				r.latestState = &state
				r.latestStateCond.L.Unlock()
			}
		}
	}
}

// asyncDiscover is a non-blocking version of Discover.
func (r *resolverWithDiscoverer) asyncDiscover(ctx context.Context) <-chan versionStateWithError {
	ch := make(chan versionStateWithError, 1)
	go func() {
		err := r.discoverer.Discover(ctx, func(vs discoverer.VersionedState) error {
			ch <- versionStateWithError{
				state: vs,
			}
			return nil
		})
		ch <- versionStateWithError{err: err}
	}()
	return ch
}
