// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package mailer

import (
	"bytes"
	"context"
	"fmt"
	"net/url"
	"strings"

	"code.gitea.io/gitea/models/db"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/optional"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/templates"
	"code.gitea.io/gitea/modules/timeutil"
	"code.gitea.io/gitea/modules/translation"
	sender_service "code.gitea.io/gitea/services/mailer/sender"
)

const (
	mailAuthActivate               templates.TplName = "user/auth/activate"
	mailAuthActivateEmail          templates.TplName = "user/auth/activate_email"
	mailAuthAccountRequestApproved templates.TplName = "user/auth/account_request_approved"
	mailAuthAccountRequestRejected templates.TplName = "user/auth/account_request_rejected"
	mailAuthAccountRequestValidate templates.TplName = "user/auth/account_request_validate"
	mailAuthAdminInvite            templates.TplName = "user/auth/admin_invite"
	mailAuthAdminInviteAccepted    templates.TplName = "user/auth/admin_invite_accepted"
	mailAuthAdminStatusChanged     templates.TplName = "user/auth/admin_status_changed"
	mailAuthNewAccountRequest      templates.TplName = "user/auth/new_account_request"
	mailAuthResetPassword          templates.TplName = "user/auth/reset_passwd"
	mailAuthRegisterNotify         templates.TplName = "user/auth/register_notify"
)

type AdminUserStatusChange struct {
	Action string
	Reason string
}

// sendUserMail sends a mail to the user
func sendUserMail(language string, u *user_model.User, tpl templates.TplName, code, subject, info string) {
	locale := translation.NewLocale(language)
	data := map[string]any{
		"locale":            locale,
		"DisplayName":       u.DisplayName(),
		"ActiveCodeLives":   timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
		"ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, locale),
		"Code":              code,
		"Language":          locale.Language(),
	}

	var content bytes.Buffer

	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
		log.Error("Template: %v", err)
		return
	}

	msg := sender_service.NewMessage(u.EmailTo(), subject, content.String())
	msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)

	SendAsync(msg)
}

func sendUserMailSync(language string, u *user_model.User, tpl templates.TplName, code, subject, info string) error {
	locale := translation.NewLocale(language)
	data := map[string]any{
		"locale":            locale,
		"DisplayName":       u.DisplayName(),
		"ActiveCodeLives":   timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
		"ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, locale),
		"Code":              code,
		"Language":          locale.Language(),
	}

	var content bytes.Buffer

	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
		return fmt.Errorf("render %s mail: %w", tpl, err)
	}

	msg := sender_service.NewMessage(u.EmailTo(), subject, content.String())
	msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)

	return SendImmediately(msg)
}

func sendSimpleUserMailSync(language string, u *user_model.User, tpl templates.TplName, subject, info string, data map[string]any) error {
	locale := translation.NewLocale(language)
	if data == nil {
		data = map[string]any{}
	}
	data["locale"] = locale
	data["DisplayName"] = u.DisplayName()
	data["Language"] = locale.Language()

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
		return fmt.Errorf("render %s mail: %w", tpl, err)
	}

	msg := sender_service.NewMessage(u.EmailTo(), subject, content.String())
	msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
	return SendImmediately(msg)
}

// SendActivateAccountMail sends an activation mail to the user (new user registration)
func SendActivateAccountMail(locale translation.Locale, u *user_model.User) error {
	if setting.MailService == nil {
		return nil
	}
	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}
	return sendUserMailSync(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account")
}

func SendAccountRequestValidationMail(locale translation.Locale, u *user_model.User, validationCode string) error {
	if setting.MailService == nil {
		return nil
	}
	validationURL := fmt.Sprintf("%suser/activate?code=%s", setting.AppURL, validationCode)
	return sendSimpleUserMailSync(locale.Language(), u, mailAuthAccountRequestValidate, locale.TrString("mail.account_request.validate"), "account request validation", map[string]any{
		"ValidationURL":           validationURL,
		"AccountRequestCodeLives": timeutil.MinutesToFriendly(user_model.AccountRequestValidationHours*60, locale),
	})
}

func SendAccountRequestApprovedMail(u *user_model.User) error {
	if setting.MailService == nil {
		return nil
	}
	locale := translation.NewLocale(u.Language)
	return sendSimpleUserMailSync(u.Language, u, mailAuthAccountRequestApproved, locale.TrString("mail.account_request.approved", setting.AppName), "account request approved", nil)
}

