package ansible

import (
	"context"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

// executeModule dispatches to the appropriate module handler.
func (e *Executor) executeModule(ctx context.Context, host string, client *SSHClient, task *Task, play *Play) (*TaskResult, error) {
	module := NormalizeModule(task.Module)

	// Apply task-level become
	if task.Become != nil && *task.Become {
		// Save old state to restore
		oldBecome := client.become
		oldUser := client.becomeUser
		oldPass := client.becomePass

		client.SetBecome(true, task.BecomeUser, "")
		
		defer client.SetBecome(oldBecome, oldUser, oldPass)
	}

	// Template the args
	args := e.templateArgs(task.Args, host, task)

	switch module {
	// Command execution
	case "ansible.builtin.shell":
		return e.moduleShell(ctx, client, args)
	case "ansible.builtin.command":
		return e.moduleCommand(ctx, client, args)
	case "ansible.builtin.raw":
		return e.moduleRaw(ctx, client, args)
	case "ansible.builtin.script":
		return e.moduleScript(ctx, client, args)

	// File operations
	case "ansible.builtin.copy":
		return e.moduleCopy(ctx, client, args, host, task)
	case "ansible.builtin.template":
		return e.moduleTemplate(ctx, client, args, host, task)
	case "ansible.builtin.file":
		return e.moduleFile(ctx, client, args)
	case "ansible.builtin.lineinfile":
		return e.moduleLineinfile(ctx, client, args)
	case "ansible.builtin.stat":
		return e.moduleStat(ctx, client, args)
	case "ansible.builtin.slurp":
		return e.moduleSlurp(ctx, client, args)
	case "ansible.builtin.fetch":
		return e.moduleFetch(ctx, client, args)
	case "ansible.builtin.get_url":
		return e.moduleGetURL(ctx, client, args)

	// Package management
	case "ansible.builtin.apt":
		return e.moduleApt(ctx, client, args)
	case "ansible.builtin.apt_key":
		return e.moduleAptKey(ctx, client, args)
	case "ansible.builtin.apt_repository":
		return e.moduleAptRepository(ctx, client, args)
	case "ansible.builtin.package":
		return e.modulePackage(ctx, client, args)
	case "ansible.builtin.pip":
		return e.modulePip(ctx, client, args)

	// Service management
	case "ansible.builtin.service":
		return e.moduleService(ctx, client, args)
	case "ansible.builtin.systemd":
		return e.moduleSystemd(ctx, client, args)

	// User/Group
	case "ansible.builtin.user":
		return e.moduleUser(ctx, client, args)
	case "ansible.builtin.group":
		return e.moduleGroup(ctx, client, args)

	// HTTP
	case "ansible.builtin.uri":
		return e.moduleURI(ctx, client, args)

	// Misc
	case "ansible.builtin.debug":
		return e.moduleDebug(args)
	case "ansible.builtin.fail":
		return e.moduleFail(args)
	case "ansible.builtin.assert":
		return e.moduleAssert(args, host)
	case "ansible.builtin.set_fact":
		return e.moduleSetFact(args)
	case "ansible.builtin.pause":
		return e.modulePause(ctx, args)
	case "ansible.builtin.wait_for":
		return e.moduleWaitFor(ctx, client, args)
	case "ansible.builtin.git":
		return e.moduleGit(ctx, client, args)
	case "ansible.builtin.unarchive":
		return e.moduleUnarchive(ctx, client, args)

	// Additional modules
	case "ansible.builtin.hostname":
		return e.moduleHostname(ctx, client, args)
	case "ansible.builtin.sysctl":
		return e.moduleSysctl(ctx, client, args)
	case "ansible.builtin.cron":
		return e.moduleCron(ctx, client, args)
	case "ansible.builtin.blockinfile":
		return e.moduleBlockinfile(ctx, client, args)
	case "ansible.builtin.include_vars":
		return e.moduleIncludeVars(args)
	case "ansible.builtin.meta":
		return e.moduleMeta(args)
	case "ansible.builtin.setup":
		return e.moduleSetup(ctx, client)
	case "ansible.builtin.reboot":
		return e.moduleReboot(ctx, client, args)

	// Community modules (basic support)
	case "community.general.ufw":
		return e.moduleUFW(ctx, client, args)
	case "ansible.posix.authorized_key":
		return e.moduleAuthorizedKey(ctx, client, args)
	case "community.docker.docker_compose":
		return e.moduleDockerCompose(ctx, client, args)

	default:
		// For unknown modules, try to execute as shell if it looks like a command
		if strings.Contains(task.Module, " ") || task.Module == "" {
			return e.moduleShell(ctx, client, args)
		}
		return nil, fmt.Errorf("unsupported module: %s", module)
	}
}

// templateArgs templates all string values in args.
func (e *Executor) templateArgs(args map[string]any, host string, task *Task) map[string]any {
	// Set inventory_hostname for templating
	e.vars["inventory_hostname"] = host

	result := make(map[string]any)
	for k, v := range args {
		switch val := v.(type) {
		case string:
			result[k] = e.templateString(val, host, task)
		case map[string]any:
			// Recurse for nested maps
			result[k] = e.templateArgs(val, host, task)
		case []any:
			// Template strings in arrays
			templated := make([]any, len(val))
			for i, item := range val {
				if s, ok := item.(string); ok {
					templated[i] = e.templateString(s, host, task)
				} else {
					templated[i] = item
				}
			}
			result[k] = templated
		default:
			result[k] = v
		}
	}
	return result
}

// --- Command Modules ---

func (e *Executor) moduleShell(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	cmd := getStringArg(args, "_raw_params", "")
	if cmd == "" {
		cmd = getStringArg(args, "cmd", "")
	}
	if cmd == "" {
		return nil, fmt.Errorf("shell: no command specified")
	}

	// Handle chdir
	if chdir := getStringArg(args, "chdir", ""); chdir != "" {
		cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
	}

	stdout, stderr, rc, err := client.RunScript(ctx, cmd)
	if err != nil {
		return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil
	}

	return &TaskResult{
		Changed: true,
		Stdout:  stdout,
		Stderr:  stderr,
		RC:      rc,
		Failed:  rc != 0,
	}, nil
}

func (e *Executor) moduleCommand(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	cmd := getStringArg(args, "_raw_params", "")
	if cmd == "" {
		cmd = getStringArg(args, "cmd", "")
	}
	if cmd == "" {
		return nil, fmt.Errorf("command: no command specified")
	}

	// Handle chdir
	if chdir := getStringArg(args, "chdir", ""); chdir != "" {
		cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil {
		return &TaskResult{Failed: true, Msg: err.Error()}, nil
	}

	return &TaskResult{
		Changed: true,
		Stdout:  stdout,
		Stderr:  stderr,
		RC:      rc,
		Failed:  rc != 0,
	}, nil
}

func (e *Executor) moduleRaw(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	cmd := getStringArg(args, "_raw_params", "")
	if cmd == "" {
		return nil, fmt.Errorf("raw: no command specified")
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil {
		return &TaskResult{Failed: true, Msg: err.Error()}, nil
	}

	return &TaskResult{
		Changed: true,
		Stdout:  stdout,
		Stderr:  stderr,
		RC:      rc,
	}, nil
}

func (e *Executor) moduleScript(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	script := getStringArg(args, "_raw_params", "")
	if script == "" {
		return nil, fmt.Errorf("script: no script specified")
	}

	// Read local script
	content, err := os.ReadFile(script)
	if err != nil {
		return nil, fmt.Errorf("read script: %w", err)
	}

	stdout, stderr, rc, err := client.RunScript(ctx, string(content))
	if err != nil {
		return &TaskResult{Failed: true, Msg: err.Error()}, nil
	}

	return &TaskResult{
		Changed: true,
		Stdout:  stdout,
		Stderr:  stderr,
		RC:      rc,
		Failed:  rc != 0,
	}, nil
}

// --- File Modules ---

func (e *Executor) moduleCopy(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) {
	dest := getStringArg(args, "dest", "")
	if dest == "" {
		return nil, fmt.Errorf("copy: dest required")
	}

	var content []byte
	var err error

	if src := getStringArg(args, "src", ""); src != "" {
		content, err = os.ReadFile(src)
		if err != nil {
			return nil, fmt.Errorf("read src: %w", err)
		}
	} else if c := getStringArg(args, "content", ""); c != "" {
		content = []byte(c)
	} else {
		return nil, fmt.Errorf("copy: src or content required")
	}

	mode := os.FileMode(0644)
	if m := getStringArg(args, "mode", ""); m != "" {
		if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
			mode = os.FileMode(parsed)
		}
	}

	err = client.Upload(ctx, strings.NewReader(string(content)), dest, mode)
	if err != nil {
		return nil, err
	}

	// Handle owner/group (best-effort, errors ignored)
	if owner := getStringArg(args, "owner", ""); owner != "" {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, dest))
	}
	if group := getStringArg(args, "group", ""); group != "" {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, dest))
	}

	return &TaskResult{Changed: true, Msg: fmt.Sprintf("copied to %s", dest)}, nil
}

