package mailbox

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strings"
	"sync"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/imapclient"
	gomail "github.com/emersion/go-message/mail"
)

// IMAPConfig connects the adapter to the mail backend (Mailu: Dovecot IMAP +
// Postfix submission). Username/Password are the resolved login (which may be a
// Dovecot master-user login like "alice*gateway").
type IMAPConfig struct {
	IMAPAddr string // host:port, e.g. mail.profullstack.com:993 (implicit TLS)
	SMTPAddr string // host:port, e.g. smtp.profullstack.com:587 (STARTTLS)
	Username string
	Password string
	// SMTPUser/SMTPPass default to Username/Password when empty.
	SMTPUser string
	SMTPPass string
	// SMTPServerName is the TLS server name verified during STARTTLS. Set it when
	// the dial host differs from the certificate name — e.g. dialing the trusted
	// local relay at 127.0.0.1:25 whose cert is mail.<host>. Empty = use the dial
	// host (the net/smtp default).
	SMTPServerName string
	// Plaintext dials IMAP without TLS. Used only for a co-located backend over
	// loopback (the Mailu gateway hitting Dovecot directly on 127.0.0.1, bypassing
	// the front's auth proxy so master-user login works) — the password never
	// leaves the host. Never enable it for a remote server.
	Plaintext bool
}

// imapTransport is a Transport backed by a single authenticated IMAP connection
// plus SMTP submission. Commands are serialized (the IMAP client is not safe for
// concurrent in-flight commands).
type imapTransport struct {
	cfg      IMAPConfig
	mu       sync.Mutex
	c        *imapclient.Client
	selected string
}

// NewIMAPTransport dials the IMAP server, logs in, and returns a Transport.
func NewIMAPTransport(cfg IMAPConfig) (Transport, error) {
	dial := imapclient.DialTLS
	if cfg.Plaintext {
		dial = imapclient.DialInsecure
	}
	c, err := dial(cfg.IMAPAddr, nil)
	if err != nil {
		return nil, fmt.Errorf("imap dial %s: %w", cfg.IMAPAddr, err)
	}
	if err := c.Login(cfg.Username, cfg.Password).Wait(); err != nil {
		_ = c.Close()
		return nil, fmt.Errorf("imap login: %w", err)
	}
	return &imapTransport{cfg: cfg, c: c}, nil
}

// VerifyLogin checks a username/password against the IMAP backend by logging in
// and immediately logging out. It returns nil only when the credentials are
// accepted. The web file browser uses this to authenticate members with their
// webmail (Mailu/Dovecot) password — the same credential Roundcube uses.
func VerifyLogin(addr, user, pass string, plaintext bool) error {
	dial := imapclient.DialTLS
	if plaintext {
		dial = imapclient.DialInsecure
	}
	c, err := dial(addr, nil)
	if err != nil {
		return fmt.Errorf("imap dial %s: %w", addr, err)
	}
	defer func() { _ = c.Close() }()
	if err := c.Login(user, pass).Wait(); err != nil {
		return fmt.Errorf("imap login: %w", err)
	}
	_ = c.Logout().Wait()
	return nil
}

func (t *imapTransport) Close() error {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.c == nil {
		return nil
	}
	_ = t.c.Logout().Wait()
	return t.c.Close()
}

func (t *imapTransport) selectMailbox(name string, readOnly bool) (*imap.SelectData, error) {
	data, err := t.c.Select(name, &imap.SelectOptions{ReadOnly: readOnly}).Wait()
	if err != nil {
		return nil, fmt.Errorf("select %s: %w", name, err)
	}
	t.selected = name
	return data, nil
}

func (t *imapTransport) ListMailboxes(_ context.Context) ([]Mailbox, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	entries, err := t.c.List("", "*", nil).Collect()
	if err != nil {
		return nil, fmt.Errorf("list: %w", err)
	}
	out := make([]Mailbox, 0, len(entries))
	for _, e := range entries {
		mb := Mailbox{Name: e.Mailbox, Path: e.Mailbox}
		st, err := t.c.Status(e.Mailbox, &imap.StatusOptions{NumMessages: true, NumUnseen: true}).Wait()
		if err == nil {
			if st.NumMessages != nil {
				mb.Total = int(*st.NumMessages)
			}
			if st.NumUnseen != nil {
				mb.Unseen = int(*st.NumUnseen)
			}
		}
		out = append(out, mb)
	}
	return out, nil
}

