// sogoms-smtp : Microservice d'envoi d'emails.
// Envoie des emails via SMTP avec support des templates YAML.
package main

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/base64"
	"flag"
	"fmt"
	"log"
	"net"
	"net/smtp"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"text/template"
	"time"

	"gopkg.in/yaml.v3"
	"sogoms.com/internal/config"
	"sogoms.com/internal/protocol"
)

var (
	socketPath = flag.String("socket", "/run/sogoms-smtp.1.sock", "Unix socket path")
	configDir  = flag.String("config", "/config", "Configuration directory")
	logsSocket = flag.String("logs-socket", "/run/sogoms-logs.1.sock", "Logs service socket")
)

// SMTPConfig représente la configuration SMTP d'une application.
type SMTPConfig struct {
	Host         string `yaml:"host"`
	Port         int    `yaml:"port"`
	User         string `yaml:"user"`
	PasswordFile string `yaml:"password_file"`
	From         string `yaml:"from"`
	FromName     string `yaml:"from_name"`
	TLS          bool   `yaml:"tls"`
	password     string
}

// EmailTemplate représente un template d'email.
type EmailTemplate struct {
	Subject  string `yaml:"subject"`
	Body     string `yaml:"body"`
	BodyHTML string `yaml:"body_html"`
}

// SMTPPool gère les connexions SMTP par application.
type SMTPPool struct {
	registry  *config.Registry
	configDir string
	configs   map[string]*SMTPConfig          // appID -> config SMTP
	templates map[string]map[string]*EmailTemplate // appID -> templateName -> template
	logsPool  *protocol.Pool
	mu        sync.RWMutex
}

func NewSMTPPool(registry *config.Registry, configDir string, logsPool *protocol.Pool) *SMTPPool {
	return &SMTPPool{
		registry:  registry,
		configDir: configDir,
		configs:   make(map[string]*SMTPConfig),
		templates: make(map[string]map[string]*EmailTemplate),
		logsPool:  logsPool,
	}
}

// Load charge les configurations SMTP et templates pour toutes les apps.
func (p *SMTPPool) Load() error {
	p.mu.Lock()
	defer p.mu.Unlock()

	for _, appID := range p.registry.Apps() {
		// Charger config SMTP depuis config/routes/{app}.yaml (section smtp:)
		smtpCfg, err := p.loadSMTPConfig(appID)
		if err != nil {
			log.Printf("[smtp] warning: no SMTP config for %s: %v", appID, err)
			continue
		}
		p.configs[appID] = smtpCfg

		// Charger templates depuis config/emails/{app}/
		templates, err := p.loadTemplates(appID)
		if err != nil {
			log.Printf("[smtp] warning: no templates for %s: %v", appID, err)
		}
		p.templates[appID] = templates

		log.Printf("[smtp] loaded config for %s (%d templates)", appID, len(templates))
	}

	return nil
}

// loadSMTPConfig charge la config SMTP depuis un fichier YAML d'application.
func (p *SMTPPool) loadSMTPConfig(appID string) (*SMTPConfig, error) {
	path := filepath.Join(p.configDir, "routes", appID+".yaml")
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	var raw struct {
		SMTP *SMTPConfig `yaml:"smtp"`
	}
	if err := yaml.Unmarshal(data, &raw); err != nil {
		return nil, err
	}
	if raw.SMTP == nil {
		return nil, fmt.Errorf("no smtp section")
	}

	cfg := raw.SMTP

	// Port par défaut
	if cfg.Port == 0 {
		if cfg.TLS {
			cfg.Port = 465
		} else {
			cfg.Port = 587
		}
	}

	// Charger le mot de passe depuis le fichier
	if cfg.PasswordFile != "" {
		passData, err := os.ReadFile(cfg.PasswordFile)
		if err != nil {
			return nil, fmt.Errorf("read password file: %w", err)
		}
		cfg.password = strings.TrimSpace(string(passData))
	}

	return cfg, nil
}

