package app

import (
	"bytes"
	"context"
	"crypto/tls"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"log"
	netmail "net/mail"
	"net/textproto"
	"sort"
	"strings"
	"time"

	"github.com/emersion/go-sasl"
	smtpserver "github.com/emersion/go-smtp"
	"golang.org/x/crypto/bcrypt"
)

const (
	defaultSubmissionMaxRecipients = 200
)

type SubmissionServers struct {
	Plain *smtpserver.Server
	TLS   *smtpserver.Server
}

func (s *SubmissionServers) Shutdown(ctx context.Context) error {
	if s == nil {
		return nil
	}
	var errs []error
	if s.Plain != nil {
		if err := s.Plain.Shutdown(ctx); err != nil && !errors.Is(err, smtpserver.ErrServerClosed) {
			errs = append(errs, err)
		}
	}
	if s.TLS != nil {
		if err := s.TLS.Shutdown(ctx); err != nil && !errors.Is(err, smtpserver.ErrServerClosed) {
			errs = append(errs, err)
		}
	}
	return errors.Join(errs...)
}

func (a *App) NewSubmissionServers(tlsConfig *tls.Config) *SubmissionServers {
	return &SubmissionServers{
		Plain: a.newSubmissionServer(a.config().SubmissionAddr, tlsConfig),
		TLS:   a.newSubmissionServer(a.config().SubmissionTLSAddr, tlsConfig),
	}
}

func (a *App) newSubmissionServer(addr string, tlsConfig *tls.Config) *smtpserver.Server {
	addr = strings.TrimSpace(addr)
	if addr == "" {
		return nil
	}
	s := smtpserver.NewServer(submissionBackend{app: a})
	s.Addr = addr
	s.Domain = a.config().PublicHostname
	s.TLSConfig = tlsConfig
	s.AllowInsecureAuth = false
	s.MaxRecipients = defaultSubmissionMaxRecipients
	s.MaxMessageBytes = int64(a.config().SubmissionMaxMessageMB) * 1024 * 1024
	s.ReadTimeout = smtpSessionTimeout
	s.WriteTimeout = smtpSessionTimeout
	s.ErrorLog = log.New(submissionLogWriter{log: a.log}, "smtp/submission ", 0)
	return s
}

func LoadServerTLSConfig(cfg Config) (*tls.Config, error) {
	certFile, keyFile := strings.TrimSpace(cfg.TLSCertFile), strings.TrimSpace(cfg.TLSKeyFile)
	if certFile == "" || keyFile == "" {
		return nil, errors.New("LANQIN_TLS_CERT_FILE and LANQIN_TLS_KEY_FILE are required when SMTP submission is enabled")
	}
	return &tls.Config{
		MinVersion: tls.VersionTLS12,
		GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
			cert, err := tls.LoadX509KeyPair(certFile, keyFile)
			if err != nil {
				return nil, err
			}
			return &cert, nil
		},
	}, nil
}

type submissionLogWriter struct {
	log slogLogger
}

func (w submissionLogWriter) Write(p []byte) (int, error) {
	if w.log != nil {
		w.log.Warn(strings.TrimSpace(string(p)))
	}
	return len(p), nil
}

type slogLogger interface {
	Warn(msg string, args ...any)
}

type submissionBackend struct {
	app *App
}

func (b submissionBackend) NewSession(*smtpserver.Conn) (smtpserver.Session, error) {
	return &submissionSession{app: b.app}, nil
}

type submissionSession struct {
	app        *App
	user       *User
	mailbox    *Mailbox
	mailFrom   string
	recipients []string
}

func (s *submissionSession) AuthMechanisms() []string {
	return []string{sasl.Plain, sasl.Login}
}

