package overlay

import (
	"fmt"
	"sync"

	"github.com/docker/libnetwork/iptables"
	"github.com/sirupsen/logrus"
)

const globalChain = "DOCKER-OVERLAY"

var filterOnce sync.Once

var filterChan = make(chan struct{}, 1)

func filterWait() func() {
	filterChan <- struct{}{}
	return func() { <-filterChan }
}

func chainExists(cname string) bool {
	// TODO IPv6 support
	iptable := iptables.GetIptable(iptables.IPv4)
	if _, err := iptable.Raw("-L", cname); err != nil {
		return false
	}

	return true
}

func setupGlobalChain() {
	// TODO IPv6 support
	iptable := iptables.GetIptable(iptables.IPv4)
	// Because of an ungraceful shutdown, chain could already be present
	if !chainExists(globalChain) {
		if err := iptable.RawCombinedOutput("-N", globalChain); err != nil {
			logrus.Errorf("could not create global overlay chain: %v", err)
			return
		}
	}

	if !iptable.Exists(iptables.Filter, globalChain, "-j", "RETURN") {
		if err := iptable.RawCombinedOutput("-A", globalChain, "-j", "RETURN"); err != nil {
			logrus.Errorf("could not install default return chain in the overlay global chain: %v", err)
		}
	}
}

func setNetworkChain(cname string, remove bool) error {
	// TODO IPv6 support
	iptable := iptables.GetIptable(iptables.IPv4)
	// Initialize the onetime global overlay chain
	filterOnce.Do(setupGlobalChain)

	exists := chainExists(cname)

	opt := "-N"
	// In case of remove, make sure to flush the rules in the chain
	if remove && exists {
		if err := iptable.RawCombinedOutput("-F", cname); err != nil {
			return fmt.Errorf("failed to flush overlay network chain %s rules: %v", cname, err)
		}
		opt = "-X"
	}

	if (!remove && !exists) || (remove && exists) {
		if err := iptable.RawCombinedOutput(opt, cname); err != nil {
			return fmt.Errorf("failed network chain operation %q for chain %s: %v", opt, cname, err)
		}
	}

	if !remove {
		if !iptable.Exists(iptables.Filter, cname, "-j", "DROP") {
			if err := iptable.RawCombinedOutput("-A", cname, "-j", "DROP"); err != nil {
				return fmt.Errorf("failed adding default drop rule to overlay network chain %s: %v", cname, err)
			}
		}
	}

	return nil
}

func addNetworkChain(cname string) error {
	defer filterWait()()

	return setNetworkChain(cname, false)
}

func removeNetworkChain(cname string) error {
	defer filterWait()()

	return setNetworkChain(cname, true)
}

func setFilters(cname, brName string, remove bool) error {
	opt := "-I"
	if remove {
		opt = "-D"
	}
	// TODO IPv6 support
	iptable := iptables.GetIptable(iptables.IPv4)

	// Every time we set filters for a new subnet make sure to move the global overlay hook to the top of the both the OUTPUT and forward chains
	if !remove {
		for _, chain := range []string{"OUTPUT", "FORWARD"} {
			exists := iptable.Exists(iptables.Filter, chain, "-j", globalChain)
			if exists {
				if err := iptable.RawCombinedOutput("-D", chain, "-j", globalChain); err != nil {
					return fmt.Errorf("failed to delete overlay hook in chain %s while moving the hook: %v", chain, err)
				}
			}

			if err := iptable.RawCombinedOutput("-I", chain, "-j", globalChain); err != nil {
				return fmt.Errorf("failed to insert overlay hook in chain %s: %v", chain, err)
			}
		}
	}

	// Insert/Delete the rule to jump to per-bridge chain
	exists := iptable.Exists(iptables.Filter, globalChain, "-o", brName, "-j", cname)
	if (!remove && !exists) || (remove && exists) {
		if err := iptable.RawCombinedOutput(opt, globalChain, "-o", brName, "-j", cname); err != nil {
			return fmt.Errorf("failed to add per-bridge filter rule for bridge %s, network chain %s: %v", brName, cname, err)
		}
	}

	exists = iptable.Exists(iptables.Filter, cname, "-i", brName, "-j", "ACCEPT")
	if (!remove && exists) || (remove && !exists) {
		return nil
	}

	if err := iptable.RawCombinedOutput(opt, cname, "-i", brName, "-j", "ACCEPT"); err != nil {
		return fmt.Errorf("failed to add overlay filter rile for network chain %s, bridge %s: %v", cname, brName, err)
	}

	return nil
}

func addFilters(cname, brName string) error {
	defer filterWait()()

	return setFilters(cname, brName, false)
}

func removeFilters(cname, brName string) error {
	defer filterWait()()

	return setFilters(cname, brName, true)
}