func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) {
	src := getStringArg(args, "src", "")
	dest := getStringArg(args, "dest", "")
	if src == "" || dest == "" {
		return nil, fmt.Errorf("template: src and dest required")
	}

	// Process template
	content, err := e.TemplateFile(src, host, task)
	if err != nil {
		return nil, fmt.Errorf("template: %w", err)
	}

	mode := os.FileMode(0644)
	if m := getStringArg(args, "mode", ""); m != "" {
		if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
			mode = os.FileMode(parsed)
		}
	}

	err = client.Upload(ctx, strings.NewReader(content), dest, mode)
	if err != nil {
		return nil, err
	}

	return &TaskResult{Changed: true, Msg: fmt.Sprintf("templated to %s", dest)}, nil
}

func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	path := getStringArg(args, "path", "")
	if path == "" {
		path = getStringArg(args, "dest", "")
	}
	if path == "" {
		return nil, fmt.Errorf("file: path required")
	}

	state := getStringArg(args, "state", "file")

	switch state {
	case "directory":
		mode := getStringArg(args, "mode", "0755")
		cmd := fmt.Sprintf("mkdir -p %q && chmod %s %q", path, mode, path)
		stdout, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
		}

	case "absent":
		cmd := fmt.Sprintf("rm -rf %q", path)
		_, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
		}

	case "touch":
		cmd := fmt.Sprintf("touch %q", path)
		_, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
		}

	case "link":
		src := getStringArg(args, "src", "")
		if src == "" {
			return nil, fmt.Errorf("file: src required for link state")
		}
		cmd := fmt.Sprintf("ln -sf %q %q", src, path)
		_, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
		}

	case "file":
		// Ensure file exists and set permissions
		if mode := getStringArg(args, "mode", ""); mode != "" {
			_, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, path))
		}
	}

	// Handle owner/group (best-effort, errors ignored)
	if owner := getStringArg(args, "owner", ""); owner != "" {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, path))
	}
	if group := getStringArg(args, "group", ""); group != "" {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, path))
	}
	if recurse := getBoolArg(args, "recurse", false); recurse {
		if owner := getStringArg(args, "owner", ""); owner != "" {
			_, _, _, _ = client.Run(ctx, fmt.Sprintf("chown -R %s %q", owner, path))
		}
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	path := getStringArg(args, "path", "")
	if path == "" {
		path = getStringArg(args, "dest", "")
	}
	if path == "" {
		return nil, fmt.Errorf("lineinfile: path required")
	}

	line := getStringArg(args, "line", "")
	regexp := getStringArg(args, "regexp", "")
	state := getStringArg(args, "state", "present")

	if state == "absent" {
		if regexp != "" {
			cmd := fmt.Sprintf("sed -i '/%s/d' %q", regexp, path)
			_, stderr, rc, _ := client.Run(ctx, cmd)
			if rc != 0 {
				return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
			}
		}
	} else {
		// state == present
		if regexp != "" {
			// Replace line matching regexp
			escapedLine := strings.ReplaceAll(line, "/", "\\/")
			cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path)
			_, _, rc, _ := client.Run(ctx, cmd)
			if rc != 0 {
				// Line not found, append
				cmd = fmt.Sprintf("echo %q >> %q", line, path)
				_, _, _, _ = client.Run(ctx, cmd)
			}
		} else if line != "" {
			// Ensure line is present
			cmd := fmt.Sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
			_, _, _, _ = client.Run(ctx, cmd)
		}
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleStat(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	path := getStringArg(args, "path", "")
	if path == "" {
		return nil, fmt.Errorf("stat: path required")
	}

	stat, err := client.Stat(ctx, path)
	if err != nil {
		return nil, err
	}

	return &TaskResult{
		Changed: false,
		Data:    map[string]any{"stat": stat},
	}, nil
}

func (e *Executor) moduleSlurp(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	path := getStringArg(args, "path", "")
	if path == "" {
		path = getStringArg(args, "src", "")
	}
	if path == "" {
		return nil, fmt.Errorf("slurp: path required")
	}

	content, err := client.Download(ctx, path)
	if err != nil {
		return nil, err
	}

	encoded := base64.StdEncoding.EncodeToString(content)

	return &TaskResult{
		Changed: false,
		Data:    map[string]any{"content": encoded, "encoding": "base64"},
	}, nil
}

func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	src := getStringArg(args, "src", "")
	dest := getStringArg(args, "dest", "")
	if src == "" || dest == "" {
		return nil, fmt.Errorf("fetch: src and dest required")
	}

	content, err := client.Download(ctx, src)
	if err != nil {
		return nil, err
	}

	// Create dest directory
	if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return nil, err
	}

	if err := os.WriteFile(dest, content, 0644); err != nil {
		return nil, err
	}

	return &TaskResult{Changed: true, Msg: fmt.Sprintf("fetched %s to %s", src, dest)}, nil
}

