package badger

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"time"

	storepkg "github.com/cirruslabs/orchard/internal/controller/store"
	"github.com/cirruslabs/orchard/pkg/resource/v1"
	"github.com/dgraph-io/badger/v3"
	"github.com/dgraph-io/badger/v3/pb"
)

func (store *Store) WatchVM(ctx context.Context, vmName string) (chan storepkg.WatchMessage[v1.VM], chan error, error) {
	readyCh := make(chan struct{}, 1)
	watchCh := make(chan storepkg.WatchMessage[v1.VM], 1)
	errCh := make(chan error, 1)
	subCtx, subCtxCancel := context.WithCancel(ctx)

	go func() {
		defer subCtxCancel()
		defer close(watchCh)
		defer close(errCh)

		var initialVM *v1.VM
		var checkedInitialVM bool

		if err := store.db.Subscribe(subCtx, func(kvList *badger.KVList) error {
			if !checkedInitialVM {
				// Notify the caller that we've subscribed, but don't block,
				// because we may observe multiple watch barriers, yet
				// we only need a single barrier to make things work
				select {
				case readyCh <- struct{}{}:
				default:
				}

				// Now that the subscription has started,
				// retrieve the initial VM, if any
				err := store.View(func(txn storepkg.Transaction) error {
					var err error

					initialVM, err = txn.GetVM(vmName)

					return err
				})
				if err != nil && !errors.Is(err, storepkg.ErrNotFound) {
					return err
				}
				if initialVM != nil {
					notification := storepkg.WatchMessage[v1.VM]{
						Type:   storepkg.WatchMessageTypeAdded,
						Object: *initialVM,
					}

					select {
					case watchCh <- notification:
					case <-subCtx.Done():
						return subCtx.Err()
					}
				}

				checkedInitialVM = true
			}

			for _, kv := range kvList.GetKv() {
				switch {
				case bytes.Equal(kv.GetKey(), WatchBarrierKey()):
					// We only need watch barriers so that the Subscribe()'s callback
					// is called at least once, thus we can simply do nothing here
				case bytes.Equal(kv.GetKey(), VMKey(vmName)):
					// Skip all KVs with versions before or equal
					// to the initial VM's version, if any
					if initialVM != nil && kv.GetVersion() <= initialVM.Version {
						continue
					}

					if kv.GetValue() == nil {
						if initialVM == nil {
							// VM is already deleted
							continue
						}

						// VM was deleted
						notification := storepkg.WatchMessage[v1.VM]{
							Type: storepkg.WatchMessageTypeDeleted,
						}

						select {
						case watchCh <- notification:
						case <-subCtx.Done():
							return subCtx.Err()
						}

						initialVM = nil
					} else {
						// VM was created or modified
						var vm v1.VM

						if err := json.Unmarshal(kv.GetValue(), &vm); err != nil {
							return err
						}

						vm.Version = kv.GetVersion()

						var watchMessageType storepkg.WatchMessageType

						if initialVM == nil {
							watchMessageType = storepkg.WatchMessageTypeAdded

							initialVM = &vm
						} else {
							watchMessageType = storepkg.WatchMessageTypeModified
						}

						notification := storepkg.WatchMessage[v1.VM]{
							Type:   watchMessageType,
							Object: vm,
						}

						select {
						case watchCh <- notification:
						case <-subCtx.Done():
							return subCtx.Err()
						}
					}
				default:
					// Ignore unexpected keys
					continue
				}
			}

			return nil
		}, []pb.Match{
			{
				Prefix: WatchBarrierKey(),
			},
			{
				Prefix: VMKey(vmName),
			},
		}); err != nil {
			errCh <- err
		}
	}()

	// Trigger the watch barrier so that Subscribe() callback gets invoked
	if err := store.notifyWatchBarrier(); err != nil {
		subCtxCancel()

		return nil, nil, err
	}

	// Wait for the Subscribe() callback to be invoked
Outer:
	for {
		select {
		case <-readyCh:
			// Subscription has started
			break Outer
		case <-time.After(time.Second):
			// Possible race with late goroutine start, re-issue watch barrier
			if err := store.notifyWatchBarrier(); err != nil {
				subCtxCancel()

				return nil, nil, err
			}
		case <-ctx.Done():
			return nil, nil, ctx.Err()
		}
	}

	return watchCh, errCh, nil
}
