package notification

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/smtp"
	"strings"
	"time"
)

// SMTPConfig holds the configuration for an SMTP notification channel.
type SMTPConfig struct {
	Host       string `json:"host"`
	Port       int    `json:"port"`
	Encryption string `json:"encryption"` // "starttls" | "tls" | "none"
	Username   string `json:"username"`
	Password   string `json:"password"`
	From       string `json:"from"`
	To         string `json:"to"`
}

// SMTPChannel delivers alert notifications via plain-text email.
type SMTPChannel struct {
	cfg SMTPConfig
	// messageIDDomain holds the public base hostname of restic-manager so
	// Message-IDs include a stable right-hand-side. Falls back to
	// "restic-manager.local" when unset.
	messageIDDomain string
}

// NewSMTPChannel builds an SMTP channel. messageIDDomain comes from
// cfg.Cfg.BaseURL — caller passes it through.
func NewSMTPChannel(cfg SMTPConfig, messageIDDomain string) *SMTPChannel {
	if messageIDDomain == "" {
		messageIDDomain = "restic-manager.local"
	}
	return &SMTPChannel{cfg: cfg, messageIDDomain: messageIDDomain}
}

// Kind returns "smtp".
func (c *SMTPChannel) Kind() string { return "smtp" }

// Send delivers the payload as a plain-text email via SMTP.
// Returns (250, latency, nil) on success.
func (c *SMTPChannel) Send(ctx context.Context, p Payload) (int, time.Duration, error) {
	t0 := time.Now()
	addr := fmt.Sprintf("%s:%d", c.cfg.Host, c.cfg.Port)

	// Dial respects ctx (we use net.Dialer).
	dialer := &net.Dialer{Timeout: 10 * time.Second}
	rawConn, err := dialer.DialContext(ctx, "tcp", addr)
	if err != nil {
		return 0, time.Since(t0), fmt.Errorf("smtp: dial %s: %w", addr, err)
	}

	var client *smtp.Client
	switch strings.ToLower(c.cfg.Encryption) {
	case "tls":
		conn := tls.Client(rawConn, &tls.Config{ServerName: c.cfg.Host, MinVersion: tls.VersionTLS12})
		client, err = smtp.NewClient(conn, c.cfg.Host)
	case "starttls", "":
		client, err = smtp.NewClient(rawConn, c.cfg.Host)
		if err == nil {
			err = client.StartTLS(&tls.Config{ServerName: c.cfg.Host, MinVersion: tls.VersionTLS12})
		}
	case "none":
		client, err = smtp.NewClient(rawConn, c.cfg.Host)
	default:
		_ = rawConn.Close()
		return 0, time.Since(t0), fmt.Errorf("smtp: unknown encryption %q", c.cfg.Encryption)
	}
	if err != nil {
		_ = rawConn.Close()
		return 0, time.Since(t0), fmt.Errorf("smtp: handshake: %w", err)
	}
	defer func() { _ = client.Quit() }()

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

	if err := client.Mail(extractAddr(c.cfg.From)); err != nil {
		return 0, time.Since(t0), fmt.Errorf("smtp: MAIL FROM: %w", err)
	}
	if err := client.Rcpt(c.cfg.To); err != nil {
		return 0, time.Since(t0), fmt.Errorf("smtp: RCPT TO: %w", err)
	}
	wc, err := client.Data()
	if err != nil {
		return 0, time.Since(t0), fmt.Errorf("smtp: DATA: %w", err)
	}
	msg := buildEmailBody(c.cfg, c.messageIDDomain, p)
	if _, err := wc.Write(msg); err != nil {
		return 0, time.Since(t0), fmt.Errorf("smtp: write: %w", err)
	}
	if err := wc.Close(); err != nil {
		return 0, time.Since(t0), fmt.Errorf("smtp: close DATA: %w", err)
	}

	return 250, time.Since(t0), nil
}

// extractAddr pulls the bare email out of a "Name <addr@host>" form.
func extractAddr(s string) string {
	if i, j := strings.LastIndex(s, "<"), strings.LastIndex(s, ">"); i >= 0 && j > i {
		return s[i+1 : j]
	}
	return s
}

// buildEmailBody assembles the RFC 5322 message bytes per the spec.
// Plain text only; subject hardcoded.
func buildEmailBody(cfg SMTPConfig, msgIDDomain string, p Payload) []byte {
	var b strings.Builder
	// Subject prefix tracks the event verb so raise vs ack vs resolve
	// are visually distinct in the inbox (and threaded by Message-ID).
	verb := "raised"
	switch p.Event {
	case EventAcknowledged:
		verb = "ack"
	case EventResolved:
		verb = "resolved"
	case EventTest:
		verb = "test"
	}
	b.WriteString("From: " + cfg.From + "\r\n")
	b.WriteString("To: " + cfg.To + "\r\n")
	b.WriteString(fmt.Sprintf("Subject: [restic-manager] [%s · %s] %s: %s\r\n", verb, p.Severity, p.HostName, p.Kind))
	b.WriteString("Date: " + p.RaisedAt.UTC().Format(time.RFC1123Z) + "\r\n")
	b.WriteString("Message-ID: <" + p.AlertID + "@" + msgIDDomain + ">\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(p.Message + "\r\n\r\n")
	b.WriteString("—\r\n")
	b.WriteString("Raised at: " + p.RaisedAt.UTC().Format(time.RFC3339) + "\r\n")
	b.WriteString("Severity:  " + p.Severity + "\r\n")
	b.WriteString("Host:      " + p.HostName + "\r\n")
	b.WriteString("Kind:      " + p.Kind + "\r\n")
	if p.Link != "" {
		b.WriteString("\r\nOpen in restic-manager:\r\n")
		b.WriteString(p.Link + "\r\n")
	}
	b.WriteString("\r\n(This message was sent by restic-manager. Acknowledge or resolve in the UI.)\r\n")
	return []byte(b.String())
}
