package main

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	"code.fbi.h-da.de/danet/gosdn/application-framework/models"
	"code.fbi.h-da.de/danet/gosdn/models/generated/arista"

	"github.com/google/uuid"
	"github.com/openconfig/ygot/ygot"
	"github.com/openconfig/ygot/ytypes"
)

var statusMap map[string]map[string]*InterfaceStatus

// NetworkElement is a NetworkElement.
type NetworkElement struct {
	// UUID represents the network element's UUID
	UUID uuid.UUID

	// Name is the network element's human readable Name
	Name string

	// Model embeds a ygot.GoStruct containing the network element details
	Model arista.Device
}

// NewNetworkElement creates a new NetworkElement.
func NewNetworkElement(id uuid.UUID, name string, networkElementModel string) *NetworkElement {
	d := &NetworkElement{
		UUID:  id,
		Model: arista.Device{},
		Name:  name,
	}

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

	opts := []ytypes.UnmarshalOpt{
		&ytypes.IgnoreExtraFields{},
	}
	// Use unmarshall from the network elements model to unmarshall ygot json in go struct.
	err = models.Unmarshal([]byte(networkElementModel), path, &d.Model, opts...)
	if err != nil {
		panic(err)
	}

	return d
}

// InterfaceStatus contains information of an interface.
type InterfaceStatus struct {
	NetworkElementName string
	Name               string
	Status             string
}

func checkIfOperationStateHasChanged(networkElementServer networkelement.NetworkElementServiceClient, networkElementID uuid.UUID) ([]InterfaceStatus, error) {
	ctx := context.Background()

	request := &networkelement.GetNetworkElementRequest{
		Timestamp:        time.Now().UnixNano(),
		NetworkElementId: networkElementID.String(),
	}

	resp, err := networkElementServer.Get(ctx, request)
	if err != nil {
		return nil, err
	}

	networkElement := NewNetworkElement(uuid.MustParse(resp.NetworkElement.Id), resp.NetworkElement.Name, resp.NetworkElement.Model)

	storedInterfaces, ok := statusMap[networkElement.Name]
	if !ok {
		addNetworkElementToStatusMap(networkElement.Name, networkElement.Model.Interfaces.Interface)
		return nil, nil
	}

	return walkThroughInterfaces(networkElement.Model.Interfaces.Interface, storedInterfaces)
}

func addNetworkElementToStatusMap(networkElementName string, interfaces map[string]*arista.OpenconfigInterfaces_Interfaces_Interface) {
	statusList := make(map[string]*InterfaceStatus)

	for _, receivedInterface := range interfaces {
		statusList[*receivedInterface.Name] = &InterfaceStatus{
			NetworkElementName: networkElementName,
			Name:               *receivedInterface.Name,
			Status:             receivedInterface.State.OperStatus.String(),
		}
	}

	statusMap[networkElementName] = statusList
}

func walkThroughInterfaces(interfaces map[string]*arista.OpenconfigInterfaces_Interfaces_Interface, storedInterfaces map[string]*InterfaceStatus) ([]InterfaceStatus, error) {
	statusList := make([]InterfaceStatus, 0)

	for _, receivedInterface := range interfaces {
		storedInterface, ok := storedInterfaces[*receivedInterface.Name]
		if !ok {
			return statusList, fmt.Errorf("could not find %s in stored interfaces", *receivedInterface.Name)
		}

		if storedInterface.Status != receivedInterface.State.OperStatus.String() {
			statusList = append(statusList, InterfaceStatus{
				NetworkElementName: storedInterface.NetworkElementName,
				Name:               *receivedInterface.Name,
				Status:             receivedInterface.State.OperStatus.String(),
			})

			storedInterface.Status = receivedInterface.State.OperStatus.String()

			statusMapCopy := statusMap

			go func() {
				b, err := json.Marshal(statusMapCopy)
				if err != nil {
					fmt.Println("error: ", err)
					return
				}
				for clientChannel := range clientChannels {
					clientChannel <- []byte(b)
				}
			}()
		}
	}

	return statusList, nil
}
