package rcon

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
)

// DockerExecDialer runs  per command, opening
// a fresh socket inside the container's network namespace each time and
// closing it on completion. Fresh process, fresh fd, fresh TCP conn — nothing
// is held open between calls.
//
// Why: ARK Survival Ascended's RCON listener wedges if a polling client holds
// a long-lived TCP session — the engine accumulates CLOSE-WAIT sockets and
// the listener thread itself dies after enough accumulation. The wedge is
// not recoverable without restarting the engine. Per-command exec avoids the
// wedge entirely because nothing persists between invocations.
//
// Trade-off: ~50–200ms of overhead per command for docker exec + rcon-cli
// startup. Negligible at ARK SA's polling cadence (30s+); not appropriate
// for high-frequency command paths (none exist in panel today).
//
// Requirements:
//   - Container has rcon-cli on PATH (acekorneya/asa_server: yes).
//   - Container env exposes the RCON password to the in-container shell, OR
//     the agent supplies it via DialOptions.Password (we pass it in).
//   - Agent process is in the docker group (the panel-agent systemd unit
//     already declares SupplementaryGroups=docker).
type DockerExecDialer struct{}

func (DockerExecDialer) Name() string { return "docker_exec_rcon" }

func (DockerExecDialer) Dial(_ context.Context, opts DialOptions) (Client, error) {
	if opts.ContainerID == "" {
		return nil, errors.New("docker_exec_rcon dial: ContainerID required")
	}
	port := ""
	if opts.Port != 0 {
		port = strconv.Itoa(opts.Port)
	}
	return &dockerExecClient{
		containerID: opts.ContainerID,
		password:    opts.Password,
		port:        port,
	}, nil
}

type dockerExecClient struct {
	containerID string
	password    string
	port        string
	mu          sync.Mutex
}

// ansiRe matches CSI escape sequences. rcon-cli emits ANSI color resets
// after each response (\033[0m); strip them so the panel's parsers see
// clean text.
var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)

// Exec runs  and returns the cleaned stdout.
func (c *dockerExecClient) Exec(ctx context.Context, cmd string) (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	args := []string{"exec", c.containerID, "rcon-cli", "--host", "localhost"}
	if c.port != "" {
		args = append(args, "--port", c.port)
	}
	if c.password != "" {
		args = append(args, "--password", c.password)
	}
	args = append(args, cmd)

	dockerCmd := exec.CommandContext(ctx, "docker", args...)
	var stdout, stderr bytes.Buffer
	dockerCmd.Stdout = &stdout
	dockerCmd.Stderr = &stderr
	if err := dockerCmd.Run(); err != nil {
		errMsg := strings.TrimSpace(stderr.String())
		if errMsg == "" {
			errMsg = err.Error()
		}
		return "", fmt.Errorf("docker exec rcon-cli: %s", errMsg)
	}
	out := ansiRe.ReplaceAllString(stdout.String(), "")
	return strings.TrimSpace(out), nil
}

// Close is a no-op — there's nothing held open between Exec calls.
func (c *dockerExecClient) Close() error { return nil }