func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	url := getStringArg(args, "url", "")
	dest := getStringArg(args, "dest", "")
	if url == "" || dest == "" {
		return nil, fmt.Errorf("get_url: url and dest required")
	}

	// Use curl or wget
	cmd := fmt.Sprintf("curl -fsSL -o %q %q || wget -q -O %q %q", dest, url, dest, url)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	// Set mode if specified (best-effort)
	if mode := getStringArg(args, "mode", ""); mode != "" {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, dest))
	}

	return &TaskResult{Changed: true}, nil
}

// --- Package Modules ---

func (e *Executor) moduleApt(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	state := getStringArg(args, "state", "present")
	updateCache := getBoolArg(args, "update_cache", false)

	var cmd string

	if updateCache {
		_, _, _, _ = client.Run(ctx, "apt-get update -qq")
	}

	switch state {
	case "present", "installed":
		if name != "" {
			cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name)
		}
	case "absent", "removed":
		cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name)
	case "latest":
		cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name)
	}

	if cmd == "" {
		return &TaskResult{Changed: false}, nil
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	url := getStringArg(args, "url", "")
	keyring := getStringArg(args, "keyring", "")
	state := getStringArg(args, "state", "present")

	if state == "absent" {
		if keyring != "" {
			_, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", keyring))
		}
		return &TaskResult{Changed: true}, nil
	}

	if url == "" {
		return nil, fmt.Errorf("apt_key: url required")
	}

	var cmd string
	if keyring != "" {
		cmd = fmt.Sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring)
	} else {
		cmd = fmt.Sprintf("curl -fsSL %q | apt-key add -", url)
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	repo := getStringArg(args, "repo", "")
	filename := getStringArg(args, "filename", "")
	state := getStringArg(args, "state", "present")

	if repo == "" {
		return nil, fmt.Errorf("apt_repository: repo required")
	}

	if filename == "" {
		// Generate filename from repo
		filename = strings.ReplaceAll(repo, " ", "-")
		filename = strings.ReplaceAll(filename, "/", "-")
		filename = strings.ReplaceAll(filename, ":", "")
	}

	path := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", filename)

	if state == "absent" {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", path))
		return &TaskResult{Changed: true}, nil
	}

	cmd := fmt.Sprintf("echo %q > %q", repo, path)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	// Update apt cache (best-effort)
	if getBoolArg(args, "update_cache", true) {
		_, _, _, _ = client.Run(ctx, "apt-get update -qq")
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) modulePackage(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	// Detect package manager and delegate
	stdout, _, _, _ := client.Run(ctx, "which apt-get yum dnf 2>/dev/null | head -1")
	stdout = strings.TrimSpace(stdout)

	if strings.Contains(stdout, "apt") {
		return e.moduleApt(ctx, client, args)
	}

	// Default to apt
	return e.moduleApt(ctx, client, args)
}

func (e *Executor) modulePip(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	state := getStringArg(args, "state", "present")
	executable := getStringArg(args, "executable", "pip3")

	var cmd string
	switch state {
	case "present", "installed":
		cmd = fmt.Sprintf("%s install %s", executable, name)
	case "absent", "removed":
		cmd = fmt.Sprintf("%s uninstall -y %s", executable, name)
	case "latest":
		cmd = fmt.Sprintf("%s install --upgrade %s", executable, name)
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

// --- Service Modules ---

func (e *Executor) moduleService(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	state := getStringArg(args, "state", "")
	enabled := args["enabled"]

	if name == "" {
		return nil, fmt.Errorf("service: name required")
	}

	var cmds []string

	if state != "" {
		switch state {
		case "started":
			cmds = append(cmds, fmt.Sprintf("systemctl start %s", name))
		case "stopped":
			cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name))
		case "restarted":
			cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name))
		case "reloaded":
			cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name))
		}
	}

	if enabled != nil {
		if getBoolArg(args, "enabled", false) {
			cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name))
		} else {
			cmds = append(cmds, fmt.Sprintf("systemctl disable %s", name))
		}
	}

	for _, cmd := range cmds {
		stdout, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
		}
	}

	return &TaskResult{Changed: len(cmds) > 0}, nil
}

