package api

// Cross-platform terminal session abstraction.
//
// On Linux / macOS the unix-tagged file (terminal_session_unix.go) wires
// startTermSession to creack/pty for a real PTY: full TTY semantics,
// resize support, interactive apps (vim, top…) work.
//
// On Windows the windows-tagged file (terminal_session_windows.go) tries
// the kernel32 ConPTY API first, with a pipe-based fallback for older
// hosts. pipeSession does NOT carry TTY signals, so most shells go silent
// — it's only kept as a last resort.
//
// Both platforms share the termSession interface, the ptySession type
// (used by unix), and the pipeSession type (used by the Windows fallback).

import (
	"io"
	"os"
	"os/exec"
	"sync"

	"github.com/creack/pty/v2"
)

// termSession is the read/write/resize/close surface used by handleTerminalWS.
type termSession interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Resize(rows, cols uint16) error
	Close() error
	Wait() error
	Pid() int
}

// ptySession wraps creack/pty's *os.File-backed PTY (unix path).
type ptySession struct {
	ptmx *os.File
	cmd  *exec.Cmd
}

func (s *ptySession) Read(p []byte) (int, error)  { return s.ptmx.Read(p) }
func (s *ptySession) Write(p []byte) (int, error) { return s.ptmx.Write(p) }
func (s *ptySession) Resize(rows, cols uint16) error {
	return pty.Setsize(s.ptmx, &pty.Winsize{Rows: rows, Cols: cols})
}
func (s *ptySession) Close() error {
	err := s.ptmx.Close()
	if s.cmd.Process != nil {
		s.cmd.Process.Kill()
	}
	return err
}
func (s *ptySession) Wait() error {
	if s.cmd.Process == nil {
		return nil
	}
	return s.cmd.Wait()
}
func (s *ptySession) Pid() int {
	if s.cmd.Process == nil {
		return 0
	}
	return s.cmd.Process.Pid
}

// pipeSession is the Windows last-resort fallback when ConPTY is not
// available: stdin pipe + merged stdout/stderr, no TTY signals. Most
// interactive shells go silent in this mode, so it should rarely be hit on
// modern Windows (10 1809+).
type pipeSession struct {
	cmd     *exec.Cmd
	stdin   io.WriteCloser
	stdout  io.ReadCloser
	stderr  io.ReadCloser
	mu      sync.Mutex
	merged  chan []byte
	closed  bool
	closeCh chan struct{}
}

func startPipeSession(cmd *exec.Cmd) (termSession, error) {
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, err
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		stdin.Close()
		return nil, err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		stdin.Close()
		stdout.Close()
		return nil, err
	}
	if err := cmd.Start(); err != nil {
		stdin.Close()
		stdout.Close()
		stderr.Close()
		return nil, err
	}
	s := &pipeSession{
		cmd:     cmd,
		stdin:   stdin,
		stdout:  stdout,
		stderr:  stderr,
		merged:  make(chan []byte, 32),
		closeCh: make(chan struct{}),
	}
	go s.pump(stdout)
	go s.pump(stderr)
	return s, nil
}

func (s *pipeSession) pump(r io.ReadCloser) {
	buf := make([]byte, 4096)
	for {
		n, err := r.Read(buf)
		if n > 0 {
			chunk := make([]byte, n)
			copy(chunk, buf[:n])
			select {
			case s.merged <- chunk:
			case <-s.closeCh:
				return
			}
		}
		if err != nil {
			return
		}
	}
}

func (s *pipeSession) Read(p []byte) (int, error) {
	select {
	case chunk, ok := <-s.merged:
		if !ok {
			return 0, io.EOF
		}
		n := copy(p, chunk)
		return n, nil
	case <-s.closeCh:
		return 0, io.EOF
	}
}

func (s *pipeSession) Write(p []byte) (int, error) {
	return s.stdin.Write(p)
}

func (s *pipeSession) Resize(rows, cols uint16) error {
	// No real TTY → resize is a no-op; the child won't get SIGWINCH.
	return nil
}

func (s *pipeSession) Close() error {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return nil
	}
	s.closed = true
	close(s.closeCh)
	s.mu.Unlock()
	s.stdin.Close()
	s.stdout.Close()
	s.stderr.Close()
	if s.cmd.Process != nil {
		s.cmd.Process.Kill()
	}
	return nil
}

func (s *pipeSession) Wait() error {
	if s.cmd.Process == nil {
		return nil
	}
	return s.cmd.Wait()
}

func (s *pipeSession) Pid() int {
	if s.cmd.Process == nil {
		return 0
	}
	return s.cmd.Process.Pid
}
