// Package email implements utility for rendering the Email.
//
// The email package supports transifex translation for email templates.
package email

import (
	"bytes"
	"embed"
	"errors"
	"html"
	"html/template"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	"github.com/owncloud/ocis/v2/services/notifications/pkg/channels"
)

var (
	//go:embed templates
	templatesFS embed.FS

	imgDir = filepath.Join("templates", "html", "img")
)

// RenderEmailTemplate is responsible to prepare a message which than can be used to notify the user via email.
func RenderEmailTemplate(mt MessageTemplate, locale, defaultLocale string, emailTemplatePath string, translationPath string, vars map[string]string) (*channels.Message, error) {
	textMt, err := NewTextTemplate(mt, locale, defaultLocale, translationPath, vars)
	if err != nil {
		return nil, err
	}
	tpl, err := parseTemplate(emailTemplatePath, mt.textTemplate)
	if err != nil {
		return nil, err
	}
	textBody, err := emailTemplate(tpl, textMt)
	if err != nil {
		return nil, err
	}
	htmlMt, err := NewHTMLTemplate(mt, locale, defaultLocale, translationPath, escapeStringMap(vars))
	if err != nil {
		return nil, err
	}
	htmlTpl, err := parseTemplate(emailTemplatePath, mt.htmlTemplate)
	if err != nil {
		return nil, err
	}
	htmlBody, err := emailTemplate(htmlTpl, htmlMt)
	if err != nil {
		return nil, err
	}
	var data map[string][]byte
	if emailTemplatePath != "" {
		data, err = readImages(emailTemplatePath)
		if err != nil {
			return nil, err
		}
	}

	return &channels.Message{
		Subject:      textMt.Subject,
		TextBody:     textBody,
		HTMLBody:     htmlBody,
		AttachInline: data,
	}, nil
}

// RenderGroupedEmailTemplate is responsible to prepare a message which than can be used to notify the user via email.
func RenderGroupedEmailTemplate(gmt GroupedMessageTemplate, vars map[string]string, locale, defaultLocale string, emailTemplatePath string, translationPath string, mts []MessageTemplate, mtsVars []map[string]string) (*channels.Message, error) {
	textMt, err := NewGroupedTextTemplate(gmt, vars, locale, defaultLocale, translationPath, mts, mtsVars)
	if err != nil {
		return nil, err
	}
	tpl, err := parseTemplate(emailTemplatePath, gmt.textTemplate)
	if err != nil {
		return nil, err
	}
	textBody, err := groupedEmailTemplate(tpl, textMt)
	if err != nil {
		return nil, err
	}

	escapedMtsVars := make([]map[string]string, 0, len(mtsVars))
	for _, m := range mtsVars {
		escapedMtsVars = append(escapedMtsVars, escapeStringMap(m))
	}
	htmlMt, err := NewGroupedHTMLTemplate(gmt, escapeStringMap(vars), locale, defaultLocale, translationPath, mts, escapedMtsVars)
	if err != nil {
		return nil, err
	}
	htmlTpl, err := parseTemplate(emailTemplatePath, gmt.htmlTemplate)
	if err != nil {
		return nil, err
	}
	htmlBody, err := groupedEmailTemplate(htmlTpl, htmlMt)
	if err != nil {
		return nil, err
	}
	var data map[string][]byte
	if emailTemplatePath != "" {
		data, err = readImages(emailTemplatePath)
		if err != nil {
			return nil, err
		}
	}

	return &channels.Message{
		Subject:      textMt.Subject,
		TextBody:     textBody,
		HTMLBody:     htmlBody,
		AttachInline: data,
	}, nil
}

// emailTemplate builds the email template. It does not use any user provided input, so it is safe to use template.HTML.
func emailTemplate(tpl *template.Template, mt MessageTemplate) (string, error) {
	str, err := executeTemplate(tpl, map[string]interface{}{
		"Greeting":     template.HTML(strings.TrimSpace(mt.Greeting)),     // #nosec G203
		"MessageBody":  template.HTML(strings.TrimSpace(mt.MessageBody)),  // #nosec G203
		"CallToAction": template.HTML(strings.TrimSpace(mt.CallToAction)), // #nosec G203
	})
	if err != nil {
		return "", err
	}
	return str, err
}

// groupedEmailTemplate builds the email template. It does not use any user provided input, so it is safe to use template.HTML.
func groupedEmailTemplate(tpl *template.Template, gmt GroupedMessageTemplate) (string, error) {
	str, err := executeTemplate(tpl, map[string]interface{}{
		"Greeting":    template.HTML(strings.TrimSpace(gmt.Greeting)),    // #nosec G203
		"MessageBody": template.HTML(strings.TrimSpace(gmt.MessageBody)), // #nosec G203
	})
	if err != nil {
		return "", err
	}
	return str, err
}

func parseTemplate(emailTemplatePath string, file string) (*template.Template, error) {
	if emailTemplatePath != "" {
		return template.ParseFiles(filepath.Join(emailTemplatePath, file))
	}
	return template.ParseFS(templatesFS, filepath.Join(file))
}

func executeTemplate(tpl *template.Template, vars any) (string, error) {
	var writer bytes.Buffer
	if err := tpl.Execute(&writer, vars); err != nil {
		return "", err
	}
	return writer.String(), nil
}

func readImages(emailTemplatePath string) (map[string][]byte, error) {
	dir := filepath.Join(emailTemplatePath, imgDir)
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	return read(entries, os.DirFS(emailTemplatePath))
}

func read(entries []fs.DirEntry, fsys fs.FS) (map[string][]byte, error) {
	list := make(map[string][]byte)
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		file, err := fs.ReadFile(fsys, filepath.Join(imgDir, e.Name()))
		if err != nil {
			// skip if the symbolic is a directory
			if errors.Is(err, syscall.EISDIR) {
				continue
			}
			return nil, err
		}
		if !validateMime(file) {
			continue
		}
		list[e.Name()] = file
	}
	return list, nil
}

// signature image formats signature https://go.dev/src/net/http/sniff.go #L:118
var signature = map[string]string{
	"\xff\xd8\xff":      "image/jpeg",
	"\x89PNG\r\n\x1a\n": "image/png",
	"GIF87a":            "image/gif",
	"GIF89a":            "image/gif",
}

// validateMime validate the mime type of image file from its first few bytes
func validateMime(incipit []byte) bool {
	for s := range signature {
		if strings.HasPrefix(string(incipit), s) {
			return true
		}
	}
	return false
}

func escapeStringMap(vars map[string]string) map[string]string {
	for k := range vars {
		vars[k] = html.EscapeString(vars[k])
	}
	return vars
}
