package handlebars

import (
	"fmt"
	"html"
	"path/filepath"

	"github.com/aymerick/raymond"
	"github.com/zk-org/zk/internal/adapter/handlebars/helpers"
	"github.com/zk-org/zk/internal/core"
	"github.com/zk-org/zk/internal/util"
	"github.com/zk-org/zk/internal/util/paths"
)

func Init(supportsUTF8 bool, logger util.Logger) {
	helpers.RegisterConcat()
	helpers.RegisterDate(logger)
	helpers.RegisterFormatDate(logger)
	helpers.RegisterJoin()
	helpers.RegisterJSON(logger)
	helpers.RegisterList(supportsUTF8)
	helpers.RegisterPrepend(logger)
	helpers.RegisterSubstring()
}

// Template renders a parsed handlebars template.
type Template struct {
	template *raymond.Template
	styler   core.Styler
}

// Styler implements core.Template.
func (t *Template) Styler() core.Styler {
	return t.styler
}

// Render implements core.Template.
func (t *Template) Render(context any) (string, error) {
	res, err := t.template.Exec(context)
	if err != nil {
		return "", fmt.Errorf("render template failed: %w", err)
	}
	return html.UnescapeString(res), nil
}

// Loader loads and holds parsed handlebars templates.
type Loader struct {
	strings     map[string]*Template
	files       map[string]*Template
	lookupPaths []string
	styler      core.Styler
	helpers     map[string]any
}

type LoaderOpts struct {
	// LookupPaths is used to resolve relative template paths.
	LookupPaths []string
	Styler      core.Styler
}

// NewLoader creates a new instance of Loader.
func NewLoader(opts LoaderOpts) *Loader {
	return &Loader{
		strings:     make(map[string]*Template),
		files:       make(map[string]*Template),
		lookupPaths: opts.LookupPaths,
		styler:      opts.Styler,
		helpers:     map[string]any{},
	}
}

// RegisterHelper declares a new template helper to be used with this loader only.
func (l *Loader) RegisterHelper(name string, helper any) {
	l.helpers[name] = helper
}

// LoadTemplate implements core.TemplateLoader.
func (l *Loader) LoadTemplate(content string) (core.Template, error) {
	// Already loaded?
	template, ok := l.strings[content]
	if ok {
		return template, nil
	}

	// Load new template.
	vendorTempl, err := raymond.Parse(content)
	if err != nil {
		return nil, fmt.Errorf("parse template failed: %w", err)
	}
	template = l.newTemplate(vendorTempl)
	l.strings[content] = template
	return template, nil
}

// LoadTemplateAt implements core.TemplateLoader.
func (l *Loader) LoadTemplateAt(path string) (core.Template, error) {
	path, ok := l.locateTemplate(path)
	if !ok {
		return nil, fmt.Errorf("cannot locate template at %s", path)
	}

	// Already loaded?
	template, ok := l.files[path]
	if ok {
		return template, nil
	}

	// Load new template.
	vendorTempl, err := raymond.ParseFile(path)
	if err != nil {
		return nil, fmt.Errorf("parse template failed: %w", err)
	}
	template = l.newTemplate(vendorTempl)
	l.files[path] = template
	return template, nil
}

// locateTemplate returns the absolute path for the given template path, by
// looking for it in the templates directories registered in this Config.
func (l *Loader) locateTemplate(path string) (string, bool) {
	if path == "" {
		return "", false
	}

	exists := func(path string) bool {
		exists, err := paths.Exists(path)
		return exists && err == nil
	}

	if filepath.IsAbs(path) {
		return path, exists(path)
	}

	for _, dir := range l.lookupPaths {
		if candidate := filepath.Join(dir, path); exists(candidate) {
			return candidate, true
		}
	}

	return path, false
}

func (l *Loader) newTemplate(vendorTempl *raymond.Template) *Template {
	vendorTempl.RegisterHelpers(l.helpers)
	return &Template{vendorTempl, l.styler}
}