func (t *imapTransport) ListMessages(_ context.Context, opts ListOptions) ([]MessageSummary, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	sel, err := t.selectMailbox(opts.Mailbox, true)
	if err != nil {
		return nil, err
	}
	n := sel.NumMessages
	if n == 0 {
		return nil, nil
	}
	start := uint32(1)
	if opts.Limit > 0 && n > uint32(opts.Limit) {
		start = n - uint32(opts.Limit) + 1
	}
	seq := imap.SeqSet(nil)
	seq.AddRange(start, n)
	bufs, err := t.c.Fetch(seq, &imap.FetchOptions{Envelope: true, Flags: true, UID: true}).Collect()
	if err != nil {
		return nil, fmt.Errorf("fetch: %w", err)
	}
	rows := make([]MessageSummary, 0, len(bufs))
	for _, b := range bufs {
		rows = append(rows, summaryFromBuf(opts.Mailbox, b))
	}
	// newest first
	for i, j := 0, len(rows)-1; i < j; i, j = i+1, j-1 {
		rows[i], rows[j] = rows[j], rows[i]
	}
	return rows, nil
}

func (t *imapTransport) ReadMessage(_ context.Context, mailbox string, uid uint32) (Message, bool, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if _, err := t.selectMailbox(mailbox, false); err != nil {
		return Message{}, false, err
	}
	set := imap.UIDSetNum(imap.UID(uid))
	section := &imap.FetchItemBodySection{}
	bufs, err := t.c.Fetch(set, &imap.FetchOptions{
		Envelope:    true,
		Flags:       true,
		UID:         true,
		BodySection: []*imap.FetchItemBodySection{section},
	}).Collect()
	if err != nil {
		return Message{}, false, fmt.Errorf("fetch uid %d: %w", uid, err)
	}
	if len(bufs) == 0 {
		return Message{}, false, nil
	}
	b := bufs[0]
	msg := Message{MessageSummary: summaryFromBuf(mailbox, b)}
	raw := b.FindBodySection(section)
	if len(raw) > 0 {
		fillBody(&msg, raw)
	}
	msg.Snippet = Snippet(msg.Text, 140)
	return msg, true, nil
}

func (t *imapTransport) Search(_ context.Context, opts SearchOptions) ([]MessageSummary, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	mailbox := opts.Mailbox
	if mailbox == "" {
		mailbox = Inbox
	}
	if _, err := t.selectMailbox(mailbox, true); err != nil {
		return nil, err
	}
	data, err := t.c.UIDSearch(&imap.SearchCriteria{Text: []string{opts.Query}}, nil).Wait()
	if err != nil {
		return nil, fmt.Errorf("search: %w", err)
	}
	uids := data.AllUIDs()
	if len(uids) == 0 {
		return nil, nil
	}
	if opts.Limit > 0 && len(uids) > opts.Limit {
		uids = uids[len(uids)-opts.Limit:]
	}
	bufs, err := t.c.Fetch(imap.UIDSetNum(uids...), &imap.FetchOptions{Envelope: true, Flags: true, UID: true}).Collect()
	if err != nil {
		return nil, fmt.Errorf("search fetch: %w", err)
	}
	rows := make([]MessageSummary, 0, len(bufs))
	for _, b := range bufs {
		rows = append(rows, summaryFromBuf(mailbox, b))
	}
	return rows, nil
}

func (t *imapTransport) Send(_ context.Context, from string, d Draft) (SendResult, error) {
	msg, msgID := buildRFC822(from, d)
	// SMTPUser may be empty for a trusted local relay (no AUTH).
	if err := smtpSend(t.cfg.SMTPAddr, t.cfg.SMTPServerName, t.cfg.SMTPUser, t.cfg.SMTPPass, from, recipients(d), msg); err != nil {
		return SendResult{}, fmt.Errorf("smtp send: %w", err)
	}
	// Best-effort copy to Sent so the message shows in the member's mailbox.
	t.mu.Lock()
	_, _ = t.c.Append(Sent, int64(len(msg)), nil).Wait() // ignore APPEND errors
	t.mu.Unlock()
	return SendResult{MessageID: msgID}, nil
}

