package controller

import (
	"context"
	"fmt"
	"net"
	"os"
	"strconv"
	"strings"

	"github.com/golang/protobuf/ptypes"
	"github.com/golang/protobuf/ptypes/any"
	c "github.com/mayuresh82/gocast/config"
	api "github.com/osrg/gobgp/api"
	gobgp "github.com/osrg/gobgp/pkg/server"
)

type Route struct {
	Net         *net.IPNet
	Communities []string
}

type Controller struct {
	localAS     int
	localIP     net.IP
	peers       []c.PeerConfig
	communities []string
	origin      uint32
	s           *gobgp.BgpServer
}

func NewController(config c.BgpConfig) (*Controller, error) {
	ctrl := &Controller{}
	var gw net.IP
	var err error

	// Normalize config: convert legacy single-peer to new multi-peer format
	peers := config.Peers
	if len(peers) == 0 {
		// Backward compatibility: convert legacy config
		if config.PeerIP != "" {
			// Explicit peer IP configured
			peers = []c.PeerConfig{{
				PeerIP: config.PeerIP,
				PeerAS: config.PeerAS,
			}}
		} else {
			// No peer IP configured - use default gateway
			gw, err = gateway()
			if err != nil {
				return nil, fmt.Errorf("Unable to get gateway ip: %v", err)
			}
			peers = []c.PeerConfig{{
				PeerIP: gw.String(),
				PeerAS: config.PeerAS,
			}}
		}
	}

	// Determine local IP
	if config.LocalIP == "" {
		// Use first peer to determine local IP
		firstPeerIP := net.ParseIP(peers[0].PeerIP)
		if firstPeerIP == nil {
			gw, err = gateway()
			if err != nil {
				return nil, fmt.Errorf("Unable to get gw ip: %v", err)
			}
			firstPeerIP = gw
		}
		gw, err = via(firstPeerIP)
		if err != nil {
			return nil, fmt.Errorf("Unable to get gw ip: %v", err)
		}
		ctrl.localIP, err = localAddress(gw)
		if err != nil {
			return nil, err
		}
	} else {
		ctrl.localIP = net.ParseIP(config.LocalIP)
	}

	ctrl.localAS = config.LocalAS
	ctrl.peers = peers
	ctrl.communities = config.Communities

	switch config.Origin {
	case "igp":
		ctrl.origin = 0
	case "egp":
		ctrl.origin = 1
	case "unknown":
		ctrl.origin = 2
	}

	s := gobgp.NewBgpServer()
	go s.Serve()
	if err := s.StartBgp(context.Background(), &api.StartBgpRequest{
		Global: &api.Global{
			As:         uint32(config.LocalAS),
			RouterId:   ctrl.localIP.String(),
			ListenPort: -1, // gobgp won't listen on tcp:179
		},
	}); err != nil {
		return nil, fmt.Errorf("Unable to start bgp: %v", err)
	}
	ctrl.s = s

	return ctrl, nil
}

func (c *Controller) addPeer(peer *c.PeerConfig) error {
	n := &api.Peer{
		Conf: &api.PeerConf{
			NeighborAddress: peer.PeerIP,
			PeerAs:          uint32(peer.PeerAS),
		},
	}

	// Enable multihop only if explicitly configured
	if peer.MultiHop != nil && *peer.MultiHop {
		n.EbgpMultihop = &api.EbgpMultihop{Enabled: true, MultihopTtl: uint32(255)}
	}

	// Configure MD5 authentication if specified
	md5Password := c.getMD5Password(peer)
	if md5Password != "" {
		n.Conf.AuthPassword = md5Password
	}

	return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n})
}

// getMD5Password retrieves the MD5 password from config or environment variable
func (c *Controller) getMD5Password(peer *c.PeerConfig) string {
	// Priority 1: Check for environment variable
	if peer.MD5EnvVar != "" {
		if password := os.Getenv(peer.MD5EnvVar); password != "" {
			return password
		}
	}

	// Priority 2: Use password from config file
	if peer.MD5Password != "" {
		return peer.MD5Password
	}

	return ""
}

