package api

import (
	"context"
	"errors"
	"time"

	pb "code.fbi.h-da.de/danet/api/go/gosdn/core"
	ppb "code.fbi.h-da.de/danet/api/go/gosdn/pnd"
	spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound"
	tpb "code.fbi.h-da.de/danet/api/go/gosdn/transport"
	nbi "code.fbi.h-da.de/danet/gosdn/northbound/client"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"

	"google.golang.org/grpc"
)

var dialOptions []grpc.DialOption

func init() {
	dialOptions = []grpc.DialOption{
		grpc.WithInsecure(),
	}
}

// Init initialises the CLI client.
func Init(addr string) error {
	ctx := context.Background()
	resp, err := getAllCore(ctx, addr)
	if err != nil {
		return err
	}
	if len(resp.Pnd) > 0 {
		pid := resp.Pnd[0].Id
		viper.Set("CLI_PND", pid)
		log.Infof("PND: %v", pid)
		if len(resp.Pnd[0].Sbi) != 0 {
			sbi := resp.Pnd[0].Sbi[0].Id
			viper.Set("CLI_SBI", sbi)
			log.Infof("SBI: %v", sbi)
		}
	}
	return viper.WriteConfig()
}

// GetIds requests all UUID information from the controller
func GetIds(addr string) ([]*ppb.PrincipalNetworkDomain, error) {
	ctx := context.Background()
	resp, err := getAllCore(ctx, addr)
	if err != nil {
		return nil, err
	}
	return resp.Pnd, nil
}

func getAllCore(ctx context.Context, addr string) (*pb.GetResponse, error) {
	coreClient, err := nbi.CoreClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}
	req := &pb.GetRequest{
		Timestamp: time.Now().UnixNano(),
		All:       true,
	}
	return coreClient.Get(ctx, req)
}

// AddPnd takes a name, description and SBI UUID to create a new
// PrincipalNetworkDomain on the controller
func AddPnd(addr, name, description, sbi string) (*pb.SetResponse, error) {
	coreClient, err := nbi.CoreClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}
	ctx := context.Background()
	req := &pb.SetRequest{
		Timestamp: time.Now().UnixNano(),
		Pnd: []*pb.SetPnd{
			{
				Name:        name,
				Description: description,
				Sbi:         sbi,
			},
		},
	}

	return coreClient.Set(ctx, req)
}

// GetPnd requests one or several PrincipalNetworkDomains from the
// controller. To request all PrincipalNetworkDomains without providing
// names or UUIDs use GetIds()
func GetPnd(addr string, args ...string) (*pb.GetResponse, error) {
	coreClient, err := nbi.CoreClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}
	if len(args) <= 0 {
		return nil, errors.New("not enough arguments")
	}
	ctx := context.Background()
	req := &pb.GetRequest{
		Timestamp: time.Now().UnixNano(),
		Pid:       args,
	}
	return coreClient.Get(ctx, req)
}

// getChanges requests all pending and unconfirmed changes from the controller
func getChanges(addr, pnd string) (*ppb.GetResponse, error) {
	ctx := context.Background()
	client, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}
	req := &ppb.GetRequest{
		Timestamp: time.Now().UnixNano(),
		Request: &ppb.GetRequest_Change{
			Change: &ppb.GetChange{
				All: true,
			},
		},
		Pid: pnd,
	}
	return client.Get(ctx, req)
}

// commit sends a commit request for one or multiple changes to the
// controller.
func commit(addr, pnd string, cuids ...string) (*ppb.SetResponse, error) {
	changes := make([]*ppb.SetChange, len(cuids))
	for i, arg := range cuids {
		changes[i] = &ppb.SetChange{
			Cuid: arg,
			Op:   ppb.SetChange_COMMIT,
		}
	}
	return commitConfirm(addr, pnd, changes)
}