func (s *submissionSession) Auth(mech string) (sasl.Server, error) {
	authenticate := func(username, password string) error {
		user, mailbox, err := s.app.authenticateSubmission(context.Background(), username, password)
		if err != nil {
			return smtpserver.ErrAuthFailed
		}
		s.user, s.mailbox = user, mailbox
		return nil
	}
	switch {
	case strings.EqualFold(mech, sasl.Plain):
		return sasl.NewPlainServer(func(_, username, password string) error {
			return authenticate(username, password)
		}), nil
	case strings.EqualFold(mech, sasl.Login):
		return &submissionLoginServer{authenticate: authenticate}, nil
	default:
		return nil, smtpserver.ErrAuthUnknownMechanism
	}
}

type submissionLoginServer struct {
	authenticate func(username, password string) error
	username     string
	step         int
}

func (s *submissionLoginServer) Next(response []byte) ([]byte, bool, error) {
	switch s.step {
	case 0:
		if response == nil {
			s.step = 1
			return []byte("Username:"), false, nil
		}
		s.username = string(response)
		s.step = 2
		return []byte("Password:"), false, nil
	case 1:
		s.username = string(response)
		s.step = 2
		return []byte("Password:"), false, nil
	case 2:
		if err := s.authenticate(s.username, string(response)); err != nil {
			return nil, false, err
		}
		s.step = 3
		return nil, true, nil
	default:
		return nil, false, sasl.ErrUnexpectedClientResponse
	}
}

func (s *submissionSession) Mail(from string, _ *smtpserver.MailOptions) error {
	if s.user == nil || s.mailbox == nil {
		return smtpserver.ErrAuthRequired
	}
	from = normalizeEmail(from)
	authorized, _, err := s.app.authorizedSender(context.Background(), s.mailbox, from)
	if err != nil || from == "" || from != authorized {
		return smtpError(553, smtpserver.EnhancedCode{5, 7, 1}, "sender must match authenticated mailbox")
	}
	s.mailFrom = from
	s.recipients = nil
	return nil
}

func (s *submissionSession) Rcpt(to string, _ *smtpserver.RcptOptions) error {
	if s.user == nil || s.mailbox == nil {
		return smtpserver.ErrAuthRequired
	}
	to = normalizeEmail(to)
	if to == "" || !strings.Contains(to, "@") {
		return smtpError(501, smtpserver.EnhancedCode{5, 1, 3}, "invalid recipient")
	}
	s.recipients = append(s.recipients, to)
	return nil
}

func (s *submissionSession) Data(r io.Reader) error {
	if s.user == nil || s.mailbox == nil {
		return smtpserver.ErrAuthRequired
	}
	if s.mailFrom == "" || len(s.recipients) == 0 {
		return smtpError(503, smtpserver.EnhancedCode{5, 5, 1}, "missing sender or recipients")
	}
	if err := s.app.submitSMTPMessage(context.Background(), s.user, s.mailbox, s.mailFrom, s.recipients, r); err != nil {
		var smtpErr *smtpserver.SMTPError
		if errors.As(err, &smtpErr) {
			return smtpErr
		}
		return smtpError(451, smtpserver.EnhancedCode{4, 0, 0}, "message submission failed")
	}
	s.Reset()
	return nil
}

func (s *submissionSession) Reset() {
	s.mailFrom = ""
	s.recipients = nil
}

func (s *submissionSession) Logout() error {
	s.Reset()
	return nil
}

func (a *App) authenticateSubmission(ctx context.Context, username, password string) (*User, *Mailbox, error) {
	address := normalizeEmail(username)
	if address == "" {
		return nil, nil, errors.New("missing username")
	}
	var mb Mailbox
	var passwordHash, created string
	row := a.db.QueryRowContext(ctx, `SELECT id,user_id,domain_id,local_part,address,display_name,password_hash,quota_mb,status,created_at
		FROM mailboxes WHERE address=? AND status='active'`, address)
	if err := row.Scan(&mb.ID, &mb.UserID, &mb.DomainID, &mb.LocalPart, &mb.Address, &mb.DisplayName, &passwordHash, &mb.QuotaMB, &mb.Status, &created); err != nil {
		return nil, nil, err
	}
	if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
		return nil, nil, err
	}
	mb.CreatedAt = parseTime(created)
	user, err := a.userByID(ctx, mb.UserID)
	if err != nil {
		return nil, nil, err
	}
	if user.Disabled {
		return nil, nil, errors.New("user disabled")
	}
	if !userHasPermission(user, PermissionMailSend) {
		return nil, nil, errors.New("send permission required")
	}
	return user, &mb, nil
}

