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/nodes"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/ports"
	routingtables "code.fbi.h-da.de/danet/gosdn/controller/topology/routing-tables"
	"github.com/google/uuid"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// RoutingTableServiceServer holds a RoutingTableServiceServer and represents a RoutingTableServiceServer.
type RoutingTableServiceServer struct {
	topopb.UnimplementedRoutingTableServiceServer
	routeService routingtables.Service
	nodeService  nodes.Service
	portService  ports.Service
}

// NewRoutingTableServiceServer returns a new RoutingTableServiceServer.
func NewRoutingTableServiceServer(
	service routingtables.Service,
	nodeService nodes.Service,
	portService ports.Service,
) *RoutingTableServiceServer {
	return &RoutingTableServiceServer{
		routeService: service,
		nodeService:  nodeService,
		portService:  portService,
	}
}

// AddRoutingTable adds a new route.
func (t *RoutingTableServiceServer) AddRoutingTable(
	ctx context.Context,
	request *topopb.AddRoutingTableRequest,
) (*topopb.AddRoutingTableResponse, error) {
	routes := []routingtables.Route{}

	for _, incomingRoute := range request.RoutingTable.Routes {
		routes = append(routes, routingtables.Route{
			ID:            getExistingOrCreateNilUUIDFromString(incomingRoute.Id),
			TargetIPRange: incomingRoute.TargetIPRange,
			NextHopIP:     incomingRoute.NextHopIP,
			Metric:        incomingRoute.Metric,
			PortID:        getExistingOrCreateNilUUIDFromString(incomingRoute.PortID),
		})
	}

	routeingTable := routingtables.RoutingTable{
		ID:     getExistingOrCreateNilUUIDFromString(request.RoutingTable.NodeID),
		NodeID: getExistingOrCreateNilUUIDFromString(request.RoutingTable.NodeID),
		Routes: routes,
	}
	_, err := t.routeService.EnsureExists(routeingTable)
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "%v", err)
	}

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

// GetRoutes returns the current routes.
func (t *RoutingTableServiceServer) GetRoutes(
	ctx context.Context,
	request *topopb.GetRoutesRequest,
) (*topopb.GetRoutesResponse, error) {
	exisitingRoutingTables, err := t.routeService.GetAll()
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "%v", err)
	}

	routingTables := []*topopb.RoutingTable{}

	for _, routingTable := range exisitingRoutingTables {
		rt := topopb.RoutingTable{}

		rt.Id = routingTable.ID.String()
		rt.NodeID = routingTable.NodeID.String()

		for _, ers := range routingTable.Routes {
			rt.Routes = append(rt.Routes, &topopb.Route{
				Id:            ers.ID.String(),
				TargetIPRange: ers.TargetIPRange,
				NextHopIP:     ers.NextHopIP,
				Metric:        ers.Metric,
				PortID:        ers.PortID.String(),
			})
		}

		routingTables = append(routingTables, &rt)
	}

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

// DeleteRoute deletes a routing table entry.
func (t *RoutingTableServiceServer) DeleteRoute(
	ctx context.Context,
	request *topopb.DeleteRoutesRequest,
) (*topopb.DeleteRoutesResponse, error) {
	routeID, err := uuid.Parse(request.Id)
	if err != nil {
		return &topopb.DeleteRoutesResponse{
			Timestamp: time.Now().UnixNano(),
			Status:    topopb.Status_STATUS_ERROR,
		}, err
	}

	foundRoute, err := t.routeService.Get(query.Query{ID: routeID})
	if err != nil {
		return &topopb.DeleteRoutesResponse{
			Timestamp: time.Now().UnixNano(),
			Status:    topopb.Status_STATUS_ERROR,
		}, err
	}

	err = t.routeService.Delete(foundRoute)
	if err != nil {
		return &topopb.DeleteRoutesResponse{
			Timestamp: time.Now().UnixNano(),
			Status:    topopb.Status_STATUS_ERROR,
		}, err
	}

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