package config

import (
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/BurntSushi/toml"
)

// Special keys recognised during option assembly.
const (
	keyArguments = "_arguments"
	keyWorkdir   = "_workdir"
	keyCommand   = "_command"
	keyPreHooks  = "_pre_hooks"
	keyPostHooks = "_post_hooks"
)

// environSuffix marks a section as containing environment variables.
const environSuffix = ".environ"

// interpolateRe matches ${section.key} references for cross-section interpolation.
var interpolateRe = regexp.MustCompile(`\$\{([^.}]+)\.([^}]+)\}`)

// ResolvedConfig holds the fully-merged result of config resolution, ready for
// the restic exec layer to consume.
type ResolvedConfig struct {
	// Command is the restic subcommand to run (may be aliased via _command).
	Command string

	// Flags maps flag names to their values. Multi-value flags have multiple
	// entries. Boolean flags (true) are represented as a nil slice.
	Flags []Flag

	// Arguments are positional args passed after the flags.
	Arguments []string

	// Workdir is the directory to chdir into before exec, or "" for cwd.
	Workdir string

	// Environ holds additional environment variables for the restic process.
	Environ map[string]string

	// PreHooks are shell commands to run before restic backup.
	PreHooks []string

	// PostHooks are shell commands to run after restic backup is attempted.
	PostHooks []string

	// SectionsRead lists which config sections contributed to this resolution.
	SectionsRead []string
}

// Flag is a single CLI flag to pass to restic.
type Flag struct {
	Name  string
	Value string // empty for boolean switches
}

// HasFlag reports whether the resolved config contains a flag with the given
// name (without leading dashes, e.g. "target" not "--target").
func (rc *ResolvedConfig) HasFlag(name string) bool {
	dashed := flagName(name)
	for _, f := range rc.Flags {
		if f.Name == dashed {
			return true
		}
	}
	return false
}

// CommandSuffix is the suffix keld recognises in .environ keys to indicate
// that the value is a shell command whose stdout should be captured and
// used as the actual variable value (with the suffix stripped).
//
// For example, RESTIC_REPOSITORY_COMMAND = "op read ..." resolves to
// RESTIC_REPOSITORY at execution time.
const CommandSuffix = "_COMMAND"

// HasEnvSource reports whether the given environment variable will have
// a value when the restic process runs. It checks three sources in
// order:
//
//  1. Direct key in [ResolvedConfig.Environ] (e.g. RESTIC_REPOSITORY)
//  2. _COMMAND variant in Environ (e.g. RESTIC_REPOSITORY_COMMAND),
//     which keld resolves before exec
//  3. Process environment via [os.Getenv]
//
// This centralises the _COMMAND convention so callers never need to
// check for it themselves.
func (rc *ResolvedConfig) HasEnvSource(key string) bool {
	if rc.Environ != nil {
		if v, ok := rc.Environ[key]; ok && v != "" {
			return true
		}
		if v, ok := rc.Environ[key+CommandSuffix]; ok && v != "" {
			return true
		}
	}
	return os.Getenv(key) != ""
}

// rawConfig is the entire parsed TOML file as nested string-keyed maps.
type rawConfig map[string]any

// Resolve loads all discovered config files, merges sections according to
// preset/command rules, and returns a ResolvedConfig.
//
// cliOverrides are applied last and should use flag names without leading
// dashes (e.g. "exclude" not "--exclude"). Each key maps to one or more values;
// boolean switches use a nil/empty slice.
func Resolve(preset, command string, cliOverrides map[string][]string) (*ResolvedConfig, error) {
	files := DiscoverFiles()
	return resolveFrom(files, preset, command, cliOverrides)
}

