package main

import (
	"a.yandex-team.ru/balancer/production/x/iptables_daemon/cbbclient"
	"a.yandex-team.ru/balancer/production/x/iptables_daemon/daemonmetrics"
	"a.yandex-team.ru/balancer/production/x/iptables_daemon/ipset"
	"a.yandex-team.ru/library/go/core/buildinfo"
	"a.yandex-team.ru/library/go/core/log"
	"a.yandex-team.ru/library/go/core/log/zap"
	"a.yandex-team.ru/library/go/core/log/zap/logrotate"
	"encoding/json"
	"flag"
	"fmt"
	"github.com/go-chi/chi/v5"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"
)

func UpdateIPSet(cbb cbbclient.IClient, ipset ipset.IIPSet, metrics daemonmetrics.IMetrics, logger log.Logger) {
	body, err := cbb.Fetch(metrics, logger)
	if err != nil {
		metrics.ReportCbbHTTPError()
		logger.Warnf("Fetching cbb response failed with error \"%v\"", err)
		return
	}
	defer body.Close()

	cmd, err := ipset.GetCmdExecutor().NewCmdWithPipe("ipset", "-")
	if err != nil {
		logger.Errorf("Starting ipset command failed with error \"%v\"", err)
		return
	}

	lastParsedTS := ipset.ProcessCBBResponse(body, cmd.GetWritePipe(), metrics, logger)
	cbb.UpdateLastTS(lastParsedTS)

	defer func(start time.Time, name string) {
		elapsed := time.Since(start).Milliseconds()
		logger.Infof("Execution of %s took %d ms", name, elapsed)
		metrics.ReportExecTimeMs(fmt.Sprintf("exec%sTimeMs", name), float64(elapsed))
	}(time.Now(), "UpdateIPSet")

	cmdRes := cmd.Wait()
	if cmdRes.Err != nil {
		logger.Errorf("UpdateIPSet: ipset command executed with error %v: %s", cmdRes.Err, cmdRes.StdErr)
		return
	}

	if len(cmdRes.StdErr) > 0 {
		logger.Errorf("UpdateIPSet: non empty stderr of ipset: %s", cmdRes.StdErr)
		return
	}

	if len(cmdRes.StdErr) > 0 {
		logger.Errorf("UpdateIPSet: non empty stdout of ipset: %s", cmdRes.StdOut)
		return
	}
}

func ListenITSUpdates(disablerFile string, disableChannel chan<- bool, logger log.Logger) {
	isDisabled := true
	for {
		if _, err := os.Stat(disablerFile); err == nil {
			if !isDisabled {
				logger.Infof("ListenITSUpdates: found file \"%s\", service is disabled", disablerFile)
				isDisabled = true
				disableChannel <- true
			}
		} else {
			if !os.IsNotExist(err) {
				logger.Fatalf("ListenITSUpdates: cannot stat file \"%s\", error \"%v\"", disablerFile, err)
			}
			if isDisabled {
				logger.Infof("ListenITSUpdates: cannot find file \"%s\", service is enabled", disablerFile)
				isDisabled = false
				disableChannel <- false
			}
		}

		// TODO make 5 configurable
		time.Sleep(time.Second * 5)
	}
}

func ServeIPSetUpdates(ipset ipset.IIPSet, cbb cbbclient.IClient, disableChannel <-chan bool, metrics daemonmetrics.IMetrics, logger log.Logger) {
	setupDone := false
	isDisabled := <-disableChannel
	logger.Infof("Starting ServeIPSetUpdates. Service disabled = \"%v\"", isDisabled)
	for {
		for isDisabled {
			logger.Infof("Waiting for service to be enabled")
			isDisabled = <-disableChannel
		}

		if !setupDone {
			ipset.Setup(logger)
			setupDone = true
		}
		UpdateIPSet(cbb, ipset, metrics, logger)

		select {
		case nowDisabled := <-disableChannel:
			if !isDisabled && nowDisabled {
				logger.Infof("Service is now disabled. Removing tables and purging rules")
				ipset.RemoveSetsAndPurgeRules(logger)
				cbb.Reset()
				setupDone = false
			}
			isDisabled = nowDisabled
		case <-cbb.WaitForReadiness(logger):
		}
	}
}

