// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package templates

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync/atomic"
	texttemplate "text/template"

	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/util"
)

var (
	rendererKey interface{} = "templatesHtmlRenderer"

	templateError    = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
	notDefinedError  = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`)
	unexpectedError  = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`)
	expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
)

type HTMLRender struct {
	templates atomic.Pointer[template.Template]
}

var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")

func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
	if respWriter, ok := w.(http.ResponseWriter); ok {
		if respWriter.Header().Get("Content-Type") == "" {
			respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
		}
		respWriter.WriteHeader(status)
	}
	t, err := h.TemplateLookup(name)
	if err != nil {
		return texttemplate.ExecError{Name: name, Err: err}
	}
	return t.Execute(w, data)
}

func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) {
	tmpls := h.templates.Load()
	if tmpls == nil {
		return nil, ErrTemplateNotInitialized
	}
	tmpl := tmpls.Lookup(name)
	if tmpl == nil {
		return nil, util.ErrNotExist
	}
	return tmpl, nil
}

func (h *HTMLRender) CompileTemplates() error {
	extSuffix := ".tmpl"
	tmpls := template.New("")
	assets := AssetFS()
	files, err := ListWebTemplateAssetNames(assets)
	if err != nil {
		return nil
	}
	for _, file := range files {
		if !strings.HasSuffix(file, extSuffix) {
			continue
		}
		name := strings.TrimSuffix(file, extSuffix)
		tmpl := tmpls.New(filepath.ToSlash(name))
		for _, fm := range NewFuncMap() {
			tmpl.Funcs(fm)
		}
		buf, err := assets.ReadFile(file)
		if err != nil {
			return err
		}
		if _, err = tmpl.Parse(string(buf)); err != nil {
			return err
		}
	}
	h.templates.Store(tmpls)
	return nil
}

// HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
	if renderer, ok := ctx.Value(rendererKey).(*HTMLRender); ok {
		return ctx, renderer
	}

	rendererType := "static"
	if !setting.IsProd {
		rendererType = "auto-reloading"
	}
	log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")

	renderer := &HTMLRender{}
	if err := renderer.CompileTemplates(); err != nil {
		wrapFatal(handleNotDefinedPanicError(err))
		wrapFatal(handleUnexpected(err))
		wrapFatal(handleExpectedEnd(err))
		wrapFatal(handleGenericTemplateError(err))
		log.Fatal("HTMLRenderer error: %v", err)
	}
	if !setting.IsProd {
		go AssetFS().WatchLocalChanges(ctx, func() {
			if err := renderer.CompileTemplates(); err != nil {
				log.Error("Template error: %v\n%s", err, log.Stack(2))
			}
		})
	}
	return context.WithValue(ctx, rendererKey, renderer), renderer
}

func wrapFatal(format string, args []interface{}) {
	if format == "" {
		return
	}
	log.FatalWithSkip(1, format, args...)
}

func handleGenericTemplateError(err error) (string, []interface{}) {
	groups := templateError.FindStringSubmatch(err.Error())
	if len(groups) != 4 {
		return "", nil
	}

	templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
	filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
	lineNumber, _ := strconv.Atoi(lineNumberStr)
	line := GetLineFromTemplate(templateName, lineNumber, "", -1)

	return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)}
}

func handleNotDefinedPanicError(err error) (string, []interface{}) {
	groups := notDefinedError.FindStringSubmatch(err.Error())
	if len(groups) != 4 {
		return "", nil
	}

	templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
	functionName, _ = strconv.Unquote(`"` + functionName + `"`)
	filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
	lineNumber, _ := strconv.Atoi(lineNumberStr)
	line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)

	return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
}

func handleUnexpected(err error) (string, []interface{}) {
	groups := unexpectedError.FindStringSubmatch(err.Error())
	if len(groups) != 4 {
		return "", nil
	}

	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
	unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
	filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
	lineNumber, _ := strconv.Atoi(lineNumberStr)
	line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)

	return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
}

func handleExpectedEnd(err error) (string, []interface{}) {
	groups := expectedEndError.FindStringSubmatch(err.Error())
	if len(groups) != 4 {
		return "", nil
	}

	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
	filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
	lineNumber, _ := strconv.Atoi(lineNumberStr)
	line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)

	return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
}

const dashSeparator = "----------------------------------------------------------------------\n"

// GetLineFromTemplate returns a line from a template with some context
func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
	bs, err := AssetFS().ReadFile(templateName + ".tmpl")
	if err != nil {
		return fmt.Sprintf("(unable to read template file: %v)", err)
	}

	sb := &strings.Builder{}

	// Write the header
	sb.WriteString(dashSeparator)

	var lineBs []byte

	// Iterate through the lines from the asset file to find the target line
	for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ {
		// Find the next new line
		end := bytes.IndexByte(bs[start:], '\n')

		// adjust the end to be a direct pointer in to []byte
		if end < 0 {
			end = len(bs)
		} else {
			end += start
		}

		// set lineBs to the current line []byte
		lineBs = bs[start:end]

		// move start to after the current new line position
		start = end + 1

		// Write 2 preceding lines + the target line
		if targetLineNum-currentLineNum < 3 {
			_, _ = sb.Write(lineBs)
			_ = sb.WriteByte('\n')
		}
	}

	// FIXME: this algorithm could provide incorrect results and mislead the developers.
	// For example: Undefined function "file" in template .....
	//     {{Func .file.Addition file.Deletion .file.Addition}}
	//             ^^^^          ^(the real error is here)
	// The pointer is added to the first one, but the second one is the real incorrect one.
	//
	// If there is a provided target to look for in the line add a pointer to it
	// e.g.                                                        ^^^^^^^
	if target != "" {
		targetPos := bytes.Index(lineBs, []byte(target))
		if targetPos >= 0 {
			position = targetPos
		}
	}
	if position >= 0 {
		// take the current line and replace preceding text with whitespace (except for tab)
		for i := range lineBs[:position] {
			if lineBs[i] != '\t' {
				lineBs[i] = ' '
			}
		}

		// write the preceding "space"
		_, _ = sb.Write(lineBs[:position])

		// Now write the ^^ pointer
		targetLen := len(target)
		if targetLen == 0 {
			targetLen = 1
		}
		_, _ = sb.WriteString(strings.Repeat("^", targetLen))
		_ = sb.WriteByte('\n')
	}

	// Finally write the footer
	sb.WriteString(dashSeparator)

	return sb.String()
}