package plugin

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"

	"github.com/alexei/tinyforge/internal/events"
	"github.com/alexei/tinyforge/internal/store"
)

// maxDeployStatusRunes bounds the persisted status. This is a defense-in-depth
// BACKSTOP, not a sanitizer.
//
// CALLER CONTRACT: deploy events are persisted indefinitely, rendered in the
// per-app timeline, AND egress off-box — error-severity events are forwarded
// to the global NotificationURL (cmd/server) and to operator-configured
// event-trigger webhooks (internal/events/dispatcher). Callers MUST therefore
// keep secrets and raw subprocess output (e.g. `docker compose` combined
// stderr, which can echo the deployed app's own secret-bearing logs) OUT of
// `status`; emit a curated, secret-free reason and keep verbose detail only in
// the returned error (server logs + admin deploy result, neither of which
// egresses). The cap below merely bounds blast radius if something slips
// through — 256 runes keeps a meaningful reason without letting a status
// become an unbounded sink.
const maxDeployStatusRunes = 256

// capDeployStatus truncates s to maxDeployStatusRunes runes, appending an
// ellipsis when it had to cut. Operating on the rune slice keeps the cut on
// a UTF-8 boundary so multibyte output can't be sliced mid-rune.
func capDeployStatus(s string) string {
	runes := []rune(s)
	if len(runes) <= maxDeployStatusRunes {
		return s
	}
	return string(runes[:maxDeployStatusRunes]) + "…"
}

// EmitDeployEvent records a workload-scoped deploy event in the event log
// and publishes it on the bus. Best-effort: logs and returns on failure,
// never blocks or fails the deploy. `source` is the per-kind event source
// string ("image","compose","static_site","dockerfile"); `status` is a
// short human status ("deploying","deployed","failed: <reason>").
//
// The metadata always carries workload_id so the per-app activity timeline
// can be reconstructed even by consumers that only read the JSON blob, and
// the dedicated workload_id column powers the indexed per-workload query.
func EmitDeployEvent(deps Deps, w Workload, source, status string) {
	// Audit logging is best-effort and must never crash a real deploy. The
	// production Deps always wires both, but guard so a missing bus/store
	// (e.g. a narrow unit test) degrades to a no-op instead of a panic.
	if deps.Store == nil || deps.Events == nil {
		return
	}

	// Derive severity from the raw status prefix BEFORE capping, then bound
	// the status that actually gets persisted/displayed/published.
	severity := "info"
	if strings.HasPrefix(status, "failed") {
		severity = "error"
	}
	status = capDeployStatus(status)
	message := fmt.Sprintf("%s: %s", w.Name, status)

	metaBytes, err := json.Marshal(map[string]string{
		"workload_id":   w.ID,
		"workload_name": w.Name,
		"status":        status,
	})
	if err != nil {
		slog.Error("plugin: marshal deploy event metadata",
			"source", source, "workload", w.ID, "error", err)
		metaBytes = []byte("{}")
	}
	metadata := string(metaBytes)

	evt, err := deps.Store.InsertEvent(store.EventLog{
		Source:     source,
		Severity:   severity,
		Message:    message,
		Metadata:   metadata,
		WorkloadID: w.ID,
	})
	if err != nil {
		slog.Error("plugin: failed to persist deploy event log",
			"source", source, "workload", w.ID, "error", err)
		return
	}

	deps.Events.Publish(events.Event{
		Type: events.EventLog,
		Payload: events.EventLogPayload{
			ID:         evt.ID,
			Source:     source,
			WorkloadID: w.ID,
			Severity:   severity,
			Message:    message,
			Metadata:   metadata,
			CreatedAt:  evt.CreatedAt,
		},
	})
}
