// Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
//
// 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 server

import (
	"encoding/json"
	log "github.com/Sirupsen/logrus"
	"github.com/osrg/gobgp/api"
	"github.com/osrg/gobgp/config"
	"github.com/osrg/gobgp/packet"
	"github.com/osrg/gobgp/policy"
	"github.com/osrg/gobgp/table"
	"net"
	"time"
)

const (
	FLOP_THRESHOLD    = time.Second * 30
	MIN_CONNECT_RETRY = 10
)

type Peer struct {
	gConf                   config.Global
	conf                    config.Neighbor
	fsm                     *FSM
	rfMap                   map[bgp.RouteFamily]bool
	capMap                  map[bgp.BGPCapabilityCode]bgp.ParameterCapabilityInterface
	adjRib                  *table.AdjRib
	peerInfo                *table.PeerInfo
	outgoing                chan *bgp.BGPMessage
	distPolicies            []*policy.Policy
	defaultDistributePolicy config.DefaultPolicyType
	isConfederationMember   bool
	isEBGP                  bool
}

func NewPeer(g config.Global, conf config.Neighbor) *Peer {
	peer := &Peer{
		gConf:  g,
		conf:   conf,
		rfMap:  make(map[bgp.RouteFamily]bool),
		capMap: make(map[bgp.BGPCapabilityCode]bgp.ParameterCapabilityInterface),
	}

	conf.NeighborState.SessionState = uint32(bgp.BGP_FSM_IDLE)
	conf.Timers.TimersState.Downtime = time.Now().Unix()
	for _, rf := range conf.AfiSafis.AfiSafiList {
		k, _ := bgp.GetRouteFamily(rf.AfiSafiName)
		peer.rfMap[k] = true
	}
	peer.peerInfo = &table.PeerInfo{
		AS:      conf.NeighborConfig.PeerAs,
		LocalID: g.GlobalConfig.RouterId,
		Address: conf.NeighborConfig.NeighborAddress,
	}
	peer.adjRib = table.NewAdjRib(peer.configuredRFlist())
	peer.fsm = NewFSM(&g, &conf)

	if conf.NeighborConfig.PeerAs != g.GlobalConfig.As {
		peer.isEBGP = true
		for _, member := range g.Confederation.ConfederationConfig.MemberAs {
			if member == conf.NeighborConfig.PeerAs {
				peer.isConfederationMember = true
				break
			}
		}
	}

	return peer
}

func (peer *Peer) isRouteServerClient() bool {
	return peer.conf.RouteServer.RouteServerConfig.RouteServerClient
}

func (peer *Peer) configuredRFlist() []bgp.RouteFamily {
	rfList := []bgp.RouteFamily{}
	for _, rf := range peer.conf.AfiSafis.AfiSafiList {
		k, _ := bgp.GetRouteFamily(rf.AfiSafiName)
		rfList = append(rfList, k)
	}
	return rfList
}