func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
	afi := api.Family_AFI_IP
	if route.Net.IP.To4() == nil {
		afi = api.Family_AFI_IP6
	}
	prefixlen, _ := route.Net.Mask.Size()
	nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{
		Prefix:    route.Net.IP.String(),
		PrefixLen: uint32(prefixlen),
	})
	a1, _ := ptypes.MarshalAny(&api.OriginAttribute{
		Origin: c.origin,
	})
	a2, _ := ptypes.MarshalAny(&api.NextHopAttribute{
		NextHop: c.localIP.String(),
	})

	// Merge communities: global + per-peer + per-route
	var allCommunities []string
	allCommunities = append(allCommunities, c.communities...)
	allCommunities = append(allCommunities, peer.Communities...)
	allCommunities = append(allCommunities, route.Communities...)

	var communities []uint32
	for _, comm := range allCommunities {
		communities = append(communities, convertCommunity(comm))
	}
	a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{
		Communities: communities,
	})
	attrs := []*any.Any{a1, a2, a3}
	return &api.Path{
		Family: &api.Family{Afi: afi, Safi: api.Family_SAFI_UNICAST},
		Nlri:   nlri,
		Pattrs: attrs,
	}
}

func (c *Controller) Announce(route *Route) error {
	var errs []error

	for i := range c.peers {
		peer := &c.peers[i]

		// Check if peer exists
		var found bool
		err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
			if p.Conf.NeighborAddress == peer.PeerIP {
				found = true
			}
		})
		if err != nil {
			errs = append(errs, fmt.Errorf("peer %s: list error: %v", peer.PeerIP, err))
			continue
		}

		// Add peer if not found
		if !found {
			if err := c.addPeer(peer); err != nil {
				errs = append(errs, fmt.Errorf("peer %s: add error: %v", peer.PeerIP, err))
				continue
			}
		}

		// Announce route to this peer
		path := c.getApiPath(route, peer)
		if _, err := c.s.AddPath(context.Background(), &api.AddPathRequest{Path: path}); err != nil {
			errs = append(errs, fmt.Errorf("peer %s: announce error: %v", peer.PeerIP, err))
			continue
		}
	}

	// Return aggregated errors if any peer failed
	if len(errs) > 0 {
		return fmt.Errorf("announcement errors: %v", errs)
	}
	return nil
}

func (c *Controller) Withdraw(route *Route) error {
	var errs []error

	for i := range c.peers {
		peer := &c.peers[i]
		path := c.getApiPath(route, peer)
		if err := c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: path}); err != nil {
			errs = append(errs, fmt.Errorf("peer %s: withdraw error: %v", peer.PeerIP, err))
			continue
		}
	}

	// Return aggregated errors if any peer failed
	if len(errs) > 0 {
		return fmt.Errorf("withdrawal errors: %v", errs)
	}
	return nil
}

func (c *Controller) PeerInfo() ([]*api.Peer, error) {
	var peers []*api.Peer
	peerMap := make(map[string]bool)

	// Build map of configured peer IPs
	for _, peer := range c.peers {
		peerMap[peer.PeerIP] = true
	}

	// Collect info for all configured peers
	err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
		if peerMap[p.Conf.NeighborAddress] {
			peers = append(peers, p)
		}
	})
	if err != nil {
		return nil, err
	}
	return peers, nil
}

func (c *Controller) Shutdown() error {
	var errs []error

	// Shutdown all peer sessions
	for _, peer := range c.peers {
		if err := c.s.ShutdownPeer(context.Background(), &api.ShutdownPeerRequest{
			Address: peer.PeerIP,
		}); err != nil {
			errs = append(errs, fmt.Errorf("peer %s: shutdown error: %v", peer.PeerIP, err))
		}
	}

	// Stop BGP server
	if err := c.s.StopBgp(context.Background(), &api.StopBgpRequest{}); err != nil {
		errs = append(errs, fmt.Errorf("stop bgp error: %v", err))
	}

	if len(errs) > 0 {
		return fmt.Errorf("shutdown errors: %v", errs)
	}
	return nil
}

func convertCommunity(comm string) uint32 {
	parts := strings.Split(comm, ":")
	first, _ := strconv.ParseUint(parts[0], 10, 32)
	second, _ := strconv.ParseUint(parts[1], 10, 32)
	return uint32(first)<<16 | uint32(second)
}
