// Package runtime implements the runtime abstraction (REQ-078, I-B-006).
//
// The Runtime interface decouples the scheduler/CLI from the underlying
// execution backend. Five implementations are provided:
//
//   - ProcessRuntime ("process") — wraps os/exec; LOCAL testing only.
//   - PodmanRuntime  ("podman")  — SSH-push podman run on the peer.
//   - WasmRuntime    ("wasm")    — wasmtime CLI via SSH (NO CGO; see
//     C01_WASMTIME_CGO_EVAL.md for the C-01 grill gate evaluation).
//   - PveVMRuntime   ("pve-vm")  — `qm` over SSH to a Proxmox peer.
//   - PveCTRuntime   ("pve-ct")  — `pct` over SSH to a Proxmox peer.
//
// The Registry is keyed by the runtime.one_of frontmatter value. The
// Alloc carries a Runtime field that can change on migration (R-004).
package runtime

import (
	"context"
	"fmt"
	"strings"

	"git.cloudinit.dev/coreci/orca/internal/jobspec"
)

// State is the lifecycle state of an alloc as observed by a Runtime.
type State string

const (
	// StatePending is the initial state before Prepare/Start.
	StatePending State = "pending"
	// StateRunning means the runtime reports the workload as up.
	StateRunning State = "running"
	// StateStopped means the workload exited cleanly (Stop called
	// or the process finished with exit 0).
	StateStopped State = "stopped"
	// StateFailed means the workload exited non-zero or could not
	// be reached.
	StateFailed State = "failed"
)

// Alloc is a runtime instance: a placement of a WorkloadSpec on a node.
// The Runtime field is the runtime.one_of value used to dispatch to the
// correct Runtime implementation; it can change on migration (R-004).
type Alloc struct {
	ID        string
	Spec      *jobspec.WorkloadSpec
	Node      string
	Namespace string
	Runtime   string
}

// Runtime is the execution-backend abstraction (REQ-078). Each method
// takes a context for cancellation/timeout. Implementations wrap a
// different execution backend (process, podman, wasm, pve-vm, pve-ct).
//
// Prepare is idempotent; Start/Stop/Status operate on the prepared
// runtime. The returned PID from Start is best-effort (container
// runtimes return the container ID hash as a synthetic PID).
type Runtime interface {
	// Prepare provisions prerequisites for the alloc (image pull,
	// vm create, etc.). It is idempotent.
	Prepare(ctx context.Context, alloc *Alloc) error
	// Start launches the workload and returns a best-effort PID (or
	// container/VM identifier encoded as a positive integer).
	Start(ctx context.Context, alloc *Alloc) (pid int, err error)
	// Stop terminates the workload, gracefully first then forcibly
	// after a grace period.
	Stop(ctx context.Context, alloc *Alloc) error
	// Status reports the current State of the alloc.
	Status(ctx context.Context, alloc *Alloc) (State, error)
}

// Registry maps runtime.one_of values to Runtime implementations. The
// zero value is NOT usable; construct one with NewRegistry.
type Registry struct {
	runtimes map[string]Runtime
}

// NewRegistry returns an empty Registry.
func NewRegistry() *Registry {
	return &Registry{runtimes: make(map[string]Runtime)}
}

// Register adds a Runtime under the given one_of key (e.g. "process",
// "podman", "wasm", "pve-vm", "pve-ct"). Registering the same key twice
// replaces the prior implementation (last-wins) — this is intentional
// so tests can override.
func (r *Registry) Register(name string, rt Runtime) {
	if r.runtimes == nil {
		r.runtimes = make(map[string]Runtime)
	}
	r.runtimes[name] = rt
}

// Get returns the Runtime registered under name, or an error if no
// runtime is registered for that key.
func (r *Registry) Get(name string) (Runtime, error) {
	rt, ok := r.runtimes[name]
	if !ok {
		return nil, fmt.Errorf("runtime: no backend registered for %q", name)
	}
	return rt, nil
}

// Prepare dispatches to the Runtime registered for alloc.Runtime. It
// returns an error if the runtime is unknown or Prepare fails.
func (r *Registry) Prepare(ctx context.Context, alloc *Alloc) error {
	rt, err := r.Get(alloc.Runtime)
	if err != nil {
		return err
	}
	return rt.Prepare(ctx, alloc)
}

// Start dispatches to the Runtime registered for alloc.Runtime.
func (r *Registry) Start(ctx context.Context, alloc *Alloc) (int, error) {
	rt, err := r.Get(alloc.Runtime)
	if err != nil {
		return 0, err
	}
	return rt.Start(ctx, alloc)
}

// Stop dispatches to the Runtime registered for alloc.Runtime.
func (r *Registry) Stop(ctx context.Context, alloc *Alloc) error {
	rt, err := r.Get(alloc.Runtime)
	if err != nil {
		return err
	}
	return rt.Stop(ctx, alloc)
}

// Status dispatches to the Runtime registered for alloc.Runtime.
func (r *Registry) Status(ctx context.Context, alloc *Alloc) (State, error) {
	rt, err := r.Get(alloc.Runtime)
	if err != nil {
		return StateFailed, err
	}
	return rt.Status(ctx, alloc)
}

// Names returns the registered runtime keys (unsorted).
func (r *Registry) Names() []string {
	out := make([]string, 0, len(r.runtimes))
	for k := range r.runtimes {
		out = append(out, k)
	}
	return out
}

// commandFor returns the command string to run for an alloc. If the
// alloc has a top-level Runtime block with a Command, that is used.
// Otherwise the first task's command is used (task-group allocs, P06).
// Returns ("", error) if no command can be derived.
func commandFor(alloc *Alloc) (string, error) {
	if alloc == nil || alloc.Spec == nil {
		return "", fmt.Errorf("runtime: nil alloc or spec")
	}
	if alloc.Spec.Runtime != nil && alloc.Spec.Runtime.Command != "" {
		return alloc.Spec.Runtime.Command, nil
	}
	if len(alloc.Spec.Tasks) > 0 && alloc.Spec.Tasks[0].Command != "" {
		return alloc.Spec.Tasks[0].Command, nil
	}
	return "", fmt.Errorf("runtime: alloc %s has no command", alloc.ID)
}

// imageFor returns the image/wasm-file path for an alloc (podman/wasm).
func imageFor(alloc *Alloc) (string, error) {
	if alloc == nil || alloc.Spec == nil || alloc.Spec.Runtime == nil {
		return "", fmt.Errorf("runtime: nil alloc/spec/runtime")
	}
	if alloc.Spec.Runtime.Image == "" {
		return "", fmt.Errorf("runtime: alloc %s has no image", alloc.ID)
	}
	return alloc.Spec.Runtime.Image, nil
}

// shellQuote single-quotes a string for safe shell interpolation over
// SSH exec. It escapes embedded single-quotes via the standard '\”
// idiom (POSIX shell). This mirrors internal/sshpush.shellQuote; the
// helper is duplicated to avoid an import cycle (sshpush is a leaf
// transport package, runtime depends on it but does not access its
// private helpers). Used to safely interpolate jobspec-supplied
// command strings into remote shell commands (REQ-119, F3 — command
// injection hardening).
func shellQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}
