package proto_netlink

import (
	"fmt"
	"sync"
	"time"

	"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 NetlinkReader struct {
	options *config.Netlink
	routingtable.ClientManager
	filter *filter.Filter

	mu     sync.RWMutex
	routes []netlink.Route
}

func NewNetlinkReader(options *config.Netlink) *NetlinkReader {
	nr := &NetlinkReader{
		options: options,
		filter:  options.ImportFilter,
	}

	nr.ClientManager = routingtable.NewClientManager(nr)

	return nr
}

// Read routes from kernel
func (nr *NetlinkReader) Read() {
	log.WithField("rt_table", nr.options.RoutingTable).Info("Started netlink server")

	// Start fetching the kernel routes after the hold time
	time.Sleep(nr.options.HoldTime)

	for {
		// Family doesn't matter. I only filter by the rt_table here
		routes, err := netlink.RouteListFiltered(4, &netlink.Route{Table: nr.options.RoutingTable}, netlink.RT_FILTER_TABLE)
		if err != nil {
			log.WithError(err).Panic("Failed to read routes from kernel")
		}

		nr.propagateChanges(routes)

		nr.mu.Lock()
		nr.routes = routes

		log.Debugf("NetlinkRouteDiff: %d", len(route.NetlinkRouteDiff(nr.routes, routes)))
		nr.mu.Unlock()

		time.Sleep(nr.options.UpdateInterval)
	}
}

// create a path from a route
func createPathFromRoute(r *netlink.Route) (*route.Path, error) {
	nlPath, err := route.NewNlPathFromRoute(r, true)

	if err != nil {
		return nil, fmt.Errorf("Error while creating path object from route object", err)
	}

	return &route.Path{
		Type:        route.NetlinkPathType,
		NetlinkPath: nlPath,
	}, nil
}

// propagate changes to all subscribed clients
func (nr *NetlinkReader) propagateChanges(routes []netlink.Route) {
	nr.removePathsFromClients(routes)
	nr.addPathsToClients(routes)
}

// Add given paths to clients
func (nr *NetlinkReader) addPathsToClients(routes []netlink.Route) {
	for _, client := range nr.ClientManager.Clients() {
		// only advertise changed routes

		nr.mu.RLock()
		advertise := route.NetlinkRouteDiff(routes, nr.routes)
		nr.mu.RUnlock()

		for _, r := range advertise {
			// Is it a BIO-Written route? if so, skip it, dont advertise it
			if r.Protocol == route.ProtoBio {
				log.WithFields(routeLogFields(r)).Debug("Skipping bio route")
				continue
			}

			// create pfx and path from route
			pfx := bnet.NewPfxFromIPNet(r.Dst)
			path, err := createPathFromRoute(&r)
			if err != nil {
				log.WithError(err).Error("Unable to create path")
				continue
			}

			// Apply filter (if existing)
			if nr.filter != nil {
				var reject bool
				// TODO: Implement filter that cann handle netlinkRoute objects
				path, reject = nr.filter.ProcessTerms(pfx, path)
				if reject {
					log.Debug("Skipping route due to filter")
					continue
				}
			}

			log.WithFields(log.Fields{
				"pfx":  pfx,
				"path": path,
			}).Debug("NetlinkReader - client.AddPath")
			client.AddPath(pfx, path)
		}
	}
}

// Remove given paths from clients
func (nr *NetlinkReader) removePathsFromClients(routes []netlink.Route) {
	for _, client := range nr.ClientManager.Clients() {
		// If there where no routes yet, just skip this funktion. There's nothing to delete
		nr.mu.RLock()
		if len(nr.routes) == 0 {
			nr.mu.RUnlock()
			break
		}

		// only withdraw changed routes
		withdraw := route.NetlinkRouteDiff(nr.routes, routes)
		nr.mu.RUnlock()

		for _, r := range withdraw {
			// Is it a BIO-Written route? if so, skip it, dont advertise it
			if r.Protocol == route.ProtoBio {
				continue
			}

			// create pfx and path from route
			pfx := bnet.NewPfxFromIPNet(r.Dst)
			path, err := createPathFromRoute(&r)
			if err != nil {
				log.WithError(err).Error("Unable to create path")
				continue
			}

			// Apply filter (if existing)
			if nr.filter != nil {
				var reject bool
				// TODO: Implement filter that cann handle netlinkRoute objects
				path, reject = nr.filter.ProcessTerms(pfx, path)
				if reject {
					continue
				}
			}

			log.WithFields(log.Fields{
				"pfx":  pfx,
				"path": path,
			}).Debug("NetlinkReader - client.RemovePath")
			client.RemovePath(pfx, path)
		}
	}
}

func routeLogFields(route netlink.Route) log.Fields {
	return log.Fields{
		"LinkIndex":  route.LinkIndex,
		"ILinkIndex": route.ILinkIndex,
		"Scope":      route.Scope,
		"Dst":        route.Dst,
		"Src":        route.Src,
		"Gw":         route.Gw,
		"MultiPath":  route.MultiPath,
		"Protocol":   route.Protocol,
		"Priority":   route.Priority,
		"Table":      route.Table,
		"Type":       route.Type,
		"Tos":        route.Tos,
		"Flags":      route.Flags,
		"MPLSDst":    route.MPLSDst,
		"NewDst":     route.NewDst,
		"Encap":      route.Encap,
		"MTU":        route.MTU,
		"AdvMSS":     route.AdvMSS,
	}
}

// Not supported
func (nr *NetlinkReader) AddPath(bnet.Prefix, *route.Path) error {
	return fmt.Errorf("Not supported")
}

// Not supported
func (nr *NetlinkReader) RemovePath(bnet.Prefix, *route.Path) bool {
	return false
}

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

func (nr *NetlinkReader) Register(routingtable.RouteTableClient) {
}

func (nr *NetlinkReader) RegisterWithOptions(routingtable.RouteTableClient, routingtable.ClientOptions) {
}

func (nr *NetlinkReader) Unregister(routingtable.RouteTableClient) {
}

func (nr *NetlinkReader) RouteCount() int64 {
	nr.mu.RLock()
	defer nr.mu.RUnlock()

	return int64(len(nr.routes))
}