package uv

import (
	"fmt"
	"os"
	"os/signal"
	"time"

	"github.com/charmbracelet/x/ansi"
	"golang.org/x/sync/errgroup"
)

// DefaultBufferSize is the default size of the input buffer used for reading
// terminal events.
const DefaultBufferSize = 4096

// DefaultEventTimeout is the default duration to wait for input events before
// timing out.
const DefaultEventTimeout = 100 * time.Millisecond

// Options represents options for creating a new [Terminal].
type Options struct {
	// BufferSize is the size of the input buffer used for reading terminal
	// events. If zero, [DefaultBufferSize] is used.
	BufferSize int

	// EventTimeout is the duration to wait for input events before timing out.
	// If zero, a default of 100 milliseconds is used.
	EventTimeout time.Duration

	// LegacyKeyEncoding represents any legacy key encoding ambiguities. By
	// default, the terminal will use its preferred key encoding settings.
	LegacyKeyEncoding LegacyKeyEncoding

	// LookupKeys whether to use a lookup table for common key sequences. If
	// true, the terminal will use a lookup table to quickly identify common
	// key sequences, reducing the need for more complex decoding logic. This
	// can improve performance for common key sequences at the cost of
	// increased memory usage.
	//
	// This is enabled by default.
	LookupKeys bool

	// UseTerminfoKeys whether to use terminfo databases key definitions to
	// build up the keys lookup table. If true, the terminal will use terminfo
	// databases key definitions to build up the keys lookup table, which can
	// provide more accurate key mappings for legacy non-xterm like terminals.
	//
	// This won't take effect if [TerminalOptions.LookupKeys] is false, since
	// the lookup table won't be used.
	//
	// This is disabled by default.
	UseTerminfoKeys bool

	// Logger is an optional logger for tracing terminal I/O operations.
	// If nil, no logging is performed.
	Logger Logger
}

// DefaultOptions returns the default [Terminal] options.
func DefaultOptions() *Options {
	return &Options{
		BufferSize:   DefaultBufferSize,
		EventTimeout: DefaultEventTimeout,
		LookupKeys:   true,
	}
}

// Terminal represents an interactive terminal application.
type Terminal struct {
	con   Console
	opts  *Options
	scr   *TerminalScreen
	pr    pollReader
	buf   []byte
	evc   chan Event
	errg  errgroup.Group
	winch chan os.Signal
	donec chan struct{}
}

// DefaultTerminal creates a new [Terminal] instance using the default standard
// console and the given options. Options can be nil to use the default
// options.
//
// This is a convenience function for creating a terminal that uses the
// standard input and output file descriptors.
func DefaultTerminal() *Terminal {
	return NewTerminal(nil, nil)
}

// ControllingTerminal creates a new [Terminal] instance using the controlling
// terminal's input and output file descriptors.
// Options can be nil to use the default options.
//
// This is a convenience function for creating a terminal that uses the
// controlling TTY of the current process.
func ControllingTerminal() (*Terminal, error) {
	con, err := ControllingConsole()
	if err != nil {
		return nil, err
	}
	return NewTerminal(con, nil), nil
}

// NewTerminal creates a new [Terminal] instance with the given console and
// options.
// Options can be nil to use the default options.
func NewTerminal(con Console, opts *Options) *Terminal {
	t := &Terminal{}
	if con == nil {
		con = DefaultConsole()
	}
	if opts == nil {
		opts = DefaultOptions()
	}
	if opts.BufferSize <= 0 {
		opts.BufferSize = DefaultBufferSize
	}
	if opts.EventTimeout <= 0 {
		opts.EventTimeout = DefaultEventTimeout
	}
	t.con = con
	t.opts = opts
	t.scr = NewTerminalScreen(t.con.Writer(), t.con.Environ())
	t.buf = make([]byte, opts.BufferSize)
	// These channels never close during the terminal's lifetime.
	t.evc = make(chan Event)
	t.winch = make(chan os.Signal, 1) // buffered to avoid missing signals
	if opts.Logger != nil {
		t.scr.rend.SetLogger(opts.Logger)
	}
	return t
}