func (e *Executor) moduleSystemd(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	// systemd is similar to service
	if getBoolArg(args, "daemon_reload", false) {
		_, _, _, _ = client.Run(ctx, "systemctl daemon-reload")
	}

	return e.moduleService(ctx, client, args)
}

// --- User/Group Modules ---

func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	state := getStringArg(args, "state", "present")

	if name == "" {
		return nil, fmt.Errorf("user: name required")
	}

	if state == "absent" {
		cmd := fmt.Sprintf("userdel -r %s 2>/dev/null || true", name)
		_, _, _, _ = client.Run(ctx, cmd)
		return &TaskResult{Changed: true}, nil
	}

	// Build useradd/usermod command
	var opts []string

	if uid := getStringArg(args, "uid", ""); uid != "" {
		opts = append(opts, "-u", uid)
	}
	if group := getStringArg(args, "group", ""); group != "" {
		opts = append(opts, "-g", group)
	}
	if groups := getStringArg(args, "groups", ""); groups != "" {
		opts = append(opts, "-G", groups)
	}
	if home := getStringArg(args, "home", ""); home != "" {
		opts = append(opts, "-d", home)
	}
	if shell := getStringArg(args, "shell", ""); shell != "" {
		opts = append(opts, "-s", shell)
	}
	if getBoolArg(args, "system", false) {
		opts = append(opts, "-r")
	}
	if getBoolArg(args, "create_home", true) {
		opts = append(opts, "-m")
	}

	// Try usermod first, then useradd
	optsStr := strings.Join(opts, " ")
	var cmd string
	if optsStr == "" {
		cmd = fmt.Sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
	} else {
		cmd = fmt.Sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
			name, optsStr, name, optsStr, name)
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	state := getStringArg(args, "state", "present")

	if name == "" {
		return nil, fmt.Errorf("group: name required")
	}

	if state == "absent" {
		cmd := fmt.Sprintf("groupdel %s 2>/dev/null || true", name)
		_, _, _, _ = client.Run(ctx, cmd)
		return &TaskResult{Changed: true}, nil
	}

	var opts []string
	if gid := getStringArg(args, "gid", ""); gid != "" {
		opts = append(opts, "-g", gid)
	}
	if getBoolArg(args, "system", false) {
		opts = append(opts, "-r")
	}

	cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
		name, strings.Join(opts, " "), name)

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