func (peer *Peer) handleBGPmessage(m *bgp.BGPMessage) ([]*table.Path, bool, []*bgp.BGPMessage) {
	bgpMsgList := []*bgp.BGPMessage{}
	pathList := []*table.Path{}
	log.WithFields(log.Fields{
		"Topic": "Peer",
		"Key":   peer.conf.NeighborConfig.NeighborAddress,
		"data":  m,
	}).Debug("received")
	update := false

	switch m.Header.Type {
	case bgp.BGP_MSG_OPEN:
		body := m.Body.(*bgp.BGPOpen)
		peer.peerInfo.ID = m.Body.(*bgp.BGPOpen).ID
		r := make(map[bgp.RouteFamily]bool)
		for _, p := range body.OptParams {
			if paramCap, y := p.(*bgp.OptionParameterCapability); y {
				for _, c := range paramCap.Capability {
					peer.capMap[c.Code()] = c
					if c.Code() == bgp.BGP_CAP_MULTIPROTOCOL {
						m := c.(*bgp.CapMultiProtocol)
						r[bgp.AfiSafiToRouteFamily(m.CapValue.AFI, m.CapValue.SAFI)] = true
					}
				}
			}
		}

		for rf, _ := range peer.rfMap {
			if _, y := r[rf]; !y {
				delete(peer.rfMap, rf)
			}
		}

		for _, rf := range peer.configuredRFlist() {
			if _, ok := r[rf]; ok {
				peer.rfMap[rf] = true
			}
		}

		// calculate HoldTime
		// RFC 4271 P.13
		// a BGP speaker MUST calculate the value of the Hold Timer
		// by using the smaller of its configured Hold Time and the Hold Time
		// received in the OPEN message.
		holdTime := float64(body.HoldTime)
		myHoldTime := peer.conf.Timers.TimersConfig.HoldTime
		if holdTime > myHoldTime {
			peer.fsm.negotiatedHoldTime = myHoldTime
		} else {
			peer.fsm.negotiatedHoldTime = holdTime
		}

	case bgp.BGP_MSG_ROUTE_REFRESH:
		rr := m.Body.(*bgp.BGPRouteRefresh)
		rf := bgp.AfiSafiToRouteFamily(rr.AFI, rr.SAFI)
		if _, ok := peer.rfMap[rf]; !ok {
			log.WithFields(log.Fields{
				"Topic": "Peer",
				"Key":   peer.conf.NeighborConfig.NeighborAddress,
				"Data":  rf,
			}).Warn("Route family isn't supported")
			break
		}
		if _, ok := peer.capMap[bgp.BGP_CAP_ROUTE_REFRESH]; ok {
			pathList = peer.adjRib.GetOutPathList(rf)
		} else {
			log.WithFields(log.Fields{
				"Topic": "Peer",
				"Key":   peer.conf.NeighborConfig.NeighborAddress,
			}).Warn("ROUTE_REFRESH received but the capability wasn't advertised")
		}

	case bgp.BGP_MSG_UPDATE:
		update = true
		peer.conf.Timers.TimersState.UpdateRecvTime = time.Now().Unix()
		body := m.Body.(*bgp.BGPUpdate)
		confedCheckRequired := !peer.isConfederationMember && peer.isEBGP
		_, err := bgp.ValidateUpdateMsg(body, peer.rfMap, confedCheckRequired)
		if err != nil {
			log.WithFields(log.Fields{
				"Topic": "Peer",
				"Key":   peer.conf.NeighborConfig.NeighborAddress,
				"error": err,
			}).Warn("malformed BGP update message")
			m := err.(*bgp.MessageError)
			if m.TypeCode != 0 {
				bgpMsgList = append(bgpMsgList, bgp.NewBGPNotificationMessage(m.TypeCode, m.SubTypeCode, m.Data))
			}
			break
		}
		table.UpdatePathAttrs4ByteAs(body)
		pathList = table.ProcessMessage(m, peer.peerInfo)
		peer.adjRib.UpdateIn(pathList)
	}
	return pathList, update, bgpMsgList
}

func (peer *Peer) getBests(loc *LocalRib) []*table.Path {
	pathList := []*table.Path{}
	for _, rf := range peer.configuredRFlist() {
		for _, paths := range loc.rib.GetPathList(rf) {
			pathList = append(pathList, paths)
		}
	}
	return pathList
}

func (peer *Peer) startFSMHandler(incoming chan *fsmMsg) {
	peer.fsm.h = NewFSMHandler(peer.fsm, incoming, peer.outgoing)
}

func (peer *Peer) PassConn(conn *net.TCPConn) {
	isEBGP := peer.gConf.GlobalConfig.As != peer.conf.NeighborConfig.PeerAs
	if isEBGP {
		ttl := 1
		SetTcpTTLSockopts(conn, ttl)
	}
	select {
	case peer.fsm.connCh <- conn:
	default:
		conn.Close()
		log.WithFields(log.Fields{
			"Topic": "Peer",
			"Key":   peer.conf.NeighborConfig.NeighborAddress,
		}).Warn("accepted conn is closed to avoid be blocked")
	}
}

func (peer *Peer) MarshalJSON() ([]byte, error) {
	return json.Marshal(peer.ToApiStruct())
}

