package email

import (
	"bytes"
	"context"
	"crypto/sha256"
	"embed"
	"errors"
	"fmt"
	templatehtml "html/template"
	"io"
	"io/fs"
	"path"
	"path/filepath"
	"strings"
	templatetxt "text/template"
	"time"

	"github.com/Gleipnir-Technology/bob"
	"github.com/Gleipnir-Technology/bob/dialect/psql"
	"github.com/Gleipnir-Technology/bob/dialect/psql/um"
	"github.com/Gleipnir-Technology/bob/types/pgtypes"
	"github.com/aarondl/opt/omit"
	"github.com/aarondl/opt/omitnull"
	"github.com/rs/zerolog/log"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db/enums"
	"source.gleipnir.technology/Gleipnir/nidus-sync/db/models"
	"source.gleipnir.technology/Gleipnir/nidus-sync/lint"
)

//go:embed template/*
var embeddedFiles embed.FS

var (
	templateByID                             map[int32]*builtTemplate
	templateInitialID                        int32
	templateReportNotificationConfirmationID int32
)

type ContentEmailRender struct {
	IsBrowser bool
	C         any
}

type templatePair struct {
	baseName    string
	messageType enums.CommsMessagetypeemail
	htmlContent string
	txtContent  string
	htmlHash    string
	txtHash     string
}

func LoadTemplates() error {
	all_templates, err := readTemplates(embeddedFiles)
	if err != nil {
		return fmt.Errorf("Failed to read templates: %w", err)
	}
	ctx := context.TODO()
	tx, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("Failed to start transaction: %w", err)
	}
	defer lint.LogOnErrRollback(tx.Rollback, ctx, "rollback")
	templateByID = make(map[int32]*builtTemplate, 0)
	for name, p := range all_templates {
		template_id, err := templateDBID(tx, name, p)
		if err != nil {
			return fmt.Errorf("Failed to add '%s' to DB: %w", name, err)
		}
		template_html, err := templatehtml.New(name).Parse(p.htmlContent)
		if err != nil {
			return fmt.Errorf("Failed to parse HTML portion of '%s': %w", name, err)
		}
		template_txt, err := templatetxt.New(name).Parse(p.txtContent)
		if err != nil {
			return fmt.Errorf("Failed to parse HTML portion of '%s': %w", name, err)
		}
		built := builtTemplate{
			name:         name,
			templateHTML: template_html,
			templateTXT:  template_txt,
		}
		templateByID[template_id] = &built
		//log.Debug().Int32("id", template_id).Str("name", name).Msg("Added template to cache")
	}
	templateInitialID, err = loadTemplateID(ctx, tx, enums.CommsMessagetypeemailInitialContact)
	if err != nil {
		return fmt.Errorf("Failed to load template ID: %s", err)
	}
	templateReportNotificationConfirmationID, err = loadTemplateID(ctx, tx, enums.CommsMessagetypeemailReportNotificationConfirmation)
	if err != nil {
		return fmt.Errorf("Failed to load report-notification-confirmation template ID: %s", err)
	}
	lint.LogOnErrCtx(tx.Commit, ctx, "commit")
	return nil
}

func RenderHTML(template_id int32, s pgtypes.HStore) (html []byte, err error) {
	data := db.ConvertFromPGData(s)
	t, ok := templateByID[template_id]
	if !ok {
		return []byte{}, fmt.Errorf("Failed to lookup template %d", template_id)
	}
	buf_html := &bytes.Buffer{}
	content := ContentEmailRender{
		C:         data,
		IsBrowser: true,
	}
	err = t.executeTemplateHTML(buf_html, content)
	if err != nil {
		return []byte{}, fmt.Errorf("Failed to render HTML template: %w", err)
	}
	return buf_html.Bytes(), nil
}

