package smtpserver

import (
	"fmt"
	"time"

	"github.com/emersion/go-sasl"
	"github.com/emersion/go-smtp"
	"mailgoserver/internal/abuseguard"
	"mailgoserver/internal/db"
)

// loginServer implements the LOGIN SASL mechanism server-side (go-sasl only ships the
// client half), mirroring the state machine aiosmtpd's built-in LOGIN handler drives:
// ask for username (unless an initial response already supplied it), then password.
type loginServer struct {
	state    int // 0: need username, 1: need password
	username string
	verify   func(username, password string) error
}

func (s *loginServer) Next(response []byte) (challenge []byte, done bool, err error) {
	switch s.state {
	case 0:
		if response == nil {
			return []byte("Username:"), false, nil
		}
		s.username = string(response)
		s.state = 1
		return []byte("Password:"), false, nil
	case 1:
		password := string(response)
		if err := s.verify(s.username, password); err != nil {
			return nil, false, err
		}
		return nil, true, nil
	default:
		return nil, false, fmt.Errorf("unexpected LOGIN state")
	}
}

// AuthMechanisms mirrors CustomSMTP._get_auth_methods's effective mechanism set
// (aiosmtpd's default LOGIN/PLAIN) once auth is allowed at all — the TLS-required gate
// itself is handled by go-smtp's own AllowInsecureAuth/isTLS check per listener.
func (s *Session) AuthMechanisms() []string {
	return []string{sasl.Login, sasl.Plain}
}

// Auth mirrors EnhancedCombinedAuthenticator.__call__ for the LOGIN/PLAIN case (the
// only mechanisms advertised): credentials are always present by the time verify runs,
// so the "no auth_data supplied" fallback branch in the Python version is unreachable
// here and isn't replicated.
func (s *Session) Auth(mech string) (sasl.Server, error) {
	switch mech {
	case sasl.Login:
		return &loginServer{verify: s.authenticate}, nil
	case sasl.Plain:
		return sasl.NewPlainServer(func(identity, username, password string) error {
			return s.authenticate(username, password)
		}), nil
	default:
		return nil, smtp.ErrAuthUnknownMechanism
	}
}

// authenticate mirrors EnhancedAuthenticator.__call__: verifies credentials, logs an
// AuthLog row either way, and on any failure returns a *smtp.SMTPError carrying the
// exact Python response code/message, arming the connection to close right after that
// response is flushed — mirroring CustomSMTP.smtp_AUTH's transport.close() override.
//
// Two independent identity types can authenticate here: a Sender (relay-only, tried
// first — unchanged from the original behavior), or a mailbox's app password (never
// its portal password — see esrv_mailbox_app_passwords), which lets a mailbox owner
// send mail as their own primary address or a send-as-enabled alias.
func (s *Session) authenticate(username, password string) error {
	sender, err := s.backend.DB.GetSenderByEmail(username)
	if err != nil {
		s.backend.Logger.Error("Authentication error: %v", err)
		_ = s.backend.DB.LogAuthAttempt("sender", username, s.peerIP, false, fmt.Sprintf("Authentication error: %v", err))
		abuseguard.RecordFailureAndMaybeBlacklist(s.backend.DB, s.backend.Cfg, s.backend.Logger, s.peerIP)
		return s.failAuth(451, "Internal server error")
	}
	if sender != nil && db.CheckPassword(password, sender.PasswordHash) {
		s.authenticatedSender = sender
		s.authType = "sender"
		s.username = username
		_ = s.backend.DB.LogAuthAttempt("sender", username, s.peerIP, true, "Successful sender authentication")
		return nil
	}

	if s.backend.Mailstore != nil {
		mbox, merr := s.backend.DB.VerifyMailboxAppPassword(username, password)
		if merr != nil {
			s.backend.Logger.Error("Mailbox authentication error: %v", merr)
			_ = s.backend.DB.LogAuthAttempt("mailbox", username, s.peerIP, false, fmt.Sprintf("Authentication error: %v", merr))
			abuseguard.RecordFailureAndMaybeBlacklist(s.backend.DB, s.backend.Cfg, s.backend.Logger, s.peerIP)
			return s.failAuth(451, "Internal server error")
		}
		if mbox != nil {
			s.authenticatedMailbox = mbox
			s.authType = "mailbox"
			s.username = username
			_ = s.backend.DB.LogAuthAttempt("mailbox", username, s.peerIP, true, "Successful mailbox app-password authentication")
			return nil
		}
	}

	_ = s.backend.DB.LogAuthAttempt("sender", username, s.peerIP, false, fmt.Sprintf("Invalid credentials for %s", username))
	abuseguard.RecordFailureAndMaybeBlacklist(s.backend.DB, s.backend.Cfg, s.backend.Logger, s.peerIP)
	return s.failAuth(535, "Authentication failed")
}

// failAuth builds the SMTPError for a failed AUTH attempt and closes the connection
// shortly after go-smtp writes this response, mirroring CustomSMTP.smtp_AUTH's
// transport.close() override. go-smtp writes the response synchronously right after
// this error is returned, so a short delay comfortably outlasts that write without
// needing to intercept the raw connection (which would break TLS detection on the
// implicit-TLS listener — see server.go).
func (s *Session) failAuth(code int, message string) error {
	conn := s.conn
	go func() {
		time.Sleep(100 * time.Millisecond)
		conn.Close()
	}()
	return &smtp.SMTPError{Code: code, EnhancedCode: smtp.NoEnhancedCode, Message: message}
}
