package zellij

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)

const (
	agentPaneName     = "agent"
	defaultChunkBytes = 16 * 1024
)

func versionArgs() []string {
	return []string{"--version"}
}

func createSessionArgs(id, layoutPath string) []string {
	return []string{
		"attach", "--create-background", id,
		"options",
		"--default-layout", layoutPath,
		"--pane-frames", "false",
		"--session-serialization", "false",
		"--show-startup-tips", "false",
		"--show-release-notes", "false",
	}
}

func listPanesArgs(id string) []string {
	return []string{"--session", id, "action", "list-panes", "--all", "--json"}
}

func pasteArgs(id, paneID, chunk string) []string {
	return []string{"--session", id, "action", "paste", "--pane-id", paneID, chunk}
}

func sendEnterArgs(id, paneID string) []string {
	return []string{"--session", id, "action", "send-keys", "--pane-id", paneID, "Enter"}
}

func dumpScreenArgs(id, paneID string) []string {
	return []string{"--session", id, "action", "dump-screen", "--pane-id", paneID, "--full"}
}

func listSessionsArgs() []string {
	return []string{"list-sessions", "--no-formatting"}
}

// deleteSessionArgs builds the teardown command. `delete-session --force`
// kills a running session AND removes its serialized resurrection state in one
// step. Plain `kill-session` is not enough: zellij can keep the session in its
// global resurrection cache as "(EXITED - attach to resurrect)", and any later
// `zellij attach <id>` (e.g. the terminal mux re-opening a pane) would resurrect
// it — re-running the agent command for a session the daemon already destroyed.
func deleteSessionArgs(id string) []string {
	return []string{"delete-session", "--force", id}
}

func attachArgs(id string) []string {
	return []string{
		"attach", id,
		"options",
		"--pane-frames", "false",
	}
}

func handleIDValue(sessionID, paneID string) string {
	return sessionID + "/" + paneID
}

func terminalPaneID(id int) string {
	return fmt.Sprintf("terminal_%d", id)
}

func buildLayout(cfg ports.RuntimeConfig, shellPath string) string {
	spec := shellLaunchSpecFor(shellPath)
	shellCommand := shellLaunchCommand(cfg, shellPath, spec)
	return layoutString(cfg.WorkspacePath, shellPath, spec.args, shellCommand)
}

type shellLaunchSpec struct {
	args []string
}

func shellLaunchSpecFor(shellPath string) shellLaunchSpec {
	base := strings.ToLower(filepathBase(shellPath))
	if strings.Contains(base, "cmd") {
		return shellLaunchSpec{args: []string{"/D", "/S", "/K"}}
	}
	if strings.Contains(base, "powershell") || strings.Contains(base, "pwsh") {
		return shellLaunchSpec{args: []string{"-NoLogo", "-NoProfile", "-NoExit", "-Command"}}
	}
	return shellLaunchSpec{args: []string{"-lc"}}
}

func layoutString(workspacePath, shellPath string, shellArgs []string, shellCommand string) string {
	return "layout {\n" +
		"  cwd " + kdlQuote(workspacePath) + "\n" +
		"  pane command=" + kdlQuote(shellPath) + " name=" + kdlQuote(agentPaneName) + " borderless=true {\n" +
		"    args " + kdlJoin(shellArgs) + " " + kdlQuote(shellCommand) + "\n" +
		"  }\n" +
		"}\n"
}

func shellLaunchCommand(cfg ports.RuntimeConfig, shellPath string, spec shellLaunchSpec) string {
	if len(spec.args) > 0 && spec.args[0] == "-NoLogo" {
		return wrapLaunchCommandPowerShell(cfg)
	}
	if len(spec.args) > 0 && spec.args[0] == "/D" {
		return wrapLaunchCommandCmd(cfg)
	}
	return wrapLaunchCommandUnix(cfg, shellPath)
}

