package mail

import (
	"crypto/rand"
	"crypto/tls"
	"encoding/hex"
	"fmt"
	"mime"
	"net"
	"net/smtp"
	"os"
	"strings"
	"time"
)

const timeout = 15 * time.Second

type Config struct {
	Host     string
	Port     string
	Username string
	Password string
	From     string
	To       string
}

func FromEnv() Config {
	return Config{
		Host:     os.Getenv("SMTP_HOST"),
		Port:     envOr("SMTP_PORT", "587"),
		Username: os.Getenv("SMTP_USERNAME"),
		Password: os.Getenv("SMTP_PASSWORD"),
		From:     os.Getenv("SMTP_FROM"),
		To:       envOr("SMTP_TO", "support@hostxtra.co.uk"),
	}
}

func (c Config) Enabled() bool {
	return c.Host != "" && c.From != "" && c.To != ""
}

func (c Config) Send(subject, body, replyTo string) error {
	return c.sendTo(c.To, subject, body, replyTo)
}

func (c Config) sendTo(to, subject, body, replyTo string) error {
	if !c.Enabled() {
		return fmt.Errorf("smtp: not configured")
	}
	if to == "" {
		return fmt.Errorf("smtp: no recipient")
	}

	addr := net.JoinHostPort(c.Host, c.Port)
	conn, err := net.DialTimeout("tcp", addr, timeout)
	if err != nil {
		return fmt.Errorf("smtp: dial %s: %w", addr, err)
	}
	_ = conn.SetDeadline(time.Now().Add(timeout))

	if c.Port == "465" {
		conn = tls.Client(conn, &tls.Config{ServerName: c.Host})
	}

	client, err := smtp.NewClient(conn, c.Host)
	if err != nil {
		conn.Close()
		return fmt.Errorf("smtp: client: %w", err)
	}
	defer client.Close()

	if c.Port != "465" {
		if ok, _ := client.Extension("STARTTLS"); ok {
			if err := client.StartTLS(&tls.Config{ServerName: c.Host}); err != nil {
				return fmt.Errorf("smtp: starttls: %w", err)
			}
		}
	}

	if c.Username != "" {
		if err := client.Auth(smtp.PlainAuth("", c.Username, c.Password, c.Host)); err != nil {
			return fmt.Errorf("smtp: auth: %w", err)
		}
	}

	if err := client.Mail(c.From); err != nil {
		return fmt.Errorf("smtp: mail from: %w", err)
	}
	for _, rcpt := range recipients(to) {
		if err := client.Rcpt(rcpt); err != nil {
			return fmt.Errorf("smtp: rcpt %s: %w", rcpt, err)
		}
	}

	w, err := client.Data()
	if err != nil {
		return fmt.Errorf("smtp: data: %w", err)
	}
	if _, err := w.Write(message(c.From, to, subject, body, replyTo)); err != nil {
		return fmt.Errorf("smtp: write: %w", err)
	}
	if err := w.Close(); err != nil {
		return fmt.Errorf("smtp: close data: %w", err)
	}
	return client.Quit()
}

func recipients(to string) []string {
	parts := strings.Split(to, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if p = strings.TrimSpace(p); p != "" {
			out = append(out, p)
		}
	}
	return out
}

func message(from, to, subject, body, replyTo string) []byte {
	var b strings.Builder
	b.WriteString("From: " + sanitizeHeader(from) + "\r\n")
	b.WriteString("To: " + sanitizeHeader(to) + "\r\n")
	if replyTo != "" {
		b.WriteString("Reply-To: " + sanitizeHeader(replyTo) + "\r\n")
	}

	b.WriteString("Date: " + time.Now().Format(time.RFC1123Z) + "\r\n")
	b.WriteString("Message-ID: " + messageID(from) + "\r\n")
	b.WriteString("Subject: " + mime.QEncoding.Encode("utf-8", sanitizeHeader(subject)) + "\r\n")
	b.WriteString("MIME-Version: 1.0\r\n")
	b.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
	b.WriteString("\r\n")
	b.WriteString(body)
	return []byte(b.String())
}

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

func sanitizeHeader(v string) string {
	return strings.NewReplacer("\r", " ", "\n", " ").Replace(v)
}

func (c Config) SendVerification(to, instanceName, link string, ttl time.Duration) error {
	body := fmt.Sprintf(`Confirm your email to finish creating %s on Vantage.

Open this link:

%s

The link works once and expires in %d hours. Until you use it, no account
exists nothing has been created and the address is not registered.

If you did not request this, ignore this email and nothing will happen.
`, instanceName, link, int(ttl.Hours()))

	return c.sendTo(to, "Confirm your Vantage organisation", body, "")
}

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}