// loadTemplates charge les templates email pour une application.
func (p *SMTPPool) loadTemplates(appID string) (map[string]*EmailTemplate, error) {
	templatesDir := filepath.Join(p.configDir, "emails", appID)
	entries, err := os.ReadDir(templatesDir)
	if err != nil {
		return nil, err
	}

	templates := make(map[string]*EmailTemplate)

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
			continue
		}

		name := strings.TrimSuffix(entry.Name(), ".yaml")
		path := filepath.Join(templatesDir, entry.Name())

		data, err := os.ReadFile(path)
		if err != nil {
			continue
		}

		var tpl EmailTemplate
		if err := yaml.Unmarshal(data, &tpl); err != nil {
			continue
		}

		templates[name] = &tpl
	}

	return templates, nil
}

// GetConfig retourne la configuration SMTP pour une application.
func (p *SMTPPool) GetConfig(appID string) (*SMTPConfig, bool) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	cfg, ok := p.configs[appID]
	return cfg, ok
}

// GetTemplate retourne un template pour une application.
func (p *SMTPPool) GetTemplate(appID, name string) (*EmailTemplate, bool) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	if tpls, ok := p.templates[appID]; ok {
		tpl, ok := tpls[name]
		return tpl, ok
	}
	return nil, false
}

// Send envoie un email.
func (p *SMTPPool) Send(appID string, to []string, subject, body, bodyHTML string) error {
	cfg, ok := p.GetConfig(appID)
	if !ok {
		return fmt.Errorf("no SMTP config for app %s", appID)
	}

	// Construire le message MIME
	msg := p.buildMessage(cfg, to, subject, body, bodyHTML)

	// Envoyer
	return p.sendMail(cfg, to, msg)
}

// buildMessage construit un message email MIME.
func (p *SMTPPool) buildMessage(cfg *SMTPConfig, to []string, subject, body, bodyHTML string) []byte {
	var buf bytes.Buffer

	// Headers
	from := cfg.From
	if cfg.FromName != "" {
		from = fmt.Sprintf("%s <%s>", cfg.FromName, cfg.From)
	}

	buf.WriteString(fmt.Sprintf("From: %s\r\n", from))
	buf.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(to, ", ")))
	buf.WriteString(fmt.Sprintf("Subject: =?UTF-8?B?%s?=\r\n", base64.StdEncoding.EncodeToString([]byte(subject))))
	buf.WriteString("MIME-Version: 1.0\r\n")

	if bodyHTML != "" {
		// Multipart
		boundary := fmt.Sprintf("boundary_%d", time.Now().UnixNano())
		buf.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary))
		buf.WriteString("\r\n")

		// Plain text part
		buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
		buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
		buf.WriteString("Content-Transfer-Encoding: base64\r\n")
		buf.WriteString("\r\n")
		buf.WriteString(base64.StdEncoding.EncodeToString([]byte(body)))
		buf.WriteString("\r\n")

		// HTML part
		buf.WriteString(fmt.Sprintf("--%s\r\n", boundary))
		buf.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
		buf.WriteString("Content-Transfer-Encoding: base64\r\n")
		buf.WriteString("\r\n")
		buf.WriteString(base64.StdEncoding.EncodeToString([]byte(bodyHTML)))
		buf.WriteString("\r\n")

		buf.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
	} else {
		// Plain text only
		buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
		buf.WriteString("Content-Transfer-Encoding: base64\r\n")
		buf.WriteString("\r\n")
		buf.WriteString(base64.StdEncoding.EncodeToString([]byte(body)))
	}

	return buf.Bytes()
}

// sendMail envoie un email via SMTP.
func (p *SMTPPool) sendMail(cfg *SMTPConfig, to []string, msg []byte) error {
	addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)

	if cfg.TLS {
		// TLS direct (port 465)
		return p.sendMailTLS(cfg, addr, to, msg)
	}

	// STARTTLS (port 587)
	return p.sendMailSTARTTLS(cfg, addr, to, msg)
}