func SendAccountRequestRejectedMail(u *user_model.User) error {
	if setting.MailService == nil {
		return nil
	}
	locale := translation.NewLocale(u.Language)
	return sendSimpleUserMailSync(u.Language, u, mailAuthAccountRequestRejected, locale.TrString("mail.account_request.rejected", setting.AppName), "account request rejected", nil)
}

// SendResetPasswordMail sends a password reset mail to the user
func SendResetPasswordMail(u *user_model.User) {
	if setting.MailService == nil {
		return
	}
	locale := translation.NewLocale(u.Language)
	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}
	sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account")
}

// SendActivateEmailMail sends confirmation email to confirm new email address
func SendActivateEmailMail(u *user_model.User, email string) error {
	if setting.MailService == nil {
		return nil
	}
	locale := translation.NewLocale(u.Language)
	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email}
	data := map[string]any{
		"locale":          locale,
		"DisplayName":     u.DisplayName(),
		"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
		"Code":            user_model.GenerateUserTimeLimitCode(opts, u),
		"Email":           email,
		"Language":        locale.Language(),
	}

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
		return fmt.Errorf("render %s mail: %w", mailAuthActivateEmail, err)
	}

	msg := sender_service.NewMessage(email, locale.TrString("mail.activate_email"), content.String())
	msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
	return SendImmediately(msg)
}

// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
func SendRegisterNotifyMail(u *user_model.User) {
	SendRegisterNotifyMailWithLocale(translation.NewLocale(u.Language), nil, u)
}

func SendRegisterNotifyMailWithLocale(locale translation.Locale, admin, u *user_model.User) {
	if setting.MailService == nil || !u.IsActive {
		return
	}
	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeAdminAccess}
	setPasswordURL := fmt.Sprintf("%suser/activate?code=%s", setting.AppURL, url.QueryEscape(user_model.GenerateUserTimeLimitCode(opts, u)))
	adminName := ""
	if admin != nil {
		adminName = admin.Name
	}
	data := map[string]any{
		"locale":         locale,
		"DisplayName":    u.DisplayName(),
		"AdminName":      adminName,
		"Username":       u.Name,
		"SetPasswordURL": setPasswordURL,
		"Language":       locale.Language(),
	}

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
		log.Error("Template: %v", err)
		return
	}

	msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String())
	msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
	SendAsync(msg)
}

func buildAdminInviteMessage(adminName string, u *user_model.User) (*sender_service.Message, error) {
	return buildAdminInviteMessageWithLocale(translation.NewLocale(u.Language), adminName, u)
}

func buildAdminInviteMessageWithLocale(locale translation.Locale, adminName string, u *user_model.User) (*sender_service.Message, error) {
	if setting.MailService == nil {
		return nil, nil
	}
	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeAdminInvite}
	inviteURL := fmt.Sprintf("%suser/activate?code=%s", setting.AppURL, url.QueryEscape(user_model.GenerateUserTimeLimitCode(opts, u)))
	data := map[string]any{
		"locale":          locale,
		"DisplayName":     u.DisplayName(),
		"AdminName":       adminName,
		"InviteURL":       inviteURL,
		"InviteCodeLives": timeutil.MinutesToFriendly(user_model.AdminInviteCodeLives, locale),
		"Language":        locale.Language(),
	}

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthAdminInvite), data); err != nil {
		return nil, fmt.Errorf("render %s mail: %w", mailAuthAdminInvite, err)
	}

	msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.admin_invite", setting.AppName), content.String())
	msg.Info = fmt.Sprintf("UID: %d, admin invitation", u.ID)
	return msg, nil
}

// SendAdminInviteMail sends an invitation mail for an admin-created disabled-registration account.
func SendAdminInviteMail(admin, u *user_model.User) {
	SendAdminInviteMailWithLocale(translation.NewLocale(u.Language), admin, u)
}

func SendAdminInviteMailWithLocale(locale translation.Locale, admin, u *user_model.User) {
	msg, err := buildAdminInviteMessageWithLocale(locale, admin.DisplayName(), u)
	if err != nil {
		log.Error("Build admin invite mail: %v", err)
		return
	}
	if msg == nil {
		return
	}
	SendAsync(msg)
}

