package common

import (
	"sync"
	"time"

	"github.com/evilsocket/opensnitch/daemon/log"
)

// default arguments for various functions
var (
	EnableRule     = true
	DoLogErrors    = true
	ForcedDelRules = true
	ReloadRules    = true
	RestoreChains  = true
	BackupChains   = true
	ReloadConf     = true

	DefaultCheckInterval = 10 * time.Second
	RulesCheckerDisabled = "0s"
)

type (
	callback     func()
	callbackBool func() bool

	// Common holds common fields and functionality of both firewalls,
	// iptables and nftables.
	Common struct {
		RulesChecker       *time.Ticker
		ErrChan            chan string
		stopChecker        chan struct{}
		RulesCheckInterval time.Duration
		QueueNum           uint16
		Running            bool
		Intercepting       bool
		FwEnabled          bool
		sync.RWMutex
	}
)

// ErrorsChan returns the channel where the errors are sent to.
func (c *Common) ErrorsChan() <-chan string {
	return c.ErrChan
}

// ErrChanEmpty checks if the errors channel is empty.
func (c *Common) ErrChanEmpty() bool {
	return len(c.ErrChan) == 0
}

// SendError sends an error to the channel of errors.
func (c *Common) SendError(err string) {
	log.Warning("%s", err)

	if len(c.ErrChan) >= cap(c.ErrChan) {
		log.Debug("fw errors channel full, emptying errChan")
		for e := range c.ErrChan {
			log.Warning("%s", e)
			if c.ErrChanEmpty() {
				break
			}
		}
		return
	}
	select {
	case c.ErrChan <- err:
	case <-time.After(100 * time.Millisecond):
		log.Warning("SendError() channel locked? REVIEW")
	}
}

func (c *Common) SetRulesCheckerInterval(interval string) {
	dur, err := time.ParseDuration(interval)
	if err != nil {
		log.Warning("Invalid rules checker interval (falling back to %s): %s", DefaultCheckInterval, err)
		c.RulesCheckInterval = DefaultCheckInterval
		return
	}

	c.RulesCheckInterval = dur
}

// SetQueueNum sets the queue number used by the firewall.
// It's the queue where all intercepted connections will be sent.
func (c *Common) SetQueueNum(qNum uint16) {
	c.Lock()
	defer c.Unlock()
	c.QueueNum = qNum
}

// IsRunning returns if the firewall is running or not.
func (c *Common) IsRunning() bool {
	c.RLock()
	defer c.RUnlock()

	return c != nil && c.Running
}

// IsFirewallEnabled returns if the firewall is running or not.
func (c *Common) IsFirewallEnabled() bool {
	c.RLock()
	defer c.RUnlock()

	return c != nil && c.FwEnabled
}

// IsIntercepting returns if the firewall is running or not.
func (c *Common) IsIntercepting() bool {
	c.RLock()
	defer c.RUnlock()

	return c != nil && c.Intercepting
}

// NewRulesChecker starts monitoring interception rules.
// We expect to have 2 rules loaded: one to intercept DNS responses and another one
// to intercept network traffic.
func (c *Common) NewRulesChecker(areRulesLoaded callbackBool, reloadRules callback) {
	c.Lock()
	defer c.Unlock()
	if c.RulesCheckInterval.String() == RulesCheckerDisabled {
		log.Info("Fw rules checker disabled ...")
		return
	}

	if c.RulesChecker != nil {
		c.RulesChecker.Stop()
		select {
		case c.stopChecker <- struct{}{}:
		case <-time.After(5 * time.Millisecond):
			log.Error("NewRulesChecker: timed out stopping monitor rules")
		}
	}
	c.stopChecker = make(chan struct{}, 1)
	log.Info("Starting new fw checker every %s ...", c.RulesCheckInterval)
	c.RulesChecker = time.NewTicker(c.RulesCheckInterval)

	go startCheckingRules(c.stopChecker, c.RulesChecker, areRulesLoaded, reloadRules)
}

// StartCheckingRules monitors if our rules are loaded.
// If the rules to intercept traffic are not loaded, we'll try to insert them again.
func startCheckingRules(exitChan <-chan struct{}, rulesChecker *time.Ticker, areRulesLoaded callbackBool, reloadRules callback) {
	for {
		select {
		case <-exitChan:
			goto Exit
		case _, active := <-rulesChecker.C:
			if !active {
				goto Exit
			}

			if areRulesLoaded() == false {
				reloadRules()
			}
		}
	}

Exit:
	log.Info("exit checking firewall rules")
}

// StopCheckingRules stops checking if firewall rules are loaded.
func (c *Common) StopCheckingRules() {
	c.Lock()
	defer c.Unlock()

	if c.RulesChecker != nil {
		select {
		case c.stopChecker <- struct{}{}:
			close(c.stopChecker)
		case <-time.After(5 * time.Millisecond):
			// We should not arrive here
			log.Error("StopCheckingRules: timed out stopping monitor rules")
		}

		c.RulesChecker.Stop()
		c.RulesChecker = nil
	}
}

func (c *Common) reloadCallback(callback func()) {
	callback()
}
