package imapserver

import (
	"fmt"
	"io"
	"mime"
	"sort"
	"strings"
	"time"

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

const envelopeDateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"

type fetchWriterOptions struct {
	bodyStructure struct {
		extended    bool // BODYSTRUCTURE
		nonExtended bool // BODY
	}
	obsolete map[*imap.FetchItemBodySection]string
}

func (c *Conn) handleFetch(dec *imapwire.Decoder, numKind NumKind) error {
	var numSet imap.NumSet
	if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() {
		return dec.Err()
	}

	var options imap.FetchOptions
	writerOptions := fetchWriterOptions{obsolete: make(map[*imap.FetchItemBodySection]string)}
	isList, err := dec.List(func() error {
		name, err := readFetchAttName(dec)
		if err != nil {
			return err
		}
		switch name {
		case "ALL", "FAST", "FULL":
			return newClientBugError("FETCH macros are not allowed in a list")
		}
		return handleFetchAtt(dec, name, &options, &writerOptions)
	})
	if err != nil {
		return err
	}
	if !isList {
		name, err := readFetchAttName(dec)
		if err != nil {
			return err
		}

		// Handle macros
		switch name {
		case "ALL":
			options.Flags = true
			options.InternalDate = true
			options.RFC822Size = true
			options.Envelope = true
		case "FAST":
			options.Flags = true
			options.InternalDate = true
			options.RFC822Size = true
		case "FULL":
			options.Flags = true
			options.InternalDate = true
			options.RFC822Size = true
			options.Envelope = true
			handleFetchBodyStructure(&options, &writerOptions, false)
		default:
			if err := handleFetchAtt(dec, name, &options, &writerOptions); err != nil {
				return err
			}
		}
	}

	if !dec.ExpectCRLF() {
		return dec.Err()
	}

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

	if numKind == NumKindUID {
		options.UID = true
	}

	w := &FetchWriter{conn: c, options: writerOptions}
	if err := c.session.Fetch(w, numSet, &options); err != nil {
		return err
	}
	return nil
}

func handleFetchAtt(dec *imapwire.Decoder, attName string, options *imap.FetchOptions, writerOptions *fetchWriterOptions) error {
	switch attName {
	case "BODYSTRUCTURE":
		handleFetchBodyStructure(options, writerOptions, true)
	case "ENVELOPE":
		options.Envelope = true
	case "FLAGS":
		options.Flags = true
	case "INTERNALDATE":
		options.InternalDate = true
	case "RFC822.SIZE":
		options.RFC822Size = true
	case "UID":
		options.UID = true
	case "RFC822": // equivalent to BODY[]
		bs := &imap.FetchItemBodySection{}
		writerOptions.obsolete[bs] = attName
		options.BodySection = append(options.BodySection, bs)
	case "RFC822.PEEK": // obsolete, equivalent to BODY.PEEK[], used by Outlook
		bs := &imap.FetchItemBodySection{Peek: true}
		writerOptions.obsolete[bs] = attName
		options.BodySection = append(options.BodySection, bs)
	case "RFC822.HEADER": // equivalent to BODY.PEEK[HEADER]
		bs := &imap.FetchItemBodySection{
			Specifier: imap.PartSpecifierHeader,
			Peek:      true,
		}
		writerOptions.obsolete[bs] = attName
		options.BodySection = append(options.BodySection, bs)
	case "RFC822.TEXT": // equivalent to BODY[TEXT]
		bs := &imap.FetchItemBodySection{
			Specifier: imap.PartSpecifierText,
		}
		writerOptions.obsolete[bs] = attName
		options.BodySection = append(options.BodySection, bs)
	case "BINARY", "BINARY.PEEK":
		part, err := readSectionBinary(dec)
		if err != nil {
			return err
		}
		partial, err := maybeReadPartial(dec)
		if err != nil {
			return err
		}
		bs := &imap.FetchItemBinarySection{
			Part:    part,
			Partial: partial,
			Peek:    attName == "BINARY.PEEK",
		}
		options.BinarySection = append(options.BinarySection, bs)
	case "BINARY.SIZE":
		part, err := readSectionBinary(dec)
		if err != nil {
			return err
		}
		bss := &imap.FetchItemBinarySectionSize{Part: part}
		options.BinarySectionSize = append(options.BinarySectionSize, bss)
	case "BODY":
		if !dec.Special('[') {
			handleFetchBodyStructure(options, writerOptions, false)
			return nil
		}
		section := imap.FetchItemBodySection{}
		err := readSection(dec, &section)
		if err != nil {
			return err
		}
		section.Partial, err = maybeReadPartial(dec)
		if err != nil {
			return err
		}
		options.BodySection = append(options.BodySection, &section)
	case "BODY.PEEK":
		if !dec.ExpectSpecial('[') {
			return dec.Err()
		}
		section := imap.FetchItemBodySection{Peek: true}
		err := readSection(dec, &section)
		if err != nil {
			return err
		}
		section.Partial, err = maybeReadPartial(dec)
		if err != nil {
			return err
		}
		options.BodySection = append(options.BodySection, &section)
	default:
		return newClientBugError("Unknown FETCH data item")
	}
	return nil
}

func handleFetchBodyStructure(options *imap.FetchOptions, writerOptions *fetchWriterOptions, extended bool) {
	if options.BodyStructure == nil || extended {
		options.BodyStructure = &imap.FetchItemBodyStructure{Extended: extended}
	}
	if extended {
		writerOptions.bodyStructure.extended = true
	} else {
		writerOptions.bodyStructure.nonExtended = true
	}
}

func readFetchAttName(dec *imapwire.Decoder) (string, error) {
	var attName string
	if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") {
		return "", dec.Err()
	}
	return strings.ToUpper(attName), nil
}