func (a *App) submitSMTPMessage(ctx context.Context, user *User, mb *Mailbox, mailFrom string, recipients []string, r io.Reader) error {
	if err := a.recordSMTPRate(ctx, user, mb); err != nil {
		if errors.Is(err, errSMTPRateLimited) {
			return smtpError(452, smtpserver.EnhancedCode{4, 7, 0}, err.Error())
		}
		return err
	}
	raw, err := io.ReadAll(r)
	if err != nil {
		return err
	}
	prepared, msg, attachments, err := a.prepareSubmittedMessage(ctx, raw, mb, mailFrom, recipients)
	if err != nil {
		return err
	}
	msg.MailboxID = mb.ID
	sentID, insertedSent, err := a.insertSentMessageOnce(ctx, msg, attachments)
	if err != nil {
		return err
	}
	if insertedSent {
		if err := a.rewriteMessageMaildir(ctx, sentID); err != nil {
			a.deleteMessage(ctx, sentID)
			if sentFolderID, ferr := a.ensureFolder(ctx, mb.ID, "Sent"); ferr == nil {
				a.deleteSentDedupeKey(ctx, mb.ID, sentFolderID, msg.MessageID)
			}
			return err
		}
	}
	a.recordSendAudit(ctx, sendAuditAccepted, sendQueueStatusQueued, sendAuditInput{UserID: user.ID, MailboxID: mb.ID, SentMessageID: sentID, Source: sendSourceSubmission, MailFrom: mailFrom, HeaderFrom: msg.From, Recipients: recipients})
	if sentID != "" {
		if _, err := a.enqueueSend(ctx, sendQueueInput{UserID: user.ID, MailboxID: mb.ID, SentMessageID: sentID, MessageID: msg.MessageID, Source: sendSourceSubmission, MailFrom: mailFrom, HeaderFrom: msg.From, Recipients: recipients, MIMEBytes: prepared, Now: a.now().UTC()}); err != nil {
			if insertedSent {
				a.deleteMessage(ctx, sentID)
				if sentFolderID, ferr := a.ensureFolder(ctx, mb.ID, "Sent"); ferr == nil {
					a.deleteSentDedupeKey(ctx, mb.ID, sentFolderID, msg.MessageID)
				}
			}
			return err
		}
	}
	return nil
}

func (a *App) prepareSubmittedMessage(ctx context.Context, raw []byte, mb *Mailbox, mailFrom string, recipients []string) ([]byte, storedMessage, []AttachmentInput, error) {
	header, body, err := readMessageHeader(raw)
	if err != nil {
		return nil, storedMessage{}, nil, smtpError(554, smtpserver.EnhancedCode{5, 6, 0}, "invalid message")
	}
	fromAddress, fromName, ok := singleHeaderAddress(header.Get("From"))
	if !ok || fromAddress == "" {
		return nil, storedMessage{}, nil, smtpError(550, smtpserver.EnhancedCode{5, 7, 1}, "From header must contain exactly one address")
	}
	authAddress, fromName, err := a.authorizedSender(ctx, mb, fromAddress)
	if err != nil || normalizeEmail(mailFrom) != authAddress || normalizeEmail(fromAddress) != authAddress {
		return nil, storedMessage{}, nil, smtpError(553, smtpserver.EnhancedCode{5, 7, 1}, "sender must match authenticated mailbox")
	}
	now := a.now().UTC()
	messageID := strings.TrimSpace(header.Get("Message-Id"))
	if messageID == "" {
		messageID = fmt.Sprintf("<%s@%s>", newID("msg"), domainPart(authAddress))
		header.Set("Message-ID", messageID)
	} else {
		header.Set("Message-ID", messageID)
	}
	sentAt := parseMailDate(header.Get("Date"))
	if sentAt.IsZero() {
		sentAt = now
		header.Set("Date", sentAt.Format(time.RFC1123Z))
	}
	header.Del("Bcc")
	prepared := serializeMessage(header, body)
	msg, attachments, err := a.parseMaildirMessage(prepared, authAddress)
	if err != nil {
		return nil, storedMessage{}, nil, smtpError(554, smtpserver.EnhancedCode{5, 6, 0}, "invalid message")
	}
	if msg.MessageID == "" {
		msg.MessageID = messageID
	}
	if msg.SentAt.IsZero() {
		msg.SentAt = sentAt
	}
	if msg.ReceivedAt.IsZero() {
		msg.ReceivedAt = sentAt
	}
	msg.From = authAddress
	msg.FromName = fromName
	msg.To = dedupeEmails(msg.To)
	msg.CC = dedupeEmails(msg.CC)
	msg.BCC = deduceBCCRecipients(recipients, addressList(header.Get("To")), addressList(header.Get("Cc")))
	msg.IsRead = true
	msg.RawPath = ""
	if msg.Subject == "" {
		msg.Subject = "(no subject)"
	}
	if msg.Snippet == "" {
		msg.Snippet = snippetFrom(msg.BodyText, msg.BodyHTML)
	}
	return prepared, msg, attachments, nil
}

