package api

import (
	"context"
	"io"
	"time"

	ppb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd"
	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"
)

// AddDevice adds a new device to the controller. The device name is optional.
// If no name is provided a name will be generated upon device creation.
func AddDevice(ctx context.Context, addr, deviceName string, opt *tpb.TransportOption, sid, pid uuid.UUID) (*ppb.SetOndListResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &ppb.SetOndListRequest{
		Timestamp: time.Now().UnixNano(),
		Ond: []*ppb.SetOnd{
			{
				Address: opt.GetAddress(),
				Sbi: &spb.SouthboundInterface{
					Id: sid.String(),
				},
				DeviceName:      deviceName,
				TransportOption: opt,
			},
		},
		Pid: pid.String(),
	}
	switch t := opt.Type; t {
	case spb.Type_TYPE_CONTAINERISED, spb.Type_TYPE_PLUGIN:
		req.Ond[0].Sbi.Id = uuid.Nil.String()
		req.Ond[0].Sbi.Type = t
		req.Ond[0].TransportOption.Type = t
	default:
	}

	return pndClient.SetOndList(ctx, req)
}

// GetDevice requests one device belonging to a given
// PrincipalNetworkDomain from the controller. If no device identifier
// is provided, an error is thrown.
func GetDevice(ctx context.Context, addr, pid string, did ...string) (*ppb.GetOndResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

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

	req := &ppb.GetOndRequest{
		Timestamp: time.Now().UnixNano(),
		Did:       did,
		Pid:       pid,
	}

	return pndClient.GetOnd(ctx, req)
}

// GetSbiSchemaTree gets the sbi tree for a sbi
func GetSbiSchemaTree(ctx context.Context, addr string, pid, sid uuid.UUID) (map[string]*yang.Entry, error) {
	sbiClient, err := nbi.SbiClient(addr, dialOptions...)
	if err != nil {
		return map[string]*yang.Entry{}, err
	}

	req := &spb.GetSchemaRequest{
		Timestamp: time.Now().UnixNano(),
		Pid:       pid.String(),
		Sid:       sid.String(),
	}

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

	sTreeBytes := []byte{}

	for {
		payload, err := sClient.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Error(err)
			sClient.CloseSend()
			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
}

// GetDevices requests all devices belonging to a given
// PrincipalNetworkDomain from the controller.
func GetDevices(ctx context.Context, addr, pid string) (*ppb.GetOndListResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

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

	return pndClient.GetOndList(ctx, req)
}

// GetPath requests a specific path
func GetPath(ctx context.Context, addr, pid, did, path string) (*ppb.GetPathResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &ppb.GetPathRequest{
		Timestamp: time.Now().UnixNano(),
		Did:       did,
		Pid:       pid,
		Path:      path,
	}

	return pndClient.GetPath(ctx, req)
}

// DeleteDevice deletes a device
func DeleteDevice(ctx context.Context, addr, pid, did string) (*ppb.DeleteOndResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &ppb.DeleteOndRequest{
		Timestamp: time.Now().UnixNano(),
		Did:       did,
		Pid:       pid,
	}

	return pndClient.DeleteOnd(ctx, req)
}