func isMsgAttNameChar(ch byte) bool {
	return ch != '[' && imapwire.IsAtomChar(ch)
}

func readSection(dec *imapwire.Decoder, section *imap.FetchItemBodySection) error {
	if dec.Special(']') {
		return nil
	}

	var dot bool
	section.Part, dot = readSectionPart(dec)
	if dot || len(section.Part) == 0 {
		var specifier string
		if dot {
			if !dec.ExpectAtom(&specifier) {
				return dec.Err()
			}
		} else {
			dec.Atom(&specifier)
		}

		switch specifier := imap.PartSpecifier(strings.ToUpper(specifier)); specifier {
		case imap.PartSpecifierNone, imap.PartSpecifierHeader, imap.PartSpecifierMIME, imap.PartSpecifierText:
			section.Specifier = specifier
		case "HEADER.FIELDS", "HEADER.FIELDS.NOT":
			if !dec.ExpectSP() {
				return dec.Err()
			}
			var err error
			headerList, err := readHeaderList(dec)
			if err != nil {
				return err
			}
			section.Specifier = imap.PartSpecifierHeader
			if specifier == "HEADER.FIELDS" {
				section.HeaderFields = headerList
			} else {
				section.HeaderFieldsNot = headerList
			}
		default:
			return newClientBugError("unknown body section specifier")
		}
	}

	if !dec.ExpectSpecial(']') {
		return dec.Err()
	}

	return nil
}

func readSectionPart(dec *imapwire.Decoder) (part []int, dot bool) {
	for {
		dot = len(part) > 0
		if dot && !dec.Special('.') {
			return part, false
		}

		var num uint32
		if !dec.Number(&num) {
			return part, dot
		}
		part = append(part, int(num))
	}
}

func readHeaderList(dec *imapwire.Decoder) ([]string, error) {
	var l []string
	err := dec.ExpectList(func() error {
		var s string
		if !dec.ExpectAString(&s) {
			return dec.Err()
		}
		l = append(l, s)
		return nil
	})
	return l, err
}

func readSectionBinary(dec *imapwire.Decoder) ([]int, error) {
	if !dec.ExpectSpecial('[') {
		return nil, dec.Err()
	}
	if dec.Special(']') {
		return nil, nil
	}

	var l []int
	for {
		var num uint32
		if !dec.ExpectNumber(&num) {
			return l, dec.Err()
		}
		l = append(l, int(num))

		if !dec.Special('.') {
			break
		}
	}

	if !dec.ExpectSpecial(']') {
		return l, dec.Err()
	}
	return l, nil
}