func loadTemplateID(ctx context.Context, tx bob.Tx, t enums.CommsMessagetypeemail) (int32, error) {
	templates, err := models.CommsEmailTemplates.Query(
		models.SelectWhere.CommsEmailTemplates.MessageType.EQ(t),
		models.SelectWhere.CommsEmailTemplates.Superceded.IsNull(),
	).All(ctx, tx)
	if err != nil {
		return 0, fmt.Errorf("Failed to query template '%s': %w", t, err)
	}
	switch len(templates) {
	case 0:
		return 0, fmt.Errorf("No matching templates for '%s", t)
	case 1:
		return templates[0].ID, nil
	default:
		return 0, fmt.Errorf("Found %d templates for '%s', should only have 1", len(templates), t)
	}
}

func readTemplates(filesystem embed.FS) (results map[string]*templatePair, err error) {
	// First pass: read files and organize by base name
	results = make(map[string]*templatePair)

	err = fs.WalkDir(filesystem, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if d.IsDir() {
			return nil
		}

		// Read file content
		content, err := filesystem.ReadFile(path)
		if err != nil {
			return fmt.Errorf("error reading template %s: %w", path, err)
		}

		// Calculate hash
		hash := fmt.Sprintf("%x", sha256.Sum256(content))

		// Extract base name and extension
		ext := strings.ToLower(filepath.Ext(path))
		baseName := strings.TrimSuffix(filepath.Base(path), ext)

		// Store in map by base name
		if _, exists := results[baseName]; !exists {
			t, err := messageTypeFromName(baseName)
			if err != nil {
				return fmt.Errorf("Cannot parse email templates: %w", err)
			}
			results[baseName] = &templatePair{
				baseName:    baseName,
				messageType: *t,
			}
		}

		// Add content based on extension
		switch ext {
		case ".html", ".htm":
			results[baseName].htmlContent = string(content)
			results[baseName].htmlHash = hash
		case ".txt":
			results[baseName].txtContent = string(content)
			results[baseName].txtHash = hash
		}

		return nil
	})

	if err != nil {
		return results, fmt.Errorf("error walking template directory: %w", err)
	}

	return results, nil
}

func templateDBID(tx bob.Tx, name string, pair *templatePair) (int32, error) {
	ctx := context.Background()

	// Skip incomplete pairs
	if pair.htmlContent == "" {
		return 0, fmt.Errorf("Bad template pair '%s': no html content")
	}
	if pair.txtContent == "" {
		return 0, fmt.Errorf("Bad template pair '%s': no txt content")
	}

	// Check if a template with these hashes already exists
	rows, err := models.CommsEmailTemplates.Query(
		models.SelectWhere.CommsEmailTemplates.ContentHashHTML.EQ(pair.htmlHash),
		models.SelectWhere.CommsEmailTemplates.ContentHashTXT.EQ(pair.txtHash),
		models.SelectWhere.CommsEmailTemplates.MessageType.EQ(pair.messageType),
	).All(ctx, tx)
	if err != nil {
		return 0, fmt.Errorf("Failed to query for existing template: %w", err)
	}
	if len(rows) > 1 {
		return 0, fmt.Errorf("Got %d template rows, should only have 1", len(rows))
	} else if len(rows) == 1 {
		return rows[0].ID, nil
	}

	// Supercede previous templates of this type
	_, err = psql.Update(
		um.Table(models.CommsEmailTemplates.Alias()),
		um.SetCol("superceded").ToArg(time.Now()),
		//um.Where(models.CommsEmailTemplates.Columns.MessageType.EQ(psql.Arg(pair.messageType))),
		um.Where(psql.Quote("message_type").EQ(psql.Arg(pair.messageType))),
		//um.Where(models.CommsEmailTemplates.Columns.Superceded.IsNull()),
		um.Where(psql.Quote("superceded").IsNull()),
	).Exec(ctx, tx)
	if err != nil {
		return 0, fmt.Errorf("error superceding templates: %w", err)
	}

	new_template, err := models.CommsEmailTemplates.Insert(&models.CommsEmailTemplateSetter{
		ContentHTML:     omit.From(pair.htmlContent),
		ContentTXT:      omit.From(pair.txtContent),
		ContentHashHTML: omit.From(pair.htmlHash),
		ContentHashTXT:  omit.From(pair.txtHash),
		Created:         omit.From(time.Now()),
		Superceded:      omitnull.FromPtr[time.Time](nil),
		MessageType:     omit.From(pair.messageType),
	}).One(ctx, tx)
	if err != nil {
		return 0, fmt.Errorf("Failed to insert new template: %w", err)
	}
	log.Info().Int32("id", new_template.ID).Str("type", string(pair.messageType)).Msg("Added new email template")

	return new_template.ID, nil
}

