package internal

import (
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/internal/imapwire"
)

const (
	DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
	DateLayout     = "2-Jan-2006"
)

const FlagRecent imap.Flag = "\\Recent" // removed in IMAP4rev2

func DecodeDateTime(dec *imapwire.Decoder) (time.Time, error) {
	var s string
	if !dec.Quoted(&s) {
		return time.Time{}, nil
	}
	t, err := time.Parse(DateTimeLayout, s)
	if err != nil {
		return time.Time{}, fmt.Errorf("in date-time: %v", err) // TODO: use imapwire.DecodeExpectError?
	}
	return t, err
}

func ExpectDateTime(dec *imapwire.Decoder) (time.Time, error) {
	t, err := DecodeDateTime(dec)
	if err != nil {
		return t, err
	}
	if !dec.Expect(!t.IsZero(), "date-time") {
		return t, dec.Err()
	}
	return t, nil
}

func ExpectDate(dec *imapwire.Decoder) (time.Time, error) {
	var s string
	if !dec.ExpectAString(&s) {
		return time.Time{}, dec.Err()
	}
	t, err := time.Parse(DateLayout, s)
	if err != nil {
		return time.Time{}, fmt.Errorf("in date: %v", err) // use imapwire.DecodeExpectError?
	}
	return t, nil
}

func ExpectFlagList(dec *imapwire.Decoder) ([]imap.Flag, error) {
	var flags []imap.Flag
	err := dec.ExpectList(func() error {
		// Some servers start the list with a space, so we need to skip it
		// https://github.com/emersion/go-imap/pull/633
		dec.SP()

		flag, err := ExpectFlag(dec)
		if err != nil {
			return err
		}
		flags = append(flags, flag)
		return nil
	})
	return flags, err
}

func ExpectCap(dec *imapwire.Decoder) (imap.Cap, error) {
	var name string
	if !dec.ExpectAtom(&name) {
		return "", dec.Err()
	}
	return canonicalCap(name), nil
}

func ExpectFlag(dec *imapwire.Decoder) (imap.Flag, error) {
	isSystem := dec.Special('\\')
	if isSystem && dec.Special('*') {
		return imap.FlagWildcard, nil // flag-perm
	}
	var name string
	if !dec.ExpectAtom(&name) {
		return "", fmt.Errorf("in flag: %w", dec.Err())
	}
	if isSystem {
		name = "\\" + name
	}
	return canonicalFlag(name), nil
}

func ExpectMailboxAttrList(dec *imapwire.Decoder) ([]imap.MailboxAttr, error) {
	var attrs []imap.MailboxAttr
	err := dec.ExpectList(func() error {
		attr, err := ExpectMailboxAttr(dec)
		if err != nil {
			return err
		}
		attrs = append(attrs, attr)
		return nil
	})
	return attrs, err
}

func ExpectMailboxAttr(dec *imapwire.Decoder) (imap.MailboxAttr, error) {
	flag, err := ExpectFlag(dec)
	return canonicalMailboxAttr(string(flag)), err
}

var (
	canonOnce        sync.Once
	canonFlag        map[string]imap.Flag
	canonMailboxAttr map[string]imap.MailboxAttr
)

func canonInit() {
	flags := []imap.Flag{
		imap.FlagSeen,
		imap.FlagAnswered,
		imap.FlagFlagged,
		imap.FlagDeleted,
		imap.FlagDraft,
		imap.FlagForwarded,
		imap.FlagMDNSent,
		imap.FlagJunk,
		imap.FlagNotJunk,
		imap.FlagPhishing,
		imap.FlagImportant,
	}
	mailboxAttrs := []imap.MailboxAttr{
		imap.MailboxAttrNonExistent,
		imap.MailboxAttrNoInferiors,
		imap.MailboxAttrNoSelect,
		imap.MailboxAttrHasChildren,
		imap.MailboxAttrHasNoChildren,
		imap.MailboxAttrMarked,
		imap.MailboxAttrUnmarked,
		imap.MailboxAttrSubscribed,
		imap.MailboxAttrRemote,
		imap.MailboxAttrAll,
		imap.MailboxAttrArchive,
		imap.MailboxAttrDrafts,
		imap.MailboxAttrFlagged,
		imap.MailboxAttrJunk,
		imap.MailboxAttrSent,
		imap.MailboxAttrTrash,
		imap.MailboxAttrImportant,
	}

	canonFlag = make(map[string]imap.Flag)
	for _, flag := range flags {
		canonFlag[strings.ToLower(string(flag))] = flag
	}

	canonMailboxAttr = make(map[string]imap.MailboxAttr)
	for _, attr := range mailboxAttrs {
		canonMailboxAttr[strings.ToLower(string(attr))] = attr
	}
}

func canonicalFlag(s string) imap.Flag {
	canonOnce.Do(canonInit)
	if flag, ok := canonFlag[strings.ToLower(s)]; ok {
		return flag
	}
	return imap.Flag(s)
}

func canonicalMailboxAttr(s string) imap.MailboxAttr {
	canonOnce.Do(canonInit)
	if attr, ok := canonMailboxAttr[strings.ToLower(s)]; ok {
		return attr
	}
	return imap.MailboxAttr(s)
}

func canonicalCap(s string) imap.Cap {
	// Only two caps are not fully uppercase
	for _, cap := range []imap.Cap{imap.CapIMAP4rev1, imap.CapIMAP4rev2} {
		if strings.EqualFold(s, string(cap)) {
			return cap
		}
	}
	return imap.Cap(strings.ToUpper(s))
}