func maybeReadPartial(dec *imapwire.Decoder) (*imap.SectionPartial, error) {
	if !dec.Special('<') {
		return nil, nil
	}
	var partial imap.SectionPartial
	if !dec.ExpectNumber64(&partial.Offset) || !dec.ExpectSpecial('.') || !dec.ExpectNumber64(&partial.Size) || !dec.ExpectSpecial('>') {
		return nil, dec.Err()
	}
	return &partial, nil
}

// FetchWriter writes FETCH responses.
type FetchWriter struct {
	conn    *Conn
	options fetchWriterOptions
}

// CreateMessage writes a FETCH response for a message.
//
// FetchResponseWriter.Close must be called.
func (cmd *FetchWriter) CreateMessage(seqNum uint32) *FetchResponseWriter {
	enc := newResponseEncoder(cmd.conn)
	enc.Atom("*").SP().Number(seqNum).SP().Atom("FETCH").SP().Special('(')
	return &FetchResponseWriter{enc: enc, options: cmd.options}
}

// FetchResponseWriter writes a single FETCH response for a message.
type FetchResponseWriter struct {
	enc     *responseEncoder
	options fetchWriterOptions

	hasItem bool
}

func (w *FetchResponseWriter) writeItemSep() {
	if w.hasItem {
		w.enc.SP()
	}
	w.hasItem = true
}

// WriteUID writes the message's UID.
func (w *FetchResponseWriter) WriteUID(uid imap.UID) {
	w.writeItemSep()
	w.enc.Atom("UID").SP().UID(uid)
}

// WriteFlags writes the message's flags.
func (w *FetchResponseWriter) WriteFlags(flags []imap.Flag) {
	w.writeItemSep()
	w.enc.Atom("FLAGS").SP().List(len(flags), func(i int) {
		w.enc.Flag(flags[i])
	})
}

// WriteRFC822Size writes the message's full size.
func (w *FetchResponseWriter) WriteRFC822Size(size int64) {
	w.writeItemSep()
	w.enc.Atom("RFC822.SIZE").SP().Number64(size)
}

// WriteInternalDate writes the message's internal date.
func (w *FetchResponseWriter) WriteInternalDate(t time.Time) {
	w.writeItemSep()
	w.enc.Atom("INTERNALDATE").SP().String(t.Format(internal.DateTimeLayout))
}

// WriteBodySection writes a body section.
//
// The returned io.WriteCloser must be closed before writing any more message
// data items.
func (w *FetchResponseWriter) WriteBodySection(section *imap.FetchItemBodySection, size int64) io.WriteCloser {
	w.writeItemSep()
	enc := w.enc.Encoder

	if obs, ok := w.options.obsolete[section]; ok {
		enc.Atom(obs)
	} else {
		writeItemBodySection(enc, section)
	}

	enc.SP()
	return w.enc.Literal(size)
}

func writeItemBodySection(enc *imapwire.Encoder, section *imap.FetchItemBodySection) {
	enc.Atom("BODY")
	enc.Special('[')
	writeSectionPart(enc, section.Part)
	if len(section.Part) > 0 && section.Specifier != imap.PartSpecifierNone {
		enc.Special('.')
	}
	if section.Specifier != imap.PartSpecifierNone {
		enc.Atom(string(section.Specifier))

		var headerList []string
		if len(section.HeaderFields) > 0 {
			headerList = section.HeaderFields
			enc.Atom(".FIELDS")
		} else if len(section.HeaderFieldsNot) > 0 {
			headerList = section.HeaderFieldsNot
			enc.Atom(".FIELDS.NOT")
		}

		if len(headerList) > 0 {
			enc.SP().List(len(headerList), func(i int) {
				enc.String(headerList[i])
			})
		}
	}
	enc.Special(']')
	if partial := section.Partial; partial != nil {
		enc.Special('<').Number(uint32(partial.Offset)).Special('>')
	}
}

