package config

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"

	"github.com/safing/portmaster/base/dataroot"
	"github.com/safing/portmaster/base/utils"
	"github.com/safing/portmaster/base/utils/debug"
	"github.com/safing/portmaster/service/mgr"
)

// ChangeEvent is the name of the config change event.
const ChangeEvent = "config change"

var (
	dataRoot *utils.DirStructure

	exportConfig bool
)

// SetDataRoot sets the data root from which the updates module derives its paths.
func SetDataRoot(root *utils.DirStructure) {
	if dataRoot == nil {
		dataRoot = root
	}
}

func init() {
	flag.BoolVar(&exportConfig, "export-config-options", false, "export configuration registry and exit")
}

func prep() error {
	SetDataRoot(dataroot.Root())
	if dataRoot == nil {
		return errors.New("data root is not set")
	}

	if exportConfig {
		module.instance.SetCmdLineOperation(exportConfigCmd)
		return mgr.ErrExecuteCmdLineOp
	}

	return registerBasicOptions()
}

func start() error {
	configFilePath = filepath.Join(dataRoot.Path, "config.json")

	// Load log level from log package after it started.
	err := loadLogLevel()
	if err != nil {
		return err
	}

	err = registerAsDatabase()
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return err
	}

	err = loadConfig(false)
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return fmt.Errorf("failed to load config file: %w", err)
	}
	return nil
}

func exportConfigCmd() error {
	// Reset the metrics instance name option, as the default
	// is set to the current hostname.
	// Config key copied from metrics.CfgOptionInstanceKey.
	option, err := GetOption("core/metrics/instance")
	if err == nil {
		option.DefaultValue = ""
	}

	data, err := json.MarshalIndent(ExportOptions(), "", "  ")
	if err != nil {
		return err
	}

	_, err = os.Stdout.Write(data)
	return err
}

// AddToDebugInfo adds all changed global config options to the given debug.Info.
func AddToDebugInfo(di *debug.Info) {
	var lines []string

	// Collect all changed settings.
	_ = ForEachOption(func(opt *Option) error {
		opt.Lock()
		defer opt.Unlock()

		if opt.ReleaseLevel <= getReleaseLevel() && opt.activeValue != nil {
			if opt.Sensitive {
				lines = append(lines, fmt.Sprintf("%s: [redacted]", opt.Key))
			} else {
				lines = append(lines, fmt.Sprintf("%s: %v", opt.Key, opt.activeValue.getData(opt)))
			}
		}

		return nil
	})
	sort.Strings(lines)

	// Add data as section.
	di.AddSection(
		fmt.Sprintf("Config: %d", len(lines)),
		debug.UseCodeSection|debug.AddContentLineBreaks,
		lines...,
	)
}

// GetActiveConfigValues returns a map with the active config values.
func GetActiveConfigValues() map[string]interface{} {
	values := make(map[string]interface{})

	// Collect active values from options.
	_ = ForEachOption(func(opt *Option) error {
		opt.Lock()
		defer opt.Unlock()

		if opt.ReleaseLevel <= getReleaseLevel() && opt.activeValue != nil {
			values[opt.Key] = opt.activeValue.getData(opt)
		}

		return nil
	})

	return values
}

// InitializeUnitTestDataroot initializes a new random tmp directory for running tests.
func InitializeUnitTestDataroot(testName string) (string, error) {
	basePath, err := os.MkdirTemp("", fmt.Sprintf("portmaster-%s", testName))
	if err != nil {
		return "", fmt.Errorf("failed to make tmp dir: %w", err)
	}

	ds := utils.NewDirStructure(basePath, utils.PublicReadPermission)
	SetDataRoot(ds)
	err = dataroot.Initialize(basePath, utils.PublicReadPermission)
	if err != nil {
		return "", fmt.Errorf("failed to initialize dataroot: %w", err)
	}

	return basePath, nil
}