package cliui

import (
	"fmt"
	"io"
	"os"
	"sync"

	tea "github.com/charmbracelet/bubbletea"

	"github.com/ignite/cli/v29/ignite/pkg/cliui/bubbleconfirm"
	"github.com/ignite/cli/v29/ignite/pkg/cliui/clispinner"
	"github.com/ignite/cli/v29/ignite/pkg/cliui/entrywriter"
	uilog "github.com/ignite/cli/v29/ignite/pkg/cliui/log"
	"github.com/ignite/cli/v29/ignite/pkg/errors"
	"github.com/ignite/cli/v29/ignite/pkg/events"
)

type sessionOptions struct {
	stdin  io.ReadCloser
	stdout io.WriteCloser
	stderr io.WriteCloser

	spinnerStart bool
	spinnerText  string

	ignoreEvents bool
	verbosity    uilog.Verbosity
}

// Session controls command line interaction with users.
type Session struct {
	options sessionOptions
	ev      events.Bus
	spinner clispinner.Spinner
	out     uilog.Output
	wg      *sync.WaitGroup
	ended   bool
	skipUI  bool
}

// Option configures session options.
type Option func(s *Session)

// WithStdout sets the starndard output for the session.
func WithStdout(stdout io.WriteCloser) Option {
	return func(s *Session) {
		s.options.stdout = stdout
	}
}

// WithStderr sets base stderr for a Session.
func WithStderr(stderr io.WriteCloser) Option {
	return func(s *Session) {
		s.options.stderr = stderr
	}
}

// WithStdin sets the starndard input for the session.
func WithStdin(stdin io.ReadCloser) Option {
	return func(s *Session) {
		s.options.stdin = stdin
	}
}

// WithVerbosity sets a verbosity level for the Session.
func WithVerbosity(v uilog.Verbosity) Option {
	return func(s *Session) {
		s.options.verbosity = v
	}
}

// IgnoreEvents configures the session to avoid displaying events.
// This is a compatibility option to be able to use the session and
// the events bus when models are used to manage CLI UI. The session
// won't handle the events when this option is present.
func IgnoreEvents() Option {
	return func(s *Session) {
		s.options.ignoreEvents = true
	}
}

// StartSpinner forces spinner to be spinning right after creation.
func StartSpinner() Option {
	return func(s *Session) {
		s.options.spinnerStart = true
	}
}

// StartSpinnerWithText forces spinner to be spinning right after creation
// with a custom status text.
func StartSpinnerWithText(text string) Option {
	return func(s *Session) {
		s.options.spinnerStart = true
		s.options.spinnerText = text
	}
}

func WithoutUserInteraction(yes bool) Option {
	return func(s *Session) {
		s.skipUI = yes
	}
}

// New creates a new Session.
func New(options ...Option) *Session {
	session := Session{
		ev: events.NewBus(),
		wg: &sync.WaitGroup{},
		options: sessionOptions{
			stdin:       os.Stdin,
			stdout:      os.Stdout,
			stderr:      os.Stderr,
			spinnerText: clispinner.DefaultText,
		},
	}

	for _, apply := range options {
		apply(&session)
	}

	logOptions := []uilog.Option{
		uilog.WithStdout(session.options.stdout),
		uilog.WithStderr(session.options.stderr),
	}

	if session.options.verbosity == uilog.VerbosityVerbose {
		logOptions = append(logOptions, uilog.Verbose())
	}

	session.out = uilog.NewOutput(logOptions...)

	if session.options.spinnerStart {
		session.StartSpinner(session.options.spinnerText)
	}

	// The main loop that prints the events uses a wait group to block
	// the session end until all the events are printed.
	if !session.options.ignoreEvents {
		session.wg.Add(1)
		go session.handleEvents()
	}

	return &session
}

// EventBus returns the event bus of the session.
func (s Session) EventBus() events.Bus {
	return s.ev
}

// Verbosity returns the verbosity level for the session output.
func (s Session) Verbosity() uilog.Verbosity {
	return s.options.verbosity
}

// NewOutput returns a new logging output bound to the session.
// The new output will use the session's verbosity, stderr and stdout.
// Label and color arguments are used to prefix the output when the
// session verbosity is verbose.
func (s Session) NewOutput(label, color string) uilog.Output {
	options := []uilog.Option{
		uilog.WithStdout(s.options.stdout),
		uilog.WithStderr(s.options.stderr),
	}

	if s.options.verbosity == uilog.VerbosityVerbose {
		options = append(options, uilog.CustomVerbose(label, color))
	}

	return uilog.NewOutput(options...)
}