func SetupLogger(logFile string, logLevel string) log.Logger {
	level, err := log.ParseLevel(logLevel)
	if err != nil {
		fmt.Printf("Unable to parse log level \"%s\", setting default \"info\"\n", logLevel)
		level = log.InfoLevel
	}
	cfg := zap.KVConfig(level)

	output := logFile
	if !strings.HasPrefix(output, "std") {
		err := logrotate.RegisterLogrotateSink(syscall.SIGHUP)
		if err != nil {
			panic(err)
		}
		logAbsPath, err := filepath.Abs(logFile)
		if err != nil {
			panic(err)
		}
		output = "logrotate://" + logAbsPath
	}

	cfg.OutputPaths = []string{output}
	return zap.Must(cfg)
}

type AppConfig struct {
	IpsetNamev4        string `json:"ipsetNamev4"`
	IpsetNamev6        string `json:"ipsetNamev6"`
	CbbDataQuery       string `json:"cbbDataQuery"`
	ItsDisablerFile    string `json:"itsDisablerFile"`
	HTTPListenPort     int    `json:"httpListenPort"`
	RefreshIntervalSec int64  `json:"refreshIntervalSec"`
	LogFile            string `json:"logFile"`
	LogLevel           string `json:"logLevel"`
	CheckEnv           bool   `json:"checkEnv"`
	PrintVersion       bool   `json:"printVersion"`
	ConfigFile         string `json:"configFile"`
}

func RunSigHandler(ipset ipset.IIPSet, logger log.Logger) {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		sig := <-sigChan
		logger.Infof("Got signal \"%v\", clearing iptables and ipsets", sig)
		ipset.RemoveSetsAndPurgeRules(logger)
		os.Exit(1)
	}()
}

type ITSListener struct {
	itsDisablerFile string
}

func (l *ITSListener) Run(logger log.Logger) <-chan bool {
	disableChannel := make(chan bool)
	go ListenITSUpdates(l.itsDisablerFile, disableChannel, logger)
	return disableChannel
}

type AppRunMode int

const (
	AppRunModeSimpleRun    AppRunMode = 0
	AppRunModePrintVersion AppRunMode = 1
	AppRunModeCheckEnv     AppRunMode = 2
)

type App struct {
	ipset          ipset.IIPSet
	cbb            cbbclient.IClient
	itsListener    *ITSListener
	httpListenPort int
	logger         log.Logger
	metrics        daemonmetrics.IMetrics
	runMode        AppRunMode
}

func ParseCommandLine() (*AppConfig, error) {
	ipsetNamev4 := flag.String("ipsetNamev4", "antirobotBannedIpv4", "the set with IPv4 addresses")
	ipsetNamev6 := flag.String("ipsetNamev6", "antirobotBannedIpv6", "the set with IPv6 addresses")
	cbbDataQuery := flag.String("cbbDataQuery", "cbb-testing-yp-3.sas.yp-c.yandex.net:300/cgi-bin/get_ips.pl?group_id=824&created_after=%d", "CBB query for getting ip list")
	itsDisablerFile := flag.String("itsDisablerFile", "./controls/iptables_disable", "name the its controls file that disables ipsets")

	httpListenPort := flag.Int("httpListenPort", 80, "port for health checks")
	refreshIntervalSec := flag.Int64("refreshIntervalSec", 60, "the interval of updating ipsets")
	logFile := flag.String("logFile", "/place/db/www/logs/iptables_daemon.log", "path to log file")
	logLevel := flag.String("logLevel", "info", "logging level")
	checkEnv := flag.Bool("checkEnv", false, "check environmentt")
	printVersion := flag.Bool("version", false, "print version and exit")
	configFile := flag.String("configFile", "iptables_daemon.cfg", "path to the configuration file (json format)")

	flag.Parse()

	cfg := AppConfig{
		IpsetNamev4:        *ipsetNamev4,
		IpsetNamev6:        *ipsetNamev6,
		CbbDataQuery:       *cbbDataQuery,
		ItsDisablerFile:    *itsDisablerFile,
		HTTPListenPort:     *httpListenPort,
		RefreshIntervalSec: *refreshIntervalSec,
		LogFile:            *logFile,
		LogLevel:           *logLevel,
		CheckEnv:           *checkEnv,
		PrintVersion:       *printVersion,
		ConfigFile:         *configFile,
	}

	// configuration file overrides console options
	if *configFile != "" && !*checkEnv && !*printVersion {
		f, err := os.Open(*configFile)
		if err != nil {
			return nil, fmt.Errorf("unable to open configuration file \"%s\", reason \"%v\"", *configFile, err)
		}
		defer f.Close()
		content, err := ioutil.ReadAll(f)
		if err != nil {
			return nil, fmt.Errorf("unable to read configuration file \"%s\", reason \"%v\"", *configFile, err)
		}
		err = json.Unmarshal([]byte(content), &cfg)
		if err != nil {
			return nil, fmt.Errorf("unable to parse configuration file \"%s\", reason \"%v\"", *configFile, err)
		}
	}
	return &cfg, nil
}