func (a *App) insertSentMessageOnce(ctx context.Context, msg storedMessage, attachments []AttachmentInput) (string, bool, error) {
	sentFolderID, err := a.ensureFolder(ctx, msg.MailboxID, "Sent")
	if err != nil {
		return "", false, err
	}
	msg.FolderID = sentFolderID
	if msg.MessageUID == "" {
		msg.MessageUID = newID("uid")
	}
	tx, err := a.db.BeginTx(ctx, nil)
	if err != nil {
		return "", false, err
	}
	committed := false
	messageIDForCleanup := ""
	defer func() {
		if !committed {
			_ = tx.Rollback()
			if messageIDForCleanup != "" {
				a.deleteMessageFiles(ctx, messageIDForCleanup)
			}
		}
	}()
	if msg.MessageID != "" {
		existing, err := sentMessageIDByMessageID(ctx, tx, msg.MailboxID, sentFolderID, msg.MessageID)
		if err == nil {
			if err := a.insertSentDedupeKeyWithDB(ctx, tx, msg.MailboxID, sentFolderID, msg.MessageID); err != nil && !errors.Is(err, errSentDedupeExists) {
				return "", false, err
			}
			if err := tx.Commit(); err != nil {
				return "", false, err
			}
			committed = true
			return existing, false, nil
		}
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			return "", false, err
		}
		if err := a.insertSentDedupeKeyWithDB(ctx, tx, msg.MailboxID, sentFolderID, msg.MessageID); err != nil {
			if errors.Is(err, errSentDedupeExists) {
				existing, qerr := sentMessageIDByMessageID(ctx, tx, msg.MailboxID, sentFolderID, msg.MessageID)
				if qerr == nil {
					if err := tx.Commit(); err != nil {
						return "", false, err
					}
					committed = true
					return existing, false, nil
				}
				if errors.Is(qerr, sql.ErrNoRows) {
					return "", false, fmt.Errorf("sent dedupe key exists without sent message: %w", errSentDedupeExists)
				}
				return "", false, qerr
			}
			return "", false, err
		}
	}
	id, err := a.insertMessageWithDB(ctx, tx, msg, attachments)
	if err != nil {
		return "", false, err
	}
	messageIDForCleanup = id
	if err := tx.Commit(); err != nil {
		return "", false, err
	}
	committed = true
	return id, true, nil
}

var errSentDedupeExists = errors.New("sent message already exists")

func (a *App) insertSentDedupeKey(ctx context.Context, mailboxID, folderID, messageID string) error {
	return a.insertSentDedupeKeyWithDB(ctx, a.db, mailboxID, folderID, messageID)
}