// --- HTTP Module ---

func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	url := getStringArg(args, "url", "")
	method := getStringArg(args, "method", "GET")

	if url == "" {
		return nil, fmt.Errorf("uri: url required")
	}

	var curlOpts []string
	curlOpts = append(curlOpts, "-s", "-S")
	curlOpts = append(curlOpts, "-X", method)

	// Headers
	if headers, ok := args["headers"].(map[string]any); ok {
		for k, v := range headers {
			curlOpts = append(curlOpts, "-H", fmt.Sprintf("%s: %v", k, v))
		}
	}

	// Body
	if body := getStringArg(args, "body", ""); body != "" {
		curlOpts = append(curlOpts, "-d", body)
	}

	// Status code
	curlOpts = append(curlOpts, "-w", "\\n%{http_code}")

	cmd := fmt.Sprintf("curl %s %q", strings.Join(curlOpts, " "), url)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil {
		return &TaskResult{Failed: true, Msg: err.Error()}, nil
	}

	// Parse status code from last line
	lines := strings.Split(strings.TrimSpace(stdout), "\n")
	statusCode := 0
	if len(lines) > 0 {
		statusCode, _ = strconv.Atoi(lines[len(lines)-1])
	}

	// Check expected status
	expectedStatus := 200
	if s, ok := args["status_code"].(int); ok {
		expectedStatus = s
	}

	failed := rc != 0 || statusCode != expectedStatus

	return &TaskResult{
		Changed: false,
		Failed:  failed,
		Stdout:  stdout,
		Stderr:  stderr,
		RC:      statusCode,
		Data:    map[string]any{"status": statusCode},
	}, nil
}

// --- Misc Modules ---

func (e *Executor) moduleDebug(args map[string]any) (*TaskResult, error) {
	msg := getStringArg(args, "msg", "")
	if v, ok := args["var"]; ok {
		msg = fmt.Sprintf("%v = %v", v, e.vars[fmt.Sprintf("%v", v)])
	}

	return &TaskResult{
		Changed: false,
		Msg:     msg,
	}, nil
}