// WriteBinarySection writes a binary section.
//
// The returned io.WriteCloser must be closed before writing any more message
// data items.
func (w *FetchResponseWriter) WriteBinarySection(section *imap.FetchItemBinarySection, size int64) io.WriteCloser {
	w.writeItemSep()
	enc := w.enc.Encoder

	enc.Atom("BINARY").Special('[')
	writeSectionPart(enc, section.Part)
	enc.Special(']').SP()
	enc.Special('~') // indicates literal8
	return w.enc.Literal(size)
}

// WriteBinarySectionSize writes a binary section size.
func (w *FetchResponseWriter) WriteBinarySectionSize(section *imap.FetchItemBinarySectionSize, size uint32) {
	w.writeItemSep()
	enc := w.enc.Encoder

	enc.Atom("BINARY.SIZE").Special('[')
	writeSectionPart(enc, section.Part)
	enc.Special(']').SP().Number(size)
}

// WriteEnvelope writes the message's envelope.
func (w *FetchResponseWriter) WriteEnvelope(envelope *imap.Envelope) {
	w.writeItemSep()
	enc := w.enc.Encoder
	enc.Atom("ENVELOPE").SP()
	writeEnvelope(enc, envelope)
}

// WriteBodyStructure writes the message's body structure (either BODYSTRUCTURE
// or BODY).
func (w *FetchResponseWriter) WriteBodyStructure(bs imap.BodyStructure) {
	if w.options.bodyStructure.nonExtended {
		w.writeBodyStructure(bs, false)
	}

	if w.options.bodyStructure.extended {
		var isExtended bool
		switch bs := bs.(type) {
		case *imap.BodyStructureSinglePart:
			isExtended = bs.Extended != nil
		case *imap.BodyStructureMultiPart:
			isExtended = bs.Extended != nil
		}
		if !isExtended {
			panic("imapserver: client requested extended body structure but a non-extended one is written back")
		}

		w.writeBodyStructure(bs, true)
	}
}

func (w *FetchResponseWriter) writeBodyStructure(bs imap.BodyStructure, extended bool) {
	item := "BODY"
	if extended {
		item = "BODYSTRUCTURE"
	}

	w.writeItemSep()
	enc := w.enc.Encoder
	enc.Atom(item).SP()
	writeBodyStructure(enc, bs, extended)
}

// Close closes the FETCH message writer.
func (w *FetchResponseWriter) Close() error {
	if w.enc == nil {
		return fmt.Errorf("imapserver: FetchResponseWriter already closed")
	}
	err := w.enc.Special(')').CRLF()
	w.enc.end()
	w.enc = nil
	return err
}

func writeEnvelope(enc *imapwire.Encoder, envelope *imap.Envelope) {
	if envelope == nil {
		envelope = new(imap.Envelope)
	}

	sender := envelope.Sender
	if sender == nil {
		sender = envelope.From
	}
	replyTo := envelope.ReplyTo
	if replyTo == nil {
		replyTo = envelope.From
	}

	enc.Special('(')
	if envelope.Date.IsZero() {
		enc.NIL()
	} else {
		enc.String(envelope.Date.Format(envelopeDateLayout))
	}
	enc.SP()
	writeNString(enc, mime.QEncoding.Encode("utf-8", envelope.Subject))
	addrs := [][]imap.Address{
		envelope.From,
		sender,
		replyTo,
		envelope.To,
		envelope.Cc,
		envelope.Bcc,
	}
	for _, l := range addrs {
		enc.SP()
		writeAddressList(enc, l)
	}
	enc.SP()
	if len(envelope.InReplyTo) > 0 {
		enc.String("<" + strings.Join(envelope.InReplyTo, "> <") + ">")
	} else {
		enc.NIL()
	}
	enc.SP()
	if envelope.MessageID != "" {
		enc.String("<" + envelope.MessageID + ">")
	} else {
		enc.NIL()
	}
	enc.Special(')')
}

func writeAddressList(enc *imapwire.Encoder, l []imap.Address) {
	if len(l) == 0 {
		enc.NIL()
		return
	}

	enc.List(len(l), func(i int) {
		addr := l[i]
		enc.Special('(')
		writeNString(enc, mime.QEncoding.Encode("utf-8", addr.Name))
		enc.SP().NIL().SP()
		writeNString(enc, addr.Mailbox)
		enc.SP()
		writeNString(enc, addr.Host)
		enc.Special(')')
	})
}

