package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// Secrets engine (DESIGN.md §5). The agent renders local templates that embed
// OpenBao secrets, e.g.:
//
//	# /etc/theta/templates/db.env.tpl
//	DB_USER="{{ bao "secret/data/nodes/node-42/db#username" }}"
//	DB_PASS="{{ bao "secret/data/nodes/node-42/db#password" }}"
//
// The agent parses the `{{ bao "path#key" }}` placeholders, fetches the secret
// values from the SSO (which holds the OpenBao access), renders each template to
// its target atomically (0600), and runs the configured reload. The agent never
// holds a Vault token.

var baoRe = regexp.MustCompile(`\{\{\s*bao\s+"([^"]+)"\s*\}\}`)

// renderSecrets renders every configured secret template. Called on a
// `render_secrets` command (signed) and on boot.
func renderSecrets(cfg *Config, exec Executor) error {
	if len(cfg.Secrets) == 0 {
		return nil
	}

	// Collect the unique secret paths referenced across all templates.
	pathSet := map[string]bool{}
	var paths []string
	for _, t := range cfg.Secrets {
		content, err := os.ReadFile(t.Template)
		if err != nil {
			log.Printf("Secrets: cannot read template %s: %v", t.Template, err)
			continue
		}
		for _, m := range baoRe.FindAllStringSubmatch(string(content), -1) {
			path := refPath(m[1])
			if !pathSet[path] {
				pathSet[path] = true
				paths = append(paths, path)
			}
		}
	}
	if len(paths) == 0 {
		return nil
	}

	secrets, err := fetchSecrets(cfg, paths)
	if err != nil {
		return err
	}

	for _, t := range cfg.Secrets {
		if err := renderOne(t, secrets, exec); err != nil {
			log.Printf("Secrets: render %s failed: %v", t.Template, err)
		}
	}
	return nil
}

func renderOne(t SecretTarget, secrets map[string]map[string]interface{}, exec Executor) error {
	content, err := os.ReadFile(t.Template)
	if err != nil {
		return err
	}

	out := baoRe.ReplaceAllStringFunc(string(content), func(match string) string {
		ref := baoRe.FindStringSubmatch(match)[1]
		path, key := refPath(ref), refKey(ref)
		if v, ok := secrets[path][key]; ok {
			return fmt.Sprintf("%v", v)
		}
		return ""
	})

	// Atomic write: temp file in the target's directory, then rename. 0600 — the
	// rendered file holds secrets.
	tmp, err := os.CreateTemp(filepath.Dir(t.Target), ".theta-secret-*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	if _, err := io.WriteString(tmp, out); err != nil {
		tmp.Close()
		os.Remove(tmpName)
		return err
	}
	tmp.Close()
	os.Chmod(tmpName, 0600)
	if err := os.Rename(tmpName, t.Target); err != nil {
		os.Remove(tmpName)
		return err
	}

	if t.Reload != "" {
		if _, err := exec.Execute("sh", "-c", t.Reload); err != nil {
			log.Printf("Secrets: reload %q failed: %v", t.Reload, err)
		}
	}
	return nil
}

// fetchSecrets asks the SSO for the given node-scoped secret paths.
func fetchSecrets(cfg *Config, paths []string) (map[string]map[string]interface{}, error) {
	base := strings.Replace(cfg.ServerURL, "wss://", "https://", 1)
	base = strings.Replace(base, "ws://", "http://", 1)
	url := strings.TrimRight(base, "/") + "/api/v1/agent/secrets"

	body, _ := json.Marshal(map[string]interface{}{"paths": paths})
	req, err := http.NewRequest("POST", url, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+cfg.Credential())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("secrets fetch failed: %s", resp.Status)
	}

	var out struct {
		Secrets map[string]map[string]interface{} `json:"secrets"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return out.Secrets, nil
}

// refPath returns the secret path from a `path#key` reference.
func refPath(ref string) string {
	path := ref
	if i := strings.Index(ref, "#"); i >= 0 {
		path = ref[:i]
	}
	if strings.HasPrefix(path, "resource/") {
		return "secret/data/resources/" + strings.TrimPrefix(path, "resource/") + "/conf"
	}
	if strings.HasPrefix(path, "resources/") {
		return "secret/data/resources/" + strings.TrimPrefix(path, "resources/") + "/conf"
	}
	if !strings.HasPrefix(path, "secret/") {
		return "secret/data/resources/" + path + "/conf"
	}
	return path
}

// refKey returns the key from a `path#key` reference.
func refKey(ref string) string {
	if i := strings.Index(ref, "#"); i >= 0 {
		return ref[i+1:]
	}
	return ""
}