// sendMailTLS envoie via TLS direct (port 465).
func (p *SMTPPool) sendMailTLS(cfg *SMTPConfig, addr string, to []string, msg []byte) error {
	tlsConfig := &tls.Config{
		ServerName: cfg.Host,
	}

	conn, err := tls.Dial("tcp", addr, tlsConfig)
	if err != nil {
		return fmt.Errorf("TLS dial: %w", err)
	}
	defer conn.Close()

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

	return p.sendMailClient(client, cfg, to, msg)
}

// sendMailSTARTTLS envoie via STARTTLS (port 587).
func (p *SMTPPool) sendMailSTARTTLS(cfg *SMTPConfig, addr string, to []string, msg []byte) error {
	conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
	if err != nil {
		return fmt.Errorf("dial: %w", err)
	}
	defer conn.Close()

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

	// STARTTLS
	tlsConfig := &tls.Config{
		ServerName: cfg.Host,
	}
	if err := client.StartTLS(tlsConfig); err != nil {
		return fmt.Errorf("STARTTLS: %w", err)
	}

	return p.sendMailClient(client, cfg, to, msg)
}

// sendMailClient envoie un email via un client SMTP établi.
func (p *SMTPPool) sendMailClient(client *smtp.Client, cfg *SMTPConfig, to []string, msg []byte) error {
	// Auth
	if cfg.User != "" && cfg.password != "" {
		auth := smtp.PlainAuth("", cfg.User, cfg.password, cfg.Host)
		if err := client.Auth(auth); err != nil {
			return fmt.Errorf("auth: %w", err)
		}
	}

	// From
	if err := client.Mail(cfg.From); err != nil {
		return fmt.Errorf("MAIL FROM: %w", err)
	}

	// To
	for _, addr := range to {
		if err := client.Rcpt(addr); err != nil {
			return fmt.Errorf("RCPT TO %s: %w", addr, err)
		}
	}

	// Data
	w, err := client.Data()
	if err != nil {
		return fmt.Errorf("DATA: %w", err)
	}

	if _, err := w.Write(msg); err != nil {
		return fmt.Errorf("write body: %w", err)
	}

	if err := w.Close(); err != nil {
		return fmt.Errorf("close data: %w", err)
	}

	return client.Quit()
}

// Log envoie un log au service logs.
func (p *SMTPPool) Log(appID, level, message string, data map[string]any) {
	if p.logsPool == nil {
		return
	}

	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()

		req := protocol.NewRequest("log_event", map[string]any{
			"app_id":     appID,
			"event_type": "email_" + level,
			"data":       data,
		})
		p.logsPool.Call(ctx, req)
	}()
}

func main() {
	flag.Parse()
	log.SetFlags(log.Ltime | log.Lshortfile)

	// Charger les configurations
	registry := config.NewRegistry(*configDir)
	if err := registry.Load(); err != nil {
		log.Fatalf("load config: %v", err)
	}
	log.Printf("[smtp] loaded apps: %v", registry.Apps())

	// Pool de logs (optionnel)
	var logsPool *protocol.Pool
	if *logsSocket != "" {
		logsPool = protocol.NewPool(*logsSocket, 2)
	}

	// Pool SMTP
	smtpPool := NewSMTPPool(registry, *configDir, logsPool)
	if err := smtpPool.Load(); err != nil {
		log.Fatalf("load SMTP config: %v", err)
	}

	// Handler des requêtes
	handler := func(ctx context.Context, req *protocol.Request) *protocol.Response {
		return handleRequest(ctx, req, smtpPool)
	}

	// Démarrer le serveur
	server := protocol.NewServer(*socketPath, handler)
	if err := server.Start(); err != nil {
		log.Fatalf("start server: %v", err)
	}

	log.Printf("[smtp] sogoms-smtp started on %s", *socketPath)

	// Attendre signal d'arrêt
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	<-sigCh

	log.Printf("[smtp] shutting down...")
	server.Stop()
}

func handleRequest(ctx context.Context, req *protocol.Request, pool *SMTPPool) *protocol.Response {
	switch req.Action {
	case "health":
		return protocol.Success(req.ID, map[string]any{"status": "ok"})
	case "send":
		return handleSend(req, pool)
	case "send_template":
		return handleSendTemplate(req, pool)
	case "send_bulk":
		return handleSendBulk(req, pool)
	default:
		return protocol.Failure(req.ID, "UNKNOWN_ACTION", "unknown action: "+req.Action)
	}
}

