package terminal

import (
	"io"
	"strings"
	"sync"

	"github.com/charmbracelet/x/vt"
)

// TermView wraps a charmbracelet/x/vt terminal emulator, providing a
// thread-safe interface for writing PTY output and rendering the screen buffer
// as an ANSI-styled string.
type TermView struct {
	term       *vt.SafeEmulator
	mu         sync.Mutex
	width      int
	height     int
	history    []string // scroll-back buffer: past screen renders
	scrollBack int      // 0 = live bottom, >0 = lines scrolled up

	dirty        bool   // set on Write, cleared on Render
	cachedView   string // last Render() output
	cacheScroll  int    // scrollBack value when cachedView was built
	bytesWritten int    // bytes written since last history snapshot
}

// NewTermView creates a new TermView with the given column and row dimensions.
// An optional responseSink receives terminal responses (device attributes,
// cursor position reports, etc.) generated by the VT emulator. Pass the PTY
// backend so the child process gets the responses it expects. If nil,
// responses are discarded.
//
// The VT emulator uses an io.Pipe for responses — writes block until read.
// A background goroutine drains the pipe to prevent deadlocks.
func NewTermView(cols, rows int, responseSink ...io.Writer) *TermView {
	emu := vt.NewSafeEmulator(cols, rows)
	tv := &TermView{
		term:   emu,
		width:  cols,
		height: rows,
	}

	// Drain the emulator's response pipe in a background goroutine.
	// Without this, any terminal query (DA, DSR, etc.) blocks the
	// emulator's Write call on the io.Pipe, deadlocking the TUI.
	var sink io.Writer
	if len(responseSink) > 0 && responseSink[0] != nil {
		sink = responseSink[0]
	}
	go func() {
		buf := make([]byte, 1024)
		for {
			n, err := emu.Read(buf)
			if n > 0 && sink != nil {
				_, _ = sink.Write(buf[:n])
			}
			if err != nil {
				return
			}
		}
	}()

	return tv
}

// Write feeds raw PTY output into the VT emulator. This processes ANSI escape
// sequences and updates the internal screen buffer. History snapshots are
// deferred to SnapshotHistory(), which should be called on a debounce timer
// rather than on every write. This avoids the expensive double-Render that
// the old implementation performed.
func (tv *TermView) Write(data []byte) {
	// Feed the VT emulator. SafeEmulator is internally thread-safe,
	// so we must NOT hold tv.mu during this call to avoid deadlock
	// with concurrent Render calls.
	_, _ = tv.term.Write(data)

	tv.mu.Lock()
	tv.dirty = true
	tv.bytesWritten += len(data)
	tv.mu.Unlock()
}

// SnapshotHistory captures the current screen into the scroll-back buffer.
// Call this on a debounce timer (e.g., every 100ms) rather than on every
// Write, to amortise the cost of Render across bursts of PTY output.
func (tv *TermView) SnapshotHistory() {
	tv.mu.Lock()
	if tv.bytesWritten == 0 {
		tv.mu.Unlock()
		return
	}
	tv.mu.Unlock()

	// Render outside the lock: SafeEmulator is thread-safe.
	rendered := tv.term.Render()

	tv.mu.Lock()
	defer tv.mu.Unlock()

	tv.history = append(tv.history, strings.Split(rendered, "\n")...)
	if len(tv.history) > 10000 {
		tv.history = tv.history[len(tv.history)-10000:]
	}
	tv.bytesWritten = 0
}

// Resize changes the dimensions of the virtual terminal.
func (tv *TermView) Resize(cols, rows int) {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	tv.width = cols
	tv.height = rows
	tv.term.Resize(cols, rows)
}

// Render returns the current screen buffer as a string with ANSI styling.
// When scrolled back, it returns lines from the history buffer instead of the
// live terminal output. Results are cached and reused until the next Write or
// scroll position change.
func (tv *TermView) Render() string {
	tv.mu.Lock()
	scrollBack := tv.scrollBack
	dirty := tv.dirty
	tv.mu.Unlock()

	// Return cached result if nothing changed
	if !dirty && scrollBack == tv.cacheScroll && tv.cachedView != "" {
		return tv.cachedView
	}

	var result string
	if scrollBack == 0 {
		// Live view: render from the VT emulator (thread-safe, no lock needed)
		result = tv.term.Render()
	} else {
		tv.mu.Lock()
		histLen := len(tv.history)
		height := tv.height
		end := histLen
		start := end - scrollBack
		if start < 0 {
			start = 0
		}
		var b strings.Builder
		b.Grow(height * (tv.width + 1))
		for i := start; i < end && i-start < height; i++ {
			if i > start {
				b.WriteByte('\n')
			}
			b.WriteString(tv.history[i])
		}
		result = b.String()
		tv.mu.Unlock()
	}

	tv.mu.Lock()
	tv.dirty = false
	tv.cachedView = result
	tv.cacheScroll = scrollBack
	tv.mu.Unlock()

	return result
}

// ScrollUp moves the viewport up by n lines.
func (tv *TermView) ScrollUp(n int) {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	tv.scrollBack += n
	if tv.scrollBack > len(tv.history) {
		tv.scrollBack = len(tv.history)
	}
}

// ScrollDown moves the viewport down by n lines. Snaps to live at 0.
func (tv *TermView) ScrollDown(n int) {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	tv.scrollBack -= n
	if tv.scrollBack < 0 {
		tv.scrollBack = 0
	}
}

// IsScrolled returns true if viewing history (not live bottom).
func (tv *TermView) IsScrolled() bool {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	return tv.scrollBack > 0
}

// SnapToBottom resets scroll to live view.
func (tv *TermView) SnapToBottom() {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	tv.scrollBack = 0
}

// Width returns the current terminal width in columns.
func (tv *TermView) Width() int {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	return tv.width
}

// Height returns the current terminal height in rows.
func (tv *TermView) Height() int {
	tv.mu.Lock()
	defer tv.mu.Unlock()
	return tv.height
}
