package nucleus

import (
	"context"
	"fmt"

	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkdomain"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkelement"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/transport"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/types"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"code.fbi.h-da.de/danet/gosdn/forks/goarista/gnmi"
	"github.com/google/uuid"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
	log "github.com/sirupsen/logrus"
)

const (
	subscribeSampleInterval uint64 = 1000000000 // 1 second in nanoseconds
	// TODO: These gNMI options are adjusted to arista gNMI fork. Change when switching to native gNMI.
	gNMISubscribeMode string = "stream"
	gNMIStreamMode    string = "on_change"
)

// NetworkElementWatcher is a component that subscribes to devices via gNMI from within the controller and handles
// responses by triggering the internal event process.
type NetworkElementWatcher struct {
	pndStore           networkdomain.PndStore
	deviceSubcriptions map[uuid.UUID]*deviceSubscriptionHelper
}

// deviceSubscriptionHelper is used to store information to stop a running subscribe go routine.
type deviceSubscriptionHelper struct {
	stopSubscribeCtx context.Context
	stopFunc         context.CancelFunc
}

// NewNetworkElementWatcher takes a pndStore to subscribe to network element paths.
func NewNetworkElementWatcher(pndStore networkdomain.PndStore) *NetworkElementWatcher {
	return &NetworkElementWatcher{
		pndStore:           pndStore,
		deviceSubcriptions: make(map[uuid.UUID]*deviceSubscriptionHelper),
	}
}

// SubToDevices subscribes to every available network element in each network domain according to provided SubscribeOptions.
// Paths should be provided in the following format [][]string{{"system", "config", "hostname"}}
// SubscribeOptions can be nil. Use nil for a fixed, pre-defined set of gNMI subscription options (streaming in sample mode each second).
func (d *NetworkElementWatcher) SubToDevices(paths [][]string, opts *gnmi.SubscribeOptions) {
	if opts == nil {
		opts = &gnmi.SubscribeOptions{
			Mode:           gNMISubscribeMode,
			StreamMode:     gNMIStreamMode,
			Paths:          paths,
			SampleInterval: subscribeSampleInterval,
		}
	}

	pnds, err := d.pndStore.GetAll()
	if err != nil {
		log.Error(err)
	}

	for _, pnd := range pnds {
		d.subscribeToPndDevices(pnd.ID().String(), pnd, opts)
	}
}

func (d *NetworkElementWatcher) subscribeToPndDevices(pndID string, pnd networkdomain.NetworkDomain, opts *gnmi.SubscribeOptions) {
	for _, mne := range pnd.NetworkElements() {
		subID := uuid.New()

		stopContext, cancel := context.WithCancel(context.Background())
		d.addToDeviceSubscriptions(subID, &deviceSubscriptionHelper{
			stopSubscribeCtx: stopContext,
			stopFunc:         cancel,
		})
		go d.callSubscribe(stopContext, pndID, mne, opts)
	}
}

func (d *NetworkElementWatcher) callSubscribe(stopContext context.Context, pndID string, mne networkelement.NetworkElement, opts *gnmi.SubscribeOptions) {
	gNMIOptionsCtx := context.Background()
	gNMIOptionsCtx = context.WithValue(gNMIOptionsCtx, types.CtxKeyOpts, opts)

	// SubscriptionInformation contains pnd ID, network element ID and name to be used in the internal subscribe to check
	// from which network element a response was sent
	if err := mne.Transport().ControlPlaneSubscribe(gNMIOptionsCtx, d.handleSubscribeResponse, &transport.SubscriptionInformation{
		PndID:       pndID,
		DeviceID:    mne.ID().String(),
		DeviceName:  mne.Name(),
		StopContext: stopContext,
	}); err != nil {
		log.Error(err)
	}
}

func (d *NetworkElementWatcher) addToDeviceSubscriptions(subID uuid.UUID, devSub *deviceSubscriptionHelper) {
	//TODO: improve handling of subscriptions, like be able to expose to apps so specific subscriptions instead of only all can be stopped in the future
	d.deviceSubcriptions[subID] = devSub
}

// StopAndRemoveAllDeviceSubscriptions stops and removes all the available running subscriptions.
func (d *NetworkElementWatcher) StopAndRemoveAllDeviceSubscriptions() {
	for key := range d.deviceSubcriptions {
		d.StopAndRemoveDeviceSubscription(key)
	}
}

// StopAndRemoveDeviceSubscription passes a subscription uuid to stop the running subscription go routing and removes the entry from the map
// of network element subscriptions.
func (d *NetworkElementWatcher) StopAndRemoveDeviceSubscription(subID uuid.UUID) {
	d.deviceSubcriptions[subID].stopFunc()
	delete(d.deviceSubcriptions, subID)
}

// handleSubscribeResponse takes the subscribe response and additional information about the network element to distinguish
// from which network element a subscribe response was sent including improved error handling.
func (d *NetworkElementWatcher) handleSubscribeResponse(resp *gpb.SubscribeResponse, subscriptionInfo *transport.SubscriptionInformation) {
	switch resp := resp.Response.(type) {
	case *gpb.SubscribeResponse_Error:
		log.Error(&customerrs.SubscribeResponseError{
			PndID:      subscriptionInfo.PndID,
			DeviceID:   subscriptionInfo.DeviceID,
			DeviceName: subscriptionInfo.DeviceName,
			Err:        fmt.Sprint("SubscribeResponse_Error"),
		})
	case *gpb.SubscribeResponse_SyncResponse:
		if !resp.SyncResponse {
			log.Error(&customerrs.SubscribeSyncResponseError{
				PndID:      subscriptionInfo.PndID,
				DeviceID:   subscriptionInfo.DeviceID,
				DeviceName: subscriptionInfo.DeviceName,
			})
		}
	case *gpb.SubscribeResponse_Update:
		d.handleSubscribeResponseUpdate(resp, subscriptionInfo)
	default:
		log.Infof("Invalid SubscribeResponse, %v", resp)
	}
}

func (d *NetworkElementWatcher) handleSubscribeResponseUpdate(resp *gpb.SubscribeResponse_Update, subscriptionInfo *transport.SubscriptionInformation) {
	pndID, err := uuid.Parse(subscriptionInfo.PndID)
	if err != nil {
		log.Error(err)
	}

	pnd, err := d.pndStore.Get(store.Query{ID: pndID})
	if err != nil {
		log.Error(err)
	}

	mne, err := pnd.GetNetworkElement(subscriptionInfo.DeviceID)
	if err != nil {
		log.Error(err)
	}

	err = mne.Transport().ProcessControlPlaneSubscribeResponse(resp, mne.GetModel(), mne.SBI().Schema())
	if err != nil {
		log.Error(err)
	} else {
		if err := pnd.UpdateNetworkElementAfterSubscribeResponse(mne); err != nil {
			log.Error(err)
		}
	}
}