func (e *Executor) moduleFail(args map[string]any) (*TaskResult, error) {
	msg := getStringArg(args, "msg", "Failed as requested")
	return &TaskResult{
		Failed: true,
		Msg:    msg,
	}, nil
}

func (e *Executor) moduleAssert(args map[string]any, host string) (*TaskResult, error) {
	that, ok := args["that"]
	if !ok {
		return nil, fmt.Errorf("assert: 'that' required")
	}

	conditions := normalizeConditions(that)
	for _, cond := range conditions {
		if !e.evalCondition(cond, host) {
			msg := getStringArg(args, "fail_msg", fmt.Sprintf("Assertion failed: %s", cond))
			return &TaskResult{Failed: true, Msg: msg}, nil
		}
	}

	return &TaskResult{Changed: false, Msg: "All assertions passed"}, nil
}

func (e *Executor) moduleSetFact(args map[string]any) (*TaskResult, error) {
	for k, v := range args {
		if k != "cacheable" {
			e.vars[k] = v
		}
	}
	return &TaskResult{Changed: true}, nil
}

func (e *Executor) modulePause(ctx context.Context, args map[string]any) (*TaskResult, error) {
	seconds := 0
	if s, ok := args["seconds"].(int); ok {
		seconds = s
	}
	if s, ok := args["seconds"].(string); ok {
		seconds, _ = strconv.Atoi(s)
	}

	if seconds > 0 {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-ctxSleep(ctx, seconds):
		}
	}

	return &TaskResult{Changed: false}, nil
}

func ctxSleep(ctx context.Context, seconds int) <-chan struct{} {
	ch := make(chan struct{})
	go func() {
		select {
		case <-ctx.Done():
		case <-sleepChan(seconds):
		}
		close(ch)
	}()
	return ch
}

func sleepChan(seconds int) <-chan struct{} {
	ch := make(chan struct{})
	go func() {
		for i := 0; i < seconds; i++ {
			select {
			case <-ch:
				return
			default:
				// Sleep 1 second at a time
			}
		}
		close(ch)
	}()
	return ch
}

func (e *Executor) moduleWaitFor(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	port := 0
	if p, ok := args["port"].(int); ok {
		port = p
	}
	host := getStringArg(args, "host", "127.0.0.1")
	state := getStringArg(args, "state", "started")
	timeout := 300
	if t, ok := args["timeout"].(int); ok {
		timeout = t
	}

	if port > 0 && state == "started" {
		cmd := fmt.Sprintf("timeout %d bash -c 'until nc -z %s %d; do sleep 1; done'",
			timeout, host, port)
		stdout, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
		}
	}

	return &TaskResult{Changed: false}, nil
}

func (e *Executor) moduleGit(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	repo := getStringArg(args, "repo", "")
	dest := getStringArg(args, "dest", "")
	version := getStringArg(args, "version", "HEAD")

	if repo == "" || dest == "" {
		return nil, fmt.Errorf("git: repo and dest required")
	}

	// Check if dest exists
	exists, _ := client.FileExists(ctx, dest+"/.git")

	var cmd string
	if exists {
		// Fetch and checkout (force to ensure clean state)
		cmd = fmt.Sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version)
	} else {
		cmd = fmt.Sprintf("git clone %q %q && cd %q && git checkout %q",
			repo, dest, dest, version)
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	src := getStringArg(args, "src", "")
	dest := getStringArg(args, "dest", "")
	remote := getBoolArg(args, "remote_src", false)

	if src == "" || dest == "" {
		return nil, fmt.Errorf("unarchive: src and dest required")
	}

	// Create dest directory (best-effort)
	_, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q", dest))

	var cmd string
	if !remote {
		// Upload local file first
		content, err := os.ReadFile(src)
		if err != nil {
			return nil, fmt.Errorf("read src: %w", err)
		}
		tmpPath := "/tmp/ansible_unarchive_" + filepath.Base(src)
		err = client.Upload(ctx, strings.NewReader(string(content)), tmpPath, 0644)
		if err != nil {
			return nil, err
		}
		src = tmpPath
		defer func() { _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", tmpPath)) }()
	}

	// Detect archive type and extract
	if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") {
		cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest)
	} else if strings.HasSuffix(src, ".tar.xz") {
		cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest)
	} else if strings.HasSuffix(src, ".tar.bz2") {
		cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest)
	} else if strings.HasSuffix(src, ".tar") {
		cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest)
	} else if strings.HasSuffix(src, ".zip") {
		cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest)
	} else {
		cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) // Guess tar
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

// --- Helpers ---

