/*
Package imapclient provides an IMAP4 client, primarily for testing the IMAP4 server.

Commands can be sent to the server free-form, but responses are parsed strictly.
Behaviour that may not be required by the IMAP4 specification may be expected by
this client.
*/
package imapclient

/*
- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs.

- todo: have mode for imap4rev1 vs imap4rev2, refusing what is not allowed. we are accepting too much now.
- todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers.
*/

import (
	"bufio"
	"fmt"
	"net"
	"reflect"
	"strings"
)

// Conn is an IMAP connection to a server.
type Conn struct {
	conn      net.Conn
	r         *bufio.Reader
	panic     bool
	tagGen    int
	record    bool // If true, bytes read are added to recordBuf. recorded() resets.
	recordBuf []byte

	LastTag      string
	CapAvailable map[Capability]struct{} // Capabilities available at server, from CAPABILITY command or response code.
	CapEnabled   map[Capability]struct{} // Capabilities enabled through ENABLE command.
}

// Error is a parse or other protocol error.
type Error struct{ err error }

func (e Error) Error() string {
	return e.err.Error()
}

func (e Error) Unwrap() error {
	return e.err
}

// New creates a new client on conn.
//
// If xpanic is true, functions that would return an error instead panic. For parse
// errors, the resulting stack traces show typically show what was being parsed.
//
// The initial untagged greeting response is read and must be "OK".
func New(conn net.Conn, xpanic bool) (client *Conn, rerr error) {
	c := Conn{
		conn:         conn,
		r:            bufio.NewReader(conn),
		panic:        xpanic,
		CapAvailable: map[Capability]struct{}{},
		CapEnabled:   map[Capability]struct{}{},
	}

	defer c.recover(&rerr)
	tag := c.xnonspace()
	if tag != "*" {
		c.xerrorf("expected untagged *, got %q", tag)
	}
	c.xspace()
	ut := c.xuntagged()
	switch x := ut.(type) {
	case UntaggedResult:
		if x.Status != OK {
			c.xerrorf("greeting, got status %q, expected OK", x.Status)
		}
		return &c, nil
	case UntaggedPreauth:
		c.xerrorf("greeting: unexpected preauth")
	case UntaggedBye:
		c.xerrorf("greeting: server sent bye")
	default:
		c.xerrorf("unexpected untagged %v", ut)
	}
	panic("not reached")
}

func (c *Conn) recover(rerr *error) {
	if c.panic {
		return
	}

	x := recover()
	if x == nil {
		return
	}
	err, ok := x.(Error)
	if !ok {
		panic(x)
	}
	*rerr = err
}

func (c *Conn) xerrorf(format string, args ...any) {
	panic(Error{fmt.Errorf(format, args...)})
}

func (c *Conn) xcheckf(err error, format string, args ...any) {
	if err != nil {
		c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
	}
}

func (c *Conn) xcheck(err error) {
	if err != nil {
		panic(err)
	}
}

// Commandf writes a free-form IMAP command to the server.
// If tag is empty, a next unique tag is assigned.
func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) {
	defer c.recover(&rerr)

	if tag == "" {
		tag = c.nextTag()
	}
	c.LastTag = tag

	_, err := fmt.Fprintf(c.conn, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
	c.xcheckf(err, "write command")
	return
}

func (c *Conn) nextTag() string {
	c.tagGen++
	return fmt.Sprintf("x%03d", c.tagGen)
}

// Response reads from the IMAP server until a tagged response line is found.
// The tag must be the same as the tag for the last written command.
// Result holds the status of the command. The caller must check if this the status is OK.
func (c *Conn) Response() (untagged []Untagged, result Result, rerr error) {
	defer c.recover(&rerr)

	for {
		tag := c.xnonspace()
		c.xspace()
		if tag == "*" {
			untagged = append(untagged, c.xuntagged())
			continue
		}

		if tag != c.LastTag {
			c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
		}

		status := c.xstatus()
		c.xspace()
		result = c.xresult(status)
		c.xcrlf()
		return
	}
}

// ReadUntagged reads a single untagged response line.
// Useful for reading lines from IDLE.
func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) {
	defer c.recover(&rerr)

	tag := c.xnonspace()
	if tag != "*" {
		c.xerrorf("got tag %q, expected untagged", tag)
	}
	c.xspace()
	ut := c.xuntagged()
	return ut, nil
}