func (t *imapTransport) SetFlags(_ context.Context, mailbox string, uid uint32, fc FlagChange) error {
	t.mu.Lock()
	defer t.mu.Unlock()
	if _, err := t.selectMailbox(mailbox, false); err != nil {
		return err
	}
	set := imap.UIDSetNum(imap.UID(uid))
	apply := func(flag imap.Flag, on bool) error {
		op := imap.StoreFlagsDel
		if on {
			op = imap.StoreFlagsAdd
		}
		return t.c.Store(set, &imap.StoreFlags{Op: op, Flags: []imap.Flag{flag}, Silent: true}, nil).Close()
	}
	if fc.Seen != nil {
		if err := apply(imap.FlagSeen, *fc.Seen); err != nil {
			return fmt.Errorf("store seen: %w", err)
		}
	}
	if fc.Flagged != nil {
		if err := apply(imap.FlagFlagged, *fc.Flagged); err != nil {
			return fmt.Errorf("store flagged: %w", err)
		}
	}
	return nil
}

func (t *imapTransport) DeleteMessage(_ context.Context, mailbox string, uid uint32) error {
	t.mu.Lock()
	defer t.mu.Unlock()
	if _, err := t.selectMailbox(mailbox, false); err != nil {
		return err
	}
	set := imap.UIDSetNum(imap.UID(uid))
	if err := t.c.Store(set, &imap.StoreFlags{Op: imap.StoreFlagsAdd, Flags: []imap.Flag{imap.FlagDeleted}, Silent: true}, nil).Close(); err != nil {
		return fmt.Errorf("store deleted: %w", err)
	}
	if err := t.c.Expunge().Close(); err != nil {
		return fmt.Errorf("expunge: %w", err)
	}
	return nil
}

func summaryFromBuf(mailbox string, b *imapclient.FetchMessageBuffer) MessageSummary {
	s := MessageSummary{Mailbox: mailbox, UID: uint32(b.UID)}
	if b.Envelope != nil {
		env := b.Envelope
		s.Subject = env.Subject
		s.Date = env.Date
		s.From = firstAddr(env.From)
		s.To = addrs(env.To)
	}
	for _, f := range b.Flags {
		switch f {
		case imap.FlagSeen:
			s.Seen = true
		case imap.FlagFlagged:
			s.Flagged = true
		}
	}
	return s
}

func firstAddr(list []imap.Address) Address {
	if len(list) == 0 {
		return Address{}
	}
	return imapAddr(list[0])
}

func addrs(list []imap.Address) []Address {
	out := make([]Address, 0, len(list))
	for _, a := range list {
		out = append(out, imapAddr(a))
	}
	return out
}

func imapAddr(a imap.Address) Address {
	addr := a.Mailbox
	if a.Host != "" {
		addr = a.Mailbox + "@" + a.Host
	}
	return Address{Name: a.Name, Address: addr}
}

// fillBody parses the raw RFC822 message into the text/html body + attachment
// metadata using go-message's mail reader.
func fillBody(msg *Message, raw []byte) {
	mr, err := gomail.CreateReader(bytes.NewReader(raw))
	if err != nil {
		// Not MIME we can parse — keep the raw text after the header break.
		msg.Text = rawBody(raw)
		return
	}
	if env := mr.Header; env.Get("Message-Id") != "" {
		msg.MessageID = env.Get("Message-Id")
	}
	for {
		p, err := mr.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			break
		}
		switch h := p.Header.(type) {
		case *gomail.InlineHeader:
			body, _ := io.ReadAll(p.Body)
			ct, _, _ := h.ContentType()
			if strings.EqualFold(ct, "text/html") {
				msg.HTML = string(body)
			} else if msg.Text == "" {
				msg.Text = string(body)
			}
		case *gomail.AttachmentHeader:
			name, _ := h.Filename()
			ct, _, _ := h.ContentType()
			body, _ := io.ReadAll(p.Body)
			msg.Attachments = append(msg.Attachments, Attachment{Filename: name, ContentType: ct, Size: len(body)})
		}
	}
	msg.HasAttachments = len(msg.Attachments) > 0
}

// rawBody returns the body after the first blank line of a raw RFC822 message.
func rawBody(raw []byte) string {
	if i := bytes.Index(raw, []byte("\r\n\r\n")); i >= 0 {
		return string(raw[i+4:])
	}
	if i := bytes.Index(raw, []byte("\n\n")); i >= 0 {
		return string(raw[i+2:])
	}
	return string(raw)
}
