package imapserver

import (
	"fmt"
	"strings"

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

func (c *Conn) handleList(dec *imapwire.Decoder) error {
	ref, pattern, options, err := readListCmd(dec)
	if err != nil {
		return err
	}

	if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
		return err
	}

	w := &ListWriter{
		conn:    c,
		options: options,
	}
	return c.session.List(w, ref, pattern, options)
}

func (c *Conn) handleLSub(dec *imapwire.Decoder) error {
	var ref string
	if !dec.ExpectSP() || !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
		return dec.Err()
	}
	pattern, err := readListMailbox(dec)
	if err != nil {
		return err
	}
	if !dec.ExpectCRLF() {
		return dec.Err()
	}

	if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
		return err
	}

	options := &imap.ListOptions{SelectSubscribed: true}
	w := &ListWriter{
		conn: c,
		lsub: true,
	}
	return c.session.List(w, ref, []string{pattern}, options)
}

func (c *Conn) writeList(data *imap.ListData) error {
	enc := newResponseEncoder(c)
	defer enc.end()

	enc.Atom("*").SP().Atom("LIST").SP()
	enc.List(len(data.Attrs), func(i int) {
		enc.MailboxAttr(data.Attrs[i])
	})
	enc.SP()
	if data.Delim == 0 {
		enc.NIL()
	} else {
		enc.Quoted(string(data.Delim))
	}
	enc.SP().Mailbox(data.Mailbox)

	var ext []string
	if data.ChildInfo != nil {
		ext = append(ext, "CHILDINFO")
	}
	if data.OldName != "" {
		ext = append(ext, "OLDNAME")
	}

	// TODO: omit extended data if the client didn't ask for it
	if len(ext) > 0 {
		enc.SP().List(len(ext), func(i int) {
			name := ext[i]
			enc.Atom(name).SP()
			switch name {
			case "CHILDINFO":
				enc.Special('(')
				if data.ChildInfo.Subscribed {
					enc.Quoted("SUBSCRIBED")
				}
				enc.Special(')')
			case "OLDNAME":
				enc.Special('(').Mailbox(data.OldName).Special(')')
			default:
				panic(fmt.Errorf("imapserver: unknown LIST extended-item %v", name))
			}
		})
	}

	return enc.CRLF()
}

func (c *Conn) writeLSub(data *imap.ListData) error {
	enc := newResponseEncoder(c)
	defer enc.end()

	enc.Atom("*").SP().Atom("LSUB").SP()
	enc.List(len(data.Attrs), func(i int) {
		enc.MailboxAttr(data.Attrs[i])
	})
	enc.SP()
	if data.Delim == 0 {
		enc.NIL()
	} else {
		enc.Quoted(string(data.Delim))
	}
	enc.SP().Mailbox(data.Mailbox)
	return enc.CRLF()
}

func readListCmd(dec *imapwire.Decoder) (ref string, patterns []string, options *imap.ListOptions, err error) {
	options = &imap.ListOptions{}

	if !dec.ExpectSP() {
		return "", nil, nil, dec.Err()
	}

	hasSelectOpts, err := dec.List(func() error {
		var selectOpt string
		if !dec.ExpectAString(&selectOpt) {
			return dec.Err()
		}
		switch strings.ToUpper(selectOpt) {
		case "SUBSCRIBED":
			options.SelectSubscribed = true
		case "REMOTE":
			options.SelectRemote = true
		case "RECURSIVEMATCH":
			options.SelectRecursiveMatch = true
		case "SPECIAL-USE":
			options.SelectSpecialUse = true
		default:
			return newClientBugError("Unknown LIST select option")
		}
		return nil
	})
	if err != nil {
		return "", nil, nil, fmt.Errorf("in list-select-opts: %w", err)
	}
	if hasSelectOpts && !dec.ExpectSP() {
		return "", nil, nil, dec.Err()
	}

	if !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
		return "", nil, nil, dec.Err()
	}

	hasPatterns, err := dec.List(func() error {
		pattern, err := readListMailbox(dec)
		if err == nil && pattern != "" {
			patterns = append(patterns, pattern)
		}
		return err
	})
	if err != nil {
		return "", nil, nil, err
	} else if hasPatterns && len(patterns) == 0 {
		return "", nil, nil, newClientBugError("LIST-EXTENDED requires a non-empty parenthesized pattern list")
	} else if !hasPatterns {
		pattern, err := readListMailbox(dec)
		if err != nil {
			return "", nil, nil, err
		}
		if pattern != "" {
			patterns = append(patterns, pattern)
		}
	}

	if dec.SP() { // list-return-opts
		var atom string
		if !dec.ExpectAtom(&atom) || !dec.Expect(strings.EqualFold(atom, "RETURN"), "RETURN") || !dec.ExpectSP() {
			return "", nil, nil, dec.Err()
		}

		err := dec.ExpectList(func() error {
			return readReturnOption(dec, options)
		})
		if err != nil {
			return "", nil, nil, fmt.Errorf("in list-return-opts: %w", err)
		}
	}

	if !dec.ExpectCRLF() {
		return "", nil, nil, dec.Err()
	}

	if options.SelectRecursiveMatch && !options.SelectSubscribed {
		return "", nil, nil, newClientBugError("The LIST RECURSIVEMATCH select option requires SUBSCRIBED")
	}

	return ref, patterns, options, nil
}

