package session

import (
	"github.com/diamondburned/cchat"
	"github.com/diamondburned/cchat-gtk/internal/gts"
	"github.com/diamondburned/cchat-gtk/internal/keyring"
	"github.com/diamondburned/cchat-gtk/internal/log"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives/buttonoverlay"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/session/commander"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
	"github.com/diamondburned/cchat/text"
	"github.com/gotk3/gotk3/gtk"
	"github.com/pkg/errors"
)

const IconSize = 32

// Controller extends server.RowController to add session.
type Controller interface {
	// GetService asks the controller for its service.
	GetService() cchat.Service
	// OnSessionDisconnect is called before a session is disconnected. This
	// function is used for cleanups.
	OnSessionDisconnect(*Row)
	// RowSelected is called when a server that can display messages (aka
	// implements ServerMessage) is called.
	RowSelected(*Row, *server.ServerRow, cchat.ServerMessage)
	// RestoreSession is called with the session ID to ask the controller to
	// restore it from keyring information.
	RestoreSession(*Row, string) // ID string, async
	// RemoveSession is called to ask the controller to remove the session from
	// the list of sessions.
	RemoveSession(*Row)
	// MoveSession is called to ask the controller to move the session to
	// somewhere else in the list of sessions.
	MoveSession(id, movingID string)
}

// Row represents a single session, including the button header and the
// children servers.
type Row struct {
	*server.Row
	Session   cchat.Session
	sessionID string // used for reconnection

	ctrl Controller

	cmder  *commander.Buffer
	cmdbtn *gtk.Button
}

func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
	row := newRow(parent, text.Rich{}, ctrl)
	row.SetSession(ses)
	return row
}

func NewLoading(parent breadcrumb.Breadcrumber, id, name string, ctrl Controller) *Row {
	row := newRow(parent, text.Rich{Content: name}, ctrl)
	row.sessionID = id
	row.Row.SetLoading()
	return row
}

func newRow(parent breadcrumb.Breadcrumber, name text.Rich, ctrl Controller) *Row {
	srow := server.NewRow(parent, name)
	srow.Button.SetPlaceholderIcon("user-invisible-symbolic", IconSize)
	srow.Show()

	// Bind the row to .session in CSS.
	primitives.AddClass(srow, "session")
	primitives.AddClass(srow, "server-list")

	// Make a commander button that's hidden by default in case.
	cmdbtn, _ := gtk.ButtonNewFromIconName("utilities-terminal-symbolic", gtk.ICON_SIZE_BUTTON)
	buttonoverlay.Take(srow.Button, cmdbtn, server.IconSize)

	row := &Row{
		Row:    srow,
		ctrl:   ctrl,
		cmdbtn: cmdbtn,
	}

	cmdbtn.Connect("clicked", row.ShowCommander)

	return row
}

// Reset extends the server row's Reset function and resets additional states.
// It resets all states back to nil, but the session ID stays.
func (r *Row) Reset() {
	r.Row.Reset()
	r.Session = nil
	r.cmder = nil
	r.cmdbtn.Hide()
}

// RemoveSession removes itself from the session list.
func (r *Row) RemoveSession() {
	// Remove the session off the list.
	r.ctrl.RemoveSession(r)

	// Asynchrously disconnect.
	go func() {
		if err := r.Session.Disconnect(); err != nil {
			log.Error(errors.Wrap(err, "Non-fatal, failed to disconnect removed session"))
		}
	}()
}

// ReconnectSession tries to reconnect with the keyring data. This is a slow
// method but it's also a very cold path.
func (r *Row) ReconnectSession() {
	// If we haven't ever connected, then don't run. In a legitimate case, this
	// shouldn't happen.
	if r.sessionID == "" {
		return
	}

	// Set the row as loading.
	r.Row.SetLoading()
	// Try to restore the session.
	r.ctrl.RestoreSession(r, r.sessionID)
}

// DisconnectSession disconnects the current session. It does nothing if the row
// does not have a session active.
func (r *Row) DisconnectSession() {
	// No-op if no session.
	if r.Session == nil {
		return
	}

	// Call the disconnect function from the controller first.
	r.ctrl.OnSessionDisconnect(r)

	// Show visually that we're disconnected first by wiping all servers.
	r.Reset()

	// Set the offline icon to the button.
	r.Button.Image.SetPlaceholderIcon("user-invisible-symbolic", IconSize)
	// Also unselect the button.
	r.Button.SetActive(false)

	// Disable the button because we're busy disconnecting. We'll re-enable them
	// once we're done reconnecting.
	r.SetSensitive(false)

	// Try and disconnect asynchronously.
	gts.Async(func() (func(), error) {
		// Disconnect and wrap the error if any. Wrap works with a nil error.
		err := errors.Wrap(r.Session.Disconnect(), "Failed to disconnect.")
		return func() {
			// Allow access to the menu
			r.SetSensitive(true)

			// Set the menu to allow disconnection.
			r.Button.SetNormalExtraMenu([]menu.Item{
				menu.SimpleItem("Connect", r.ReconnectSession),
				menu.SimpleItem("Remove", r.RemoveSession),
			})
		}, err
	})
}

// KeyringSession returns a keyring session, or nil if the session cannot be
// saved.
func (r *Row) KeyringSession() *keyring.Session {
	return keyring.ConvertSession(r.Session, r.Button.GetText())
}

// ID returns the session ID.
func (r *Row) ID() string {
	return r.sessionID
}

// SetFailed sets the initial connect status to failed. Do note that session can
// have 2 types of loading: loading the session and loading the server list.
// This one sets the former.
func (r *Row) SetFailed(err error) {
	// SetFailed, but also add the callback to retry.
	r.Row.SetFailed(err, r.ReconnectSession)
}

// SetSession binds the session and marks the row as ready. It extends SetDone.
func (r *Row) SetSession(ses cchat.Session) {
	r.Session = ses
	r.sessionID = ses.ID()
	r.SetLabelUnsafe(ses.Name())
	r.SetIconer(ses)

	// Set the commander, if any. The function will return nil if the assertion
	// returns nil. As such, we assert with an ignored ok bool, allowing cmd to
	// be nil.
	cmd, _ := ses.(commander.SessionCommander)
	r.cmder = commander.NewBuffer(r.ctrl.GetService(), cmd)
	// Show the command button if the session actually supports the commander.
	if r.cmder != nil {
		r.cmdbtn.Show()
	}

	// Bind extra menu items before loading. These items won't be clickable
	// during loading.
	r.SetNormalExtraMenu([]menu.Item{
		menu.SimpleItem("Disconnect", r.DisconnectSession),
		menu.SimpleItem("Remove", r.RemoveSession),
	})

	// Preload now.
	r.SetServerList(ses, r)
	r.Load()
}

func (r *Row) RowSelected(server *server.ServerRow, smsg cchat.ServerMessage) {
	r.ctrl.RowSelected(r, server, smsg)
}

// BindMover binds with the ID stored in the parent container to be used in the
// method itself. The ID may or may not have to do with session.
func (r *Row) BindMover(id string) {
	primitives.BindDragSortable(r.Button, "GTK_TOGGLE_BUTTON", id, r.ctrl.MoveSession)
}

// ShowCommander shows the commander dialog, or it does nothing if session does
// not implement commander.
func (r *Row) ShowCommander() {
	if r.cmder == nil {
		return
	}
	r.cmder.ShowDialog()
}
