package infra

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
	"sync"
	"syscall"
	"time"

	"github.com/creack/pty"
)

// PTYCaptureIdle launches a command inside a pseudo-terminal (PTY) and captures
// all output until the terminal has been idle for at least idle duration, or
// maxDur has elapsed. Before sending inputs it waits warmup to let the process
// initialize. Between each input step it waits stepDelay.
//
// The returned string is the raw PTY output, ANSI escape sequences included.
// To turn it into plain text: use vt_render_go_tui to reconstruct the 2D screen
// layout for TUIs with absolute cursor positioning (claude, htop), or
// strip_ansi_go_core for sequential, log-like output.
func PTYCaptureIdle(
	ctx context.Context,
	name string,
	args []string,
	warmup time.Duration,
	inputs []string,
	stepDelay time.Duration,
	idle time.Duration,
	maxDur time.Duration,
) (string, error) {
	cmd := exec.CommandContext(ctx, name, args...)

	ptmx, err := pty.Start(cmd)
	if err != nil {
		return "", fmt.Errorf("pty_capture_idle: pty.Start: %w", err)
	}

	// Set a reasonable terminal size so TUIs render without truncating.
	if szErr := pty.Setsize(ptmx, &pty.Winsize{Rows: 40, Cols: 120}); szErr != nil {
		// Non-fatal: continue even if resize fails.
		_ = szErr
	}

	var (
		mu       sync.Mutex
		buf      bytes.Buffer
		lastByte = time.Now()
	)

	// Reader goroutine: copy PTY output into buf and track last-byte time.
	readDone := make(chan struct{})
	go func() {
		defer close(readDone)
		tmp := make([]byte, 4096)
		for {
			n, rerr := ptmx.Read(tmp)
			if n > 0 {
				mu.Lock()
				buf.Write(tmp[:n])
				lastByte = time.Now()
				mu.Unlock()
			}
			if rerr != nil {
				// EIO/EOF is normal on Linux when the PTY master is closed
				// after the child exits. Not a real error.
				return
			}
		}
	}()

	start := time.Now()

	// Wait for warmup so the TUI/CLI has time to initialize.
	select {
	case <-time.After(warmup):
	case <-ctx.Done():
		_ = ptmx.Close()
		<-readDone
		mu.Lock()
		out := buf.String()
		mu.Unlock()
		return out, fmt.Errorf("pty_capture_idle: context cancelled during warmup: %w", ctx.Err())
	}

	// Send inputs one by one with stepDelay between them.
	for _, in := range inputs {
		if _, werr := ptmx.Write([]byte(in)); werr != nil {
			// PTY may have closed already; stop sending.
			break
		}
		select {
		case <-time.After(stepDelay):
		case <-ctx.Done():
			goto done
		}
	}

done:
	// Poll until idle or maxDur.
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			mu.Lock()
			sinceLastByte := time.Since(lastByte)
			mu.Unlock()

			elapsed := time.Since(start)
			if sinceLastByte >= idle || elapsed >= maxDur {
				goto shutdown
			}
		case <-ctx.Done():
			goto shutdown
		}
	}

shutdown:
	// Close the PTY master. This signals EOF to the reader goroutine.
	_ = ptmx.Close()

	// Graceful shutdown: SIGTERM first, then SIGKILL after 2s.
	if cmd.Process != nil {
		_ = cmd.Process.Signal(syscall.SIGTERM)
		killTimer := time.NewTimer(2 * time.Second)
		waitCh := make(chan error, 1)
		go func() { waitCh <- cmd.Wait() }()
		select {
		case <-waitCh:
			// Process exited cleanly.
		case <-killTimer.C:
			_ = cmd.Process.Kill()
			<-waitCh
		}
		killTimer.Stop()
	}

	<-readDone

	mu.Lock()
	out := buf.String()
	mu.Unlock()

	return out, nil
}
