package rcon

import (
	"context"
	"errors"
	"fmt"
	"io"
	"sync"
	"time"
)

// StdioDialer "RCON"s a game by attaching to the container's stdin and writing
// the admin console command, trusting the game to execute it. It's the correct
// choice for games whose admin surface is stdin-only — Valheim, Terraria,
// Barotrauma, Enshrouded, Sons Of The Forest — where no TCP RCON exists.
//
// v1 is write-only: Exec returns empty string. Command output arrives via the
// panel's log pipeline (docker logs → agent → bus → SSE) because the game's
// replies appear on the same stdout the log stream is tailing. A future
// revision could correlate a response window with each write if we ever need
// structured replies for state polling.
//
// Requires the container to have been created with OpenStdin=true; the module
// resolver auto-enables that whenever manifest.Rcon.Adapter == "stdio".
type StdioDialer struct{}

// Name identifies this adapter in the module manifest.
func (StdioDialer) Name() string { return "stdio" }

// Dial attaches to the container's stdio stream. A background goroutine drains
// the multiplexed stdout/stderr so the stream never blocks from our side; the
// docker logs pipeline is the canonical consumer of that output.
func (StdioDialer) Dial(ctx context.Context, opts DialOptions) (Client, error) {
	if opts.Stdio == nil {
		return nil, errors.New("stdio dial: Stdio backend required (agent must provide one)")
	}
	if opts.ContainerID == "" {
		return nil, errors.New("stdio dial: ContainerID required")
	}
	session, err := opts.Stdio.AttachStdio(ctx, opts.ContainerID)
	if err != nil {
		return nil, fmt.Errorf("stdio attach %q: %w", opts.ContainerID, err)
	}
	c := &stdioClient{session: session}
	go func() {
		// Drain the attached stream so Docker doesn't buffer indefinitely.
		// We deliberately discard: the log pipeline already surfaces this
		// output to the panel via a separate docker logs call.
		_, _ = io.Copy(io.Discard, session)
	}()
	return c, nil
}

type stdioClient struct {
	session io.ReadWriteCloser
	mu      sync.Mutex
	closed  bool
}

// Exec writes cmd + "\n" to the container's stdin. Returns empty string; the
// game's response (if any) lands in the log stream and is surfaced in the
// Console tab.
//
// The write is bounded by ctx (and a hard 10s fallback). A raw Write to a
// hijacked stdin pipe can block FOREVER if the container's stdin read side
// stalls (game paused, buffer full, or the process not draining stdin). Because
// the agent dispatches RCON inline on its single recv loop, an unbounded write
// here freezes the WHOLE agent — no stop/start/anything — until restart. That's
// the Valheim+stdio "stop never lands" hang. We run the write in a goroutine and
// abandon it on timeout so the dispatcher stays responsive.
func (c *stdioClient) Exec(ctx context.Context, cmd string) (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return "", errors.New("stdio: session closed")
	}
	if ctx == nil {
		ctx = context.Background()
	}
	wctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()
	done := make(chan error, 1)
	go func() {
		_, err := c.session.Write([]byte(cmd + "\n"))
		done <- err
	}()
	select {
	case err := <-done:
		if err != nil {
			return "", fmt.Errorf("stdio write: %w", err)
		}
		return "", nil
	case <-wctx.Done():
		// The write is wedged — leave the goroutine parked (it'll unblock if the
		// pipe ever drains, or die when the session closes) but DON'T let it hold
		// up the caller / the agent's recv loop.
		return "", fmt.Errorf("stdio write timed out after 10s (container stdin not draining): %w", wctx.Err())
	}
}

// Close shuts down the hijacked attach connection.
func (c *stdioClient) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return nil
	}
	c.closed = true
	return c.session.Close()
}