// SpinnerMessage change the spinner message.
func (s *Session) SpinnerMessage(text string) {
	s.StopSpinner()
	s.StartSpinner(text)
}

// StartSpinner starts the spinner.
func (s *Session) StartSpinner(text string) {
	if s.options.ignoreEvents {
		return
	}

	// Verbose mode must not render the spinner but instead
	// it should just print the text to display next to the
	// app label otherwise the verbose logs would be printed
	// with an invalid format.
	if s.options.verbosity == uilog.VerbosityVerbose {
		fmt.Fprint(s.out.Stdout(), text)
		return
	}

	if s.spinner == nil {
		s.spinner = clispinner.New(clispinner.WithWriter(s.out.Stdout()))
	}

	s.spinner.SetText(text).Start()
}

// StopSpinner stops the spinner.
func (s Session) StopSpinner() {
	if s.spinner == nil {
		return
	}

	s.spinner.Stop()
}

// PauseSpinner pauses spinner and returns a function to restart the spinner.
func (s Session) PauseSpinner() (restart func()) {
	isActive := s.spinner != nil && s.spinner.IsActive()
	if isActive {
		s.spinner.Stop()
	}

	return func() {
		if isActive {
			s.spinner.Start()
		}
	}
}

// Printf prints formatted arbitrary message.
func (s Session) Printf(format string, a ...interface{}) error {
	defer s.PauseSpinner()()
	_, err := fmt.Fprintf(s.out.Stdout(), format, a...)
	return err
}

// Println prints arbitrary message with line break.
func (s Session) Println(messages ...interface{}) error {
	defer s.PauseSpinner()()
	_, err := fmt.Fprintln(s.out.Stdout(), messages...)
	return err
}

// Print prints arbitrary message.
func (s Session) Print(messages ...interface{}) error {
	defer s.PauseSpinner()()
	_, err := fmt.Fprint(s.out.Stdout(), messages...)
	return err
}

// Ask asks questions in the terminal and collect answers.
func (s Session) Ask(questions ...bubbleconfirm.Question) error {
	// If the flag yes was set true, we skip the user interaction
	if s.skipUI {
		return nil
	}
	defer s.PauseSpinner()()
	// TODO provide writer from the session
	return bubbleconfirm.Ask(questions...)
}

// ErrAbort is returned when the user aborts the operation.
var ErrAbort = errors.New("aborted or not confirmed")

// AskConfirm asks a yes/no question using a bubbletea dialog.
func (s Session) AskConfirm(message string) error {
	if s.skipUI {
		return nil
	}

	defer s.PauseSpinner()()

	// Create and run the bubbletea program
	p := tea.NewProgram(bubbleconfirm.NewModel(message))

	// Run the program
	m, err := p.Run()
	if err != nil {
		return err
	}

	// Type assert to our model
	confirmModel, ok := m.(bubbleconfirm.Model)
	if !ok {
		return errors.New("could not assert type to bubbleconfirm.Model")
	}

	// Check the result
	if confirmModel.Choice() != bubbleconfirm.Yes {
		return ErrAbort
	}

	return nil
}

// PrintTable prints table data.
func (s Session) PrintTable(header []string, entries ...[]string) error {
	defer s.PauseSpinner()()
	return entrywriter.MustWrite(s.out.Stdout(), header, entries...)
}

// End finishes the session by stopping the spinner and the event bus.
// Once the session is ended it should not be used anymore.
func (s *Session) End() {
	if s.ended {
		return
	}

	s.StopSpinner()
	s.ev.Stop()
	s.wg.Wait()
	s.ended = true
}

func (s *Session) handleEvents() {
	defer s.wg.Done()

	stdout := s.out.Stdout()

	for e := range s.ev.Events() {
		switch e.ProgressIndication {
		case events.IndicationStart:
			s.StartSpinner(e.String())
		case events.IndicationUpdate:
			if s.spinner == nil {
				// When the spinner is not initialized print the event
				fmt.Fprintf(stdout, "%s\n", e)
			} else {
				// Otherwise update the spinner with a new text
				s.spinner.SetText(e.String())
			}
		case events.IndicationFinish:
			s.StopSpinner()
			fmt.Fprintf(stdout, "%s\n", e)
		case events.IndicationNone:
			fallthrough
		default:
			// The text printed here won't be removed when the spinner stops
			resume := s.PauseSpinner()
			fmt.Fprintf(stdout, "%s\n", e)
			resume()
		}
	}
}