func resolveFrom(files []string, preset, command string, cliOverrides map[string][]string) (*ResolvedConfig, error) {
	raw, err := loadFiles(files)
	if err != nil {
		return nil, err
	}

	sections := buildSectionOrder(preset, command)
	envSections := make([]string, len(sections))
	for i, s := range sections {
		envSections[i] = s + environSuffix
	}

	// Merge option sections in order.
	merged := make(map[string]any)
	var sectionsRead []string
	for _, sect := range sections {
		tbl, ok := lookupSection(raw, sect)
		if !ok {
			continue
		}
		// Skip sub-tables (nested commands / environ) — only merge leaf keys.
		for k, v := range tbl {
			if _, isMap := v.(map[string]any); isMap {
				continue
			}
			merged[k] = v
		}
		sectionsRead = append(sectionsRead, sect)
	}

	// Merge environ sections.
	environ := make(map[string]string)
	for _, sect := range envSections {
		tbl, ok := lookupSection(raw, sect)
		if !ok {
			continue
		}
		for k, v := range tbl {
			environ[k] = ExpandPath(fmt.Sprint(v))
		}
	}

	// Perform cross-section interpolation on merged values.
	interpolate(merged, raw)

	// Apply CLI overrides last.
	for k, vals := range cliOverrides {
		if len(vals) == 0 {
			merged[k] = true
		} else if len(vals) == 1 {
			merged[k] = vals[0]
		} else {
			iface := make([]any, len(vals))
			for i, v := range vals {
				iface[i] = v
			}
			merged[k] = iface
		}
	}

	return assemble(merged, command, environ, sectionsRead), nil
}

// loadFiles reads and merges all TOML config files. Later files override
// earlier ones at the top-level section granularity.
func loadFiles(files []string) (rawConfig, error) {
	combined := make(rawConfig)
	for _, f := range files {
		data, err := os.ReadFile(f)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, fmt.Errorf("reading config %s: %w", f, err)
		}
		var parsed rawConfig
		if err := toml.Unmarshal(data, &parsed); err != nil {
			return nil, fmt.Errorf("parsing config %s: %w", f, err)
		}
		// Merge top-level keys; later files win.
		for k, v := range parsed {
			if existing, ok := combined[k]; ok {
				if eMap, eOk := existing.(map[string]any); eOk {
					if vMap, vOk := v.(map[string]any); vOk {
						for mk, mv := range vMap {
							eMap[mk] = mv
						}
						continue
					}
				}
			}
			combined[k] = v
		}
	}
	return combined, nil
}

// buildSectionOrder returns the list of config sections to read, in ascending
// priority order, for the given preset and command.
//
// For a plain preset "foo" with command "backup":
//
//	[global] -> [global.backup] -> [foo] -> [foo.backup]
//
// For a split preset "home@nas" with command "backup":
//
//	[global] -> [global.backup] -> [@nas] -> [@nas.backup] ->
//	[home@] -> [home@.backup] -> [home@nas] -> [home@nas.backup]
func buildSectionOrder(preset, command string) []string {
	sections := []string{"global"}
	if command != "" {
		sections = append(sections, "global."+command)
	}

	if preset == "" {
		return sections
	}

	if idx := strings.Index(preset, "@"); idx >= 0 {
		// Split preset: "prefix@suffix"
		// Parts in reverse order of the split, then the full preset.
		parts := splitPreset(preset)
		for _, part := range parts {
			sections = append(sections, part)
			if command != "" {
				sections = append(sections, part+"."+command)
			}
		}
		// Full preset (only if not already the sole part).
		if len(parts) != 1 || parts[0] != preset {
			sections = append(sections, preset)
			if command != "" {
				sections = append(sections, preset+"."+command)
			}
		}
	} else {
		sections = append(sections, preset)
		if command != "" {
			sections = append(sections, preset+"."+command)
		}
	}

	return sections
}

// splitPreset splits "prefix@suffix" into ["@suffix", "prefix@"] — the two
// halves that get their own section lookups before the full preset.
func splitPreset(preset string) []string {
	idx := strings.Index(preset, "@")
	if idx < 0 {
		return []string{preset}
	}
	suffix := preset[idx:]   // "@nas"
	prefix := preset[:idx+1] // "home@"

	return []string{suffix, prefix}
}

