package mailimp

import (
	"crypto/tls"
	"time"

	"github.com/tech/sendico/notification/interface/api/localizer"
	mb "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/builder"
	"github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal/mailkey"
	mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail/messagebuilder"
	"github.com/tech/sendico/pkg/domainprovider"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	mutil "github.com/tech/sendico/pkg/mutil/config"
	mduration "github.com/tech/sendico/pkg/mutil/duration"
	mail "github.com/xhit/go-simple-mail/v2"
	"go.uber.org/zap"
)

// Client implements a mail client
type Client struct {
	logger mlogger.Logger
	server *mail.SMTPServer
	client *mail.SMTPClient
	from   string
	l      localizer.Localizer
	dp     domainprovider.DomainProvider
}

// Config represents the mail configuration
type GSMConfig struct {
	Username    *string `mapstructure:"username,omitempty" yaml:"username,omitempty"`
	UsernameEnv *string `mapstructure:"username_env,omitempty" yaml:"username_env,omitempty"`
	Password    *string `mapstructure:"password" yaml:"password"`
	PasswordEnv *string `mapstructure:"password_env" yaml:"password_env"`
	Host        string  `mapstructure:"host" yaml:"host"`
	Port        int     `mapstructure:"port" yaml:"port"`
	From        string  `mapstructure:"from" yaml:"from"`
	TimeOut     int     `mapstructure:"network_timeout" yaml:"network_timeout"`
}

func (c *Client) sendImp(m mmail.Message, msg *mail.Email) error {
	err := msg.Send(c.client)
	if err != nil {
		c.logger.Warn("Error sending email", zap.Error(err), zap.String("template_id", m.TemplateID()), zap.Strings("recipients", msg.GetRecipients()))
	} else {
		c.logger.Info("Email sent", zap.Strings("recipients", msg.GetRecipients()), zap.String("template_id", m.TemplateID()))
	}
	// TODO: add amplitude notification
	return err
}

// Send sends an email message to the provided address and with the provided subject
func (c *Client) Send(r mmail.MailBuilder) error {
	// New email simple html with inline and CC

	r.AddData("ServiceName", c.l.ServiceName()).AddData("SupportMail", c.l.SupportMail())
	m, err := r.Build()
	if err != nil {
		c.logger.Warn("Failed to build message", zap.Error(err))
		return err
	}
	body, err := m.Body(c.l, c.dp)
	if err != nil {
		c.logger.Warn("Failed to build message body", zap.Error(err))
		return err
	}
	if (len(body) == 0) || (len(m.Recipients()) == 0) {
		c.logger.Warn("Malformed messge", zap.String("template_id", m.TemplateID()),
			zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
			zap.Int("body_size", len(body)))
		return merrors.InvalidArgument("malformed message", "message.body", "message.recipients")
	}
	subj, err := mailkey.Subject(c.l, m.Parameters(), m.TemplateID(), m.Locale())
	if err != nil {
		c.logger.Warn("Failed to localize subject", zap.Error(err), zap.String("template_id", m.TemplateID()),
			zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
			zap.Int("body_size", len(body)))
		return err
	}
	msg := mail.NewMSG()
	msg.SetFrom(c.from).
		AddTo(m.Recipients()...).
		SetSubject(subj).
		SetBody(mail.TextHTML, body)

	// Call Send and pass the client
	if err = c.sendImp(m, msg); err != nil {
		c.logger.Info("Failed to send an email, attempting to reconnect...",
			zap.Error(err),
			zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))

		c.client = nil
		c.client, err = c.server.Connect()
		if err != nil {
			c.logger.Warn("Failed to reconnect mail client",
				zap.Error(err),
				zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
			return err
		}
		c.logger.Info("Connection has been successfully restored",
			zap.Error(err),
			zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))

		err = c.sendImp(m, msg)
		if err != nil {
			c.logger.Warn("Failed to send message after mail client recreation",
				zap.Error(err),
				zap.String("template", m.TemplateID()), zap.Strings("addresses", m.Recipients()), zap.String("locale", m.Locale()))
			return err
		}
	}

	return err
}

func (c *Client) MailBuilder() mmail.MailBuilder {
	return mb.NewMessageBuilder()
}

// NewClient return a new mail
func NewClient(logger mlogger.Logger, l localizer.Localizer, dp domainprovider.DomainProvider, config *GSMConfig) *Client {
	smtpServer := mail.NewSMTPClient()

	// SMTP Server
	smtpServer.Host = config.Host
	if config.Port < 1 {
		logger.Warn("Invalid mail client port configuration, defaulting to 465", zap.Int("port", config.Port))
		config.Port = 465
	}
	smtpServer.Port = config.Port
	smtpServer.Username = mutil.GetConfigValue(logger, "username", "username_env", config.Username, config.UsernameEnv)
	smtpServer.Password = mutil.GetConfigValue(logger, "password", "password_env", config.Password, config.PasswordEnv)
	smtpServer.Encryption = mail.EncryptionSSL

	// Since v2.3.0 you can specified authentication type:
	// - PLAIN (default)
	// - LOGIN
	// - CRAM-MD5
	// server.Authentication = mail.AuthPlain

	// Variable to keep alive connection
	smtpServer.KeepAlive = true

	// Timeout for connect to SMTP Server
	smtpServer.ConnectTimeout = mduration.Param2Duration(config.TimeOut, time.Second)

	// Timeout for send the data and wait respond
	smtpServer.SendTimeout = mduration.Param2Duration(config.TimeOut, time.Second)

	// Keep certificate verification enabled.
	smtpServer.TLSConfig = &tls.Config{InsecureSkipVerify: false}

	// SMTP client
	lg := logger.Named("client")
	smtpClient, err := smtpServer.Connect()
	if err != nil {
		lg.Warn("Failed to connect", zap.Error(err))
	} else {
		lg.Info("Connected successfully", zap.String("username", smtpServer.Username), zap.String("host", config.Host))
	}

	from := config.From + " <" + smtpServer.Username + ">"

	return &Client{
		logger: lg,
		server: smtpServer,
		client: smtpClient,
		from:   from,
		l:      l,
		dp:     dp,
	}
}
