// Package mailrelay is the hub's app-email passthrough: it re-emits a raw MIME message
// from a customer box to Resend over SMTP, UNCHANGED. This is deliberately separate from
// internal/notify (the hub's own structured-alert path, which uses the Resend HTTP API):
// the spike proved parse-then-API silently drops inline CID images, so app mail must be
// raw-SMTP passthrough (SPIKE-smtp-app-relay-2026-06-28.md §4). The Resend key lives only
// here, hub-side — never on a box.
package mailrelay

import (
	"bytes"
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/mail"
	"net/smtp"
	"strings"
	"time"
)

// Sender delivers a raw message to the upstream mail provider. Resend applies DKIM; the
// bytes are passed through unchanged (no parse/re-encode). The seam keeps the API handler
// unit-testable with a fake (no real Resend in unit tests).
type Sender interface {
	Send(ctx context.Context, raw []byte, mailFrom string, rcptTo []string) error
}

// ResendSMTP is the production passthrough sender: STARTTLS to smtp.resend.com:587,
// AUTH LOGIN resend/<key>, then raw MAIL/RCPT/DATA.
type ResendSMTP struct {
	apiKey string
	addr   string // host:port
	host   string // host (for TLS ServerName + auth)
	helo   string // EHLO name
}

// NewResendSMTP builds a sender for the live Resend SMTP endpoint.
func NewResendSMTP(apiKey string) *ResendSMTP {
	return &ResendSMTP{
		apiKey: apiKey,
		addr:   "smtp.resend.com:587",
		host:   "smtp.resend.com",
		helo:   "felhom.eu",
	}
}

// Send re-emits the raw message to Resend. A connection/auth/transport failure or an SMTP
// refusal is returned as an error (the API handler maps it to an HTTP status); on success
// it returns nil.
func (s *ResendSMTP) Send(ctx context.Context, raw []byte, mailFrom string, rcptTo []string) error {
	if len(rcptTo) == 0 {
		return fmt.Errorf("no recipients")
	}
	d := net.Dialer{Timeout: 15 * time.Second}
	conn, err := d.DialContext(ctx, "tcp", s.addr)
	if err != nil {
		return fmt.Errorf("dialing resend: %w", err)
	}
	c, err := smtp.NewClient(conn, s.host)
	if err != nil {
		conn.Close()
		return fmt.Errorf("smtp client: %w", err)
	}
	defer c.Close()

	if err := c.Hello(s.helo); err != nil {
		return fmt.Errorf("EHLO: %w", err)
	}
	if ok, _ := c.Extension("STARTTLS"); !ok {
		return fmt.Errorf("resend did not offer STARTTLS")
	}
	if err := c.StartTLS(&tls.Config{ServerName: s.host, MinVersion: tls.VersionTLS12}); err != nil {
		return fmt.Errorf("STARTTLS: %w", err)
	}
	if err := c.Auth(&loginAuth{username: "resend", password: s.apiKey}); err != nil {
		return fmt.Errorf("AUTH LOGIN: %w", err)
	}
	if err := c.Mail(mailFrom); err != nil {
		return fmt.Errorf("MAIL FROM: %w", err)
	}
	for _, rcpt := range rcptTo {
		if err := c.Rcpt(rcpt); err != nil {
			return fmt.Errorf("RCPT TO <%s>: %w", rcpt, err)
		}
	}
	wc, err := c.Data()
	if err != nil {
		return fmt.Errorf("DATA: %w", err)
	}
	if _, err := wc.Write(raw); err != nil {
		wc.Close()
		return fmt.Errorf("writing message: %w", err)
	}
	if err := wc.Close(); err != nil {
		// The 250/5xx verdict lands here (Resend returns the queued-id on 250, or e.g. a
		// 550 for an unverified From domain).
		return fmt.Errorf("delivery refused: %w", err)
	}
	return c.Quit()
}

// loginAuth implements the non-standard SMTP AUTH LOGIN exchange as a net/smtp.Auth
// (stdlib ships PlainAuth only). Resend's SMTP accepts LOGIN with username "resend" and
// the API key as the password (spike §4).
type loginAuth struct {
	username, password string
}

func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
	return "LOGIN", nil, nil
}

func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
	if !more {
		return nil, nil
	}
	switch strings.ToLower(strings.TrimSpace(string(fromServer))) {
	case "username:":
		return []byte(a.username), nil
	case "password:":
		return []byte(a.password), nil
	default:
		return nil, fmt.Errorf("unexpected server challenge: %q", fromServer)
	}
}

// FromDomain parses the From *header* of a raw MIME message (not the envelope — Resend
// checks the header domain; the unverified-domain 550 lands at DATA). Returns the
// lowercased domain.
func FromDomain(raw []byte) (string, error) {
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return "", fmt.Errorf("parsing message headers: %w", err)
	}
	from := strings.TrimSpace(msg.Header.Get("From"))
	if from == "" {
		return "", fmt.Errorf("missing From header")
	}
	addr, err := mail.ParseAddress(from)
	if err != nil {
		list, lerr := mail.ParseAddressList(from)
		if lerr != nil || len(list) == 0 {
			return "", fmt.Errorf("parsing From address %q: %w", from, err)
		}
		addr = list[0]
	}
	at := strings.LastIndex(addr.Address, "@")
	if at < 0 || at == len(addr.Address)-1 {
		return "", fmt.Errorf("From address %q has no domain", addr.Address)
	}
	return strings.ToLower(addr.Address[at+1:]), nil
}