// confirm sends a confirm request for one or multiple changes to the
// controller
func confirm(addr, pnd string, cuids ...string) (*ppb.SetResponse, error) {
	changes := make([]*ppb.SetChange, len(cuids))
	for i, arg := range cuids {
		changes[i] = &ppb.SetChange{
			Cuid: arg,
			Op:   ppb.SetChange_CONFIRM,
		}
	}
	return commitConfirm(addr, pnd, changes)
}

func commitConfirm(addr, pnd string, changes []*ppb.SetChange) (*ppb.SetResponse, error) {
	ctx := context.Background()
	client, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}
	req := &ppb.SetRequest{
		Timestamp: time.Now().UnixNano(),
		Change:    changes,
		Pid:       pnd,
	}
	return client.Set(ctx, req)
}

// 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(addr, deviceName string, opt *tpb.TransportOption, sid, pid uuid.UUID) (*ppb.SetResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &ppb.SetRequest{
		Timestamp: time.Now().UnixNano(),
		Ond: []*ppb.SetOnd{
			{
				Address: opt.GetAddress(),
				Sbi: &spb.SouthboundInterface{
					Id: sid.String(),
				},
				DeviceName:      deviceName,
				TransportOption: opt,
			},
		},
		Pid: pid.String(),
	}
	if opt.Csbi {
		req.Ond[0].Sbi.Id = uuid.Nil.String()
		req.Ond[0].Sbi.Type = spb.Type_CONTAINERISED
		req.Ond[0].TransportOption.Csbi = true
	}
	ctx := context.Background()
	return pndClient.Set(ctx, req)
}

// getDevice requests one or multiple devices belonging to a given
// PrincipalNetworkDomain from the controller. If no device identifier
// is provided, all devices are requested.
func getDevice(addr, pid string, did ...string) (*ppb.GetResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	var all bool
	if len(did) == 0 {
		all = true
	}

	req := &ppb.GetRequest{
		Timestamp: time.Now().UnixNano(),
		Request: &ppb.GetRequest_Ond{
			Ond: &ppb.GetOnd{
				All: all,
				Did: did,
			},
		},
		Pid: pid,
	}
	ctx := context.Background()
	return pndClient.Get(ctx, req)
}

func getPath(addr, pid, did, path string) (*ppb.GetResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &ppb.GetRequest{
		Timestamp: time.Now().UnixNano(),
		Request: &ppb.GetRequest_Path{
			Path: &ppb.GetPath{
				Did:  did,
				Path: path,
			},
		},
		Pid: pid,
	}
	ctx := context.Background()
	return pndClient.Get(ctx, req)
}

func deleteDevice(addr, pid, did string) (*ppb.DeleteResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}

	req := &ppb.DeleteRequest{
		Timestamp: time.Now().UnixNano(),
		Type:      ppb.DeleteRequest_OND,
		Uuid:      did,
		Pid:       pid,
	}
	ctx := context.Background()
	return pndClient.Delete(ctx, req)
}

// change creates a ChangeRequest for the specified OND. ApiOperations are
// used to specify the type of the change (update, replace, delete as specified
// in https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#34-modifying-state)
// For delete operations the value field needs to contain an empty string.
func changeRequest(addr, did, pid, path, value string, op ppb.ApiOperation) (*ppb.SetResponse, error) {
	req := &ppb.ChangeRequest{
		Id:    did,
		Path:  path,
		Value: value,
		ApiOp: op,
	}
	return sendChangeRequest(addr, pid, req)
}

func sendChangeRequest(addr, pid string, req *ppb.ChangeRequest) (*ppb.SetResponse, error) {
	pndClient, err := nbi.PndClient(addr, dialOptions...)
	if err != nil {
		return nil, err
	}
	ctx := context.Background()
	r := &ppb.SetRequest{
		Timestamp:     time.Now().UnixNano(),
		ChangeRequest: []*ppb.ChangeRequest{req},
		Pid:           pid,
	}
	return pndClient.Set(ctx, r)
}