package emitter

import (
	"fmt"
	"strings"

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

// SocketEmitter renders the systemd directives that implement the
// R-007 socket-plumbing contract: workloads bind to
// /run/orca/alloc-<id>/port-<name>.sock unless overridden via
// service.bind = "127.0.0.1" (the only documented opt-in).
//
// The systemd side of the contract uses two directives:
//
//   - RuntimeDirectory=orca/alloc-<alloc-id> — systemd creates
//     /run/orca/alloc-<alloc-id>/ owned by the service user (orca:orca)
//     with mode 0750. The directory is removed when the unit stops
//     (RuntimeDirectory= semantics). P08 emits one RuntimeDirectory=
//     line per port so each port's socket directory is created; the
//     alloc-id placeholder is spec.Name (the real alloc-id is assigned
//     by the scheduler at submit time — see allocIDFor).
//
//   - ExecStartPre= — only when service.bind is "127.0.0.1" (the TCP
//     opt-in). In that case the workload binds a TCP port directly
//     (no socket), and the ExecStartPre is a placeholder that records
//     the bind (the actual bind happens in the process; the directive
//     is a no-op marker so operators can see the bind mode in the unit
//     file). When service.bind is empty (the default), the workload
//     binds the socket and no ExecStartPre is emitted for sockets.
//
// The socket path format is /run/orca/alloc-<alloc-id>/port-<port-name>.sock
// where alloc-id is a PLACEHOLDER (spec.Name) — the real alloc-id is
// assigned at submit time by the scheduler. The placeholder is
// documented in the rendered unit via a comment so operators reading
// the unit file understand the substitution.
//
// P08 is a PLAN/plumbing layer — the actual socket activation (socket
// unit files, systemd socket-activation passing the pre-bound socket
// fd to the process) lands in v0.10. P08 just renders the
// RuntimeDirectory= lines and the optional TCP-bind ExecStartPre so
// the directory exists at runtime.
type SocketEmitter struct{}

// runtimeDirectoryRoot is the systemd RuntimeDirectory path root.
// systemd joins this with the RuntimeDirectory= value to create
// /run/orca/alloc-<id>. The leading slash is implicit in systemd
// (RuntimeDirectory= is relative to /run).
const runtimeDirectoryRoot = "orca"

// SocketPath returns the R-007 socket path for a port on the given
// alloc-id. The alloc-id is the placeholder spec.Name when the real
// alloc-id is not yet known (the scheduler assigns the real alloc-id
// at submit time).
func SocketPath(allocID, portName string) string {
	return fmt.Sprintf("/run/orca/alloc-%s/port-%s.sock", allocID, portName)
}

// RenderSocketLines renders the systemd directives that implement
// the R-007 socket plumbing for the given spec. The lines are returned
// WITHOUT a trailing newline so the caller (the systemd emitter) can
// append them to the [Service] block with consistent formatting.
//
// The returned lines are:
//
//   - one RuntimeDirectory= line per port (so each port's socket
//     directory is created by systemd at unit start).
//   - a comment documenting the alloc-id placeholder.
//   - when service.bind is "127.0.0.1", an ExecStartPre= marker that
//     records the TCP opt-in (the actual bind is in the process).
//
// Returns an empty slice when the spec has no ports (no socket
// plumbing needed — e.g. a Job or a port-less DaemonSet).
func (SocketEmitter) RenderSocketLines(spec *jobspec.WorkloadSpec) []string {
	if spec == nil || len(spec.Ports) == 0 {
		return nil
	}
	allocID := allocIDForSocket(spec)
	var lines []string
	// One RuntimeDirectory= per port. systemd dedupes identical
	// values, but we emit one per port so the unit file is
	// self-documenting (each port maps to a directory entry).
	for _, p := range spec.Ports {
		lines = append(lines, fmt.Sprintf("RuntimeDirectory=%s/alloc-%s", runtimeDirectoryRoot, allocID))
		// Document the socket path this directory serves. systemd
		// ignores comment lines (lines starting with '#').
		lines = append(lines, fmt.Sprintf("# socket: %s", SocketPath(allocID, p.Name)))
	}
	// TCP opt-in: when service.bind is 127.0.0.1, the workload binds
	// a TCP port directly instead of the socket. We emit an
	// ExecStartPre marker so the bind mode is visible in the unit
	// file. The actual bind is in the process; the marker is a
	// no-op (echo to journald).
	if spec.Service != nil && strings.TrimSpace(spec.Service.Bind) != "" {
		if isTCPOptIn(spec.Service.Bind) {
			for _, p := range spec.Ports {
				lines = append(lines, fmt.Sprintf("ExecStartPre=/bin/echo orca: bind %s port %s (tcp, R-007 opt-in)", spec.Service.Bind, p.Name))
			}
		}
	}
	return lines
}

// allocIDForSocket returns the alloc-id placeholder for the spec. The
// real alloc-id is assigned by the scheduler at submit time; P08 uses
// spec.Name as a deterministic placeholder so the rendered unit is
// stable across re-renders. This mirrors the Traefik emitter's
// allocIDFor (which uses the node hostname for the Traefik
// dynamic-config server URL); the systemd unit is per-alloc, so
// spec.Name is the right placeholder here.
func allocIDForSocket(spec *jobspec.WorkloadSpec) string {
	if spec == nil || strings.TrimSpace(spec.Name) == "" {
		return "<allocID>"
	}
	return spec.Name
}

// isTCPOptIn returns true when the bind value is the documented
// 127.0.0.1 TCP opt-in (R-007). Other valid IPs (::1, etc.) are also
// TCP opt-ins (any non-empty bind opts out of the socket default); we
// only emit the marker for 127.0.0.1 because that is the only
// documented opt-in per the PRD — other IPs are accepted by the
// schema validator but are operator-specific and we do not
// second-guess them.
func isTCPOptIn(bind string) bool {
	return strings.TrimSpace(bind) == "127.0.0.1"
}
