package adminconsole

import (
	"bytes"
	"embed"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"path"
	"strings"
)

//go:embed templates
var templatesFS embed.FS

//go:embed assets
var assetsFS embed.FS

// Renderer holds the parsed admin console templates. It composes one template set
// per content page, each combining the shared layout (the page chrome and the
// "layout" entry template) with that page's "content" block, so rendering a page
// is a single ExecuteTemplate call against "layout".
type Renderer struct {
	pages map[string]*template.Template
}

// PageData is the view model passed to every admin console page. Title is the
// document title; ActiveNav marks the highlighted navigation entry; Data carries
// the page-specific payload (one of the *View types in views.go).
type PageData struct {
	Title     string
	ActiveNav string
	Data      any
}

// NewRenderer parses the embedded layout and every content page under
// templates/pages. It fails when a template cannot be parsed.
func NewRenderer() (*Renderer, error) {
	base, err := template.New("layout").ParseFS(templatesFS, "templates/layout.gohtml")
	if err != nil {
		return nil, fmt.Errorf("parse admin console layout: %w", err)
	}

	pageFiles, err := fs.Glob(templatesFS, "templates/pages/*.gohtml")
	if err != nil {
		return nil, fmt.Errorf("enumerate admin console pages: %w", err)
	}
	if len(pageFiles) == 0 {
		return nil, fmt.Errorf("admin console: no page templates found under templates/pages")
	}

	pages := make(map[string]*template.Template, len(pageFiles))
	for _, file := range pageFiles {
		name := strings.TrimSuffix(path.Base(file), ".gohtml")
		clone, err := base.Clone()
		if err != nil {
			return nil, fmt.Errorf("clone admin console layout for %q: %w", name, err)
		}
		if _, err := clone.ParseFS(templatesFS, file); err != nil {
			return nil, fmt.Errorf("parse admin console page %q: %w", name, err)
		}
		pages[name] = clone
	}

	return &Renderer{pages: pages}, nil
}

// MustNewRenderer is like NewRenderer but panics on error. The templates are
// embedded at build time, so a parse failure is a programmer error.
func MustNewRenderer() *Renderer {
	renderer, err := NewRenderer()
	if err != nil {
		panic(err)
	}
	return renderer
}

// Render writes the named page, wrapped in the shared layout, to w using data. It
// renders into an intermediate buffer first, so a mid-render failure never emits
// a partial document. It returns an error for an unknown page or a failed render.
func (r *Renderer) Render(w io.Writer, page string, data PageData) error {
	tmpl, ok := r.pages[page]
	if !ok {
		return fmt.Errorf("admin console: unknown page %q", page)
	}

	var buf bytes.Buffer
	if err := tmpl.ExecuteTemplate(&buf, "layout", data); err != nil {
		return fmt.Errorf("render admin console page %q: %w", page, err)
	}

	_, err := buf.WriteTo(w)
	return err
}

// Assets returns the embedded static asset tree rooted at the assets directory,
// suitable for serving under /_gm/assets/.
func Assets() (fs.FS, error) {
	return fs.Sub(assetsFS, "assets")
}
