package iostreams

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"sync"
	"time"

	"golang.org/x/term"
)

// IOStreams provides the standard streams for the CLI along with TTY detection,
// color support, pager integration, and other terminal helpers.
type IOStreams struct {
	In     io.Reader
	Out    io.Writer
	ErrOut io.Writer

	// Private fields for state
	isStdinTTY  bool
	isStdoutTTY bool
	isStderrTTY bool

	pagerProcess *exec.Cmd
	pagerPipe    io.WriteCloser
	originalOut  io.Writer

	colorScheme *ColorScheme

	spinnerMu     sync.Mutex
	spinnerCancel chan struct{}
}

// New creates an IOStreams wired to the real os.Stdin, os.Stdout, and os.Stderr,
// with TTY status auto-detected. Setting FGJ_FORCE_TTY=1 forces all streams to
// be treated as TTYs.
func New() *IOStreams {
	forceTTY := os.Getenv("FGJ_FORCE_TTY") != ""

	stdinTTY := forceTTY || (isTerminal(os.Stdin.Fd()))
	stdoutTTY := forceTTY || (isTerminal(os.Stdout.Fd()))
	stderrTTY := forceTTY || (isTerminal(os.Stderr.Fd()))

	return &IOStreams{
		In:          os.Stdin,
		Out:         os.Stdout,
		ErrOut:      os.Stderr,
		isStdinTTY:  stdinTTY,
		isStdoutTTY: stdoutTTY,
		isStderrTTY: stderrTTY,
	}
}

// Test creates an IOStreams backed by bytes.Buffers, suitable for unit tests.
// All TTY flags are false.
func Test() *IOStreams {
	return &IOStreams{
		In:          &bytes.Buffer{},
		Out:         &bytes.Buffer{},
		ErrOut:      &bytes.Buffer{},
		isStdinTTY:  false,
		isStdoutTTY: false,
		isStderrTTY: false,
	}
}

// IsStdinTTY reports whether standard input is connected to a terminal.
func (s *IOStreams) IsStdinTTY() bool {
	return s.isStdinTTY
}

// IsStdoutTTY reports whether standard output is connected to a terminal.
func (s *IOStreams) IsStdoutTTY() bool {
	return s.isStdoutTTY
}

// IsStderrTTY reports whether standard error is connected to a terminal.
func (s *IOStreams) IsStderrTTY() bool {
	return s.isStderrTTY
}

// TerminalWidth returns the width of the terminal connected to stdout. If stdout
// is not a terminal, it returns 80.
func (s *IOStreams) TerminalWidth() int {
	if !s.isStdoutTTY {
		return 80
	}
	if f, ok := s.Out.(*os.File); ok {
		w, _, err := term.GetSize(int(f.Fd()))
		if err == nil && w > 0 {
			return w
		}
	}
	return 80
}

// ColorEnabled returns true when color output should be used. Color is enabled
// when stdout is a TTY and the NO_COLOR environment variable is not set.
func (s *IOStreams) ColorEnabled() bool {
	if os.Getenv("NO_COLOR") != "" {
		return false
	}
	return s.isStdoutTTY
}

// ColorScheme returns a lazily-initialized ColorScheme that respects the current
// color settings.
func (s *IOStreams) ColorScheme() *ColorScheme {
	if s.colorScheme == nil {
		s.colorScheme = NewColorScheme(s.ColorEnabled())
	}
	return s.colorScheme
}