// handleSend envoie un email simple.
// Params: app_id, to (string ou []string), subject, body, body_html (optionnel)
func handleSend(req *protocol.Request, pool *SMTPPool) *protocol.Response {
	appID, ok := req.Params["app_id"].(string)
	if !ok || appID == "" {
		return protocol.Failure(req.ID, "MISSING_APP_ID", "app_id is required")
	}

	// To peut être string ou []string
	var to []string
	switch v := req.Params["to"].(type) {
	case string:
		to = []string{v}
	case []any:
		for _, addr := range v {
			if s, ok := addr.(string); ok {
				to = append(to, s)
			}
		}
	default:
		return protocol.Failure(req.ID, "MISSING_TO", "to is required (string or array)")
	}

	if len(to) == 0 {
		return protocol.Failure(req.ID, "MISSING_TO", "to is required")
	}

	subject, ok := req.Params["subject"].(string)
	if !ok || subject == "" {
		return protocol.Failure(req.ID, "MISSING_SUBJECT", "subject is required")
	}

	body, _ := req.Params["body"].(string)
	bodyHTML, _ := req.Params["body_html"].(string)

	if body == "" && bodyHTML == "" {
		return protocol.Failure(req.ID, "MISSING_BODY", "body or body_html is required")
	}

	// Envoyer
	if err := pool.Send(appID, to, subject, body, bodyHTML); err != nil {
		pool.Log(appID, "error", "send failed", map[string]any{
			"to":    to,
			"error": err.Error(),
		})
		return protocol.Failure(req.ID, "SEND_ERROR", err.Error())
	}

	pool.Log(appID, "sent", "email sent", map[string]any{
		"to":      to,
		"subject": subject,
	})

	return protocol.Success(req.ID, map[string]any{"sent": true, "recipients": len(to)})
}

// handleSendTemplate envoie un email à partir d'un template.
// Params: app_id, to, template, data (map pour le template)
func handleSendTemplate(req *protocol.Request, pool *SMTPPool) *protocol.Response {
	appID, ok := req.Params["app_id"].(string)
	if !ok || appID == "" {
		return protocol.Failure(req.ID, "MISSING_APP_ID", "app_id is required")
	}

	// To
	var to []string
	switch v := req.Params["to"].(type) {
	case string:
		to = []string{v}
	case []any:
		for _, addr := range v {
			if s, ok := addr.(string); ok {
				to = append(to, s)
			}
		}
	default:
		return protocol.Failure(req.ID, "MISSING_TO", "to is required")
	}

	if len(to) == 0 {
		return protocol.Failure(req.ID, "MISSING_TO", "to is required")
	}

	templateName, ok := req.Params["template"].(string)
	if !ok || templateName == "" {
		return protocol.Failure(req.ID, "MISSING_TEMPLATE", "template is required")
	}

	// Charger le template
	tpl, ok := pool.GetTemplate(appID, templateName)
	if !ok {
		return protocol.Failure(req.ID, "TEMPLATE_NOT_FOUND", "template not found: "+templateName)
	}

	// Data pour le template
	data := make(map[string]any)
	if d, ok := req.Params["data"].(map[string]any); ok {
		data = d
	}

	// Rendre le template
	subject, err := renderTemplate(tpl.Subject, data)
	if err != nil {
		return protocol.Failure(req.ID, "TEMPLATE_ERROR", "subject: "+err.Error())
	}

	body, err := renderTemplate(tpl.Body, data)
	if err != nil {
		return protocol.Failure(req.ID, "TEMPLATE_ERROR", "body: "+err.Error())
	}

	var bodyHTML string
	if tpl.BodyHTML != "" {
		bodyHTML, err = renderTemplate(tpl.BodyHTML, data)
		if err != nil {
			return protocol.Failure(req.ID, "TEMPLATE_ERROR", "body_html: "+err.Error())
		}
	}

	// Envoyer
	if err := pool.Send(appID, to, subject, body, bodyHTML); err != nil {
		pool.Log(appID, "error", "send_template failed", map[string]any{
			"to":       to,
			"template": templateName,
			"error":    err.Error(),
		})
		return protocol.Failure(req.ID, "SEND_ERROR", err.Error())
	}

	pool.Log(appID, "sent", "template email sent", map[string]any{
		"to":       to,
		"template": templateName,
	})

	return protocol.Success(req.ID, map[string]any{"sent": true, "recipients": len(to), "template": templateName})
}

