package rcon

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"strings"
	"sync"
	"time"
)

// TelnetDialer speaks 7DTD-style line-oriented telnet: on connect the server
// sends a "password:" prompt; after the client writes the password and a
// newline, commands are one-line writes and responses are everything read
// until the connection goes quiet for a short idle period.
type TelnetDialer struct{}

func (TelnetDialer) Name() string { return "telnet" }

func (TelnetDialer) Dial(ctx context.Context, opts DialOptions) (Client, error) {
	if opts.Host == "" || opts.Port == 0 {
		return nil, errors.New("telnet dial: host and port are required")
	}
	addr := fmt.Sprintf("%s:%d", opts.Host, opts.Port)

	timeout := opts.ConnectTimeout
	if timeout == 0 {
		timeout = 5 * time.Second
	}

	d := &net.Dialer{Timeout: timeout}
	conn, err := d.DialContext(ctx, "tcp", addr)
	if err != nil {
		return nil, fmt.Errorf("telnet dial %s: %w", addr, err)
	}

	idle := opts.ReadIdle
	if idle == 0 {
		idle = 250 * time.Millisecond
	}
	c := &telnetClient{
		conn:     conn,
		readIdle: idle,
	}
	if err := c.authenticate(opts.Password); err != nil {
		_ = conn.Close()
		return nil, fmt.Errorf("telnet auth: %w", err)
	}
	return c, nil
}

// telnetClient is safe for concurrent Exec via mu serialization.
type telnetClient struct {
	conn     net.Conn
	readIdle time.Duration

	mu sync.Mutex
}

func (c *telnetClient) authenticate(password string) error {
	// Read until the first "password:" prompt arrives, with a 5s cap.
	if _, err := c.readUntil("password:", 5*time.Second); err != nil {
		return fmt.Errorf("read password prompt: %w", err)
	}
	if _, err := c.conn.Write([]byte(password + "\n")); err != nil {
		return fmt.Errorf("write password: %w", err)
	}
	// Drain whatever welcome/banner text the server sends after auth.
	_, _ = c.readQuiet(1500*time.Millisecond, 1500*time.Millisecond)
	return nil
}

// Exec sends one line-terminated command and returns everything the server
// writes back before going quiet for readIdle.
func (c *telnetClient) Exec(ctx context.Context, cmd string) (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Wire ctx deadline into the conn's read/write timeouts.
	if dl, ok := ctx.Deadline(); ok {
		_ = c.conn.SetDeadline(dl)
		defer c.conn.SetDeadline(time.Time{}) //nolint:errcheck
	}

	if _, err := c.conn.Write([]byte(cmd + "\n")); err != nil {
		return "", fmt.Errorf("write cmd: %w", err)
	}
	return c.readQuiet(c.firstByteTimeout(), c.readIdle)
}

func (c *telnetClient) Close() error {
	return c.conn.Close()
}

// firstByteTimeout is how long we wait for the server to START replying to a
// command. A busy 7DTD server (heavy load, blood moon, GC pause) can take well
// over readIdle to send its first byte, so this must be generous — otherwise
// readQuiet times out with zero bytes and returns an empty response even though
// the command executed fine on the server.
func (c *telnetClient) firstByteTimeout() time.Duration {
	return 4 * time.Second
}

// readQuiet reads a command response. It waits up to `first` for the first byte
// (server may be slow to start replying under load), then once bytes arrive it
// uses the short `idle` gap to detect end-of-response. Returns what it read, or
// an error on a real (non-timeout) failure.
func (c *telnetClient) readQuiet(first, idle time.Duration) (string, error) {
	var buf bytes.Buffer
	tmp := make([]byte, 4096)
	for {
		// Before any data, allow the longer first-byte window; after data
		// starts flowing, switch to the short idle gap.
		wait := idle
		if buf.Len() == 0 {
			wait = first
		}
		_ = c.conn.SetReadDeadline(time.Now().Add(wait))
		n, err := c.conn.Read(tmp)
		if n > 0 {
			buf.Write(tmp[:n])
		}
		if err != nil {
			var ne net.Error
			if errors.As(err, &ne) && ne.Timeout() {
				// Timeout with data → normal end of response.
				// Timeout with NO data even after the first-byte window → the
				// server genuinely sent nothing (or is unreachable); return
				// empty and let the caller decide.
				return buf.String(), nil
			}
			if errors.Is(err, io.EOF) {
				return buf.String(), nil
			}
			return buf.String(), err
		}
	}
}

// readUntil reads up to `overall` time waiting for `substr` (case-insensitive)
// to appear in the accumulated buffer. Returns what it saw either way.
func (c *telnetClient) readUntil(substr string, overall time.Duration) (string, error) {
	var buf bytes.Buffer
	tmp := make([]byte, 4096)
	deadline := time.Now().Add(overall)
	want := strings.ToLower(substr)
	for time.Now().Before(deadline) {
		_ = c.conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
		n, err := c.conn.Read(tmp)
		if n > 0 {
			buf.Write(tmp[:n])
			if strings.Contains(strings.ToLower(buf.String()), want) {
				return buf.String(), nil
			}
		}
		if err != nil {
			var ne net.Error
			if errors.As(err, &ne) && ne.Timeout() {
				continue
			}
			return buf.String(), err
		}
	}
	return buf.String(), fmt.Errorf("timed out after %s waiting for %q", overall, substr)
}
