package module

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"text/template"
)

// Render writes out each ConfigFile declared by the manifest into dataPath,
// applying `values` (which typically combines user config_values + generated
// secrets) via Go text/template. Existing files are overwritten.
//
// Templates are resolved relative to the manifest's Dir. If a ConfigFile
// declares no Template, it is skipped — operators can also manage those
// files by direct edit via the (future) SFTP/file manager.
//
// Render uses each ConfigFile's default Template. Callers that know the
// instance's Steam branch should use RenderForBranch so version-specific
// templates (7DTD 3.0's serverconfig.v3.xml.tmpl) are selected.
func Render(manifest *Manifest, dataPath string, values map[string]string) error {
	return RenderForBranch(manifest, dataPath, values, "")
}

// RenderForBranch is Render with branch-aware template selection: each
// ConfigFile renders cf.TemplateForBranch(branch) instead of cf.Template, so
// an instance on a 3.0 branch picks serverconfig.v3.xml.tmpl while ≤2.6
// instances (branch "") keep the default template. branch is the normalized
// Steam branch (see resolveCreateBranch); "" means the module default.
func RenderForBranch(manifest *Manifest, dataPath string, values map[string]string, branch string) error {
	if manifest == nil {
		return fmt.Errorf("render: manifest is nil")
	}
	if manifest.Dir == "" {
		return fmt.Errorf("render: manifest.Dir is empty (loader must set it)")
	}
	for _, cf := range manifest.ConfigFiles {
		tmpl := cf.TemplateForBranch(branch)
		if tmpl == "" {
			continue
		}
		if err := renderOne(manifest.Dir, cf, tmpl, dataPath, values); err != nil {
			return err
		}
	}
	return nil
}

func renderOne(moduleDir string, cf ConfigFile, templateRel string, dataPath string, values map[string]string) error {
	templatePath := filepath.Join(moduleDir, templateRel)
	tmplData, err := os.ReadFile(templatePath)
	if err != nil {
		return fmt.Errorf("read template %s: %w", templatePath, err)
	}
	tmpl, err := template.New(cf.Path).Option("missingkey=zero").Parse(string(tmplData))
	if err != nil {
		return fmt.Errorf("parse template %s: %w", templateRel, err)
	}

	var buf bytes.Buffer
	ctx := struct {
		Values map[string]string
	}{Values: values}
	if err := tmpl.Execute(&buf, ctx); err != nil {
		return fmt.Errorf("execute template %s: %w", templateRel, err)
	}

	outPath := filepath.Join(dataPath, cf.Path)
	if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
		return fmt.Errorf("mkdir %s: %w", filepath.Dir(outPath), err)
	}
	if err := os.WriteFile(outPath, buf.Bytes(), 0o644); err != nil {
		return fmt.Errorf("write %s: %w", outPath, err)
	}
	return nil
}

// GenerateSecrets emits a random value for each manifest Secret flagged
// Generated. Values are URL-safe base64 of 24 bytes — safe in XML, shell,
// URLs, and long enough to resist brute force on a local RCON surface.
//
// Callers typically merge this with user-supplied config_values before
// rendering templates and resolving RCON passwords.
func GenerateSecrets(manifest *Manifest) (map[string]string, error) {
	if manifest == nil {
		return nil, nil
	}
	out := map[string]string{}
	for _, s := range manifest.Secrets {
		if !s.Generated {
			continue
		}
		val, err := randomToken(24)
		if err != nil {
			return nil, fmt.Errorf("generate secret %q: %w", s.Name, err)
		}
		out[s.Name] = val
	}
	return out, nil
}

// randomToken returns a URL-safe base64 string of n random bytes.
func randomToken(nBytes int) (string, error) {
	b := make([]byte, nBytes)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}
