package smtp

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	gosmtp "github.com/emersion/go-smtp"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/mail/connect"
	"github.com/ultisuite/ulti-backend/internal/mail/credentials"
	mailoauth "github.com/ultisuite/ulti-backend/internal/mail/oauth"
)

type Sender struct {
	db          *pgxpool.Pool
	logger      *slog.Logger
	credentials *credentials.Manager
	oauth       *mailoauth.Service
}

func NewSender(db *pgxpool.Pool, credManager *credentials.Manager, oauthSvc *mailoauth.Service) *Sender {
	return &Sender{
		db:          db,
		logger:      slog.Default().With("component", "smtp-sender"),
		credentials: credManager,
		oauth:       oauthSvc,
	}
}

type SendRequest struct {
	AccountID   string
	From        string
	To          []string
	Cc          []string
	Bcc         []string
	Subject     string
	BodyText    string
	BodyHTML    string
	InReplyTo   string
	References  []string
	Attachments []SendAttachment
}

func (s *Sender) Send(ctx context.Context, req *SendRequest) error {
	var host string
	var port int
	var useTLS bool
	var creds []byte

	err := s.db.QueryRow(ctx, `
		SELECT smtp_host, smtp_port, smtp_tls, credentials
		FROM mail_accounts WHERE id = $1
	`, req.AccountID).Scan(&host, &port, &useTLS, &creds)
	if err != nil {
		return fmt.Errorf("query account: %w", err)
	}

	cred, err := s.resolveCredential(ctx, req.AccountID, creds)
	if err != nil {
		return fmt.Errorf("decrypt credentials: %w", err)
	}
	addr := fmt.Sprintf("%s:%d", host, port)

	msg := buildMessage(req)

	allRecipients := make([]string, 0, len(req.To)+len(req.Cc)+len(req.Bcc))
	allRecipients = append(allRecipients, req.To...)
	allRecipients = append(allRecipients, req.Cc...)
	allRecipients = append(allRecipients, req.Bcc...)

	auth, err := connect.SMTPClient(cred)
	if err != nil {
		return fmt.Errorf("smtp auth: %w", err)
	}

	var sendErr error
	if useTLS {
		sendErr = gosmtp.SendMailTLS(addr, auth, req.From, allRecipients, strings.NewReader(msg))
	} else {
		sendErr = gosmtp.SendMail(addr, auth, req.From, allRecipients, strings.NewReader(msg))
	}

	if sendErr != nil {
		return fmt.Errorf("send: %w", sendErr)
	}

	s.logger.Info("email sent", "from", req.From, "to", req.To, "subject", req.Subject)
	return nil
}

func generateMessageID(from string) string {
	domain := "ultimail.local"
	if i := strings.LastIndex(from, "@"); i >= 0 && i < len(from)-1 {
		domain = from[i+1:]
	}
	token := make([]byte, 16)
	if _, err := rand.Read(token); err != nil {
		token = []byte(fmt.Sprintf("%d", time.Now().UnixNano()))
	}
	return fmt.Sprintf("<%s@%s>", hex.EncodeToString(token), domain)
}

func (s *Sender) resolveCredential(ctx context.Context, accountID string, creds []byte) (credentials.Credential, error) {
	if len(creds) == 0 {
		return credentials.Credential{}, errors.New("missing credentials")
	}
	if !credentials.IsEncrypted(creds) {
		return credentials.Credential{}, errors.New("plaintext credentials forbidden")
	}
	if s.credentials == nil {
		return credentials.Credential{}, errors.New("credential manager not configured")
	}
	cred, err := s.credentials.DecryptCredential(creds)
	if err != nil {
		return cred, err
	}
	if s.oauth != nil && cred.IsOAuth() {
		return mailoauth.RefreshAccountCredential(ctx, s.db, s.credentials, s.oauth, accountID, cred)
	}
	return cred, nil
}

// kept for tests that referenced parseCredentials
func (s *Sender) parseCredentials(creds []byte) (string, string, error) {
	if s.credentials == nil {
		return "", "", errors.New("credential manager not configured")
	}
	return s.credentials.Decrypt(creds)
}