// StartPager starts an external pager process and redirects Out to its stdin.
// It checks FGJ_PAGER, then PAGER, then defaults to "less". If LESS is not
// already set, it is set to "FRX" for a good default experience.
func (s *IOStreams) StartPager() error {
	if !s.isStdoutTTY {
		return nil
	}

	pagerCmd := os.Getenv("FGJ_PAGER")
	if pagerCmd == "" {
		pagerCmd = os.Getenv("PAGER")
	}
	if pagerCmd == "" {
		pagerCmd = "less"
	}

	if os.Getenv("LESS") == "" {
		os.Setenv("LESS", "FRX")
	}

	parts := strings.Fields(pagerCmd)
	//nolint:gosec // pager command is user-configured
	cmd := exec.Command(parts[0], parts[1:]...)
	cmd.Stdout = s.Out
	cmd.Stderr = s.ErrOut

	pipe, err := cmd.StdinPipe()
	if err != nil {
		return fmt.Errorf("creating pager pipe: %w", err)
	}

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("starting pager: %w", err)
	}

	s.pagerProcess = cmd
	s.pagerPipe = pipe
	s.originalOut = s.Out
	s.Out = pipe

	return nil
}

// StopPager closes the pager's stdin pipe and waits for the process to exit.
// It restores Out to the original writer.
func (s *IOStreams) StopPager() {
	if s.pagerPipe == nil {
		return
	}

	_ = s.pagerPipe.Close()
	_ = s.pagerProcess.Wait()

	s.Out = s.originalOut
	s.pagerPipe = nil
	s.pagerProcess = nil
	s.originalOut = nil
}

// spinnerFrames are the Braille-based animation frames for the spinner.
var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}

// StartSpinner shows an animated spinner with the given label on stderr. It only
// runs when stderr is a TTY. Call StopSpinner to halt it.
func (s *IOStreams) StartSpinner(label string) {
	if !s.isStderrTTY {
		return
	}

	s.spinnerMu.Lock()
	defer s.spinnerMu.Unlock()

	// Stop any existing spinner first.
	if s.spinnerCancel != nil {
		close(s.spinnerCancel)
		s.spinnerCancel = nil
	}

	cancel := make(chan struct{})
	s.spinnerCancel = cancel

	go func() {
		ticker := time.NewTicker(80 * time.Millisecond)
		defer ticker.Stop()

		i := 0
		for {
			select {
			case <-cancel:
				// Clear the spinner line.
				fmt.Fprintf(s.ErrOut, "\r\033[K")
				return
			case <-ticker.C:
				frame := spinnerFrames[i%len(spinnerFrames)]
				fmt.Fprintf(s.ErrOut, "\r%s %s", frame, label)
				i++
			}
		}
	}()
}

// StopSpinner halts the spinner and clears the line on stderr.
func (s *IOStreams) StopSpinner() {
	s.spinnerMu.Lock()
	defer s.spinnerMu.Unlock()

	if s.spinnerCancel != nil {
		close(s.spinnerCancel)
		s.spinnerCancel = nil
	}
}

// OpenInBrowser opens the given URL in the user's default browser.
func (s *IOStreams) OpenInBrowser(url string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		cmd = exec.Command("open", url)
	case "windows":
		cmd = exec.Command("cmd", "/c", "start", url)
	default: // linux, freebsd, etc.
		cmd = exec.Command("xdg-open", url)
	}
	return cmd.Start()
}

// ConfirmAction prompts the user with a yes/no question and returns their
// answer. It returns an error if stdin is not a TTY (non-interactive).
func (s *IOStreams) ConfirmAction(prompt string) (bool, error) {
	if !s.isStdinTTY {
		return false, fmt.Errorf("cannot prompt for confirmation: not an interactive terminal")
	}

	fmt.Fprintf(s.ErrOut, "%s [y/N]: ", prompt)

	var response string
	if _, err := fmt.Fscan(s.In, &response); err != nil {
		return false, fmt.Errorf("reading response: %w", err)
	}

	response = strings.ToLower(strings.TrimSpace(response))
	return response == "y" || response == "yes", nil
}

// NewTablePrinter creates a TablePrinter that writes to this IOStreams' output.
func (s *IOStreams) NewTablePrinter() *TablePrinter {
	return NewTablePrinter(s)
}

// isTerminal reports whether the given file descriptor is a terminal.
func isTerminal(fd uintptr) bool {
	return term.IsTerminal(int(fd))
}
