package modules

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"text/template"
	"time"

	"gopkg.in/yaml.v3"
)

// FunctionDef is a single callable function within a module.
type FunctionDef struct {
	Exec   string            `yaml:"exec"`   // Go text/template that renders to a shell command
	Params map[string]string `yaml:"params"` // default parameter values
}

// ModuleDef maps function names to their definitions.
type ModuleDef map[string]FunctionDef

// Registry holds all loaded modules.
type Registry struct {
	Modules map[string]ModuleDef `yaml:"modules"`
}

// LoadRegistry reads a modules.yaml file.
// A missing file is not an error — returns an empty Registry so callers don't need to
// special-case systems without a modules.yaml.
func LoadRegistry(path string) (*Registry, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return &Registry{}, nil
		}
		return nil, fmt.Errorf("read modules %q: %w", path, err)
	}
	var r Registry
	if err := yaml.Unmarshal(data, &r); err != nil {
		return nil, fmt.Errorf("parse modules %q: %w", path, err)
	}
	return &r, nil
}

// Resolve looks up module/function, merges params (defaults overridden by overrides),
// and renders the exec template. Returns the shell command string ready for sh -c.
func (r *Registry) Resolve(module, function string, overrides map[string]string) (string, error) {
	if r == nil || r.Modules == nil {
		return "", fmt.Errorf("no modules loaded")
	}
	mod, ok := r.Modules[module]
	if !ok {
		return "", fmt.Errorf("unknown module %q", module)
	}
	fn, ok := mod[function]
	if !ok {
		return "", fmt.Errorf("module %q has no function %q", module, function)
	}

	// Merge: start with defaults, apply overrides on top.
	params := make(map[string]string, len(fn.Params)+len(overrides))
	for k, v := range fn.Params {
		params[k] = v
	}
	for k, v := range overrides {
		params[k] = v
	}

	tmpl, err := template.New("exec").Funcs(funcMap()).Parse(fn.Exec)
	if err != nil {
		return "", fmt.Errorf("module %q function %q: parse template: %w", module, function, err)
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, params); err != nil {
		return "", fmt.Errorf("module %q function %q: render template: %w", module, function, err)
	}
	return buf.String(), nil
}

// Execute resolves the module/function and runs it via sh -c.
func (r *Registry) Execute(module, function string, overrides map[string]string) error {
	shell, err := r.Resolve(module, function, overrides)
	if err != nil {
		return err
	}
	c := exec.Command("sh", "-c", shell)
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	return c.Run()
}

// funcMap returns the template helper functions available in exec templates.
func funcMap() template.FuncMap {
	return template.FuncMap{
		// env returns the value of an environment variable.
		// Use this to keep secrets (tokens, passwords) out of modules.yaml.
		"env": os.Getenv,

		// envDefault returns the value of an environment variable, or
		// fallback if the variable is empty/unset.
		"envDefault": func(key, fallback string) string {
			if v := os.Getenv(key); v != "" {
				return v
			}
			return fallback
		},

		// expiry converts a duration string like "+1h" to a Unix epoch timestamp string.
		// Pass "" or "0" to get "0" (no expiry). Supports Go duration syntax (e.g. "30m", "2h").
		// Note: time.ParseDuration does not support days (d) or weeks (w).
		"expiry": func(s string) string {
			if s == "" || s == "0" {
				return "0"
			}
			d, err := time.ParseDuration(s)
			if err != nil {
				return "0"
			}
			return strconv.FormatInt(time.Now().Add(d).Unix(), 10)
		},
	}
}
