package server

import (
	"bytes"
	"fmt"
	"net"

	bnet "github.com/bio-routing/bio-rd/net"
	"github.com/bio-routing/bio-rd/protocols/bgp/packet"
	"github.com/bio-routing/bio-rd/route"
	"github.com/bio-routing/bio-rd/routingtable"
	"github.com/bio-routing/bio-rd/routingtable/adjRIBIn"
	"github.com/bio-routing/bio-rd/routingtable/adjRIBOut"
)

type establishedState struct {
	fsm *FSM
}

func newEstablishedState(fsm *FSM) *establishedState {
	return &establishedState{
		fsm: fsm,
	}
}

func (s establishedState) run() (state, string) {
	if !s.fsm.ribsInitialized {
		err := s.init()
		if err != nil {
			return newCeaseState(), fmt.Sprintf("Init failed: %v", err)
		}
	}

	for {
		select {
		case e := <-s.fsm.eventCh:
			switch e {
			case ManualStop:
				return s.manualStop()
			case AutomaticStop:
				return s.automaticStop()
			case Cease:
				return s.cease()
			default:
				continue
			}
		case <-s.fsm.holdTimer.C:
			return s.holdTimerExpired()
		case <-s.fsm.keepaliveTimer.C:
			return s.keepaliveTimerExpired()
		case recvMsg := <-s.fsm.msgRecvCh:
			return s.msgReceived(recvMsg)
		}
	}
}

func (s *establishedState) init() error {
	s.fsm.adjRIBIn = adjRIBIn.New(s.fsm.peer.importFilter)
	s.fsm.adjRIBIn.Register(s.fsm.rib)

	host, _, err := net.SplitHostPort(s.fsm.con.LocalAddr().String())
	if err != nil {
		return fmt.Errorf("Unable to get local address: %v", err)
	}
	hostIP := net.ParseIP(host)
	if hostIP == nil {
		return fmt.Errorf("Unable to parse address: %v", err)
	}

	n := &routingtable.Neighbor{
		Type:              route.BGPPathType,
		Address:           bnet.IPv4ToUint32(s.fsm.peer.addr),
		IBGP:              s.fsm.peer.localASN == s.fsm.peer.peerASN,
		LocalASN:          s.fsm.peer.localASN,
		RouteServerClient: s.fsm.peer.routeServerClient,
		LocalAddress:      bnet.IPv4ToUint32(hostIP),
		CapAddPathRX:      s.fsm.capAddPathSend,
	}

	s.fsm.adjRIBOut = adjRIBOut.New(n, s.fsm.peer.exportFilter)
	clientOptions := routingtable.ClientOptions{
		BestOnly: true,
	}
	if s.fsm.capAddPathSend {
		s.fsm.updateSender = newUpdateSenderAddPath(s.fsm)
		clientOptions = s.fsm.peer.addPathSend
	} else {
		s.fsm.updateSender = newUpdateSender(s.fsm)
	}

	s.fsm.adjRIBOut.Register(s.fsm.updateSender)
	s.fsm.rib.RegisterWithOptions(s.fsm.adjRIBOut, clientOptions)

	s.fsm.ribsInitialized = true
	return nil
}

func (s *establishedState) uninit() {
	s.fsm.adjRIBIn.Unregister(s.fsm.rib)
	s.fsm.rib.Unregister(s.fsm.adjRIBOut)
	s.fsm.adjRIBOut.Unregister(s.fsm.updateSender)

	s.fsm.adjRIBIn = nil
	s.fsm.adjRIBOut = nil

	s.fsm.ribsInitialized = false
}

func (s *establishedState) manualStop() (state, string) {
	s.fsm.sendNotification(packet.Cease, 0)
	s.uninit()
	stopTimer(s.fsm.connectRetryTimer)
	s.fsm.con.Close()
	s.fsm.connectRetryCounter = 0
	return newIdleState(s.fsm), "Manual stop event"
}

func (s *establishedState) automaticStop() (state, string) {
	s.fsm.sendNotification(packet.Cease, 0)
	s.uninit()
	stopTimer(s.fsm.connectRetryTimer)
	s.fsm.con.Close()
	s.fsm.connectRetryCounter++
	return newIdleState(s.fsm), "Automatic stop event"
}

func (s *establishedState) cease() (state, string) {
	s.fsm.sendNotification(packet.Cease, 0)
	s.uninit()
	s.fsm.con.Close()
	return newCeaseState(), "Cease"
}

