package manager

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net"
	"net/netip"
	"sort"
	"strings"

	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/client/internal/statemanager"
)

const (
	ForwardingFormatPrefix = "netbird-fwd-"
	ForwardingFormat       = "netbird-fwd-%s-%t"
	PreroutingFormat       = "netbird-prerouting-%s-%t"
	NatFormat              = "netbird-nat-%s-%t"
)

// Rule abstraction should be implemented by each firewall manager
//
// Each firewall type for different OS can use different type
// of the properties to hold data of the created rule
type Rule interface {
	// ID returns the rule id
	ID() string
}

// RuleDirection is the traffic direction which a rule is applied
type RuleDirection int

const (
	// RuleDirectionIN applies to filters that handlers incoming traffic
	RuleDirectionIN RuleDirection = iota
	// RuleDirectionOUT applies to filters that handlers outgoing traffic
	RuleDirectionOUT
)

// Action is the action to be taken on a rule
type Action int

const (
	// ActionAccept is the action to accept a packet
	ActionAccept Action = iota
	// ActionDrop is the action to drop a packet
	ActionDrop
)

// Manager is the high level abstraction of a firewall manager
//
// It declares methods which handle actions required by the
// Netbird client for ACL and routing functionality
type Manager interface {
	Init(stateManager *statemanager.Manager) error

	// AllowNetbird allows netbird interface traffic
	AllowNetbird() error

	// AddPeerFiltering adds a rule to the firewall
	//
	// If comment argument is empty firewall manager should set
	// rule ID as comment for the rule
	AddPeerFiltering(
		ip net.IP,
		proto Protocol,
		sPort *Port,
		dPort *Port,
		action Action,
		ipsetName string,
		comment string,
	) ([]Rule, error)

	// DeletePeerRule from the firewall by rule definition
	DeletePeerRule(rule Rule) error

	// IsServerRouteSupported returns true if the firewall supports server side routing operations
	IsServerRouteSupported() bool

	AddRouteFiltering(source []netip.Prefix, destination netip.Prefix, proto Protocol, sPort *Port, dPort *Port, action Action) (Rule, error)

	// DeleteRouteRule deletes a routing rule
	DeleteRouteRule(rule Rule) error

	// AddNatRule inserts a routing NAT rule
	AddNatRule(pair RouterPair) error

	// RemoveNatRule removes a routing NAT rule
	RemoveNatRule(pair RouterPair) error

	// SetLegacyManagement sets the legacy management mode
	SetLegacyManagement(legacy bool) error

	// Reset firewall to the default state
	Reset(stateManager *statemanager.Manager) error

	// Flush the changes to firewall controller
	Flush() error

	SetLogLevel(log.Level)

	EnableRouting() error

	DisableRouting() error

	// AddDNATRule adds a DNAT rule
	AddDNATRule(ForwardRule) (Rule, error)

	// DeleteDNATRule deletes a DNAT rule
	DeleteDNATRule(Rule) error
}

func GenKey(format string, pair RouterPair) string {
	return fmt.Sprintf(format, pair.ID, pair.Inverse)
}

// LegacyManager defines the interface for legacy management operations
type LegacyManager interface {
	RemoveAllLegacyRouteRules() error
	GetLegacyManagement() bool
	SetLegacyManagement(bool)
}

// SetLegacyManagement sets the route manager to use legacy management
func SetLegacyManagement(router LegacyManager, isLegacy bool) error {
	oldLegacy := router.GetLegacyManagement()

	if oldLegacy != isLegacy {
		router.SetLegacyManagement(isLegacy)
		log.Debugf("Set legacy management to %v", isLegacy)
	}

	// client reconnected to a newer mgmt, we need to clean up the legacy rules
	if !isLegacy && oldLegacy {
		if err := router.RemoveAllLegacyRouteRules(); err != nil {
			return fmt.Errorf("remove legacy routing rules: %v", err)
		}

		log.Debugf("Legacy routing rules removed")
	}

	return nil
}

// GenerateSetName generates a unique name for an ipset based on the given sources.
func GenerateSetName(sources []netip.Prefix) string {
	// sort for consistent naming
	SortPrefixes(sources)

	var sourcesStr strings.Builder
	for _, src := range sources {
		sourcesStr.WriteString(src.String())
	}

	hash := sha256.Sum256([]byte(sourcesStr.String()))
	shortHash := hex.EncodeToString(hash[:])[:8]

	return fmt.Sprintf("nb-%s", shortHash)
}

// MergeIPRanges merges overlapping IP ranges and returns a slice of non-overlapping netip.Prefix
func MergeIPRanges(prefixes []netip.Prefix) []netip.Prefix {
	if len(prefixes) == 0 {
		return prefixes
	}

	merged := []netip.Prefix{prefixes[0]}
	for _, prefix := range prefixes[1:] {
		last := merged[len(merged)-1]
		if last.Contains(prefix.Addr()) {
			// If the current prefix is contained within the last merged prefix, skip it
			continue
		}
		if prefix.Contains(last.Addr()) {
			// If the current prefix contains the last merged prefix, replace it
			merged[len(merged)-1] = prefix
		} else {
			// Otherwise, add the current prefix to the merged list
			merged = append(merged, prefix)
		}
	}

	return merged
}

// SortPrefixes sorts the given slice of netip.Prefix in place.
// It sorts first by IP address, then by prefix length (most specific to least specific).
func SortPrefixes(prefixes []netip.Prefix) {
	sort.Slice(prefixes, func(i, j int) bool {
		addrCmp := prefixes[i].Addr().Compare(prefixes[j].Addr())
		if addrCmp != 0 {
			return addrCmp < 0
		}

		// If IP addresses are the same, compare prefix lengths (longer prefixes first)
		return prefixes[i].Bits() > prefixes[j].Bits()
	})
}
