package terminal

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"

	"github.com/creack/pty"
)

const (
	terminalInputDelay = 2 * time.Millisecond
)

type Options struct {
	Command string
	Args    []string
	Env     []string
	Rows    uint16
	Cols    uint16
}

type Output struct {
	Text string
}

type Snapshot struct {
	Tail string
}

type Session interface {
	Output() <-chan Output
	Done() <-chan error
	WritePrompt(ctx context.Context, prompt string) error
	WriteInput(ctx context.Context, data []byte) error
	Resize(rows, cols uint16) error
	Signal(sig os.Signal) error
	Snapshot() Snapshot
	Close() error
}

type sessionImpl struct {
	cmd      *exec.Cmd
	input    io.WriteCloser
	output   io.ReadCloser
	tail     *TailBuffer
	outputCh chan Output
	doneCh   chan error
	closed   bool
	mu       sync.Mutex
}

func StartSession(ctx context.Context, opts Options) (Session, error) {
	cmd := exec.Command(opts.Command, opts.Args...)
	if len(opts.Env) > 0 {
		cmd.Env = append(cmd.Environ(), opts.Env...)
	}
	rows := opts.Rows
	if rows == 0 {
		rows = 80
	}
	cols := opts.Cols
	if cols == 0 {
		cols = 240
	}
	ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{
		Rows: rows,
		Cols: cols,
	})
	if err != nil {
		return nil, fmt.Errorf("pty start: %w", err)
	}

	s := &sessionImpl{
		cmd:      cmd,
		input:    ptmx,
		output:   ptmx,
		tail:     NewTailBuffer(2048),
		outputCh: make(chan Output, 1024),
		doneCh:   make(chan error, 1),
	}

	go s.readLoop()

	return s, nil
}

func (s *sessionImpl) Output() <-chan Output {
	return s.outputCh
}

func (s *sessionImpl) Done() <-chan error {
	return s.doneCh
}

func (s *sessionImpl) readLoop() {
	buf := make([]byte, 4096)
	for {
		n, err := s.output.Read(buf)
		if n > 0 {
			text := string(buf[:n])
			s.tail.Append(text)
			s.outputCh <- Output{Text: text}
		}
		if err != nil {
			s.doneCh <- s.cmd.Wait()
			close(s.outputCh)
			return
		}
	}
}

func (s *sessionImpl) WritePrompt(ctx context.Context, prompt string) error {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return errors.New("terminal session closed")
	}
	s.mu.Unlock()

	for _, r := range prompt {
		s.mu.Lock()
		if s.closed {
			s.mu.Unlock()
			return errors.New("terminal session closed")
		}
		_, err := io.WriteString(s.input, string(r))
		s.mu.Unlock()
		if err != nil {
			return err
		}
		timer := time.NewTimer(terminalInputDelay)
		select {
		case <-ctx.Done():
			timer.Stop()
			return ctx.Err()
		case <-timer.C:
		}
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return errors.New("terminal session closed")
	}
	_, err := io.WriteString(s.input, "\r")
	return err
}

func (s *sessionImpl) Snapshot() Snapshot {
	return Snapshot{
		Tail: s.tail.String(),
	}
}

func (s *sessionImpl) Resize(rows, cols uint16) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return errors.New("terminal session closed")
	}
	if rows == 0 || cols == 0 {
		return errors.New("invalid terminal size: rows and cols must be greater than 0")
	}
	f, ok := s.input.(*os.File)
	if !ok {
		return errors.New("terminal input is not a file")
	}
	return pty.Setsize(f, &pty.Winsize{Rows: rows, Cols: cols})
}

func (s *sessionImpl) WriteInput(ctx context.Context, data []byte) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return errors.New("terminal session closed")
	}
	_, err := s.input.Write(data)
	return err
}

func (s *sessionImpl) Signal(sig os.Signal) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return errors.New("terminal session closed")
	}
	if s.cmd == nil || s.cmd.Process == nil {
		return errors.New("no process to signal")
	}
	return s.cmd.Process.Signal(sig)
}

func (s *sessionImpl) Close() error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return nil
	}
	s.closed = true
	err := s.input.Close()
	if s.cmd != nil && s.cmd.Process != nil {
		_ = s.cmd.Process.Kill()
	}
	if isAlreadyClosedError(err) {
		return nil
	}
	return err
}

func isAlreadyClosedError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, os.ErrClosed) {
		return true
	}
	errStr := err.Error()
	return strings.Contains(errStr, "file already closed") ||
		strings.Contains(errStr, "use of closed file")
}

// TailBuffer holds a bounded buffer of terminal output.
type TailBuffer struct {
	mu  sync.Mutex
	buf strings.Builder
	max int
}

// NewTailBuffer creates a new TailBuffer with the given maximum capacity in bytes.
func NewTailBuffer(max int) *TailBuffer {
	return &TailBuffer{max: max}
}

// Append appends a string to the buffer, truncating from the front if it exceeds capacity.
func (tb *TailBuffer) Append(s string) {
	tb.mu.Lock()
	defer tb.mu.Unlock()
	tb.buf.WriteString(s)
	raw := tb.buf.String()
	if len(raw) <= tb.max {
		return
	}
	tb.buf.Reset()
	tb.buf.WriteString(raw[len(raw)-tb.max:])
}

// String returns the contents of the buffer.
func (tb *TailBuffer) String() string {
	tb.mu.Lock()
	defer tb.mu.Unlock()
	return tb.buf.String()
}