func getStringArg(args map[string]any, key, def string) string {
	if v, ok := args[key]; ok {
		if s, ok := v.(string); ok {
			return s
		}
		return fmt.Sprintf("%v", v)
	}
	return def
}

func getBoolArg(args map[string]any, key string, def bool) bool {
	if v, ok := args[key]; ok {
		switch b := v.(type) {
		case bool:
			return b
		case string:
			lower := strings.ToLower(b)
			return lower == "true" || lower == "yes" || lower == "1"
		}
	}
	return def
}

// --- Additional Modules ---

func (e *Executor) moduleHostname(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	if name == "" {
		return nil, fmt.Errorf("hostname: name required")
	}

	// Set hostname
	cmd := fmt.Sprintf("hostnamectl set-hostname %q || hostname %q", name, name)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	// Update /etc/hosts if needed (best-effort)
	_, _, _, _ = client.Run(ctx, fmt.Sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name))

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	value := getStringArg(args, "value", "")
	state := getStringArg(args, "state", "present")

	if name == "" {
		return nil, fmt.Errorf("sysctl: name required")
	}

	if state == "absent" {
		// Remove from sysctl.conf
		cmd := fmt.Sprintf("sed -i '/%s/d' /etc/sysctl.conf", name)
		_, _, _, _ = client.Run(ctx, cmd)
		return &TaskResult{Changed: true}, nil
	}

	// Set value
	cmd := fmt.Sprintf("sysctl -w %s=%s", name, value)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	// Persist if requested (best-effort)
	if getBoolArg(args, "sysctl_set", true) {
		cmd = fmt.Sprintf("grep -q '^%s' /etc/sysctl.conf && sed -i 's/^%s.*/%s=%s/' /etc/sysctl.conf || echo '%s=%s' >> /etc/sysctl.conf",
			name, name, name, value, name, value)
		_, _, _, _ = client.Run(ctx, cmd)
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	name := getStringArg(args, "name", "")
	job := getStringArg(args, "job", "")
	state := getStringArg(args, "state", "present")
	user := getStringArg(args, "user", "root")

	minute := getStringArg(args, "minute", "*")
	hour := getStringArg(args, "hour", "*")
	day := getStringArg(args, "day", "*")
	month := getStringArg(args, "month", "*")
	weekday := getStringArg(args, "weekday", "*")

	if state == "absent" {
		if name != "" {
			// Remove by name (comment marker)
			cmd := fmt.Sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -",
				user, name, job, user)
			_, _, _, _ = client.Run(ctx, cmd)
		}
		return &TaskResult{Changed: true}, nil
	}

	// Build cron entry
	schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
	entry := fmt.Sprintf("%s %s # %s", schedule, job, name)

	// Add to crontab
	cmd := fmt.Sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -",
		user, name, entry, user)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	path := getStringArg(args, "path", "")
	if path == "" {
		path = getStringArg(args, "dest", "")
	}
	if path == "" {
		return nil, fmt.Errorf("blockinfile: path required")
	}

	block := getStringArg(args, "block", "")
	marker := getStringArg(args, "marker", "# {mark} ANSIBLE MANAGED BLOCK")
	state := getStringArg(args, "state", "present")
	create := getBoolArg(args, "create", false)

	beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1)
	endMarker := strings.Replace(marker, "{mark}", "END", 1)

	if state == "absent" {
		// Remove block
		cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q",
			strings.ReplaceAll(beginMarker, "/", "\\/"),
			strings.ReplaceAll(endMarker, "/", "\\/"),
			path)
		_, _, _, _ = client.Run(ctx, cmd)
		return &TaskResult{Changed: true}, nil
	}

	// Create file if needed (best-effort)
	if create {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("touch %q", path))
	}

	// Remove existing block and add new one
	escapedBlock := strings.ReplaceAll(block, "'", "'\\''")
	cmd := fmt.Sprintf(`
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
cat >> %q << 'BLOCK_EOF'
%s
%s
%s
BLOCK_EOF
`, strings.ReplaceAll(beginMarker, "/", "\\/"),
		strings.ReplaceAll(endMarker, "/", "\\/"),
		path, path, beginMarker, escapedBlock, endMarker)

	stdout, stderr, rc, err := client.RunScript(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) {
	file := getStringArg(args, "file", "")
	if file == "" {
		file = getStringArg(args, "_raw_params", "")
	}

	if file != "" {
		// Would need to read and parse the vars file
		// For now, just acknowledge
		return &TaskResult{Changed: false, Msg: "include_vars: " + file}, nil
	}

	return &TaskResult{Changed: false}, nil
}

