//go:build !android

package iptables

import (
	"context"
	"fmt"
	"strings"

	"github.com/coreos/go-iptables/iptables"
	log "github.com/sirupsen/logrus"

	firewall "github.com/netbirdio/netbird/client/firewall/manager"
)

const (
	Ipv4Forwarding = "netbird-rt-forwarding"
	ipv4Nat        = "netbird-rt-nat"
)

// constants needed to manage and create iptable rules
const (
	tableFilter             = "filter"
	tableNat                = "nat"
	chainFORWARD            = "FORWARD"
	chainPOSTROUTING        = "POSTROUTING"
	chainRTNAT              = "NETBIRD-RT-NAT"
	chainRTFWD              = "NETBIRD-RT-FWD"
	routingFinalForwardJump = "ACCEPT"
	routingFinalNatJump     = "MASQUERADE"
)

type routerManager struct {
	ctx            context.Context
	stop           context.CancelFunc
	iptablesClient *iptables.IPTables
	rules          map[string][]string
}

func newRouterManager(parentCtx context.Context, iptablesClient *iptables.IPTables) (*routerManager, error) {
	ctx, cancel := context.WithCancel(parentCtx)
	m := &routerManager{
		ctx:            ctx,
		stop:           cancel,
		iptablesClient: iptablesClient,
		rules:          make(map[string][]string),
	}

	err := m.cleanUpDefaultForwardRules()
	if err != nil {
		log.Errorf("failed to cleanup routing rules: %s", err)
		return nil, err
	}
	err = m.createContainers()
	if err != nil {
		log.Errorf("failed to create containers for route: %s", err)
	}
	return m, err
}

// InsertRoutingRules inserts an iptables rule pair to the forwarding chain and if enabled, to the nat chain
func (i *routerManager) InsertRoutingRules(pair firewall.RouterPair) error {
	err := i.insertRoutingRule(firewall.ForwardingFormat, tableFilter, chainRTFWD, routingFinalForwardJump, pair)
	if err != nil {
		return err
	}

	err = i.insertRoutingRule(firewall.InForwardingFormat, tableFilter, chainRTFWD, routingFinalForwardJump, firewall.GetInPair(pair))
	if err != nil {
		return err
	}

	if !pair.Masquerade {
		return nil
	}

	err = i.addNATRule(firewall.NatFormat, tableNat, chainRTNAT, routingFinalNatJump, pair)
	if err != nil {
		return err
	}

	err = i.addNATRule(firewall.InNatFormat, tableNat, chainRTNAT, routingFinalNatJump, firewall.GetInPair(pair))
	if err != nil {
		return err
	}

	return nil
}

