package nucleus

import (
	"fmt"

	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	"code.fbi.h-da.de/danet/gosdn/controller/event"
	eventInterfaces "code.fbi.h-da.de/danet/gosdn/controller/interfaces/event"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkelement"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/plugin"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"github.com/google/uuid"
	"github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/ygot/ygot"

	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
	log "github.com/sirupsen/logrus"
)

const (
	// NetworkElementEventTopic is the used topic for network element related entity changes.
	NetworkElementEventTopic = "managedNetworkElement"
)

// NetworkElementService provides a network element service implementation.
// This services provides abstraction between the user (e.g a PND) and the matching store (e.g. networkElementStore).
type NetworkElementService struct {
	networkElementStore networkelement.Store
	pluginService       plugin.Service
	eventService        eventInterfaces.Service
}

// NewNetworkElementService creates a network element service.
func NewNetworkElementService(
	networkElementStore networkelement.Store,
	pluginService plugin.Service,
	eventService eventInterfaces.Service,
) networkelement.Service {
	return &NetworkElementService{
		networkElementStore: networkElementStore,
		pluginService:       pluginService,
		eventService:        eventService,
	}
}

// Get takes a network element's UUID or name and returns the network element.
func (s *NetworkElementService) Get(query store.Query) (networkelement.NetworkElement, error) {
	loadedNetworkElement, err := s.networkElementStore.Get(query)
	if err != nil {
		return nil, err
	}

	mne, err := s.createNetworkElementFromStore(loadedNetworkElement)
	if err != nil {
		return nil, err
	}

	return mne, nil
}

// GetAll returns all stored network elements.
func (s *NetworkElementService) GetAll() ([]networkelement.NetworkElement, error) {
	var mnes []networkelement.NetworkElement

	loadedNetworkElements, err := s.networkElementStore.GetAll()
	if err != nil {
		return nil, err
	}

	for _, loadedNetworkElement := range loadedNetworkElements {
		mne, err := s.createNetworkElementFromStore(loadedNetworkElement)
		if err != nil {
			return nil, err
		}

		mnes = append(mnes, mne)
	}

	return mnes, nil
}

// GetAllAsLoaded returns all stored network elements as LoadedNetworkElement.
// This method should be used if there is no need for a networkelement.NetworkElement, since
// requesting network element information through this method is a lot faster than the
// usual `GetAll` method.
func (s *NetworkElementService) GetAllAsLoaded() ([]networkelement.LoadedNetworkElement, error) {
	loadedNetworkElements, err := s.networkElementStore.GetAll()
	if err != nil {
		return nil, err
	}

	return loadedNetworkElements, nil
}

// Add adds a network element to the network element store.
func (s *NetworkElementService) Add(networkElementToAdd networkelement.NetworkElement) error {
	err := s.networkElementStore.Add(networkElementToAdd)
	if err != nil {
		return err
	}

	pubEvent := event.NewAddEvent(networkElementToAdd.ID())
	if err := s.eventService.PublishEvent(NetworkElementEventTopic, pubEvent); err != nil {
		go func() {
			s.eventService.Reconnect()

			retryErr := s.eventService.RetryPublish(NetworkElementEventTopic, pubEvent)
			if retryErr != nil {
				log.Error(retryErr)
			}
		}()
	}

	return nil
}