func (a *App) insertSentDedupeKeyWithDB(ctx context.Context, db dbExecutor, mailboxID, folderID, messageID string) error {
	if strings.TrimSpace(messageID) == "" {
		return nil
	}
	res, err := db.ExecContext(ctx, `INSERT OR IGNORE INTO sent_message_dedupe_keys(mailbox_id,folder_id,message_id,created_at) VALUES(?,?,?,?)`, mailboxID, folderID, messageID, a.now().UTC().Format(time.RFC3339Nano))
	if err != nil {
		return err
	}
	if rows, err := res.RowsAffected(); err == nil && rows == 0 {
		return errSentDedupeExists
	}
	return nil
}

func sentMessageIDByMessageID(ctx context.Context, db dbQueryer, mailboxID, folderID, messageID string) (string, error) {
	var existing string
	err := db.QueryRowContext(ctx, `SELECT id FROM messages WHERE mailbox_id=? AND folder_id=? AND message_id=? AND message_id <> '' LIMIT 1`, mailboxID, folderID, messageID).Scan(&existing)
	return existing, err
}

func (a *App) deleteSentDedupeKey(ctx context.Context, mailboxID, folderID, messageID string) {
	if strings.TrimSpace(messageID) == "" {
		return
	}
	_, _ = a.db.ExecContext(ctx, `DELETE FROM sent_message_dedupe_keys WHERE mailbox_id=? AND folder_id=? AND message_id=?`, mailboxID, folderID, messageID)
}

func readMessageHeader(raw []byte) (textproto.MIMEHeader, []byte, error) {
	msg, err := netmail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return nil, nil, err
	}
	body, err := io.ReadAll(msg.Body)
	if err != nil {
		return nil, nil, err
	}
	return textproto.MIMEHeader(msg.Header), body, nil
}

func serializeMessage(header textproto.MIMEHeader, body []byte) []byte {
	var buf bytes.Buffer
	keys := make([]string, 0, len(header))
	for key := range header {
		keys = append(keys, key)
	}
	sort.SliceStable(keys, func(i, j int) bool {
		return textproto.CanonicalMIMEHeaderKey(keys[i]) < textproto.CanonicalMIMEHeaderKey(keys[j])
	})
	for _, key := range keys {
		values := header[key]
		canonical := textproto.CanonicalMIMEHeaderKey(key)
		for _, value := range values {
			fmt.Fprintf(&buf, "%s: %s\r\n", canonical, strings.ReplaceAll(strings.ReplaceAll(value, "\r", ""), "\n", " "))
		}
	}
	buf.WriteString("\r\n")
	buf.Write(body)
	return buf.Bytes()
}

func singleHeaderAddress(value string) (string, string, bool) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", "", false
	}
	items, err := netmail.ParseAddressList(value)
	if err != nil || len(items) != 1 {
		decoded := decodeMIMEHeader(value)
		items, err = netmail.ParseAddressList(decoded)
		if err != nil || len(items) != 1 {
			return "", "", false
		}
	}
	item := items[0]
	return normalizeEmail(item.Address), strings.TrimSpace(decodeMIMEHeader(item.Name)), true
}

func deduceBCCRecipients(envelope, to, cc []string) []string {
	visible := map[string]bool{}
	for _, item := range append(to, cc...) {
		if email := normalizeEmail(item); email != "" {
			visible[email] = true
		}
	}
	var out []string
	seen := map[string]bool{}
	for _, item := range envelope {
		email := normalizeEmail(item)
		if email == "" || visible[email] || seen[email] {
			continue
		}
		seen[email] = true
		out = append(out, email)
	}
	return out
}

func domainPart(email string) string {
	parts := strings.SplitN(normalizeEmail(email), "@", 2)
	if len(parts) != 2 || parts[1] == "" {
		return "lanqin.local"
	}
	return parts[1]
}

func smtpError(code int, enhanced smtpserver.EnhancedCode, message string) *smtpserver.SMTPError {
	return &smtpserver.SMTPError{Code: code, EnhancedCode: enhanced, Message: message}
}
