package main

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/influxdata/toml"
	"github.com/influxdata/toml/ast"

	"github.com/influxdata/telegraf/config"
)

type instance struct {
	category   string
	name       string
	enabled    bool
	dataformat []string
}

type selection struct {
	plugins map[string][]instance
}

func ImportConfigurations(files, dirs []string) (*selection, int, error) {
	sel := &selection{
		plugins: make(map[string][]instance),
	}

	// Gather all configuration files
	var filenames []string
	filenames = append(filenames, files...)

	for _, dir := range dirs {
		// Walk the directory and get the packages
		elements, err := os.ReadDir(dir)
		if err != nil {
			return nil, 0, fmt.Errorf("reading directory %q failed: %w", dir, err)
		}

		for _, element := range elements {
			if element.IsDir() || filepath.Ext(element.Name()) != ".conf" {
				continue
			}

			filenames = append(filenames, filepath.Join(dir, element.Name()))
		}
	}
	if len(filenames) == 0 {
		return sel, 0, errors.New("no configuration files given or found")
	}

	// Do the actual import
	err := sel.importFiles(filenames)
	return sel, len(filenames), err
}

func (s *selection) Filter(p packageCollection) (*packageCollection, error) {
	enabled := packageCollection{
		packages: make(map[string][]packageInfo),
	}

	implicitlyConfigured := make(map[string]bool)
	for category, pkgs := range p.packages {
		for _, pkg := range pkgs {
			key := category + "." + pkg.Plugin
			instances, found := s.plugins[key]
			if !found {
				continue
			}

			// The package was configured so add it to the enabled list
			enabled.packages[category] = append(enabled.packages[category], pkg)

			// Check if the instances configured a data-format and decide if it
			// is a parser or serializer depending on the plugin type.
			// If no data-format was configured, check the default settings in
			// case this plugin supports a data-format setting but the user
			// didn't set it.
			for _, instance := range instances {
				for _, dataformat := range instance.dataformat {
					switch category {
					case "inputs":
						implicitlyConfigured["parsers."+dataformat] = true
					case "processors":
						implicitlyConfigured["parsers."+dataformat] = true
						// The execd processor requires both a parser and serializer
						if pkg.Plugin == "execd" {
							implicitlyConfigured["serializers."+dataformat] = true
						}
					case "outputs":
						implicitlyConfigured["serializers."+dataformat] = true
					}
				}
				if len(instance.dataformat) == 0 {
					if pkg.DefaultParser != "" {
						implicitlyConfigured["parsers."+pkg.DefaultParser] = true
					}
					if pkg.DefaultSerializer != "" {
						implicitlyConfigured["serializers."+pkg.DefaultSerializer] = true
					}
				}
			}
		}
	}

	// Iterate over all plugins AGAIN to add the implicitly configured packages
	// such as parsers and serializers
	for category, pkgs := range p.packages {
		for _, pkg := range pkgs {
			key := category + "." + pkg.Plugin

			// Skip the plugins that were explicitly configured as we already
			// added them above.
			if _, found := s.plugins[key]; found {
				continue
			}

			// Add the package if it was implicitly configured e.g. by a
			// 'data_format' setting or by a default value for the data-format
			if _, implicit := implicitlyConfigured[key]; implicit {
				enabled.packages[category] = append(enabled.packages[category], pkg)
			}
		}
	}

	// Check if all packages in the config were covered
	available := make(map[string]bool)
	for category, pkgs := range p.packages {
		for _, pkg := range pkgs {
			available[category+"."+pkg.Plugin] = true
		}
	}

	var unknown []string
	for pkg := range s.plugins {
		if !available[pkg] {
			unknown = append(unknown, pkg)
		}
	}
	for pkg := range implicitlyConfigured {
		if !available[pkg] {
			unknown = append(unknown, pkg)
		}
	}
	if len(unknown) > 0 {
		return nil, fmt.Errorf("configured but unknown packages %q", strings.Join(unknown, ","))
	}

	return &enabled, nil
}

func (s *selection) importFiles(configurations []string) error {
	for _, cfg := range configurations {
		buf, _, err := config.LoadConfigFile(cfg)
		if err != nil {
			return fmt.Errorf("reading %q failed: %w", cfg, err)
		}

		if err := s.extractPluginsFromConfig(buf); err != nil {
			return fmt.Errorf("extracting plugins from %q failed: %w", cfg, err)
		}
	}

	return nil
}

func (s *selection) extractPluginsFromConfig(buf []byte) error {
	table, err := toml.Parse(trimBOM(buf))
	if err != nil {
		return fmt.Errorf("parsing TOML failed: %w", err)
	}

	for category, subtbl := range table.Fields {
		// Check if we should handle the category, i.e. it contains plugins
		// to configure.
		var valid bool
		for _, c := range categories {
			if c == category {
				valid = true
				break
			}
		}
		if !valid {
			continue
		}

		categoryTbl, ok := subtbl.(*ast.Table)
		if !ok {
			continue
		}

		for name, data := range categoryTbl.Fields {
			key := category + "." + name
			cfg := instance{
				category: category,
				name:     name,
				enabled:  true,
			}

			// We need to check the data_format field to get all required
			// parsers and serializers
			pluginTables, ok := data.([]*ast.Table)
			if ok {
				for _, subsubtbl := range pluginTables {
					var dataformat string
					for field, fieldData := range subsubtbl.Fields {
						if field != "data_format" {
							continue
						}
						kv := fieldData.(*ast.KeyValue)
						option := kv.Value.(*ast.String)
						dataformat = option.Value
					}
					if dataformat != "" {
						cfg.dataformat = append(cfg.dataformat, dataformat)
					}
				}
			}
			s.plugins[key] = append(s.plugins[key], cfg)
		}
	}

	return nil
}

func trimBOM(f []byte) []byte {
	return bytes.TrimPrefix(f, []byte("\xef\xbb\xbf"))
}
