package routeupdater

import (
	"fmt"
	"net"
	"sort"
	"strconv"
	"strings"

	"github.com/filtr/infrastructure/go/openvpn-route-updater/internal/dnsresolver"
	"github.com/filtr/infrastructure/go/openvpn-route-updater/internal/logger"

	"github.com/pkg/errors"
)

// Route simple struct with route id and actual route value
type Route struct {
	RouteID    string
	RouteValue string
}

// OpenVPNGroup group info structure
type OpenVPNGroup struct {
	PrivateRoutes []Route
	PublicRoutes  []Route
	Name          string
}

// OpenVPNGroupManager group manager
type OpenVPNGroupManager struct {
	log   logger.Logger
	Group *OpenVPNGroup
}

// SetLogger - set logger -_-
func (c *OpenVPNGroupManager) SetLogger(log logger.Logger) {
	c.log = log
}

// SortRoutesListByID - sort routes by the last part of id( access_to.num_by_which_we_sort)
func SortRoutesListByID(routes []Route) []Route {
	sort.Slice(routes, func(i, j int) bool {
		a, _ := strconv.Atoi(strings.Split(routes[i].RouteID, ".")[1])
		b, _ := strconv.Atoi(strings.Split(routes[j].RouteID, ".")[1])
		return a < b
	})
	return routes
}

// NewGroup get list of private and public routes of OpenVPN group
func NewGroup(log logger.Logger, name string, cli CliInterface) (*OpenVPNGroup, error) {
	groupProperties, err := cli.GetGroupProps(name)
	if err != nil {
		return nil, err
	}
	group := &OpenVPNGroup{Name: name}
	log.Debug(groupProperties)
	for k, v := range groupProperties {
		if strings.HasPrefix(k, "access_to.") {
			isPrivate, err := IsRoutePrivate(v.(string))
			if err != nil {
				return nil, err
			}
			if isPrivate {
				group.PrivateRoutes = append(group.PrivateRoutes, Route{RouteID: k, RouteValue: v.(string)})
			} else {
				group.PublicRoutes = append(group.PublicRoutes, Route{RouteID: k, RouteValue: v.(string)})
			}
		}
	}
	group.PrivateRoutes = SortRoutesListByID(group.PrivateRoutes)
	group.PublicRoutes = SortRoutesListByID(group.PublicRoutes)
	log.Debug(group)
	return group, nil
}

// Refresh fully refreshes the list of group routes from the cli
func (c *OpenVPNGroupManager) Refresh(cli CliInterface) error {
	groupProperties, err := cli.GetGroupProps(c.Group.Name)
	if err != nil {
		return err
	}
	c.Group.PublicRoutes = nil
	c.Group.PrivateRoutes = nil
	c.log.Debug(groupProperties)
	for k, v := range groupProperties {
		if strings.HasPrefix(k, "access_to.") {
			isPrivate, err := IsRoutePrivate(v.(string))
			if err != nil {
				return err
			}
			if isPrivate {
				c.Group.PrivateRoutes = append(c.Group.PrivateRoutes, Route{RouteID: k, RouteValue: v.(string)})
			} else {
				c.Group.PublicRoutes = append(c.Group.PublicRoutes, Route{RouteID: k, RouteValue: v.(string)})
			}
		}
	}
	c.Group.PrivateRoutes = SortRoutesListByID(c.Group.PrivateRoutes)
	c.Group.PublicRoutes = SortRoutesListByID(c.Group.PublicRoutes)
	c.log.Debug(c.Group)
	return nil
}

// ReindexRoutes - reindex all routes ids
func (c *OpenVPNGroupManager) ReindexRoutes() bool {
	reindexed := false
	for i := range c.Group.PrivateRoutes {
		if c.Group.PrivateRoutes[i].RouteID != fmt.Sprintf("access_to.%v", i) {
			c.Group.PrivateRoutes[i].RouteID = fmt.Sprintf("access_to.%v", i)
			reindexed = true
		}
	}
	for i := range c.Group.PublicRoutes {
		if c.Group.PublicRoutes[i].RouteID != fmt.Sprintf("access_to.%v", i+len(c.Group.PrivateRoutes)) {
			c.Group.PublicRoutes[i].RouteID = fmt.Sprintf("access_to.%v", i+len(c.Group.PrivateRoutes))
			reindexed = true
		}
	}
	return reindexed
}

// DeletePublicRoutesCli - remove all public routes
func (c *OpenVPNGroupManager) DeletePublicRoutesCli(cli CliInterface) error {
	for i := len(c.Group.PublicRoutes) - 1; i >= 0; i-- {
		err := cli.RemoveRoute(c.Group.Name, c.Group.PublicRoutes[i].RouteID)
		if err != nil {
			return err
		}
	}
	c.Group.PublicRoutes = nil
	return nil
}