// handleSendBulk envoie des emails en masse.
// Params: app_id, recipients ([]map avec to, subject, body ou template+data)
func handleSendBulk(req *protocol.Request, pool *SMTPPool) *protocol.Response {
	appID, ok := req.Params["app_id"].(string)
	if !ok || appID == "" {
		return protocol.Failure(req.ID, "MISSING_APP_ID", "app_id is required")
	}

	recipients, ok := req.Params["recipients"].([]any)
	if !ok || len(recipients) == 0 {
		return protocol.Failure(req.ID, "MISSING_RECIPIENTS", "recipients array is required")
	}

	sent := 0
	failed := 0
	var errors []string

	for i, r := range recipients {
		rcpt, ok := r.(map[string]any)
		if !ok {
			failed++
			errors = append(errors, fmt.Sprintf("#%d: invalid recipient format", i))
			continue
		}

		// To
		var to []string
		switch v := rcpt["to"].(type) {
		case string:
			to = []string{v}
		case []any:
			for _, addr := range v {
				if s, ok := addr.(string); ok {
					to = append(to, s)
				}
			}
		}

		if len(to) == 0 {
			failed++
			errors = append(errors, fmt.Sprintf("#%d: missing to", i))
			continue
		}

		var subject, body, bodyHTML string

		// Template ou direct
		if templateName, ok := rcpt["template"].(string); ok && templateName != "" {
			tpl, ok := pool.GetTemplate(appID, templateName)
			if !ok {
				failed++
				errors = append(errors, fmt.Sprintf("#%d: template not found: %s", i, templateName))
				continue
			}

			data := make(map[string]any)
			if d, ok := rcpt["data"].(map[string]any); ok {
				data = d
			}

			var err error
			subject, err = renderTemplate(tpl.Subject, data)
			if err != nil {
				failed++
				errors = append(errors, fmt.Sprintf("#%d: subject template error", i))
				continue
			}

			body, _ = renderTemplate(tpl.Body, data)
			if tpl.BodyHTML != "" {
				bodyHTML, _ = renderTemplate(tpl.BodyHTML, data)
			}
		} else {
			subject, _ = rcpt["subject"].(string)
			body, _ = rcpt["body"].(string)
			bodyHTML, _ = rcpt["body_html"].(string)
		}

		if subject == "" {
			failed++
			errors = append(errors, fmt.Sprintf("#%d: missing subject", i))
			continue
		}

		if body == "" && bodyHTML == "" {
			failed++
			errors = append(errors, fmt.Sprintf("#%d: missing body", i))
			continue
		}

		// Envoyer
		if err := pool.Send(appID, to, subject, body, bodyHTML); err != nil {
			failed++
			errors = append(errors, fmt.Sprintf("#%d: %s", i, err.Error()))
			continue
		}

		sent++
	}

	pool.Log(appID, "bulk", "bulk send completed", map[string]any{
		"sent":   sent,
		"failed": failed,
	})

	result := map[string]any{
		"sent":   sent,
		"failed": failed,
		"total":  len(recipients),
	}
	if len(errors) > 0 {
		result["errors"] = errors
	}

	return protocol.Success(req.ID, result)
}

// renderTemplate rend un template Go text/template avec les données.
func renderTemplate(text string, data map[string]any) (string, error) {
	if text == "" {
		return "", nil
	}

	tpl, err := template.New("email").Parse(text)
	if err != nil {
		return "", err
	}

	var buf bytes.Buffer
	if err := tpl.Execute(&buf, data); err != nil {
		return "", err
	}

	return buf.String(), nil
}