func (s *establishedState) holdTimerExpired() (state, string) {
	s.fsm.sendNotification(packet.HoldTimeExpired, 0)
	s.uninit()
	stopTimer(s.fsm.connectRetryTimer)
	s.fsm.con.Close()
	s.fsm.connectRetryCounter++
	return newIdleState(s.fsm), "Holdtimer expired"
}

func (s *establishedState) keepaliveTimerExpired() (state, string) {
	err := s.fsm.sendKeepalive()
	if err != nil {
		stopTimer(s.fsm.connectRetryTimer)
		s.fsm.con.Close()
		s.fsm.connectRetryCounter++
		return newIdleState(s.fsm), fmt.Sprintf("Failed to send keepalive: %v", err)
	}

	s.fsm.keepaliveTimer.Reset(s.fsm.keepaliveTime)
	return newEstablishedState(s.fsm), s.fsm.reason
}

func (s *establishedState) msgReceived(data []byte) (state, string) {
	msg, err := packet.Decode(bytes.NewBuffer(data))
	if err != nil {
		switch bgperr := err.(type) {
		case packet.BGPError:
			s.fsm.sendNotification(bgperr.ErrorCode, bgperr.ErrorSubCode)
		}
		stopTimer(s.fsm.connectRetryTimer)
		s.fsm.con.Close()
		s.fsm.connectRetryCounter++
		return newIdleState(s.fsm), "Failed to decode BGP message"
	}
	switch msg.Header.Type {
	case packet.NotificationMsg:
		return s.notification()
	case packet.UpdateMsg:
		return s.update(msg)
	case packet.KeepaliveMsg:
		return s.keepaliveReceived()
	default:
		return s.unexpectedMessage()
	}
}

func (s *establishedState) notification() (state, string) {
	stopTimer(s.fsm.connectRetryTimer)
	s.uninit()
	s.fsm.con.Close()
	s.fsm.connectRetryCounter++
	return newIdleState(s.fsm), "Received NOTIFICATION"
}

func (s *establishedState) update(msg *packet.BGPMessage) (state, string) {
	if s.fsm.holdTime != 0 {
		s.fsm.holdTimer.Reset(s.fsm.holdTime)
	}

	u := msg.Body.(*packet.BGPUpdate)
	s.withdraws(u)
	s.updates(u)

	return newEstablishedState(s.fsm), s.fsm.reason
}

func (s *establishedState) withdraws(u *packet.BGPUpdate) {
	for r := u.WithdrawnRoutes; r != nil; r = r.Next {
		pfx := bnet.NewPfx(r.IP, r.Pfxlen)
		s.fsm.adjRIBIn.RemovePath(pfx, nil)
	}
}

func (s *establishedState) updates(u *packet.BGPUpdate) {
	for r := u.NLRI; r != nil; r = r.Next {
		pfx := bnet.NewPfx(r.IP, r.Pfxlen)

		path := &route.Path{
			Type: route.BGPPathType,
			BGPPath: &route.BGPPath{
				Source: bnet.IPv4ToUint32(s.fsm.peer.addr),
			},
		}

		for pa := u.PathAttributes; pa != nil; pa = pa.Next {
			switch pa.TypeCode {
			case packet.OriginAttr:
				path.BGPPath.Origin = pa.Value.(uint8)
			case packet.LocalPrefAttr:
				path.BGPPath.LocalPref = pa.Value.(uint32)
			case packet.MEDAttr:
				path.BGPPath.MED = pa.Value.(uint32)
			case packet.NextHopAttr:
				path.BGPPath.NextHop = pa.Value.(uint32)
			case packet.ASPathAttr:
				path.BGPPath.ASPath = pa.ASPathString()
				path.BGPPath.ASPathLen = pa.ASPathLen()
			case packet.CommunitiesAttr:
				path.BGPPath.Communities = pa.CommunityString()
			case packet.LargeCommunitiesAttr:
				path.BGPPath.LargeCommunities = pa.LargeCommunityString()
			}
		}
		s.fsm.adjRIBIn.AddPath(pfx, path)
	}
}

func (s *establishedState) keepaliveReceived() (state, string) {
	if s.fsm.holdTime != 0 {
		s.fsm.holdTimer.Reset(s.fsm.holdTime)
	}
	return newEstablishedState(s.fsm), s.fsm.reason
}

func (s *establishedState) unexpectedMessage() (state, string) {
	s.fsm.sendNotification(packet.FiniteStateMachineError, 0)
	s.uninit()
	stopTimer(s.fsm.connectRetryTimer)
	s.fsm.con.Close()
	s.fsm.connectRetryCounter++
	return newIdleState(s.fsm), "FSM Error"
}