func (peer *Peer) ToApiStruct() *api.Peer {

	f := peer.fsm
	c := f.pConf

	remoteCap := make([]*api.Capability, 0, len(peer.capMap))
	for _, c := range peer.capMap {
		remoteCap = append(remoteCap, c.ToApiStruct())
	}

	caps := capabilitiesFromConfig(&peer.gConf, &peer.conf)
	localCap := make([]*api.Capability, 0, len(caps))
	for _, c := range caps {
		localCap = append(localCap, c.ToApiStruct())
	}

	conf := &api.PeerConf{
		RemoteIp:          c.NeighborConfig.NeighborAddress.String(),
		Id:                peer.peerInfo.ID.To4().String(),
		RemoteAs:          c.NeighborConfig.PeerAs,
		RemoteCap:         remoteCap,
		LocalCap:          localCap,
		KeepaliveInterval: uint32(peer.conf.Timers.TimersConfig.KeepaliveInterval),
		Holdtime:          uint32(peer.conf.Timers.TimersConfig.HoldTime),
	}

	s := &c.NeighborState
	timer := &c.Timers

	uptime := int64(0)
	if timer.TimersState.Uptime != 0 {
		uptime = int64(time.Now().Sub(time.Unix(timer.TimersState.Uptime, 0)).Seconds())
	}
	downtime := int64(0)
	if timer.TimersState.Downtime != 0 {
		downtime = int64(time.Now().Sub(time.Unix(timer.TimersState.Downtime, 0)).Seconds())
	}

	advertized := uint32(0)
	received := uint32(0)
	accepted := uint32(0)
	if f.state == bgp.BGP_FSM_ESTABLISHED {
		for _, rf := range peer.configuredRFlist() {
			advertized += uint32(peer.adjRib.GetOutCount(rf))
			received += uint32(peer.adjRib.GetInCount(rf))
			// FIXME: we should store 'accepted' in memory
			for _, p := range peer.adjRib.GetInPathList(rf) {
				applied, path := peer.applyDistributePolicies(p)
				if applied && path == nil || !applied && peer.defaultDistributePolicy != config.DEFAULT_POLICY_TYPE_ACCEPT_ROUTE {
					continue
				}
				accepted += 1
			}
		}
	}

	keepalive := uint32(0)
	if f.negotiatedHoldTime != 0 {
		if f.negotiatedHoldTime < timer.TimersConfig.HoldTime {
			keepalive = uint32(f.negotiatedHoldTime / 3)
		} else {
			keepalive = uint32(timer.TimersConfig.KeepaliveInterval)
		}
	}

	info := &api.PeerInfo{
		BgpState:                  f.state.String(),
		AdminState:                f.adminState.String(),
		FsmEstablishedTransitions: s.EstablishedCount,
		TotalMessageOut:           s.Messages.Sent.Total,
		TotalMessageIn:            s.Messages.Received.Total,
		UpdateMessageOut:          s.Messages.Sent.Update,
		UpdateMessageIn:           s.Messages.Received.Update,
		KeepAliveMessageOut:       s.Messages.Sent.Keepalive,
		KeepAliveMessageIn:        s.Messages.Received.Keepalive,
		OpenMessageOut:            s.Messages.Sent.Open,
		OpenMessageIn:             s.Messages.Received.Open,
		NotificationOut:           s.Messages.Sent.Notification,
		NotificationIn:            s.Messages.Received.Notification,
		RefreshMessageOut:         s.Messages.Sent.Refresh,
		RefreshMessageIn:          s.Messages.Received.Refresh,
		DiscardedOut:              s.Messages.Sent.Discarded,
		DiscardedIn:               s.Messages.Received.Discarded,
		Uptime:                    uptime,
		Downtime:                  downtime,
		Received:                  received,
		Accepted:                  accepted,
		Advertized:                advertized,
		OutQ:                      uint32(len(peer.outgoing)),
		Flops:                     s.Flops,
		NegotiatedHoldtime:        uint32(f.negotiatedHoldTime),
		KeepaliveInterval:         keepalive,
	}

	return &api.Peer{
		Conf: conf,
		Info: info,
	}
}

func (peer *Peer) setDistributePolicy(policyMap map[string]*policy.Policy) {
	// configure distribute policy
	policyConf := peer.conf.ApplyPolicy
	distPolicies := make([]*policy.Policy, 0)
	for _, policyName := range policyConf.ApplyPolicyConfig.DistributePolicy {
		log.WithFields(log.Fields{
			"Topic":      "Peer",
			"Key":        peer.conf.NeighborConfig.NeighborAddress,
			"PolicyName": policyName,
		}).Info("distribute policy installed")
		if pol, ok := policyMap[policyName]; ok {
			log.Debug("distribute policy : ", pol)
			distPolicies = append(distPolicies, pol)
		}
	}
	peer.distPolicies = distPolicies
	peer.defaultDistributePolicy = policyConf.ApplyPolicyConfig.DefaultDistributePolicy
}

func (peer *Peer) applyDistributePolicies(original *table.Path) (bool, *table.Path) {
	policies := peer.distPolicies
	var d Direction = POLICY_DIRECTION_DISTRIBUTE

	return applyPolicy("Peer", peer.conf.NeighborConfig.NeighborAddress.String(), d, policies, original)
}