// lookupSection finds a dotted section name in the raw config tree.
// "global.backup" looks up raw["global"]["backup"].
func lookupSection(raw rawConfig, section string) (map[string]any, bool) {
	parts := strings.Split(section, ".")
	var current any = (map[string]any)(raw)
	for _, p := range parts {
		m, ok := current.(map[string]any)
		if !ok {
			return nil, false
		}
		current, ok = m[p]
		if !ok {
			return nil, false
		}
	}
	m, ok := current.(map[string]any)
	if !ok {
		return nil, false
	}
	return m, true
}

// interpolate resolves ${section.key} references in merged values.
func interpolate(merged map[string]any, raw rawConfig) {
	for k, v := range merged {
		s, ok := v.(string)
		if !ok {
			continue
		}
		merged[k] = interpolateRe.ReplaceAllStringFunc(s, func(match string) string {
			sub := interpolateRe.FindStringSubmatch(match)
			if len(sub) != 3 {
				return match
			}
			sect, key := sub[1], sub[2]
			tbl, ok := lookupSection(raw, sect)
			if !ok {
				return match
			}
			val, ok := tbl[key]
			if !ok {
				return match
			}
			return fmt.Sprint(val)
		})
	}
}

// assemble converts the merged key-value map into a ResolvedConfig.
func assemble(merged map[string]any, command string, environ map[string]string, sectionsRead []string) *ResolvedConfig {
	rc := &ResolvedConfig{
		Command:      command,
		Environ:      environ,
		SectionsRead: sectionsRead,
	}

	// Extract special keys.
	if args, ok := merged[keyArguments]; ok {
		rc.Arguments = toStringSlice(args)
		delete(merged, keyArguments)
	}
	if wd, ok := merged[keyWorkdir]; ok {
		rc.Workdir = ExpandPath(fmt.Sprint(wd))
		delete(merged, keyWorkdir)
	}
	if cmd, ok := merged[keyCommand]; ok {
		rc.Command = fmt.Sprint(cmd)
		delete(merged, keyCommand)
	}
	if command == "backup" {
		if hooks, ok := merged[keyPreHooks]; ok {
			rc.PreHooks = toStringSlice(hooks)
		}
		if hooks, ok := merged[keyPostHooks]; ok {
			rc.PostHooks = toStringSlice(hooks)
		}
	}
	delete(merged, keyPreHooks)
	delete(merged, keyPostHooks)

	// Build flags.
	for k, v := range merged {
		switch val := v.(type) {
		case bool:
			if val {
				rc.Flags = append(rc.Flags, Flag{Name: flagName(k)})
			}
		case []any:
			for _, elem := range val {
				rc.Flags = append(rc.Flags, Flag{
					Name:  flagName(k),
					Value: fmt.Sprint(elem),
				})
			}
		default:
			s := fmt.Sprint(val)
			// Multi-line string values (newline-separated) become repeated flags.
			lines := strings.Split(s, "\n")
			for _, line := range lines {
				rc.Flags = append(rc.Flags, Flag{
					Name:  flagName(k),
					Value: ExpandPath(line),
				})
			}
		}
	}

	// Expand path references in arguments.
	for i, a := range rc.Arguments {
		rc.Arguments[i] = ExpandPath(a)
	}

	return rc
}

// keyAliases maps TOML config key names that don't match restic's actual CLI
// flag names. For example, "repository" is a natural config key but restic's
// flag is "--repo".
var keyAliases = map[string]string{
	"repository": "repo",
}

// flagName returns the CLI flag form of a key: single-char keys get "-k",
// longer keys get "--key". Known aliases are resolved first.
func flagName(key string) string {
	if alias, ok := keyAliases[key]; ok {
		key = alias
	}
	if len(key) == 1 {
		return "-" + key
	}
	return "--" + key
}

// toStringSlice coerces a value (string or []any) into []string.
func toStringSlice(v any) []string {
	switch val := v.(type) {
	case string:
		return strings.Fields(val)
	case []any:
		out := make([]string, 0, len(val))
		for _, elem := range val {
			out = append(out, fmt.Sprint(elem))
		}
		return out
	default:
		return []string{fmt.Sprint(v)}
	}
}

// IsDryRun reports whether the KELD_DRYRUN environment variable is set.
func IsDryRun() bool {
	return os.Getenv("KELD_DRYRUN") != ""
}
