package proto_netlink

import (
	"fmt"
	"sync"

	"github.com/bio-routing/bio-rd/config"
	bnet "github.com/bio-routing/bio-rd/net"
	"github.com/bio-routing/bio-rd/route"
	"github.com/bio-routing/bio-rd/routingtable"
	"github.com/bio-routing/bio-rd/routingtable/filter"
	log "github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"
)

type NetlinkWriter struct {
	options *config.Netlink
	filter  *filter.Filter

	// Routingtable for buffering, to ensure no double writes (a.k.a rtnetlink: file exists)
	mu sync.RWMutex
	pt map[bnet.Prefix][]*route.Path
}

func NewNetlinkWriter(options *config.Netlink) *NetlinkWriter {
	return &NetlinkWriter{
		options: options,
		filter:  options.ExportFilter,
		pt:      make(map[bnet.Prefix][]*route.Path),
	}
}

// Not supported
func (nw *NetlinkWriter) UpdateNewClient(routingtable.RouteTableClient) error {
	return fmt.Errorf("Not supported")
}

// Not supported
func (nw *NetlinkWriter) Register(routingtable.RouteTableClient) {
	log.Error("Not supported")
}

// Not supported
func (nw *NetlinkWriter) RegisterWithOptions(routingtable.RouteTableClient, routingtable.ClientOptions) {
	log.Error("Not supported")
}

// Not supported
func (nw *NetlinkWriter) Unregister(routingtable.RouteTableClient) {
	log.Error("Not supported")
}

// RouteCount returns the number of stored routes
func (nw *NetlinkWriter) RouteCount() int64 {
	nw.mu.RLock()
	defer nw.mu.RUnlock()
	return int64(len(nw.pt))
}

// Add a path to the Kernel using netlink
// This function is triggered by the loc_rib, cause we are subscribed as
// client in the loc_rib
func (nw *NetlinkWriter) AddPath(pfx bnet.Prefix, path *route.Path) error {
	// check if for this prefix already a route is existing
	existingPaths, ok := nw.pt[pfx]

	// if no route exists, add that route
	if existingPaths == nil || !ok {
		paths := make([]*route.Path, 1)
		paths = append(paths, path)
		nw.pt[pfx] = paths

		// add the route to kernel
		return nw.addKernel(pfx, path)
	}

	// if the new path is already in, don't do anything
	for _, ePath := range existingPaths {
		if ePath.Equal(path) {
			return nil
		}
	}

	existingPaths = append(existingPaths, path)
	nw.pt[pfx] = existingPaths

	// now add to netlink
	return nw.addKernel(pfx, path)
}

// Remove a path from the Kernel using netlink
// This function is triggered by the loc_rib, cause we are subscribed as
// client in the loc_rib
func (nw *NetlinkWriter) RemovePath(pfx bnet.Prefix, path *route.Path) bool {
	// check if for this prefix already a route is existing
	existingPaths, ok := nw.pt[pfx]

	// if no route exists, nothing to do
	if existingPaths == nil || !ok {
		return true
	}

	// if the new path is already in: remove
	removeIdx := 0
	remove := false
	for idx, ePath := range existingPaths {
		if ePath.Equal(path) {
			removeIdx = idx

			remove = true
			err := nw.removeKernel(pfx, path)
			if err != nil {
				log.WithError(err).Errorf("Error while removing path %s for prefix %s", path.String(), pfx.String())
				remove = false
			}

			break
		}
	}

	if remove {
		existingPaths = append(existingPaths[:removeIdx], existingPaths[removeIdx+1:]...)
		nw.pt[pfx] = existingPaths
	}

	return true
}

// Add pfx/path to kernel
func (nw *NetlinkWriter) addKernel(pfx bnet.Prefix, path *route.Path) error {
	route, err := nw.createRoute(pfx, path)
	if err != nil {
		log.Errorf("Error while creating route: %v", err)
		return fmt.Errorf("Error while creating route: %v", err)
	}

	log.WithFields(log.Fields{
		"Prefix": pfx.String(),
		"Table":  route.Table,
	}).Debug("AddPath to netlink")

	err = netlink.RouteAdd(route)
	if err != nil {
		log.Errorf("Error while adding route: %v", err)
		return fmt.Errorf("Error while adding route: %v", err)
	}

	return nil
}

// remove pfx/path from kernel
func (nw *NetlinkWriter) removeKernel(pfx bnet.Prefix, path *route.Path) error {
	log.WithFields(log.Fields{
		"Prefix": pfx.String(),
	}).Debug("Remove from netlink")

	route, err := nw.createRoute(pfx, path)
	if err != nil {
		return fmt.Errorf("Error while creating route: %v", err)
	}

	err = netlink.RouteDel(route)
	if err != nil {
		return fmt.Errorf("Error while removing route: %v", err)
	}

	return nil
}

// create a route from a prefix and a path
func (nw *NetlinkWriter) createRoute(pfx bnet.Prefix, path *route.Path) (*netlink.Route, error) {
	if path.Type != route.NetlinkPathType {
	}

	switch path.Type {
	case route.NetlinkPathType:
		return nw.createRouteFromNetlink(pfx, path)

	case route.BGPPathType:
		return nw.createRouteFromBGPPath(pfx, path)

	default:
		return nil, fmt.Errorf("PathType %d is not supported for adding to netlink", path.Type)
	}
}

func (nw *NetlinkWriter) createRouteFromNetlink(pfx bnet.Prefix, path *route.Path) (*netlink.Route, error) {
	nlPath := path.NetlinkPath

	log.WithFields(log.Fields{
		"Dst":      nlPath.Dst,
		"Src":      nlPath.Src,
		"NextHop":  nlPath.NextHop,
		"Priority": nlPath.Priority,
		"Protocol": nlPath.Protocol,
		"Type":     nlPath.Type,
		"Table":    nw.options.RoutingTable,
	}).Debug("created route")

	return &netlink.Route{
		Dst:      nlPath.Dst.GetIPNet(),
		Src:      nlPath.Src.Bytes(),
		Gw:       nlPath.NextHop.Bytes(),
		Priority: nlPath.Priority,
		Type:     nlPath.Type,
		Table:    nw.options.RoutingTable, // config dependent
		Protocol: route.ProtoBio,          // fix
	}, nil
}

func (nw *NetlinkWriter) createRouteFromBGPPath(pfx bnet.Prefix, path *route.Path) (*netlink.Route, error) {
	bgpPath := path.BGPPath

	log.WithFields(log.Fields{
		"Dst":           pfx,
		"NextHop":       bgpPath.NextHop,
		"Protocol":      "BGP",
		"BGPIdentifier": bgpPath.BGPIdentifier,
		"Table":         nw.options.RoutingTable,
	}).Debug("created route")

	return &netlink.Route{
		Dst:      pfx.GetIPNet(),
		Gw:       bgpPath.NextHop.Bytes(),
		Table:    nw.options.RoutingTable, // config dependent
		Protocol: route.ProtoBio,          // fix
	}, nil

}