package templates

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"text/template"

	"github.com/mudler/LocalAI/pkg/utils"

	"github.com/Masterminds/sprig/v3"

	"github.com/nikolalohinski/gonja/v2"
	"github.com/nikolalohinski/gonja/v2/exec"
)

// Keep this in sync with config.TemplateConfig. Is there a more idiomatic way to accomplish this in go?
// Technically, order doesn't _really_ matter, but the count must stay in sync, see tests/integration/reflect_test.go
type TemplateType int

type templateCache struct {
	mu             sync.Mutex
	templatesPath  string
	templates      map[TemplateType]map[string]*template.Template
	jinjaTemplates map[TemplateType]map[string]*exec.Template
}

func newTemplateCache(templatesPath string) *templateCache {
	tc := &templateCache{
		templatesPath:  templatesPath,
		templates:      make(map[TemplateType]map[string]*template.Template),
		jinjaTemplates: make(map[TemplateType]map[string]*exec.Template),
	}
	return tc
}

func (tc *templateCache) initializeTemplateMapKey(tt TemplateType) {
	if _, ok := tc.templates[tt]; !ok {
		tc.templates[tt] = make(map[string]*template.Template)
	}
}

func (tc *templateCache) existsInModelPath(s string) bool {
	return utils.ExistsInPath(tc.templatesPath, s)
}
func (tc *templateCache) loadTemplateIfExists(templateType TemplateType, templateName string) error {

	// Check if the template was already loaded
	if _, ok := tc.templates[templateType][templateName]; ok {
		return nil
	}

	// Check if the model path exists
	// skip any error here - we run anyway if a template does not exist
	modelTemplateFile := fmt.Sprintf("%s.tmpl", templateName)

	dat := ""
	file := filepath.Join(tc.templatesPath, modelTemplateFile)

	// Security check
	if err := utils.VerifyPath(modelTemplateFile, tc.templatesPath); err != nil {
		return fmt.Errorf("template file outside path: %s", file)
	}

	// can either be a file in the system or a string with the template
	if tc.existsInModelPath(modelTemplateFile) {
		d, err := os.ReadFile(file)
		if err != nil {
			return err
		}
		dat = string(d)
	} else {
		dat = templateName
	}

	// Parse the template
	tmpl, err := template.New("prompt").Funcs(sprig.FuncMap()).Parse(dat)
	if err != nil {
		return err
	}
	tc.templates[templateType][templateName] = tmpl

	return nil
}

func (tc *templateCache) initializeJinjaTemplateMapKey(tt TemplateType) {
	if _, ok := tc.jinjaTemplates[tt]; !ok {
		tc.jinjaTemplates[tt] = make(map[string]*exec.Template)
	}
}

func (tc *templateCache) loadJinjaTemplateIfExists(templateType TemplateType, templateName string) error {
	// Check if the template was already loaded
	if _, ok := tc.jinjaTemplates[templateType][templateName]; ok {
		return nil
	}

	// Check if the model path exists
	// skip any error here - we run anyway if a template does not exist
	modelTemplateFile := fmt.Sprintf("%s.tmpl", templateName)

	dat := ""
	file := filepath.Join(tc.templatesPath, modelTemplateFile)

	// Security check
	if err := utils.VerifyPath(modelTemplateFile, tc.templatesPath); err != nil {
		return fmt.Errorf("template file outside path: %s", file)
	}

	// can either be a file in the system or a string with the template
	if utils.ExistsInPath(tc.templatesPath, modelTemplateFile) {
		d, err := os.ReadFile(file)
		if err != nil {
			return err
		}
		dat = string(d)
	} else {
		dat = templateName
	}

	tmpl, err := gonja.FromString(dat)
	if err != nil {
		return err
	}
	tc.jinjaTemplates[templateType][templateName] = tmpl

	return nil
}

func (tc *templateCache) evaluateJinjaTemplate(templateType TemplateType, templateNameOrContent string, in map[string]interface{}) (string, error) {
	tc.mu.Lock()
	defer tc.mu.Unlock()

	tc.initializeJinjaTemplateMapKey(templateType)
	m, ok := tc.jinjaTemplates[templateType][templateNameOrContent]
	if !ok {
		// return "", fmt.Errorf("template not loaded: %s", templateName)
		loadErr := tc.loadJinjaTemplateIfExists(templateType, templateNameOrContent)
		if loadErr != nil {
			return "", loadErr
		}
		m = tc.jinjaTemplates[templateType][templateNameOrContent] // ok is not important since we check m on the next line, and wealready checked
	}
	if m == nil {
		return "", fmt.Errorf("failed loading a template for %s", templateNameOrContent)
	}

	var buf bytes.Buffer

	data := exec.NewContext(in)

	if err := m.Execute(&buf, data); err != nil {
		return "", err
	}
	return buf.String(), nil
}

func (tc *templateCache) evaluateTemplate(templateType TemplateType, templateNameOrContent string, in interface{}) (string, error) {
	tc.mu.Lock()
	defer tc.mu.Unlock()

	tc.initializeTemplateMapKey(templateType)
	m, ok := tc.templates[templateType][templateNameOrContent]
	if !ok {
		// return "", fmt.Errorf("template not loaded: %s", templateName)
		loadErr := tc.loadTemplateIfExists(templateType, templateNameOrContent)
		if loadErr != nil {
			return "", loadErr
		}
		m = tc.templates[templateType][templateNameOrContent] // ok is not important since we check m on the next line, and wealready checked
	}
	if m == nil {
		return "", fmt.Errorf("failed loading a template for %s", templateNameOrContent)
	}

	var buf bytes.Buffer

	if err := m.Execute(&buf, in); err != nil {
		return "", err
	}
	return buf.String(), nil
}