package msg

import (
	"bytes"
	"fmt"
	"time"

	"git.sr.ht/~rjarry/aerc/app"
	"git.sr.ht/~rjarry/aerc/commands"
	"git.sr.ht/~rjarry/aerc/config"
	"git.sr.ht/~rjarry/aerc/lib"
	"git.sr.ht/~rjarry/aerc/lib/log"
	"git.sr.ht/~rjarry/aerc/lib/marker"
	"git.sr.ht/~rjarry/aerc/lib/ui"
	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/types"
)

type Move struct {
	CreateFolders     bool                     `opt:"-p" desc:"Create missing folders if required."`
	Account           string                   `opt:"-a" complete:"CompleteAccount" desc:"Move to specified account."`
	MultiFileStrategy *types.MultiFileStrategy `opt:"-m" action:"ParseMFS" complete:"CompleteMFS" desc:"Multi-file strategy."`
	Folder            string                   `opt:"folder" complete:"CompleteFolder" desc:"Target folder."`
}

func init() {
	commands.Register(Move{})
}

func (Move) Description() string {
	return "Move the selected message(s) to the specified folder."
}

func (Move) Context() commands.CommandContext {
	return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
}

func (Move) Aliases() []string {
	return []string{"mv", "move"}
}

func (m *Move) ParseMFS(arg string) error {
	if arg != "" {
		mfs, ok := types.StrToStrategy[arg]
		if !ok {
			return fmt.Errorf("invalid multi-file strategy %s", arg)
		}
		m.MultiFileStrategy = &mfs
	}
	return nil
}

func (*Move) CompleteAccount(arg string) []string {
	return commands.FilterList(app.AccountNames(), arg, commands.QuoteSpace)
}

func (m *Move) CompleteFolder(arg string) []string {
	var acct *app.AccountView
	if len(m.Account) > 0 {
		acct, _ = app.Account(m.Account)
	} else {
		acct = app.SelectedAccount()
	}
	if acct == nil {
		return nil
	}
	return commands.FilterList(acct.Directories().List(), arg, nil)
}

func (Move) CompleteMFS(arg string) []string {
	return commands.FilterList(types.StrategyStrs(), arg, nil)
}

func (m Move) Execute(args []string) error {
	h := newHelper()
	acct, err := h.account()
	if err != nil {
		return err
	}
	store, err := h.store()
	if err != nil {
		return err
	}
	uids, err := h.markedOrSelectedUids()
	if err != nil {
		return err
	}

	next := findNextNonDeleted(uids, store)
	marker := store.Marker()
	marker.ClearVisualMark()

	if len(m.Account) == 0 {
		store.Move(uids, m.Folder, m.CreateFolders, m.MultiFileStrategy,
			func(msg types.WorkerMessage) {
				m.CallBack(msg, acct, uids, next, marker, false)
			})
		return nil
	}

	destAcct, err := app.Account(m.Account)
	if err != nil {
		return err
	}

	destStore := destAcct.Store()
	if destStore == nil {
		app.PushError(fmt.Sprintf("No message store in %s", m.Account))
		return nil
	}

	var messages []*types.FullMessage
	fetchDone := make(chan bool, 1)
	store.FetchFull(uids, func(fm *types.FullMessage) {
		messages = append(messages, fm)
		if len(messages) == len(uids) {
			fetchDone <- true
		}
	})

	// Since this operation can take some time with some backends
	// (e.g. IMAP), provide some feedback to inform the user that
	// something is happening
	app.PushStatus("Moving messages...", 10*time.Second)

	var appended []models.UID
	var timeout bool
	go func() {
		defer log.PanicHandler()

		select {
		case <-fetchDone:
			break
		case <-time.After(30 * time.Second):
			// TODO: find a better way to determine if store.FetchFull()
			// has finished with some errors.
			app.PushError("Failed to fetch all messages")
			if len(messages) == 0 {
				return
			}
		}

	AppendLoop:
		for _, fm := range messages {
			done := make(chan bool, 1)
			uid := fm.Content.Uid
			buf := new(bytes.Buffer)
			_, err = buf.ReadFrom(fm.Content.Reader)
			if err != nil {
				log.Errorf("could not get reader for uid %d", uid)
				break
			}
			destStore.Append(
				m.Folder,
				models.SeenFlag,
				time.Now(),
				buf,
				buf.Len(),
				func(msg types.WorkerMessage) {
					switch msg := msg.(type) {
					case *types.Done:
						appended = append(appended, uid)
						done <- true
					case *types.Error:
						log.Errorf("AppendMessage failed: %v", msg.Error)
						done <- false
					}
				},
			)
			select {
			case ok := <-done:
				if !ok {
					break AppendLoop
				}
			case <-time.After(30 * time.Second):
				log.Warnf("timed-out: appended %d of %d", len(appended), len(messages))
				timeout = true
				break AppendLoop
			}
		}
		if len(appended) > 0 {
			mfs := types.Refuse
			store.Delete(appended, &mfs, func(msg types.WorkerMessage) {
				m.CallBack(msg, acct, appended, next, marker, timeout)
			})
		}
	}()
	return nil
}

func (m Move) CallBack(
	msg types.WorkerMessage,
	acct *app.AccountView,
	uids []models.UID,
	next *models.MessageInfo,
	marker marker.Marker,
	timeout bool,
) {
	switch msg := msg.(type) {
	case *types.Done:
		var s string
		if len(uids) > 1 {
			s = "%d messages moved to %s"
		} else {
			s = "%d message moved to %s"
		}
		dest := m.Folder
		if len(m.Account) > 0 {
			dest = fmt.Sprintf("%s in %s", m.Folder, m.Account)
		}
		if timeout {
			s = "timed-out: only " + s
			app.PushError(fmt.Sprintf(s, len(uids), dest))
		} else {
			app.PushStatus(fmt.Sprintf(s, len(uids), dest), 10*time.Second)
		}
		if store := acct.Store(); store != nil {
			handleDone(acct, next, store)
		}
	case *types.Error:
		app.PushError(msg.Error.Error())
		marker.Remark()
	case *types.Unsupported:
		marker.Remark()
		app.PushError("error, unsupported for this worker")
	}
}

func handleDone(
	acct *app.AccountView,
	next *models.MessageInfo,
	store *lib.MessageStore,
) {
	h := newHelper()
	mv, isMsgView := h.msgProvider.(*app.MessageViewer)
	switch {
	case isMsgView && !config.Ui.NextMessageOnDelete:
		app.RemoveTab(h.msgProvider, true)
	case isMsgView:
		if next == nil {
			app.RemoveTab(h.msgProvider, true)
			acct.Messages().Select(-1)
			ui.Invalidate()
			return
		}
		lib.NewMessageStoreView(next, mv.MessageView().SeenFlagSet(),
			store, app.CryptoProvider(), app.DecryptKeys,
			func(view lib.MessageView, err error) {
				if err != nil {
					app.PushError(err.Error())
					return
				}
				nextMv, err := app.NewMessageViewer(acct, view)
				if err != nil {
					app.PushError(err.Error())
					return
				}
				app.ReplaceTab(mv, nextMv, next.Envelope.Subject, true)
			})
	default:
		if next == nil {
			// We moved the last message, select the new last message
			// instead of the first message
			acct.Messages().Select(-1)
		}
	}
}