type builtTemplate struct {
	name         string
	templateHTML *templatehtml.Template
	templateTXT  *templatetxt.Template
}

func (bt *builtTemplate) executeTemplateHTML(w io.Writer, content any) error {
	if bt.templateHTML == nil {
		file := templateFileHTML(bt.name)
		templ, err := parseFromDiskHTML(file)
		if err != nil {
			return fmt.Errorf("Failed to parse template file: %w", err)
		}
		if templ == nil {
			lint.Write(w, []byte("Failed to read from disk: "))
			return errors.New("Template parsing failed")
		}
		//log.Debug().Str("name", templ.Name()).Msg("Parsed template")
		return templ.ExecuteTemplate(w, bt.name, content)
	} else {
		return bt.templateHTML.ExecuteTemplate(w, bt.name, content)
	}
}
func (bt *builtTemplate) executeTemplateTXT(w io.Writer, content any) error {
	if bt.templateTXT == nil {
		file := templateFileTXT(bt.name)
		templ, err := parseFromDiskTXT(file)
		if err != nil {
			return fmt.Errorf("Failed to parse template file: %w", err)
		}
		if templ == nil {
			lint.Write(w, []byte("Failed to read from disk: "))
			return errors.New("Template parsing failed")
		}
		//log.Debug().Str("name", templ.Name()).Msg("Parsed template")
		return templ.ExecuteTemplate(w, bt.name, content)
	} else {
		return bt.templateTXT.ExecuteTemplate(w, bt.name, content)
	}
}
func templateFileHTML(name string) string {
	return fmt.Sprintf("comms/template/%s.html", name)
}
func templateFileTXT(name string) string {
	return fmt.Sprintf("comms/template/%s.txt", name)
}

func messageTypeFromName(n string) (*enums.CommsMessagetypeemail, error) {
	for _, t := range enums.AllCommsMessagetypeemail() {
		if n == string(t) {
			return &t, nil
		}
	}
	return nil, fmt.Errorf("Unrecognized email type '%s'", n)
}

func parseFromDiskHTML(file string) (*templatehtml.Template, error) {
	name := path.Base(file)
	//log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
	templ, err := templatehtml.New(name).ParseFiles(file)
	if err != nil {
		return nil, fmt.Errorf("Failed to parse %s: %w", file, err)
	}
	return templ, nil
}

func parseFromDiskTXT(file string) (*templatetxt.Template, error) {
	name := path.Base(file)
	//log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
	templ, err := templatetxt.New(name).ParseFiles(file)
	if err != nil {
		return nil, fmt.Errorf("Failed to parse %s: %w", file, err)
	}
	return templ, nil
}

func publicReportID(s string) string {
	if len(s) != 12 {
		return s
	}
	return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
}

func renderEmailTemplates(template_id int32, data map[string]string) (text string, html string, err error) {
	buf_txt := &bytes.Buffer{}
	t, ok := templateByID[template_id]
	if !ok {
		return "", "", fmt.Errorf("Failed to lookup template %d", template_id)
	}
	content := ContentEmailRender{
		C:         data,
		IsBrowser: false,
	}
	err = t.executeTemplateTXT(buf_txt, content)
	if err != nil {
		return "", "", fmt.Errorf("Failed to render TXT template: %w", err)
	}
	buf_html := &bytes.Buffer{}
	err = t.executeTemplateHTML(buf_html, content)
	if err != nil {
		return "", "", fmt.Errorf("Failed to render HTML template: %w", err)
	}
	return buf_txt.String(), buf_html.String(), nil
}