func RunMetrics(ipset ipset.IIPSet, metrics daemonmetrics.IMetrics, logger log.Logger) {
	for {
		metrics.Flush()
		time.Sleep(time.Second * 60)
		ipset.UpdateIpsetMetrics(metrics, logger)
		ipset.UpdateIptablesDropMetrics(metrics, logger)
	}
}

func NewApp(cfg *AppConfig) *App {
	if cfg.PrintVersion {
		return &App{
			logger:  SetupLogger("stdout", cfg.LogLevel),
			runMode: AppRunModePrintVersion,
		}
	}
	if cfg.CheckEnv {
		return &App{
			ipset:   ipset.New(cfg.IpsetNamev4, cfg.IpsetNamev6, &ipset.CmdExecutor{}),
			logger:  SetupLogger("stdout", cfg.LogLevel),
			runMode: AppRunModeCheckEnv,
		}
	}
	app := &App{
		ipset:          ipset.New(cfg.IpsetNamev4, cfg.IpsetNamev6, &ipset.CmdExecutor{}),
		cbb:            cbbclient.New(cfg.CbbDataQuery, cfg.RefreshIntervalSec),
		itsListener:    &ITSListener{cfg.ItsDisablerFile},
		httpListenPort: cfg.HTTPListenPort,
		logger:         SetupLogger(cfg.LogFile, cfg.LogLevel),
		metrics:        daemonmetrics.New(),
		runMode:        AppRunModeSimpleRun,
	}

	return app
}

func (a *App) Run() {
	switch a.runMode {
	case AppRunModePrintVersion:
		fmt.Printf("build info: revision \"%s\", date \"%s\"", buildinfo.Info.SVNRevision, buildinfo.Info.Date)
	case AppRunModeCheckEnv:
		a.ipset.CheckInstalledModules(a.logger)
	case AppRunModeSimpleRun:
		rand.Seed(time.Now().UnixNano())

		a.logger.Infof("App::Run: Starting new instance with build info: revision \"%s\", date \"%s\"", buildinfo.Info.SVNRevision, buildinfo.Info.Date)

		go ServeIPSetUpdates(a.ipset, a.cbb, a.itsListener.Run(a.logger), a.metrics, a.logger)

		go RunMetrics(a.ipset, a.metrics, a.logger)

		go RunSigHandler(a.ipset, a.logger)

		router := chi.NewRouter()
		router.Get("/ping", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("Ok")) })
		metricsHandler := a.metrics.GetSolomonHTTPHandler()
		router.Get("/solomon", metricsHandler.ServeHTTP)
		srv := &http.Server{
			Handler:      router,
			Addr:         fmt.Sprintf(":%d", a.httpListenPort),
			WriteTimeout: 5 * time.Second,
			ReadTimeout:  5 * time.Second,
		}

		a.logger.Fatalf("App::Run: Failed to start http server: %v", srv.ListenAndServe())
	}
}

func main() {
	cfg, err := ParseCommandLine()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Got error on parsing configuration \"%v\"\n", err)
		flag.Usage()
		os.Exit(1)
	}

	app := NewApp(cfg)
	app.Run()
}
