package handler

import (
	"bytes"
	"os"
	"path/filepath"
	"strings"

	"codeberg.org/VARASYS/ZDDC/zddc/internal/convert"
	"gopkg.in/yaml.v3"
)

// resolveTemplateSet builds the convert.TemplateSet for an HTML/PDF render of a
// markdown source. It starts from the baked-in defaults for the doctype named in
// the document's `template:` front matter (default "report"), then overlays any
// per-project / per-party overrides found in the `.zddc.d/templates/` cascade.
//
// The cascade walks from the document's directory up to fsRoot; a nearer level
// (e.g. working/<party>/.zddc.d/templates/) overrides a farther one (e.g.
// working/.zddc.d/templates/), which overrides the embedded default. Overrides
// may replace the named doctype template, any shared partial (_head.html, …), or
// introduce an entirely new doctype the front matter names.
func resolveTemplateSet(fsRoot, docDir string, source []byte) convert.TemplateSet {
	name := templateNameFromFrontMatter(source) // "" when absent/invalid
	ts := convert.DefaultTemplateSet(name)       // primary falls back to report

	dirs := templateCascadeDirs(fsRoot, docDir)

	// If the named doctype isn't a baked-in default but an override provides it,
	// adopt the override as the primary template.
	if name != "" {
		primary := name + ".html"
		if b := firstTemplateOverride(dirs, primary); b != nil {
			ts.Name = primary
			ts.Files[primary] = b
		}
	}

	// Overlay overrides for every file in the set (primary + partials).
	for fname := range ts.Files {
		if b := firstTemplateOverride(dirs, fname); b != nil {
			ts.Files[fname] = b
		}
	}
	return ts
}

// templateCascadeDirs returns the `<level>/.zddc.d/templates` directories from
// docDir up to fsRoot, nearest (most specific) first. Levels outside fsRoot are
// skipped (path-containment guard).
func templateCascadeDirs(fsRoot, docDir string) []string {
	root := filepath.Clean(fsRoot)
	d := filepath.Clean(docDir)
	var dirs []string
	for {
		if d == root || strings.HasPrefix(d, root+string(filepath.Separator)) {
			dirs = append(dirs, filepath.Join(d, ReservedSidecar, "templates"))
		}
		if d == root {
			break
		}
		parent := filepath.Dir(d)
		if parent == d {
			break
		}
		d = parent
	}
	return dirs
}

// firstTemplateOverride returns the bytes of the first existing `<dir>/<name>`
// across dirs (nearest first), or nil. name is reduced to a base name so it can
// never escape the templates dir.
func firstTemplateOverride(dirs []string, name string) []byte {
	base := filepath.Base(name)
	if base == "" || base == "." || base == ".." {
		return nil
	}
	for _, dir := range dirs {
		if b, err := os.ReadFile(filepath.Join(dir, base)); err == nil {
			return b
		}
	}
	return nil
}

// templateNameFromFrontMatter extracts a sanitized `template:` doctype name from
// a markdown document's leading YAML front matter. Returns "" when there is no
// front matter, no `template:` field, or the value isn't a safe bare name.
func templateNameFromFrontMatter(source []byte) string {
	fm := leadingFrontMatter(source)
	if fm == nil {
		return ""
	}
	var doc struct {
		Template string `yaml:"template"`
	}
	if err := yaml.Unmarshal(fm, &doc); err != nil {
		return ""
	}
	return sanitizeTemplateName(doc.Template)
}

// leadingFrontMatter returns the YAML between an opening `---` line (which must
// be the very first line) and the next `---` or `...` line, or nil if absent.
func leadingFrontMatter(src []byte) []byte {
	s := bytes.TrimPrefix(src, []byte{0xEF, 0xBB, 0xBF}) // strip a UTF-8 BOM
	if !bytes.HasPrefix(s, []byte("---\n")) && !bytes.HasPrefix(s, []byte("---\r\n")) {
		return nil
	}
	lines := bytes.Split(s, []byte("\n"))
	var buf bytes.Buffer
	for i := 1; i < len(lines); i++ {
		ln := bytes.TrimRight(lines[i], "\r")
		if bytes.Equal(ln, []byte("---")) || bytes.Equal(ln, []byte("...")) {
			return buf.Bytes()
		}
		buf.Write(lines[i])
		buf.WriteByte('\n')
	}
	return nil // unterminated block
}

// sanitizeTemplateName allows only a bare basename of [A-Za-z0-9_-] so a
// `template:` value can't traverse paths or name a partial. Returns "" if the
// value is empty or contains any other character.
func sanitizeTemplateName(name string) string {
	name = strings.TrimSpace(name)
	if name == "" {
		return ""
	}
	for _, r := range name {
		switch {
		case r == '-' || r == '_':
		case r >= 'a' && r <= 'z':
		case r >= 'A' && r <= 'Z':
		case r >= '0' && r <= '9':
		default:
			return ""
		}
	}
	return name
}