func wrapLaunchCommandUnix(cfg ports.RuntimeConfig, shellPath string) string {
	path := cfg.Env["PATH"]
	if path == "" {
		path = getenv("PATH")
	}

	var b strings.Builder
	for _, key := range sortedKeys(cfg.Env) {
		if key == "PATH" {
			continue
		}
		b.WriteString("export ")
		b.WriteString(key)
		b.WriteString("=")
		b.WriteString(shellQuote(cfg.Env[key]))
		b.WriteString("; ")
	}
	if path != "" {
		b.WriteString("export PATH=")
		b.WriteString(shellQuote(path))
		b.WriteString("; ")
	}
	b.WriteString(quoteArgvUnix(cfg.Argv))
	b.WriteString("; exec ")
	b.WriteString(shellQuote(shellPath))
	b.WriteString(" -i")
	return b.String()
}

func wrapLaunchCommandPowerShell(cfg ports.RuntimeConfig) string {
	path := cfg.Env["PATH"]
	if path == "" {
		path = getenv("PATH")
	}

	var b strings.Builder
	for _, key := range sortedKeys(cfg.Env) {
		if key == "PATH" {
			continue
		}
		b.WriteString("$env:")
		b.WriteString(key)
		b.WriteString(" = ")
		b.WriteString(psQuote(cfg.Env[key]))
		b.WriteString("; ")
	}
	if path != "" {
		b.WriteString("$env:PATH = ")
		b.WriteString(psQuote(path))
		b.WriteString("; ")
	}
	b.WriteString(quoteArgvPowerShell(cfg.Argv))
	return b.String()
}

func wrapLaunchCommandCmd(cfg ports.RuntimeConfig) string {
	path := cfg.Env["PATH"]
	if path == "" {
		path = getenv("PATH")
	}

	var b strings.Builder
	for _, key := range sortedKeys(cfg.Env) {
		if key == "PATH" {
			continue
		}
		b.WriteString("set \"")
		b.WriteString(key)
		b.WriteString("=")
		b.WriteString(cmdQuote(cfg.Env[key]))
		b.WriteString("\" && ")
	}
	if path != "" {
		b.WriteString("set \"PATH=")
		b.WriteString(cmdQuote(path))
		b.WriteString("\" && ")
	}
	b.WriteString(quoteArgvCmd(cfg.Argv))
	return b.String()
}

func validateEnvKeys(env map[string]string) error {
	for key := range env {
		if !validEnvKey(key) {
			return fmt.Errorf("zellij runtime: invalid env key %q", key)
		}
	}
	return nil
}

func validEnvKey(key string) bool {
	if key == "" {
		return false
	}
	for i, r := range key {
		if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
			continue
		}
		if i > 0 && r >= '0' && r <= '9' {
			continue
		}
		return false
	}
	return true
}

func sortedKeys(m map[string]string) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

func shellQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}

func psQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}

func cmdQuote(s string) string {
	return strings.ReplaceAll(s, "\"", "\"\"")
}

// quoteArgvUnix renders argv as a POSIX-shell command, single-quoting each
// argument so a value with spaces stays one word under `sh -lc`.
func quoteArgvUnix(argv []string) string {
	parts := make([]string, len(argv))
	for i, a := range argv {
		parts[i] = shellQuote(a)
	}
	return strings.Join(parts, " ")
}

// quoteArgvPowerShell renders argv for `powershell -Command`. The call operator
// `&` is required so a quoted first token is invoked as a command rather than
// echoed as a string literal.
func quoteArgvPowerShell(argv []string) string {
	if len(argv) == 0 {
		return ""
	}
	parts := make([]string, len(argv))
	for i, a := range argv {
		parts[i] = psQuote(a)
	}
	return "& " + strings.Join(parts, " ")
}

// quoteArgvCmd renders argv for cmd.exe, wrapping each argument in double quotes
// (doubling any embedded quote) so spaces don't split a single argument.
func quoteArgvCmd(argv []string) string {
	parts := make([]string, len(argv))
	for i, a := range argv {
		parts[i] = "\"" + strings.ReplaceAll(a, "\"", "\"\"") + "\""
	}
	return strings.Join(parts, " ")
}

func kdlQuote(s string) string {
	return strconv.Quote(s)
}

func kdlJoin(args []string) string {
	parts := make([]string, 0, len(args))
	for _, arg := range args {
		parts = append(parts, kdlQuote(arg))
	}
	return strings.Join(parts, " ")
}

func filepathBase(path string) string {
	if path == "" {
		return ""
	}
	i := strings.LastIndexAny(path, `/\`)
	if i < 0 {
		return path
	}
	return path[i+1:]
}
