package api

import (
	"context"
	"errors"
	"io"
	"time"

	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	pipb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/plugin-internal"
	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
	nbi "code.fbi.h-da.de/danet/gosdn/controller/northbound/client"

	"github.com/google/uuid"
	"github.com/openconfig/goyang/pkg/yang"
	"github.com/openconfig/ygot/ygot"
	log "github.com/sirupsen/logrus"
)

// AddNetworkElement adds a new network element to the controller. The network element name is optional.
// If no name is provided a name will be generated upon network element creation.
func AddNetworkElement(ctx context.Context, addr, mneName string, opt *tpb.TransportOption, pluginID, pid uuid.UUID) (*mnepb.AddListResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.AddListRequest{
		Timestamp: time.Now().UnixNano(),
		Mne: []*mnepb.SetMne{
			{
				Address:         opt.GetAddress(),
				MneName:         mneName,
				PluginId:        pluginID.String(),
				Pid:             pid.String(),
				TransportOption: opt,
			},
		},
		Pid: pid.String(),
	}
	switch t := opt.Type; t {
	case spb.Type_TYPE_CONTAINERISED, spb.Type_TYPE_PLUGIN:
		req.Mne[0].TransportOption.Type = t
	default:
	}

	return client.AddList(ctx, req)
}

// GetNetworkElement requests one network element belonging to a given
// PrincipalNetworkDomain from the controller. If no network element identifier
// is provided, an error is thrown.
func GetNetworkElement(ctx context.Context, addr, pid string, mneid string) (*mnepb.GetResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	if len(mneid) == 0 {
		return nil, err
	}

	req := &mnepb.GetRequest{
		Timestamp: time.Now().UnixNano(),
		Mneid:     mneid,
		Pid:       pid,
	}

	return client.Get(ctx, req)
}

// GetPluginSchemaTree gets the schema tree for a plugin.
func GetPluginSchemaTree(ctx context.Context, addr string, pluginID uuid.UUID) (map[string]*yang.Entry, error) {
	pluginClient, err := nbi.PluginClient(addr, dialOptions...)
	if err != nil {
		return map[string]*yang.Entry{}, err
	}

	req := &pipb.GetPluginSchemaRequest{
		Timestamp: time.Now().UnixNano(),
		Pid:       pluginID.String(),
	}

	ctx, cancel := context.WithTimeout(ctx, time.Minute*10)
	defer cancel()
	sClient, err := pluginClient.GetPluginSchema(ctx, req)
	if err != nil {
		return map[string]*yang.Entry{}, err
	}

	sTreeBytes := []byte{}

	for {
		payload, err := sClient.Recv()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			log.Error(err)

			closeErr := sClient.CloseSend()
			if closeErr != nil {
				return nil, err
			}

			return map[string]*yang.Entry{}, err
		}
		sTreeBytes = append(sTreeBytes, payload.Chunk...)
	}

	sTreeMap, err := ygot.GzipToSchema(sTreeBytes)
	if err != nil {
		return map[string]*yang.Entry{}, err
	}

	return sTreeMap, nil
}

// GetFlattenedNetworkElement requests a network elements belonging to a given
// PrincipalNetworkDomain from the controller.
func GetFlattenedNetworkElement(ctx context.Context, addr, pid string, mneid string) (*mnepb.GetFlattenedResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.GetFlattenedRequest{
		Timestamp: time.Now().UnixNano(),
		Mneid:     mneid,
		Pid:       pid,
	}

	return client.GetFlattened(ctx, req)
}

// GetFlattenedNetworkElements requests all network elements belonging to a given
// PrincipalNetworkDomain from the controller.
func GetFlattenedNetworkElements(ctx context.Context, addr, pid string) (*mnepb.GetAllFlattenedResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.GetAllFlattenedRequest{
		Timestamp: time.Now().UnixNano(),
		Pid:       pid,
	}

	return client.GetAllFlattened(ctx, req)
}

// GetPath requests a specific path.
func GetPath(ctx context.Context, addr, pid, mneid, path string) (*mnepb.GetPathResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.GetPathRequest{
		Timestamp: time.Now().UnixNano(),
		Mneid:     mneid,
		Pid:       pid,
		Path:      path,
	}

	return client.GetPath(ctx, req)
}

func GetIntendedPath(ctx context.Context, addr, pid, mneid, intendedPath string) (*mnepb.GetIntendedPathResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.GetIntendedPathRequest{
		Timestamp:    time.Now().UnixNano(),
		Mneid:        mneid,
		Pid:          pid,
		IntendedPath: intendedPath,
	}

	return client.GetIntendedPath(ctx, req)
}

// SubscribePath subscribes to paths on a network element.
func SubscribePath(ctx context.Context, addr, pid, mneid string, slist *mnepb.SubscriptionList) (mnepb.NetworkElementService_SubscribePathClient, error) {
	log.Println("subscribePath called")
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.SubscribePathRequest{
		Timestamp: time.Now().UnixNano(),
		Mneid:     mneid,
		Pid:       pid,
		Sublist:   slist,
	}

	return client.SubscribePath(ctx, req)
}

// DeleteNetworkElement deletes a network element.
func DeleteNetworkElement(ctx context.Context, addr, pid, mneid string) (*mnepb.DeleteResponse, error) {
	client, err := nbi.NetworkElementClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &mnepb.DeleteRequest{
		Timestamp: time.Now().UnixNano(),
		Mneid:     mneid,
		Pid:       pid,
	}

	return client.Delete(ctx, req)
}