func readListMailbox(dec *imapwire.Decoder) (string, error) {
	var mailbox string
	if !dec.String(&mailbox) {
		if !dec.Expect(dec.Func(&mailbox, isListChar), "list-char") {
			return "", dec.Err()
		}
	}
	return utf7.Decode(mailbox)
}

func isListChar(ch byte) bool {
	switch ch {
	case '%', '*': // list-wildcards
		return true
	case ']': // resp-specials
		return true
	default:
		return imapwire.IsAtomChar(ch)
	}
}

func readReturnOption(dec *imapwire.Decoder, options *imap.ListOptions) error {
	var name string
	if !dec.ExpectAtom(&name) {
		return dec.Err()
	}

	switch strings.ToUpper(name) {
	case "SUBSCRIBED":
		options.ReturnSubscribed = true
	case "CHILDREN":
		options.ReturnChildren = true
	case "SPECIAL-USE":
		options.ReturnSpecialUse = true
	case "STATUS":
		if !dec.ExpectSP() {
			return dec.Err()
		}
		options.ReturnStatus = new(imap.StatusOptions)
		return dec.ExpectList(func() error {
			return readStatusItem(dec, options.ReturnStatus)
		})
	default:
		return newClientBugError("Unknown LIST RETURN options")
	}
	return nil
}

// ListWriter writes LIST responses.
type ListWriter struct {
	conn    *Conn
	options *imap.ListOptions
	lsub    bool
}

// WriteList writes a single LIST response for a mailbox.
func (w *ListWriter) WriteList(data *imap.ListData) error {
	if w.lsub {
		return w.conn.writeLSub(data)
	}

	if err := w.conn.writeList(data); err != nil {
		return err
	}
	if w.options.ReturnStatus != nil && data.Status != nil {
		if err := w.conn.writeStatus(data.Status, w.options.ReturnStatus); err != nil {
			return err
		}
	}
	return nil
}

// MatchList checks whether a reference and a pattern matches a mailbox.
func MatchList(name string, delim rune, reference, pattern string) bool {
	var delimStr string
	if delim != 0 {
		delimStr = string(delim)
	}

	if delimStr != "" && strings.HasPrefix(pattern, delimStr) {
		reference = ""
		pattern = strings.TrimPrefix(pattern, delimStr)
	}
	if reference != "" {
		if delimStr != "" && !strings.HasSuffix(reference, delimStr) {
			reference += delimStr
		}
		if !strings.HasPrefix(name, reference) {
			return false
		}
		name = strings.TrimPrefix(name, reference)
	}

	return matchList(name, delimStr, pattern)
}

func matchList(name, delim, pattern string) bool {
	// TODO: optimize

	i := strings.IndexAny(pattern, "*%")
	if i == -1 {
		// No more wildcards
		return name == pattern
	}

	// Get parts before and after wildcard
	chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]

	// Check that name begins with chunk
	if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
		return false
	}
	name = strings.TrimPrefix(name, chunk)

	// Expand wildcard
	var j int
	for j = 0; j < len(name); j++ {
		if wildcard == '%' && string(name[j]) == delim {
			break // Stop on delimiter if wildcard is %
		}
		// Try to match the rest from here
		if matchList(name[j:], delim, rest) {
			return true
		}
	}

	return matchList(name[j:], delim, rest)
}