func (e *Executor) moduleMeta(args map[string]any) (*TaskResult, error) {
	// meta module controls play execution
	// Most actions are no-ops for us
	return &TaskResult{Changed: false}, nil
}

func (e *Executor) moduleSetup(ctx context.Context, client *SSHClient) (*TaskResult, error) {
	// Gather facts - similar to what we do in gatherFacts
	return &TaskResult{Changed: false, Msg: "facts gathered"}, nil
}

func (e *Executor) moduleReboot(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	preRebootDelay := 0
	if d, ok := args["pre_reboot_delay"].(int); ok {
		preRebootDelay = d
	}

	msg := getStringArg(args, "msg", "Reboot initiated by Ansible")

	if preRebootDelay > 0 {
		cmd := fmt.Sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg)
		_, _, _, _ = client.Run(ctx, cmd)
	} else {
		_, _, _, _ = client.Run(ctx, fmt.Sprintf("shutdown -r now '%s' &", msg))
	}

	return &TaskResult{Changed: true, Msg: "Reboot initiated"}, nil
}

func (e *Executor) moduleUFW(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	rule := getStringArg(args, "rule", "")
	port := getStringArg(args, "port", "")
	proto := getStringArg(args, "proto", "tcp")
	state := getStringArg(args, "state", "")

	var cmd string

	// Handle state (enable/disable)
	if state != "" {
		switch state {
		case "enabled":
			cmd = "ufw --force enable"
		case "disabled":
			cmd = "ufw disable"
		case "reloaded":
			cmd = "ufw reload"
		case "reset":
			cmd = "ufw --force reset"
		}
		if cmd != "" {
			stdout, stderr, rc, err := client.Run(ctx, cmd)
			if err != nil || rc != 0 {
				return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
			}
			return &TaskResult{Changed: true}, nil
		}
	}

	// Handle rule
	if rule != "" && port != "" {
		switch rule {
		case "allow":
			cmd = fmt.Sprintf("ufw allow %s/%s", port, proto)
		case "deny":
			cmd = fmt.Sprintf("ufw deny %s/%s", port, proto)
		case "reject":
			cmd = fmt.Sprintf("ufw reject %s/%s", port, proto)
		case "limit":
			cmd = fmt.Sprintf("ufw limit %s/%s", port, proto)
		}

		stdout, stderr, rc, err := client.Run(ctx, cmd)
		if err != nil || rc != 0 {
			return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
		}
	}

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	user := getStringArg(args, "user", "")
	key := getStringArg(args, "key", "")
	state := getStringArg(args, "state", "present")

	if user == "" || key == "" {
		return nil, fmt.Errorf("authorized_key: user and key required")
	}

	// Get user's home directory
	stdout, _, _, err := client.Run(ctx, fmt.Sprintf("getent passwd %s | cut -d: -f6", user))
	if err != nil {
		return nil, fmt.Errorf("get home dir: %w", err)
	}
	home := strings.TrimSpace(stdout)
	if home == "" {
		home = "/root"
		if user != "root" {
			home = "/home/" + user
		}
	}

	authKeysPath := filepath.Join(home, ".ssh", "authorized_keys")

	if state == "absent" {
		// Remove key
		escapedKey := strings.ReplaceAll(key, "/", "\\/")
		cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
		_, _, _, _ = client.Run(ctx, cmd)
		return &TaskResult{Changed: true}, nil
	}

	// Ensure .ssh directory exists (best-effort)
	_, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
		filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath)))

	// Add key if not present
	cmd := fmt.Sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q",
		key[:40], authKeysPath, key, authKeysPath)
	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	// Fix permissions (best-effort)
	_, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod 600 %q && chown %s:%s %q",
		authKeysPath, user, user, authKeysPath))

	return &TaskResult{Changed: true}, nil
}

func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
	projectSrc := getStringArg(args, "project_src", "")
	state := getStringArg(args, "state", "present")

	if projectSrc == "" {
		return nil, fmt.Errorf("docker_compose: project_src required")
	}

	var cmd string
	switch state {
	case "present":
		cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
	case "absent":
		cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc)
	case "restarted":
		cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc)
	default:
		cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
	}

	stdout, stderr, rc, err := client.Run(ctx, cmd)
	if err != nil || rc != 0 {
		return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
	}

	// Heuristic for changed
	changed := true
	if strings.Contains(stdout, "Up to date") || strings.Contains(stderr, "Up to date") {
		changed = false
	}

	return &TaskResult{Changed: changed, Stdout: stdout}, nil
}