// GetSize returns the current size of the terminal in characters and pixels.
func (t *Terminal) GetSize() (width, height int, err error) {
	w, h, err := t.con.GetSize()
	if err != nil {
		return 0, 0, fmt.Errorf("getting terminal size: %w", err)
	}
	return int(w), int(h), nil
}

// GetWinsize returns the current size of the terminal as a [Winsize] struct.
// This includes both character dimensions (columns and rows) and pixel
// dimensions (xpixel and ypixel).
//
// Note that this only returns the pixel dimensions on Unix-like systems that
// support the TIOCGWINSZ ioctl. On other platforms, the pixel dimensions may
// be zero or not available.
func (t *Terminal) GetWinsize() (*Winsize, error) {
	ws, err := t.con.GetWinsize()
	if err != nil {
		return nil, fmt.Errorf("getting terminal winsize: %w", err)
	}
	return ws, nil
}

// Screen returns the terminal's screen.
func (t *Terminal) Screen() *TerminalScreen {
	return t.scr
}

// Events returns the terminal's event channel.
func (t *Terminal) Events() <-chan Event {
	return t.evc
}

// Start starts the terminal application event loop. This is a non-blocking
// call. Use [Terminal.Wait] to wait for the terminal to exit.
func (t *Terminal) Start() error {
	_, err := t.con.MakeRaw()
	if err != nil {
		return fmt.Errorf("failed to set terminal to raw mode: %w", err)
	}

	evs := newEventScanner()
	evs.lookup = t.opts.LookupKeys
	if evs.lookup {
		evs.table = buildKeysTable(t.opts.LegacyKeyEncoding, t.con.Getenv("TERM"), t.opts.UseTerminfoKeys)
	}
	if t.opts.Logger != nil {
		evs.setLogger(t.opts.Logger)
	}
	bufc := make(chan []byte)
	t.donec = make(chan struct{})
	t.pr, err = newPollReader(t.con.Reader())
	if err != nil {
		return fmt.Errorf("failed to create poll reader: %w", err)
	}

	// input loop
	t.errg.Go(func() error {
		for {
			n, err := t.pr.Read(t.buf)
			if err != nil {
				return fmt.Errorf("reading terminal input: %w", err)
			}
			select {
			case bufc <- t.buf[:n]:
			case <-t.donec:
				return nil
			}
		}
	})

	// event loop
	sendEvents := func(buf []byte, expired bool) int {
		n, events := evs.scanEvents(buf, expired)
		for _, ev := range events {
			t.handleEvent(ev)
			t.SendEvent(ev)
		}
		return n
	}
	t.errg.Go(func() error {
		var buf []byte
		timer := time.NewTimer(t.opts.EventTimeout)
		timeout := time.Now().Add(t.opts.EventTimeout)

		for {
			select {
			case <-t.donec:
				return nil
			case <-timer.C:
				expired := len(buf) > 0 && time.Now().After(timeout)
				n := sendEvents(buf, expired)
				if n > 0 {
					buf = buf[min(n, len(buf)):]
				}
				if len(buf) > 0 {
					timer.Reset(t.opts.EventTimeout)
				}
			case data := <-bufc:
				buf = append(buf, data...)
				n := sendEvents(buf, false)
				timeout = time.Now().Add(t.opts.EventTimeout)
				timer.Stop()
				if n > 0 {
					buf = buf[min(n, len(buf)):]
				}
				if len(buf) > 0 {
					timer.Reset(t.opts.EventTimeout)
				}
			}
		}
	})

	sendWinsize := func() error {
		ws, err := t.con.GetWinsize()
		if err != nil {
			return fmt.Errorf("getting terminal size: %w", err)
		}
		if ws.Col > 0 && ws.Row > 0 {
			t.SendEvent(WindowSizeEvent{
				Width:  int(ws.Col),
				Height: int(ws.Row),
			})
		}
		if ws.Xpixel > 0 && ws.Ypixel > 0 {
			t.SendEvent(PixelSizeEvent{
				Width:  int(ws.Xpixel),
				Height: int(ws.Ypixel),
			})
		}
		return nil
	}

	// winch handler
	NotifyWinch(t.winch)
	t.errg.Go(func() error {
		for {
			select {
			case <-t.donec:
				return nil
			case <-t.winch:
				if err := sendWinsize(); err != nil {
					return err
				}
			}
		}
	})

	// init window size
	t.errg.Go(func() error {
		if err := sendWinsize(); err != nil {
			return err
		}
		return nil
	})

	// Restore any previous screen state.
	t.scr.Restore()
	// Query whether the terminal supports Unicode core mode (DEC mode 2027) so
	// we can negotiate grapheme-cluster width. The response is handled in the
	// event loop.
	t.scr.requestGraphemeWidth()
	if err := t.scr.Flush(); err != nil {
		return fmt.Errorf("failed to flush terminal screen: %w", err)
	}

	return nil
}