// UpdateModel updates a existing network element with a new model provided as string.
func (s *NetworkElementService) UpdateModel(networkElementID uuid.UUID, modelAsString string) error {
	exisitingNetworkElement, err := s.Get(store.Query{ID: networkElementID})
	if err != nil {
		return err
	}

	// Create 'root' path to be able to load the whole model from the store.
	path, err := ygot.StringToPath("/", ygot.StructuredPath)
	if err != nil {
		return err
	}

	typedValue := &gnmi.TypedValue{
		Value: &gnmi.TypedValue_JsonIetfVal{
			JsonIetfVal: []byte(modelAsString),
		},
	}

	// Use unmarshall from the network elements SBI to unmarshall ygot json in go struct.
	err = exisitingNetworkElement.GetPlugin().SetNode(path, typedValue)
	if err != nil {
		return err
	}

	err = s.networkElementStore.Update(exisitingNetworkElement)
	if err != nil {
		return err
	}

	// TODO (faseid): check if we want to add the paths with values here instead of empty map!
	pubEvent := event.NewMneUpdateEvent(networkElementID, map[string]string{})
	if err := s.eventService.PublishEvent(NetworkElementEventTopic, pubEvent); err != nil {
		go func() {
			s.eventService.Reconnect()

			retryErr := s.eventService.RetryPublish(NetworkElementEventTopic, pubEvent)
			if retryErr != nil {
				log.Error(retryErr)
			}
		}()
	}

	return nil
}

// Update updates a existing network element.
func (s *NetworkElementService) Update(networkElementToUpdate networkelement.NetworkElement) error {
	err := s.networkElementStore.Update(networkElementToUpdate)
	if err != nil {
		return err
	}

	// TODO (faseid): check if we want to add the paths with values here instead of empty map!
	pubEvent := event.NewMneUpdateEvent(networkElementToUpdate.ID(), map[string]string{})
	if err := s.eventService.PublishEvent(NetworkElementEventTopic, pubEvent); err != nil {
		go func() {
			s.eventService.Reconnect()

			retryErr := s.eventService.RetryPublish(NetworkElementEventTopic, pubEvent)
			if retryErr != nil {
				log.Error(retryErr)
			}
		}()
	}

	return nil
}

// Delete deletes a network element from the network element store.
func (s *NetworkElementService) Delete(networkElementToDelete networkelement.NetworkElement) error {
	err := s.networkElementStore.Delete(networkElementToDelete)
	if err != nil {
		return err
	}

	pubEvent := event.NewDeleteEvent(networkElementToDelete.ID())
	if err := s.eventService.PublishEvent(NetworkElementEventTopic, pubEvent); err != nil {
		go func() {
			s.eventService.Reconnect()

			retryErr := s.eventService.RetryPublish(NetworkElementEventTopic, pubEvent)
			if retryErr != nil {
				log.Error(retryErr)
			}
		}()
	}

	return nil
}

func (s *NetworkElementService) createNetworkElementFromStore(loadedNetworkElement networkelement.LoadedNetworkElement) (networkelement.NetworkElement, error) {
	if loadedNetworkElement.Plugin == "" {
		return nil, fmt.Errorf("can not get device, no running plugin found for network element")
	}

	pluginForNetworkElement, err := s.pluginService.Get(store.Query{ID: uuid.MustParse(loadedNetworkElement.Plugin)})
	if err != nil {
		return nil, err
	}

	mne, err := NewNetworkElement(
		loadedNetworkElement.Name,
		uuid.MustParse(loadedNetworkElement.ID),
		&tpb.TransportOption{
			Address:  loadedNetworkElement.TransportAddress,
			Username: loadedNetworkElement.TransportUsername,
			Password: loadedNetworkElement.TransportPassword,
			TransportOption: &tpb.TransportOption_GnmiTransportOption{
				GnmiTransportOption: &tpb.GnmiTransportOption{},
			},
			Type: spb.Type_TYPE_OPENCONFIG,
		},
		uuid.MustParse(loadedNetworkElement.PndID),
		pluginForNetworkElement,
		loadedNetworkElement.Metadata,
	)
	if err != nil {
		return nil, err
	}

	// Create 'root' path to be able to load the whole model from the store.
	path, err := ygot.StringToPath("/", ygot.StructuredPath)
	if err != nil {
		return nil, err
	}

	typedValue := &gnmi.TypedValue{
		Value: &gnmi.TypedValue_JsonIetfVal{
			JsonIetfVal: []byte(loadedNetworkElement.Model),
		},
	}

	// Use unmarshall from the network elements SBI to unmarshall ygot json in go struct.
	err = mne.GetPlugin().SetNode(path, typedValue)
	if err != nil {
		return nil, err
	}

	return mne, nil
}