func writeNString(enc *imapwire.Encoder, s string) {
	if s == "" {
		enc.NIL()
	} else {
		enc.String(s)
	}
}

func writeSectionPart(enc *imapwire.Encoder, part []int) {
	if len(part) == 0 {
		return
	}

	var l []string
	for _, num := range part {
		l = append(l, fmt.Sprintf("%v", num))
	}
	enc.Atom(strings.Join(l, "."))
}

func writeBodyStructure(enc *imapwire.Encoder, bs imap.BodyStructure, extended bool) {
	enc.Special('(')
	switch bs := bs.(type) {
	case *imap.BodyStructureSinglePart:
		writeBodyType1part(enc, bs, extended)
	case *imap.BodyStructureMultiPart:
		writeBodyTypeMpart(enc, bs, extended)
	default:
		panic(fmt.Errorf("unknown body structure type %T", bs))
	}
	enc.Special(')')
}

func writeBodyType1part(enc *imapwire.Encoder, bs *imap.BodyStructureSinglePart, extended bool) {
	enc.String(bs.Type).SP().String(bs.Subtype).SP()
	writeBodyFldParam(enc, bs.Params)
	enc.SP()
	writeNString(enc, bs.ID)
	enc.SP()
	writeNString(enc, bs.Description)
	enc.SP()
	if bs.Encoding == "" {
		enc.String("7bit")
	} else {
		// Outlook for iOS chokes on upper-case encodings
		enc.String(strings.ToLower(bs.Encoding))
	}
	enc.SP().Number(bs.Size)

	if msg := bs.MessageRFC822; msg != nil {
		enc.SP()
		writeEnvelope(enc, msg.Envelope)
		enc.SP()
		writeBodyStructure(enc, msg.BodyStructure, extended)
		enc.SP().Number64(msg.NumLines)
	} else if text := bs.Text; text != nil {
		enc.SP().Number64(text.NumLines)
	}

	if !extended {
		return
	}
	ext := bs.Extended

	enc.SP()
	enc.NIL() // MD5
	enc.SP()
	writeBodyFldDsp(enc, ext.Disposition)
	enc.SP()
	writeBodyFldLang(enc, ext.Language)
	enc.SP()
	writeNString(enc, ext.Location)
}

func writeBodyTypeMpart(enc *imapwire.Encoder, bs *imap.BodyStructureMultiPart, extended bool) {
	if len(bs.Children) == 0 {
		panic("imapserver: imap.BodyStructureMultiPart must have at least one child")
	}
	for _, child := range bs.Children {
		// ABNF for body-type-mpart doesn't have SP between body entries, and
		// Outlook for iOS chokes on SP
		writeBodyStructure(enc, child, extended)
	}

	enc.SP().String(bs.Subtype)

	if !extended {
		return
	}
	ext := bs.Extended

	enc.SP()
	writeBodyFldParam(enc, ext.Params)
	enc.SP()
	writeBodyFldDsp(enc, ext.Disposition)
	enc.SP()
	writeBodyFldLang(enc, ext.Language)
	enc.SP()
	writeNString(enc, ext.Location)
}

func writeBodyFldParam(enc *imapwire.Encoder, params map[string]string) {
	if len(params) == 0 {
		enc.NIL()
		return
	}

	var l []string
	for k := range params {
		l = append(l, k)
	}
	sort.Strings(l)

	enc.List(len(l), func(i int) {
		k := l[i]
		v := params[k]
		enc.String(k).SP().String(v)
	})
}

func writeBodyFldDsp(enc *imapwire.Encoder, disp *imap.BodyStructureDisposition) {
	if disp == nil {
		enc.NIL()
		return
	}

	enc.Special('(').String(disp.Value).SP()
	writeBodyFldParam(enc, disp.Params)
	enc.Special(')')
}

func writeBodyFldLang(enc *imapwire.Encoder, l []string) {
	if len(l) == 0 {
		enc.NIL()
	} else {
		enc.List(len(l), func(i int) {
			enc.String(l[i])
		})
	}
}