// SendAdminInviteMailSync sends an invitation mail immediately for a pending admin invitation.
func SendAdminInviteMailSync(adminName string, u *user_model.User) error {
	msg, err := buildAdminInviteMessage(adminName, u)
	if err != nil || msg == nil {
		return err
	}
	return SendImmediately(msg)
}

// SendAdminInviteAcceptedMail notifies an administrator that their invitation was accepted.
func SendAdminInviteAcceptedMail(admin, invitedUser *user_model.User) {
	if setting.MailService == nil {
		return
	}
	locale := translation.NewLocale(admin.Language)
	data := map[string]any{
		"locale":          locale,
		"AdminName":       admin.DisplayName(),
		"DisplayName":     invitedUser.DisplayName(),
		"Username":        invitedUser.Name,
		"InvitedUserLink": fmt.Sprintf("%s%s", strings.TrimSuffix(setting.AppURL, "/"), invitedUser.HomeLink()),
		"Language":        locale.Language(),
	}

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthAdminInviteAccepted), data); err != nil {
		log.Error("Template: %v", err)
		return
	}

	msg := sender_service.NewMessage(admin.EmailTo(), locale.TrString("mail.admin_invite.accepted", invitedUser.Name, setting.AppName), content.String())
	msg.Info = fmt.Sprintf("UID: %d, admin invitation accepted by UID: %d", admin.ID, invitedUser.ID)
	SendAsync(msg)
}

// SendAdminUserStatusChangedMail notifies a user when an administrator changes account access status.
func SendAdminUserStatusChangedMail(admin, u *user_model.User, changes []AdminUserStatusChange) {
	if setting.MailService == nil || len(changes) == 0 {
		return
	}
	locale := translation.NewLocale(u.Language)
	data := map[string]any{
		"locale":      locale,
		"DisplayName": u.DisplayName(),
		"AdminName":   admin.DisplayName(),
		"Changes":     changes,
		"Language":    locale.Language(),
	}

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthAdminStatusChanged), data); err != nil {
		log.Error("Template: %v", err)
		return
	}

	msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.admin_status_changed", setting.AppName), content.String())
	msg.Info = fmt.Sprintf("UID: %d, admin status changed by UID: %d", u.ID, admin.ID)
	SendAsync(msg)
}

// SendNewAccountRequestMail sends a notification mail to administrators when a new account request is waiting for approval.
func SendNewAccountRequestMail(ctx context.Context, requester *user_model.User) error {
	if setting.MailService == nil || !setting.Service.RegisterManualConfirm {
		return nil
	}

	admins, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
		Types:       []user_model.UserType{user_model.UserTypeIndividual},
		IsAdmin:     optional.Some(true),
		IsActive:    optional.Some(true),
		ListOptions: db.ListOptionsAll,
	})
	if err != nil {
		return err
	}

	langMap := make(map[string][]*user_model.User)
	for _, admin := range admins {
		enabled, err := user_model.IsEmailNotificationNewAccountRequestsEnabled(ctx, admin)
		if err != nil {
			return err
		}
		if !enabled {
			continue
		}
		langMap[admin.Language] = append(langMap[admin.Language], admin)
	}

	for lang, tos := range langMap {
		if err := sendNewAccountRequestMailPerLang(lang, tos, requester); err != nil {
			return err
		}
	}
	return nil
}

func sendNewAccountRequestMailPerLang(lang string, emailTos []*user_model.User, requester *user_model.User) error {
	locale := translation.NewLocale(lang)
	subject := locale.TrString("mail.new_account_request", setting.AppName)
	reviewURL := fmt.Sprintf("%s-/admin/users/%d/edit", setting.AppURL, requester.ID)
	data := map[string]any{
		"locale":         locale,
		"RequesterName":  requester.DisplayName(),
		"RequesterEmail": requester.Email,
		"ReviewURL":      reviewURL,
		"Language":       locale.Language(),
	}

	var content bytes.Buffer
	if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthNewAccountRequest), data); err != nil {
		return err
	}

	for _, to := range emailTos {
		msg := sender_service.NewMessage(to.EmailTo(), subject, content.String())
		msg.Info = fmt.Sprintf("UID: %d, new account request notification", requester.ID)
		SendAsync(msg)
	}
	return nil
}