// insertRoutingRule inserts an iptables rule
func (i *routerManager) insertRoutingRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
	var err error

	ruleKey := firewall.GenKey(keyFormat, pair.ID)
	rule := genRuleSpec(jump, pair.Source, pair.Destination)
	existingRule, found := i.rules[ruleKey]
	if found {
		err = i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
		if err != nil {
			return fmt.Errorf("error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
		}
		delete(i.rules, ruleKey)
	}

	err = i.iptablesClient.Insert(table, chain, 1, rule...)
	if err != nil {
		return fmt.Errorf("error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
	}

	i.rules[ruleKey] = rule

	return nil
}

// RemoveRoutingRules removes an iptables rule pair from forwarding and nat chains
func (i *routerManager) RemoveRoutingRules(pair firewall.RouterPair) error {
	err := i.removeRoutingRule(firewall.ForwardingFormat, tableFilter, chainRTFWD, pair)
	if err != nil {
		return err
	}

	err = i.removeRoutingRule(firewall.InForwardingFormat, tableFilter, chainRTFWD, firewall.GetInPair(pair))
	if err != nil {
		return err
	}

	if !pair.Masquerade {
		return nil
	}

	err = i.removeRoutingRule(firewall.NatFormat, tableNat, chainRTNAT, pair)
	if err != nil {
		return err
	}

	err = i.removeRoutingRule(firewall.InNatFormat, tableNat, chainRTNAT, firewall.GetInPair(pair))
	if err != nil {
		return err
	}

	return nil
}

func (i *routerManager) removeRoutingRule(keyFormat, table, chain string, pair firewall.RouterPair) error {
	var err error

	ruleKey := firewall.GenKey(keyFormat, pair.ID)
	existingRule, found := i.rules[ruleKey]
	if found {
		err = i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
		if err != nil {
			return fmt.Errorf("error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
		}
	}
	delete(i.rules, ruleKey)

	return nil
}

func (i *routerManager) RouteingFwChainName() string {
	return chainRTFWD
}

func (i *routerManager) Reset() error {
	err := i.cleanUpDefaultForwardRules()
	if err != nil {
		return err
	}
	i.rules = make(map[string][]string)
	return nil
}

func (i *routerManager) cleanUpDefaultForwardRules() error {
	err := i.cleanJumpRules()
	if err != nil {
		return err
	}

	log.Debug("flushing routing related tables")
	ok, err := i.iptablesClient.ChainExists(tableFilter, chainRTFWD)
	if err != nil {
		log.Errorf("failed check chain %s,error: %v", chainRTFWD, err)
		return err
	} else if ok {
		err = i.iptablesClient.ClearAndDeleteChain(tableFilter, chainRTFWD)
		if err != nil {
			log.Errorf("failed cleaning chain %s,error: %v", chainRTFWD, err)
			return err
		}
	}

	ok, err = i.iptablesClient.ChainExists(tableNat, chainRTNAT)
	if err != nil {
		log.Errorf("failed check chain %s,error: %v", chainRTNAT, err)
		return err
	} else if ok {
		err = i.iptablesClient.ClearAndDeleteChain(tableNat, chainRTNAT)
		if err != nil {
			log.Errorf("failed cleaning chain %s,error: %v", chainRTNAT, err)
			return err
		}
	}
	return nil
}

func (i *routerManager) createContainers() error {
	if i.rules[Ipv4Forwarding] != nil {
		return nil
	}

	errMSGFormat := "failed creating chain %s,error: %v"
	err := i.createChain(tableFilter, chainRTFWD)
	if err != nil {
		return fmt.Errorf(errMSGFormat, chainRTFWD, err)
	}

	err = i.createChain(tableNat, chainRTNAT)
	if err != nil {
		return fmt.Errorf(errMSGFormat, chainRTNAT, err)
	}

	err = i.addJumpRules()
	if err != nil {
		return fmt.Errorf("error while creating jump rules: %v", err)
	}

	return nil
}

// addJumpRules create jump rules to send packets to NetBird chains
func (i *routerManager) addJumpRules() error {
	rule := []string{"-j", chainRTFWD}
	err := i.iptablesClient.Insert(tableFilter, chainFORWARD, 1, rule...)
	if err != nil {
		return err
	}
	i.rules[Ipv4Forwarding] = rule

	rule = []string{"-j", chainRTNAT}
	err = i.iptablesClient.Insert(tableNat, chainPOSTROUTING, 1, rule...)
	if err != nil {
		return err
	}
	i.rules[ipv4Nat] = rule

	return nil
}

// cleanJumpRules cleans jump rules that was sending packets to NetBird chains
func (i *routerManager) cleanJumpRules() error {
	var err error
	errMSGFormat := "failed cleaning rule from chain %s,err: %v"
	rule, found := i.rules[Ipv4Forwarding]
	if found {
		err = i.iptablesClient.DeleteIfExists(tableFilter, chainFORWARD, rule...)
		if err != nil {
			return fmt.Errorf(errMSGFormat, chainFORWARD, err)
		}
	}
	rule, found = i.rules[ipv4Nat]
	if found {
		err = i.iptablesClient.DeleteIfExists(tableNat, chainPOSTROUTING, rule...)
		if err != nil {
			return fmt.Errorf(errMSGFormat, chainPOSTROUTING, err)
		}
	}

	rules, err := i.iptablesClient.List("nat", "POSTROUTING")
	if err != nil {
		return fmt.Errorf("failed to list rules: %s", err)
	}

	for _, ruleString := range rules {
		if !strings.Contains(ruleString, "NETBIRD") {
			continue
		}
		rule := strings.Fields(ruleString)
		err := i.iptablesClient.DeleteIfExists("nat", "POSTROUTING", rule[2:]...)
		if err != nil {
			return fmt.Errorf("failed to delete postrouting jump rule: %s", err)
		}
	}

	rules, err = i.iptablesClient.List(tableFilter, "FORWARD")
	if err != nil {
		return fmt.Errorf("failed to list rules in FORWARD chain: %s", err)
	}

	for _, ruleString := range rules {
		if !strings.Contains(ruleString, "NETBIRD") {
			continue
		}
		rule := strings.Fields(ruleString)
		err := i.iptablesClient.DeleteIfExists(tableFilter, "FORWARD", rule[2:]...)
		if err != nil {
			return fmt.Errorf("failed to delete FORWARD jump rule: %s", err)
		}
	}
	return nil
}

func (i *routerManager) createChain(table, newChain string) error {
	chains, err := i.iptablesClient.ListChains(table)
	if err != nil {
		return fmt.Errorf("couldn't get %s table chains, error: %v", table, err)
	}

	shouldCreateChain := true
	for _, chain := range chains {
		if chain == newChain {
			shouldCreateChain = false
		}
	}

	if shouldCreateChain {
		err = i.iptablesClient.NewChain(table, newChain)
		if err != nil {
			return fmt.Errorf("couldn't create chain %s in %s table, error: %v", newChain, table, err)
		}

		// Add the loopback return rule to the NAT chain
		loopbackRule := []string{"-o", "lo", "-j", "RETURN"}
		err = i.iptablesClient.Insert(table, newChain, 1, loopbackRule...)
		if err != nil {
			return fmt.Errorf("failed to add loopback return rule to %s: %v", chainRTNAT, err)
		}

		err = i.iptablesClient.Append(table, newChain, "-j", "RETURN")
		if err != nil {
			return fmt.Errorf("couldn't create chain %s default rule, error: %v", newChain, err)
		}

	}
	return nil
}

// addNATRule appends an iptables rule pair to the nat chain
func (i *routerManager) addNATRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
	ruleKey := firewall.GenKey(keyFormat, pair.ID)
	rule := genRuleSpec(jump, pair.Source, pair.Destination)
	existingRule, found := i.rules[ruleKey]
	if found {
		err := i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
		if err != nil {
			return fmt.Errorf("error while removing existing NAT rule for %s: %v", pair.Destination, err)
		}
		delete(i.rules, ruleKey)
	}

	// inserting after loopback ignore rule
	err := i.iptablesClient.Insert(table, chain, 2, rule...)
	if err != nil {
		return fmt.Errorf("error while appending new NAT rule for %s: %v", pair.Destination, err)
	}

	i.rules[ruleKey] = rule

	return nil
}

// genRuleSpec generates rule specification
func genRuleSpec(jump, source, destination string) []string {
	return []string{"-s", source, "-d", destination, "-j", jump}
}

func getIptablesRuleType(table string) string {
	ruleType := "forwarding"
	if table == tableNat {
		ruleType = "nat"
	}
	return ruleType
}