type LocalRib struct {
	rib                 *table.TableManager
	importPolicies      []*policy.Policy
	defaultImportPolicy config.DefaultPolicyType
	exportPolicies      []*policy.Policy
	defaultExportPolicy config.DefaultPolicyType
}

func NewLocalRib(owner string, rfList []bgp.RouteFamily, policyMap map[string]*policy.Policy) *LocalRib {
	return &LocalRib{
		rib: table.NewTableManager(owner, rfList),
	}
}

func (loc *LocalRib) OwnerName() string {
	return loc.rib.OwnerName()
}

func (loc *LocalRib) isGlobal() bool {
	return loc.OwnerName() == "global"
}

func (loc *LocalRib) setPolicy(peer *Peer, policyMap map[string]*policy.Policy) {
	// configure import policy
	policyConf := peer.conf.ApplyPolicy
	inPolicies := make([]*policy.Policy, 0)
	for _, policyName := range policyConf.ApplyPolicyConfig.ImportPolicy {
		log.WithFields(log.Fields{
			"Topic":      "Peer",
			"Key":        peer.conf.NeighborConfig.NeighborAddress,
			"PolicyName": policyName,
		}).Info("import policy installed")
		if pol, ok := policyMap[policyName]; ok {
			log.Debug("import policy : ", pol)
			inPolicies = append(inPolicies, pol)
		}
	}
	loc.importPolicies = inPolicies
	loc.defaultImportPolicy = policyConf.ApplyPolicyConfig.DefaultImportPolicy

	// configure export policy
	outPolicies := make([]*policy.Policy, 0)
	for _, policyName := range policyConf.ApplyPolicyConfig.ExportPolicy {
		log.WithFields(log.Fields{
			"Topic":      "Peer",
			"Key":        peer.conf.NeighborConfig.NeighborAddress,
			"PolicyName": policyName,
		}).Info("export policy installed")
		if pol, ok := policyMap[policyName]; ok {
			log.Debug("export policy : ", pol)
			outPolicies = append(outPolicies, pol)
		}
	}
	loc.exportPolicies = outPolicies
	loc.defaultExportPolicy = policyConf.ApplyPolicyConfig.DefaultExportPolicy
}

// apply policies to the path
// if multiple policies are defined,
// this function applies each policy to the path in the order that
// policies are stored in the array passed to this function.
//
// the way of applying statements inside a single policy
//   - apply statement until the condition in the statement matches.
//     if the condition matches the path, apply the action on the statement and
//     return value that indicates 'applied' to caller of this function
//   - if no statement applied, then process the next policy
//
// if no policy applied, return value that indicates 'not applied' to the caller of this function
//
// return values:
//	bool -- indicates that any of policy applied to the path that is passed to this function
//  table.Path -- indicates new path object that is the result of modification according to
//                policy's action.
//                If the applied policy doesn't have a modification action,
//                then return the path itself that is passed to this function, otherwise return
//                modified path.
//                If action of the policy is 'reject', return nil
//
func (loc *LocalRib) applyPolicies(d Direction, original *table.Path) (bool, *table.Path) {
	var policies []*policy.Policy
	switch d {
	case POLICY_DIRECTION_EXPORT:
		policies = loc.exportPolicies
	case POLICY_DIRECTION_IMPORT:
		policies = loc.importPolicies
	}
	return applyPolicy("Loc", loc.OwnerName(), d, policies, original)
}

func applyPolicy(component, owner string, d Direction, policies []*policy.Policy, original *table.Path) (bool, *table.Path) {
	var applied bool = true
	for _, pol := range policies {
		if result, action, newpath := pol.Apply(original); result {
			log.Debug("newpath: ", newpath)
			if action == policy.ROUTE_TYPE_REJECT {
				log.WithFields(log.Fields{
					"Topic": component,
					"Key":   owner,
					"NRLI":  original.GetNlri(),
					"Dir":   d,
				}).Debug("path was rejected")
				// return applied, nil, this means path was rejected
				return applied, nil
			} else {
				// return applied, new path
				return applied, newpath
			}
		}
	}

	log.WithFields(log.Fields{
		"Topic": component,
		"Key":   owner,
		"Len":   len(policies),
		"NRLI":  original,
		"Dir":   d,
	}).Debug("no policy applied")
	return !applied, original
}