// DeletePrivateRoutesCli - remove all public routes
func (c *OpenVPNGroupManager) DeletePrivateRoutesCli(cli CliInterface) error {
	for i := len(c.Group.PrivateRoutes) - 1; i >= 0; i-- {
		err := cli.RemoveRoute(c.Group.Name, c.Group.PrivateRoutes[i].RouteID)
		if err != nil {
			return err
		}
	}
	c.Group.PrivateRoutes = nil
	return nil
}

// GetPublicRouteValues - slice of route values only for Public routes
func (c OpenVPNGroupManager) GetPublicRouteValues() []string {
	var res []string
	for _, route := range c.Group.PublicRoutes {
		res = append(res, route.RouteValue)
	}
	return res
}

// GetPrivateRouteValues - slice of route values only for Public routes
func (c OpenVPNGroupManager) GetPrivateRouteValues() []string {
	var res []string
	for _, route := range c.Group.PrivateRoutes {
		res = append(res, route.RouteValue)
	}
	return res
}

// SetPublicRoutes - set public routes from parameter (index starts after depending on the amount of private routes)
func (c *OpenVPNGroupManager) SetPublicRoutes(routes []string) error {
	c.Group.PublicRoutes = nil
	for i, elem := range routes {
		isPrivate, err := IsRoutePrivate(elem)
		if err != nil {
			return err
		}
		if !isPrivate {
			route := &Route{RouteID: fmt.Sprintf("access_to.%v", i+len(c.Group.PrivateRoutes)), RouteValue: elem}
			c.Group.PublicRoutes = append(c.Group.PublicRoutes, *route)
		}
	}
	return nil
}

// SetPrivateRoutes - set private routes from parameter
func (c *OpenVPNGroupManager) SetPrivateRoutes(routes []string) error {
	c.Group.PrivateRoutes = nil
	for i, elem := range routes {
		isPrivate, err := IsRoutePrivate(elem)
		if err != nil {
			return err
		}
		if isPrivate {
			route := &Route{RouteID: fmt.Sprintf("access_to.%v", i), RouteValue: elem}
			c.Group.PrivateRoutes = append(c.Group.PrivateRoutes, *route)
		}
	}
	return nil
}

// ApplyPublicRoutes - apply all public routes for the group
func (c OpenVPNGroupManager) ApplyPublicRoutes(cli CliInterface) error {
	for _, route := range c.Group.PublicRoutes {
		err := cli.SetRoute(c.Group.Name, route.RouteID, route.RouteValue)
		if err != nil {
			return errors.Wrapf(err, "Error during route %v", route)
		}
	}
	return nil
}

// ApplyPrivateRoutes - apply all public routes for the group
func (c OpenVPNGroupManager) ApplyPrivateRoutes(cli CliInterface) error {
	for _, route := range c.Group.PrivateRoutes {
		err := cli.SetRoute(c.Group.Name, route.RouteID, route.RouteValue)
		if err != nil {
			return errors.Wrapf(err, "Error during route %v", route)
		}
	}
	return nil
}

// IsRoutePrivate - check whether route is private
func IsRoutePrivate(route string) (bool, error) {
	net, err := RouteValueToIPNet(route)
	if err != nil {
		return false, err
	}
	isPrivate, err := dnsresolver.CheckIPRangePrivate(net)
	if err != nil {
		return false, err
	}
	return isPrivate, nil
}

// RouteValueToIPNet convert route string to IP object
func RouteValueToIPNet(route string) (*net.IPNet, error) {
	var start int
	for index, ch := range route {
		if string(ch) == ":" {
			start = index + 1
		}
	}
	_, ipNet, err := net.ParseCIDR(route[start:])
	if err != nil {
		return nil, errors.Wrapf(err, "Can't parse cidr %v", route[start:])
	}
	return ipNet, nil
}

func RoutesToIPList(routes []string) ([]net.IP, error) {
	var ips []net.IP
	for _, r := range routes {
		ipnet, err := RouteValueToIPNet(r)
		if err != nil {
			return nil, err
		}
		ips = append(ips, ipnet.IP.To4())
	}
	return ips, nil
}

// IPNetToRouteValue convert IPNet to route string
func IPNetToRouteValue(ipNet *net.IPNet) string {
	return "+SUBNET:" + ipNet.String()
}

// SingleIPToRouteValue convert single IP address to route string format
func SingleIPToRouteValue(ip net.IP) string {
	return "+SUBNET:" + ip.String() + "/32"
}

// IPListToRouteValues - convert list of IPs to list of routes
func IPListToRouteValues(ips []net.IP) []string {
	var res []string
	for _, ip := range ips {
		res = append(res, SingleIPToRouteValue(ip))
	}
	return res
}
