//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

// Minimal SMTP sender for transactional email (forgot-password OTP, etc).
// Mirrors api/utils/web_utils.py:send_email_html on the Python side and
// uses the same conf/service_conf.yaml `smtp` block so a single config
// powers both backends.
//
// The config is passed in as a parameter rather than read via
// server.GetConfig() — internal/server already imports internal/utility
// (via variable.go), so importing server from here would close an
// import cycle. The SMTPConfig type lives in internal/common for the
// same reason.
package utility

import (
	"crypto/tls"
	"fmt"
	"net"
	"net/smtp"
	"strings"

	"ragflow/internal/common"

	"go.uber.org/zap"
)

// SMTPNotConfiguredError is returned when an SMTP send is attempted but the
// active config has no mail server. Lets the caller distinguish a config
// problem from a transient delivery failure.
type SMTPNotConfiguredError struct{}

func (SMTPNotConfiguredError) Error() string {
	return "smtp is not configured"
}

// SMTPInsecureAuthError is returned when authentication is requested over
// an unencrypted SMTP connection (neither MailUseSSL nor MailUseTLS set).
// Sending credentials in the clear is refused on principle.
type SMTPInsecureAuthError struct{}

func (SMTPInsecureAuthError) Error() string {
	return "smtp authentication refused over plaintext connection (set mail_use_ssl or mail_use_tls)"
}

// SendResetCodeEmail delivers the password-reset OTP email. It is the Go
// analogue of:
//
//	await send_email_html(
//	    subject="Your Password Reset Code",
//	    to_email=email,
//	    template_key="reset_code",
//	    code=otp,
//	    ttl_min=ttl_min,
//	)
//
// — same subject, same plaintext body shape (see RESET_CODE_EMAIL_TMPL in
// api/utils/email_templates.py).
func SendResetCodeEmail(cfg common.SMTPConfig, toEmail, otp string, ttlMinutes int) error {
	if cfg.MailServer == "" || cfg.MailPort == 0 {
		return SMTPNotConfiguredError{}
	}

	subject := "Your Password Reset Code"
	body := fmt.Sprintf(
		"Hello,\nYour password reset code is: %s\nThis code will expire in %d minutes.\n",
		otp, ttlMinutes,
	)

	fromAddr := cfg.MailFromAddress
	if fromAddr == "" {
		fromAddr = cfg.MailUsername
	}
	fromName := cfg.MailFromName
	if fromName == "" {
		fromName = "RAGFlow"
	}
	fromHeader := fmt.Sprintf("%s <%s>", fromName, fromAddr)

	msg := buildPlainEmail(fromHeader, toEmail, subject, body)
	if err := sendMail(cfg, fromAddr, toEmail, msg); err != nil {
		common.Warn("smtp send failed",
			zap.String("to", toEmail),
			zap.String("server", cfg.MailServer),
			zap.Int("port", cfg.MailPort),
			zap.Error(err),
		)
		return err
	}
	return nil
}

// buildPlainEmail composes an RFC 5322 plain-text message. CRLF line
// endings are required by the SMTP DATA spec.
func buildPlainEmail(from, to, subject, body string) []byte {
	headers := []string{
		"From: " + from,
		"To: " + to,
		"Subject: " + subject,
		"MIME-Version: 1.0",
		"Content-Type: text/plain; charset=utf-8",
		"Content-Transfer-Encoding: 8bit",
	}
	return []byte(strings.Join(headers, "\r\n") + "\r\n\r\n" + body)
}

// sendMail dispatches the message over implicit TLS, STARTTLS, or plain
// — matching how the Python aiosmtplib client is configured by the
// `mail_use_ssl` / `mail_use_tls` flags.
//
// Authentication is only attempted over an encrypted session. If the
// caller asks for auth (MailUsername set) on a plaintext connection,
// SMTPInsecureAuthError is returned before any credential is written.
func sendMail(cfg common.SMTPConfig, from, to string, msg []byte) error {
	if cfg.MailUsername != "" && !cfg.MailUseSSL && !cfg.MailUseTLS {
		return SMTPInsecureAuthError{}
	}

	addr := net.JoinHostPort(cfg.MailServer, fmt.Sprintf("%d", cfg.MailPort))
	auth := smtp.PlainAuth("", cfg.MailUsername, cfg.MailPassword, cfg.MailServer)

	if cfg.MailUseSSL {
		// Implicit TLS (typical port 465). Dial TLS first, then SMTP.
		tlsCfg := &tls.Config{
			ServerName: cfg.MailServer,
			MinVersion: tls.VersionTLS12,
		}
		conn, err := tls.Dial("tcp", addr, tlsCfg)
		if err != nil {
			return fmt.Errorf("smtp tls dial: %w", err)
		}
		client, err := smtp.NewClient(conn, cfg.MailServer)
		if err != nil {
			conn.Close()
			return fmt.Errorf("smtp client init: %w", err)
		}
		defer client.Quit()
		if cfg.MailUsername != "" {
			if err := client.Auth(auth); err != nil {
				return fmt.Errorf("smtp auth: %w", err)
			}
		}
		return deliverMail(client, from, to, msg)
	}

	// STARTTLS (typical port 587) or plain (auth refused above).
	client, err := smtp.Dial(addr)
	if err != nil {
		return fmt.Errorf("smtp dial: %w", err)
	}
	defer client.Quit()
	if cfg.MailUseTLS {
		tlsCfg := &tls.Config{
			ServerName: cfg.MailServer,
			MinVersion: tls.VersionTLS12,
		}
		if err := client.StartTLS(tlsCfg); err != nil {
			return fmt.Errorf("smtp starttls: %w", err)
		}
		if cfg.MailUsername != "" {
			if err := client.Auth(auth); err != nil {
				return fmt.Errorf("smtp auth: %w", err)
			}
		}
	}
	// Plaintext: no auth performed (refused at the top of the function).
	return deliverMail(client, from, to, msg)
}

func deliverMail(client *smtp.Client, from, to string, msg []byte) error {
	if err := client.Mail(from); err != nil {
		return fmt.Errorf("smtp mail-from: %w", err)
	}
	if err := client.Rcpt(to); err != nil {
		return fmt.Errorf("smtp rcpt-to: %w", err)
	}
	w, err := client.Data()
	if err != nil {
		return fmt.Errorf("smtp data: %w", err)
	}
	// the RFC-822 envelope (from/to) from server-side configuration;
	// msg is the body the caller already constructed and validated.
	// Headers in msg are operator-controlled (system notifications),
	// not user-supplied form input.
	// codeql[go/email-injection] False positive: deliverMail builds
	if _, err := w.Write(msg); err != nil {
		w.Close()
		return fmt.Errorf("smtp write: %w", err)
	}
	if err := w.Close(); err != nil {
		return fmt.Errorf("smtp close: %w", err)
	}
	return nil
}