// Readline reads a line, including CRLF.
// Used with IDLE and synchronous literals.
func (c *Conn) Readline() (line string, rerr error) {
	defer c.recover(&rerr)

	line, err := c.r.ReadString('\n')
	c.xcheckf(err, "read line")
	return line, nil
}

// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it
// is returned without leading "+ " and without trailing crlf. Otherwise, a command
// response is returned. A successfully read continuation can return an empty line.
// Callers should check rerr and result.Status being empty to check if a
// continuation was read.
func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) {
	if !c.peek('+') {
		untagged, result, rerr = c.Response()
		c.xcheckf(rerr, "reading non-continuation response")
		c.xerrorf("response status %q, expected OK", result.Status)
	}
	c.xtake("+ ")
	line, err := c.Readline()
	c.xcheckf(err, "read line")
	line = strings.TrimSuffix(line, "\r\n")
	return
}

// Writelinef writes the formatted format and args as a single line, adding CRLF.
// Used with IDLE and synchronous literals.
func (c *Conn) Writelinef(format string, args ...any) (rerr error) {
	defer c.recover(&rerr)

	s := fmt.Sprintf(format, args...)
	_, err := fmt.Fprintf(c.conn, "%s\r\n", s)
	c.xcheckf(err, "writeline")
	return nil
}

// Write writes directly to the connection. Write errors do take the connections
// panic mode into account, i.e. Write can panic.
func (c *Conn) Write(buf []byte) (n int, rerr error) {
	defer c.recover(&rerr)

	n, rerr = c.conn.Write(buf)
	c.xcheckf(rerr, "write")
	return n, nil
}

// WriteSyncLiteral first writes the synchronous literal size, then read the
// continuation "+" and finally writes the data.
func (c *Conn) WriteSyncLiteral(s string) (rerr error) {
	defer c.recover(&rerr)

	_, err := fmt.Fprintf(c.conn, "{%d}\r\n", len(s))
	c.xcheckf(err, "write sync literal size")
	line, err := c.Readline()
	c.xcheckf(err, "read line")
	if !strings.HasPrefix(line, "+") {
		c.xerrorf("no continuation received for sync literal")
	}
	_, err = c.conn.Write([]byte(s))
	c.xcheckf(err, "write literal data")
	return nil
}

// Transactf writes format and args as an IMAP command, using Commandf with an
// empty tag. I.e. format must not contain a tag. Transactf then reads a response
// using ReadResponse and checks the result status is OK.
func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) {
	defer c.recover(&rerr)

	err := c.Commandf("", format, args...)
	if err != nil {
		return nil, Result{}, err
	}
	return c.ResponseOK()
}

func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
	untagged, result, rerr = c.Response()
	if rerr != nil {
		return nil, Result{}, rerr
	}
	if result.Status != OK {
		c.xerrorf("response status %q, expected OK", result.Status)
	}
	return untagged, result, rerr
}

func (c *Conn) xgetUntagged(l []Untagged, dst any) {
	if len(l) != 1 {
		c.xerrorf("got %d untagged, expected 1: %v", len(l), l)
	}
	got := l[0]
	gotv := reflect.ValueOf(got)
	dstv := reflect.ValueOf(dst)
	if gotv.Type() != dstv.Type().Elem() {
		c.xerrorf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
	}
	dstv.Elem().Set(gotv)
}

// Close closes the connection without writing anything to the server.
// You may want to call Logout. Closing a connection with a mailbox with deleted
// message not yet expunged will not expunge those messages.
func (c *Conn) Close() error {
	var err error
	if c.conn != nil {
		err = c.conn.Close()
		c.conn = nil
	}
	return err
}