// handleEvent reacts to internal events before they are forwarded to the
// application. It negotiates Unicode core mode (DEC mode 2027): when the
// terminal reports the mode is in a settable or active state, it enables
// grapheme-cluster width so wide-glyph measurement matches the terminal. The
// event is still forwarded to the application.
//
// The mode is only enabled for settable/active states (mode set, reset but
// settable, or permanently set). It is explicitly not enabled when the mode is
// not recognized or permanently reset, since those terminals cannot honor it.
// Enabling goes through the screen's locked write path so it cannot race with
// the application's Render/Flush.
func (t *Terminal) handleEvent(ev Event) {
	if mr, ok := ev.(ModeReportEvent); ok {
		if mr.Mode == ansi.ModeUnicodeCore &&
			(mr.Value.IsSet() || mr.Value == ansi.ModeReset) {
			t.scr.enableGraphemeWidth()
		}
	}
}

// Wait waits for the terminal event loop to exit and returns any error that
// occurred.
func (t *Terminal) Wait() error {
	if err := t.errg.Wait(); err != nil {
		return fmt.Errorf("terminal event loop error: %w", err)
	}
	return nil
}

// Stop stops the terminal event loop. It is safe to call Stop without a
// prior [Terminal.Start], and safe to call Stop multiple times in a row.
// After Stop returns, [Terminal.Start] may be called again to resume.
func (t *Terminal) Stop() error {
	if t.donec != nil {
		select {
		case <-t.donec:
			// Already closed.
		default:
			close(t.donec)
		}
	}
	if t.winch != nil {
		signal.Stop(t.winch)
	}
	if t.pr != nil {
		t.pr.Cancel()
		_ = t.pr.Close()
		t.pr = nil
	}
	t.scr.Reset()
	if err := t.scr.Flush(); err != nil {
		_ = t.con.Restore()
		return fmt.Errorf("failed to flush terminal screen: %w", err)
	}
	if err := t.con.Restore(); err != nil {
		return fmt.Errorf("failed to restore terminal state: %w", err)
	}
	return nil
}

// SendEvent sends an event to the terminal's event channel.
//
// This can be used to inject custom events into the terminal's event loop,
// such as timer events, signals, or application-specific events.
func (t *Terminal) SendEvent(e Event) {
	select {
	case t.evc <- e:
	case <-t.donec:
	}
}

// Write writes data directly to the terminal's console output.
//
// This is a low-level operation that bypasses the terminal screen buffering
// and writes directly to the console output handler, usually [os.Stdout] or
// the controlling TTY.
func (t *Terminal) Write(p []byte) (n int, err error) {
	return t.con.Write(p)
}

// Read reads data from the terminal's console input.
//
// This is a low-level operation that bypasses the terminal event processing
// and reads directly from the console input handler, usually [os.Stdin] or the
// controlling TTY. Use this method with caution, as it may interfere with the
// terminal's event loop and screen management.
func (t *Terminal) Read(p []byte) (n int, err error) {
	return t.con.Read(p)
}
