package server

import (
	"context"
	"time"

	topopb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/topology"
	query "code.fbi.h-da.de/danet/gosdn/controller/store"
	"code.fbi.h-da.de/danet/gosdn/controller/topology"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/links"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/nodes"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/ports"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/ports/configuration"
	"github.com/google/uuid"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// TopologyServer holds a topologyService and represents a TopologyServiceServer.
type TopologyServer struct {
	topopb.UnimplementedTopologyServiceServer
	topologyService topology.Service
	nodeService     nodes.Service
	portService     ports.Service
}

// NewTopologyServer receives a topologyService and returns a new TopologyServer.
func NewTopologyServer(
	service topology.Service,
	nodeService nodes.Service,
	portService ports.Service,
) *TopologyServer {
	return &TopologyServer{
		topologyService: service,
		nodeService:     nodeService,
		portService:     portService,
	}
}

// AddLink adds a new link to the topology.
func (t *TopologyServer) AddLink(ctx context.Context, request *topopb.AddLinkRequest) (*topopb.AddLinkResponse, error) {
	sourceNode, sourcePort, err := t.ensureNodeAndPortExists(request.Link.SourceNode, request.Link.SourcePort)
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "%v", err)
	}

	targetNode, targetPort, err := t.ensureNodeAndPortExists(request.Link.TargetNode, request.Link.TargetPort)
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "%v", err)
	}

	link := links.Link{
		ID:         uuid.New(),
		Name:       request.Link.Name,
		SourceNode: sourceNode,
		SourcePort: sourcePort,
		TargetNode: targetNode,
		TargetPort: targetPort,
	}
	err = t.topologyService.AddLink(link)
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "%v", err)
	}

	return &topopb.AddLinkResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    topopb.Status_STATUS_OK,
	}, nil
}

// GetTopology returns the current topology in the form of all links.
func (t *TopologyServer) GetTopology(ctx context.Context, request *topopb.GetTopologyRequest) (*topopb.GetTopologyResponse, error) {
	topo, err := t.topologyService.GetAll()
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "%v", err)
	}

	topology := &topopb.Topology{}

	for _, link := range topo {
		topology.Links = append(topology.Links, &topopb.Link{
			Id:   link.ID.String(),
			Name: link.Name,
			SourceNode: &topopb.Node{
				Id:   link.SourceNode.ID.String(),
				Name: link.SourceNode.Name,
			},
			SourcePort: &topopb.Port{
				Id:   link.SourcePort.ID.String(),
				Name: link.SourcePort.Name,
				Configuration: &topopb.Configuration{
					Ip:           link.SourcePort.Configuration.IP.String(),
					PrefixLength: link.SourcePort.Configuration.PrefixLength,
				},
			},
			TargetNode: &topopb.Node{
				Id:   link.TargetNode.ID.String(),
				Name: link.TargetNode.Name,
			},
			TargetPort: &topopb.Port{
				Id:   link.TargetPort.ID.String(),
				Name: link.TargetPort.Name,
				Configuration: &topopb.Configuration{
					Ip:           link.TargetPort.Configuration.IP.String(),
					PrefixLength: link.TargetPort.Configuration.PrefixLength,
				},
			},
		})
	}

	return &topopb.GetTopologyResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    topopb.Status_STATUS_OK,
		Toplogy:   topology,
	}, nil
}

// DeleteLink deletes a link.
func (t *TopologyServer) DeleteLink(ctx context.Context, request *topopb.DeleteLinkRequest) (*topopb.DeleteLinkResponse, error) {
	linkID, err := uuid.Parse(request.Id)
	if err != nil {
		return &topopb.DeleteLinkResponse{
			Timestamp: time.Now().UnixNano(),
			Status:    topopb.Status_STATUS_ERROR,
		}, err
	}

	foundLink, err := t.topologyService.Get(query.Query{ID: linkID})
	if err != nil {
		return &topopb.DeleteLinkResponse{
			Timestamp: time.Now().UnixNano(),
			Status:    topopb.Status_STATUS_ERROR,
		}, err
	}

	err = t.topologyService.DeleteLink(foundLink)
	if err != nil {
		return &topopb.DeleteLinkResponse{
			Timestamp: time.Now().UnixNano(),
			Status:    topopb.Status_STATUS_ERROR,
		}, err
	}

	return &topopb.DeleteLinkResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    topopb.Status_STATUS_OK,
	}, nil
}

func (t *TopologyServer) ensureNodeAndPortExists(incomingNode *topopb.Node, incomingPort *topopb.Port) (nodes.Node, ports.Port, error) {
	node, err := t.nodeService.EnsureExists(
		nodes.Node{
			ID:   getExistingOrCreateNilUUIDFromString(incomingNode.Id),
			Name: incomingNode.Name,
		},
	)
	if err != nil {
		return node, ports.Port{}, status.Errorf(codes.Aborted, "%v", err)
	}

	portConf, err := configuration.New(incomingPort.Configuration.Ip, incomingPort.Configuration.PrefixLength)
	if err != nil {
		return node, ports.Port{}, status.Errorf(codes.Aborted, "%v", err)
	}
	port, err := t.portService.EnsureExists(
		ports.Port{
			ID:            getExistingOrCreateNilUUIDFromString(incomingPort.Id),
			Name:          incomingPort.Name,
			Configuration: portConf,
		},
	)
	if err != nil {
		return nodes.Node{}, port, status.Errorf(codes.Aborted, "%v", err)
	}

	return node, port, nil
}

func getExistingOrCreateNilUUIDFromString(id string) uuid.UUID {
	if len(id) == 0 {
		return uuid.Nil
	}

	parsedID, err := uuid.Parse(id)
	if err != nil {
		return uuid.Nil
	}

	return parsedID
}
