package protocol

import (
	"bytes"
	"fmt"
	"strconv"
	"time"

	"github.com/emersion/go-imap"
	imapclient "github.com/emersion/go-imap/client"
	"github.com/emersion/go-imap/commands"
	"github.com/emersion/go-imap/responses"
)

const (
	appendUIDCode = imap.StatusRespCode("APPENDUID")
	modifiedCode  = imap.StatusRespCode("MODIFIED")
)

func appendDraftMessage(
	session *imapclient.Client,
	handle *imapSessionHandle,
	mailbox string,
	message []byte,
	fetchModSeq bool,
) (DraftRef, error) {
	// The bulk timeout covers ONLY the APPEND literal itself. The follow-up
	// SELECT/MODSEQ fetch are metadata commands and must run on the tight
	// default again as soon as the literal is written.
	previousTimeout := session.Timeout
	if handle != nil && !handle.ceiling.IsZero() {
		session.Timeout = time.Until(handle.ceiling)
	}
	status, err := session.Execute(&commands.Append{
		Mailbox: mailbox,
		Flags:   []string{imap.DraftFlag},
		Date:    time.Time{},
		Message: bytes.NewReader(message),
	}, nil)
	session.Timeout = previousTimeout
	if err != nil {
		return DraftRef{}, mutationUnknown("append draft", err)
	}
	if status == nil {
		return DraftRef{}, mutationUnknown("append draft", nil)
	}
	if err := status.Err(); err != nil {
		return DraftRef{}, &ProtocolError{
			Class:     ErrorFailed,
			Operation: "append draft",
			Cause:     err,
		}
	}
	uidValidity, uid, ok := parseAppendUID(status)
	if !ok {
		return DraftRef{}, mutationUnknown("append draft", nil)
	}
	ref := DraftRef{
		Mailbox:     mailbox,
		UIDValidity: uidValidity,
		UID:         uid,
	}
	if !fetchModSeq {
		return ref, nil
	}
	selected, err := session.Select(mailbox, true)
	if err != nil {
		return ref, nil
	}
	if selected.UidValidity != uidValidity {
		return ref, nil
	}
	appended, err := fetchOneUID(
		session,
		uid,
		[]imap.FetchItem{imap.FetchUid, imap.FetchItem("MODSEQ")},
	)
	if err != nil {
		return ref, nil
	}
	if appended == nil {
		return ref, nil
	}
	ref.ModSeq, err = messageModSeq(appended)
	if err != nil || ref.ModSeq == 0 {
		ref.ModSeq = 0
	}
	return ref, nil
}

func parseAppendUID(status *imap.StatusResp) (uint32, uint32, bool) {
	if status.Code != appendUIDCode || len(status.Arguments) != 2 {
		return 0, 0, false
	}
	uidValidity, err := parseUint32(status.Arguments[0])
	if err != nil || uidValidity == 0 {
		return 0, 0, false
	}
	rawSet, ok := status.Arguments[1].(string)
	if !ok {
		return 0, 0, false
	}
	uidSet, err := imap.ParseSeqSet(rawSet)
	if err != nil || len(uidSet.Set) != 1 ||
		uidSet.Set[0].Start == 0 ||
		uidSet.Set[0].Start != uidSet.Set[0].Stop {
		return 0, 0, false
	}
	return uidValidity, uidSet.Set[0].Start, true
}

func messageModSeq(message *imap.Message) (uint64, error) {
	if message == nil {
		return 0, fmt.Errorf("MODSEQ response is missing")
	}
	raw, ok := message.Items[imap.FetchItem("MODSEQ")]
	if !ok {
		return 0, fmt.Errorf("MODSEQ response is missing")
	}
	values, ok := raw.([]interface{})
	if !ok || len(values) != 1 {
		return 0, fmt.Errorf("MODSEQ response is invalid")
	}
	value, err := parseUint64(values[0])
	if err != nil || value == 0 {
		return 0, fmt.Errorf("MODSEQ response is invalid")
	}
	return value, nil
}

func conditionallyDeleteDraft(
	session *imapclient.Client,
	ref DraftRef,
) error {
	sequence := new(imap.SeqSet)
	sequence.AddNum(ref.UID)
	var updated *imap.Message
	handler := responses.HandlerFunc(func(resp imap.Resp) error {
		name, fields, ok := imap.ParseNamedResp(resp)
		if !ok || name != "FETCH" || len(fields) < 2 {
			return responses.ErrUnhandled
		}
		sequenceNumber, err := imap.ParseNumber(fields[0])
		if err != nil {
			return err
		}
		messageFields, ok := fields[1].([]interface{})
		if !ok {
			return fmt.Errorf("conditional STORE response is invalid")
		}
		message := &imap.Message{SeqNum: sequenceNumber}
		if err := message.Parse(messageFields); err != nil {
			return err
		}
		if message.Uid != ref.UID {
			return responses.ErrUnhandled
		}
		updated = message
		return nil
	})
	status, err := session.Execute(&imap.Command{
		Name: "UID",
		Arguments: []interface{}{
			imap.RawString("STORE"),
			sequence,
			[]interface{}{
				imap.RawString("UNCHANGEDSINCE"),
				imap.RawString(strconv.FormatUint(ref.ModSeq, 10)),
			},
			imap.RawString("+FLAGS"),
			[]interface{}{imap.RawString(imap.DeletedFlag)},
		},
	}, handler)
	if err != nil {
		return mutationUnknown("conditionally delete draft", err)
	}
	if status == nil {
		return mutationUnknown("conditionally delete draft", nil)
	}
	if status.Code == modifiedCode {
		return conflictError("replace draft")
	}
	if err := status.Err(); err != nil {
		return &ProtocolError{
			Class:     ErrorFailed,
			Operation: "conditionally delete draft",
			Cause:     err,
		}
	}
	if updated == nil || !hasFlag(updated.Flags, imap.DeletedFlag) {
		return conflictError("replace draft")
	}
	modSeq, err := messageModSeq(updated)
	if err != nil || modSeq <= ref.ModSeq {
		return conflictError("replace draft")
	}
	return nil
}

func uidExpunge(
	session *imapclient.Client,
	uid uint32,
) error {
	sequence := new(imap.SeqSet)
	sequence.AddNum(uid)
	status, err := session.Execute(&imap.Command{
		Name: "UID",
		Arguments: []interface{}{
			imap.RawString("EXPUNGE"),
			sequence,
		},
	}, nil)
	if err != nil {
		return mutationUnknown("expunge replaced draft", err)
	}
	if status == nil {
		return mutationUnknown("expunge replaced draft", nil)
	}
	if err := status.Err(); err != nil {
		return &ProtocolError{
			Class:     ErrorFailed,
			Operation: "expunge replaced draft",
			Cause:     err,
		}
	}
	return nil
}

func parseUint32(value interface{}) (uint32, error) {
	raw, ok := value.(string)
	if !ok {
		return 0, fmt.Errorf("number is not an atom")
	}
	parsed, err := strconv.ParseUint(raw, 10, 32)
	return uint32(parsed), err
}

func parseUint64(value interface{}) (uint64, error) {
	raw, ok := value.(string)
	if !ok {
		return 0, fmt.Errorf("number is not an atom")
	}
	return strconv.ParseUint(raw, 10, 64)
}

func mutationUnknown(operation string, cause error) error {
	return &ProtocolError{
		Class:     ErrorUnknown,
		Operation: operation,
		Cause:     cause,
	}
}
