package plugin

import (
	"log/slog"

	"github.com/alexei/tinyforge/internal/crypto"
)

// ResolveSharedSecrets returns the applicable shared secrets (global, then
// app-scoped overlaying global) as a decrypted KEY->VALUE map. Decrypt
// failures log + skip the one entry (mirroring BuildWorkloadEnv). Best-effort:
// a store error logs and returns an empty map so a shared-secret outage never
// fails a deploy.
//
// The store orders the rows global-first (then app), so iterating in order and
// writing into the map makes a later app-scoped entry with the same Name
// overwrite the global default — the intended global < app precedence.
//
// NOTE: the compose plugin intentionally does NOT call this — compose env is
// YAML-defined and shared-secret support for compose is an explicit
// out-of-scope follow-up.
func ResolveSharedSecrets(deps Deps, appID, sourceName string) map[string]string {
	merged := map[string]string{}
	rows, err := deps.Store.ListApplicableSharedSecrets(appID)
	if err != nil {
		slog.Warn(sourceName+": list shared secrets", "app", appID, "error", err)
		return merged
	}
	for _, sec := range rows {
		value := sec.Value
		if sec.Encrypted {
			decrypted, err := crypto.Decrypt(deps.EncKey, sec.Value)
			if err != nil {
				slog.Warn(sourceName+": decrypt shared secret",
					"app", appID, "name", sec.Name, "error", err)
				continue
			}
			value = decrypted
		}
		merged[sec.Name] = value
	}
	return merged
}

// BuildWorkloadEnv flattens the applicable shared secrets plus workload_env
// rows into the KEY=VALUE list Docker expects. Shared by the source plugins
// (static, dockerfile) so they all handle decrypt failures the same way.
//
// Shared secrets are the low-precedence base layer; workload_env rows overlay
// them so a workload's own config always wins on a key conflict. A workload
// with no applicable shared secrets starts from an empty base, so the output
// is identical to the workload_env-only behavior that predated shared secrets.
//
// Encrypted rows are decrypted lazily so plaintext never lives in the store
// output. A decrypt failure logs and skips the entry rather than failing the
// whole deploy: bricking a sync/build because one rotated key missed an env
// entry would be worse than running with the variable unset and surfacing the
// warning.
//
// appID is the workload's app_id (plugin.Workload.GroupID), used to resolve
// app-scoped shared secrets. sourceName is the slog prefix the caller wants on
// the warning lines (e.g. "static source" / "dockerfile source") so existing
// log scrapers keep matching the per-source message text.
func BuildWorkloadEnv(deps Deps, workloadID, appID, sourceName string) []string {
	// Base layer: shared secrets (global, then app overlaying global).
	merged := ResolveSharedSecrets(deps, appID, sourceName)

	rows, err := deps.Store.ListWorkloadEnv(workloadID)
	if err != nil {
		slog.Warn(sourceName+": list workload env", "workload", workloadID, "error", err)
		// Still return whatever shared secrets resolved; a workload_env
		// outage shouldn't drop the shared defaults a deploy already has.
		return flattenEnvMap(merged)
	}
	for _, e := range rows {
		value := e.Value
		if e.Encrypted {
			decrypted, err := crypto.Decrypt(deps.EncKey, e.Value)
			if err != nil {
				slog.Warn(sourceName+": decrypt env value",
					"workload", workloadID, "key", e.Key, "error", err)
				continue
			}
			value = decrypted
		}
		merged[e.Key] = value // workload_env overrides shared secrets
	}
	return flattenEnvMap(merged)
}

// flattenEnvMap turns a KEY->VALUE map into the KEY=VALUE slice Docker
// expects. Order is unspecified (map iteration) — Docker treats env as a
// set, and callers that need determinism sort downstream.
func flattenEnvMap(m map[string]string) []string {
	out := make([]string, 0, len(m))
	for k, v := range m {
		out = append(out, k+"="+v)
	}
	return out
}
