//go:generate ../../../tools/readme_config_includer/generator
package fail2ban

import (
	_ "embed"
	"errors"
	"fmt"
	"os/exec"
	"strconv"
	"strings"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

var (
	execCommand    = exec.Command // execCommand is used to mock commands in tests.
	metricsTargets = []struct {
		target string
		field  string
	}{
		{
			target: "Currently failed:",
			field:  "failed",
		},
		{
			target: "Currently banned:",
			field:  "banned",
		},
	}
)

const cmd = "fail2ban-client"

type Fail2ban struct {
	UseSudo bool   `toml:"use_sudo"`
	Socket  string `toml:"socket"`
	path    string
}

func (*Fail2ban) SampleConfig() string {
	return sampleConfig
}

func (f *Fail2ban) Init() error {
	// Set defaults
	if f.path == "" {
		path, err := exec.LookPath(cmd)
		if err != nil {
			return fmt.Errorf("looking up %q failed: %w", cmd, err)
		}
		f.path = path
	}

	// Check parameters
	if f.path == "" {
		return fmt.Errorf("%q not found", cmd)
	}

	return nil
}

func (f *Fail2ban) Gather(acc telegraf.Accumulator) error {
	if len(f.path) == 0 {
		return errors.New("fail2ban-client not found: verify that fail2ban is installed and that fail2ban-client is in your PATH")
	}

	name := f.path
	var args []string

	if f.UseSudo {
		name = "sudo"
		args = append(args, f.path)
	}

	if f.Socket != "" {
		args = append(args, "--socket", f.Socket)
	}
	args = append(args, "status")

	cmd := execCommand(name, args...)
	out, err := cmd.Output()
	if err != nil {
		return fmt.Errorf("failed to run command %q: %w - %s", strings.Join(cmd.Args, " "), err, string(out))
	}
	lines := strings.Split(string(out), "\n")
	const targetString = "Jail list:"
	var jails []string
	for _, line := range lines {
		idx := strings.LastIndex(line, targetString)
		if idx < 0 {
			// not target line, skip.
			continue
		}
		jails = strings.Split(strings.TrimSpace(line[idx+len(targetString):]), ", ")
		break
	}

	for _, jail := range jails {
		// Skip over empty jails
		if jail == "" {
			continue
		}
		fields := make(map[string]interface{})
		cmd := execCommand(name, append(args, jail)...)
		out, err := cmd.Output()
		if err != nil {
			return fmt.Errorf("failed to run command %q: %w - %s", strings.Join(cmd.Args, " "), err, string(out))
		}

		lines := strings.Split(string(out), "\n")
		for _, line := range lines {
			key, value := extractCount(line)
			if key != "" {
				fields[key] = value
			}
		}
		acc.AddFields("fail2ban", fields, map[string]string{"jail": jail})
	}
	return nil
}

func extractCount(line string) (string, int) {
	for _, metricsTarget := range metricsTargets {
		idx := strings.LastIndex(line, metricsTarget.target)
		if idx < 0 {
			continue
		}
		ban := strings.TrimSpace(line[idx+len(metricsTarget.target):])
		banCount, err := strconv.Atoi(ban)
		if err != nil {
			return "", -1
		}
		return metricsTarget.field, banCount
	}
	return "", -1
}

func init() {
	inputs.Add("fail2ban", func() telegraf.Input {
		return &Fail2ban{}
	})
}
