/*
 * Copyright 2018-2024 Open Networking Foundation (ONF) and the ONF Contributors

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package device

import (
	"context"
	"fmt"

	"github.com/opencord/voltha-go/rw_core/route"
	"github.com/opencord/voltha-lib-go/v7/pkg/log"
	ofp "github.com/opencord/voltha-protos/v5/go/openflow_13"
	"github.com/opencord/voltha-protos/v5/go/voltha"
)

// GetRoute returns a route
func (agent *LogicalAgent) GetRoute(ctx context.Context, ingressPortNo uint32, egressPortNo uint32) ([]route.Hop, error) {
	logger.Debugw(ctx, "getting-route", log.Fields{"ingress-port": ingressPortNo, "egress-port": egressPortNo})
	routes := make([]route.Hop, 0)

	// Note: A port value of 0 is equivalent to a nil port

	// Controller-bound flow
	if egressPortNo != 0 && ((egressPortNo & 0x7fffffff) == uint32(ofp.OfpPortNo_OFPP_CONTROLLER)) {
		logger.Debugw(ctx, "controller-flow", log.Fields{"ingressPortNo": ingressPortNo, "egressPortNo": egressPortNo})
		if agent.isNNIPort(ingressPortNo) {
			// This is a trap on the NNI Port
			if agent.deviceRoutes.IsRoutesEmpty() {
				// If there are no routes set (usually when the logical device has only NNI port(s), then just return an
				// route with same IngressHop and EgressHop
				hop := route.Hop{DeviceID: agent.rootDeviceID, Ingress: ingressPortNo, Egress: ingressPortNo}
				routes = append(routes, hop)
				routes = append(routes, hop)
				return routes, nil
			}

			// Return a 'half' route to make the flow decomposer logic happy
			routes, err := agent.deviceRoutes.GetHalfRoute(false, ingressPortNo, 0)
			if err != nil {
				return nil, fmt.Errorf("no upstream route from:%d to:%d :%w", ingressPortNo, egressPortNo, route.ErrNoRoute)
			}
			return routes, nil
		}
		// Treat it as if the output port is the first NNI of the OLT
		var err error
		if egressPortNo, err = agent.getAnyNNIPort(); err != nil {
			logger.Warnw(ctx, "no-nni-port", log.Fields{"error": err})
			return nil, err
		}
	}

	// If ingress port is not specified (nil), it may be a wildcarded route if egress port is OFPP_CONTROLLER or a nni
	// logical port, in which case we need to create a half-route where only the egress hop is filled, the first hop is nil
	if ingressPortNo == 0 && agent.isNNIPort(egressPortNo) {
		routes, err := agent.deviceRoutes.GetHalfRoute(true, ingressPortNo, egressPortNo)
		if err != nil {
			return nil, fmt.Errorf("no upstream route from:%d to:%d :%w", ingressPortNo, egressPortNo, route.ErrNoRoute)
		}
		return routes, nil
	}

	// If egress port is not specified (nil), we can also can return a "half" route
	if egressPortNo == 0 {
		routes, err := agent.deviceRoutes.GetHalfRoute(false, ingressPortNo, egressPortNo)
		if err != nil {
			return nil, fmt.Errorf("no downstream route from:%d to:%d :%w", ingressPortNo, egressPortNo, route.ErrNoRoute)
		}
		return routes, nil
	}

	// Return the pre-calculated route
	return agent.deviceRoutes.GetRoute(ctx, ingressPortNo, egressPortNo)
}

// GetDeviceRoutes returns device graph
func (agent *LogicalAgent) GetDeviceRoutes() *route.DeviceRoutes {
	return agent.deviceRoutes
}

// rebuildRoutes rebuilds the device routes
func (agent *LogicalAgent) buildRoutes(ctx context.Context) error {
	logger.Debugf(ctx, "building-routes", log.Fields{"logical-device-id": agent.logicalDeviceID})
	if err := agent.requestQueue.WaitForGreenLight(ctx); err != nil {
		return err
	}
	defer agent.requestQueue.RequestComplete()

	if err := agent.deviceRoutes.ComputeRoutes(ctx, agent.listLogicalDevicePorts(ctx)); err != nil {
		return err
	}
	if err := agent.deviceRoutes.Print(ctx); err != nil {
		return err
	}
	return nil
}

func (agent *LogicalAgent) removeRoutes(ctx context.Context) error {
	if err := agent.requestQueue.WaitForGreenLight(ctx); err != nil {
		return err
	}
	defer agent.requestQueue.RequestComplete()

	agent.deviceRoutes.RemoveRoutes()

	return nil
}

// updateRoutes updates the device routes
func (agent *LogicalAgent) updateRoutes(ctx context.Context, deviceID string, devicePorts map[uint32]*voltha.Port, lp *voltha.LogicalPort, lps map[uint32]*voltha.LogicalPort) error {
	logger.Debugw(ctx, "updateRoutes", log.Fields{"logical-device-id": agent.logicalDeviceID, "device-id": deviceID, "port:": lp})

	if err := agent.deviceRoutes.AddPort(ctx, lp, deviceID, devicePorts, lps); err != nil {
		return err
	}
	if err := agent.deviceRoutes.Print(ctx); err != nil {
		return err
	}
	return nil
}

// updateAllRoutes updates the device routes using all the logical ports on that device
func (agent *LogicalAgent) updateAllRoutes(ctx context.Context, deviceID string, devicePorts map[uint32]*voltha.Port) error {
	logger.Debugw(ctx, "updateAllRoutes", log.Fields{"logical-device-id": agent.logicalDeviceID, "device-id": deviceID, "ports-count": len(devicePorts)})

	if err := agent.deviceRoutes.AddAllPorts(ctx, deviceID, devicePorts, agent.listLogicalDevicePorts(ctx)); err != nil {
		return err
	}
	if err := agent.deviceRoutes.Print(ctx); err != nil {
		return err
	}
	return nil
}
