package emitter

import (
	"errors"
	"fmt"
	"strings"

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

// SyncthingEmitter is the Layer-4 emitter for the per-namespace
// Syncthing config files (REQ-081). For every volume in spec.Volumes
// that carries a `replicate:` list, the emitter renders one Syncthing
// `config.xml` at /etc/syncthing/orca-<ns>-<volume>.xml containing the
// content-addressed folder (storage.FolderID), the device list (all
// peers in the namespace), and the volume path.
//
// Syncthing configs are kind-agnostic — they apply to any workload
// (Job, Service, DaemonSet) that declares a replicated volume. The
// emitter is therefore registered on the Registry under every
// kind:runtime key the other emitters use, but it is intended to be
// composed by the caller (the caller renders both the systemd unit and
// the Syncthing config for the same spec). For the v0.9-P09 spike the
// emitter is invoked directly; the composition lands in a later phase.
//
// The emitter is CLI-side only: it renders the XML; the SSH-push
// transport SCPs the file to each peer; the Syncthing apt package on
// the peer reads it. No Syncthing Go client is linked.
type SyncthingEmitter struct{}

// syncthingConfigDir is the canonical directory for rendered Syncthing
// configs on a peer (R-005). The emitter writes one file per
// replicated volume.
const syncthingConfigDir = "/etc/syncthing"

// localDeviceAddress is the address the local peer's own device entry
// uses. "dynamic" tells Syncthing this peer is the listener (it does
// not dial out to itself).
const localDeviceAddress = "dynamic"

// peerDeviceAddressTemplate renders the Sync listen address for a
// remote peer. The peer hostname (from the ReplicateTo list) is used
// as the host; the default Sync port is 22000.
const peerDeviceAddressTemplate = "tcp://%s:22000"

// Render renders one Syncthing config XML file per replicated volume
// in the spec. A volume is "replicated" when its VolumeSpec carries a
// non-empty `replicate:` list — encoded in VolumeSpec.Source as the
// comma-separated peer list prefixed with `replicate:` (e.g.
// `replicate:peer-b,peer-c`). This keeps the VolumeSpec shape stable
// (the v0.9 VolumeSpec has no explicit Replicate field; the emitter
// parses it from Source).
//
// For each replicated volume, the emitter:
//
//  1. Builds a VolumeReplication (namespace = spec.Name's namespace,
//     volume name, source path = VolumeSpec.Target).
//  2. Builds the peer device list (the local peer + every peer in the
//     `replicate:` list). The local peer's device ID is derived
//     deterministically from the node hostname (the real device ID is
//     discovered from the peer registry in a later phase; for the
//     spike a deterministic placeholder keeps the rendered config
//     byte-stable).
//  3. Calls storage.RenderSyncthingConfig + storage.RenderSyncthingXML
//     to produce the config file content.
//
// Returns an error if the spec is nil or the node is nil (the node is
// required to identify the local peer). Workloads with no replicated
// volumes return an empty (non-nil) slice — the emitter is a no-op for
// them.
func (SyncthingEmitter) Render(spec *jobspec.WorkloadSpec, node *Node) ([]File, error) {
	if spec == nil {
		return nil, errors.New("emitter/syncthing: spec is nil")
	}
	if node == nil {
		return nil, errors.New("emitter/syncthing: node is nil (local peer unknown)")
	}
	var files []File
	for _, vol := range spec.Volumes {
		peers, ok := parseReplicateList(vol.Source)
		if !ok || len(peers) == 0 {
			continue
		}
		path := vol.Target
		if strings.TrimSpace(path) == "" {
			path = vol.Source
		}
		rep := storage.VolumeReplication{
			Namespace:   spec.Name,
			VolumeName:  vol.Name,
			SourcePath:  path,
			ReplicateTo: peers,
			SyncMode:    "sendreceive",
		}
		devices := buildSyncthingDevices(node, peers)
		cfg, err := storage.RenderSyncthingConfig(rep, devices)
		if err != nil {
			return nil, fmt.Errorf("emitter/syncthing: render config for volume %q: %w", vol.Name, err)
		}
		xml, err := storage.RenderSyncthingXML(cfg)
		if err != nil {
			return nil, fmt.Errorf("emitter/syncthing: render xml for volume %q: %w", vol.Name, err)
		}
		files = append(files, File{
			Path:    fmt.Sprintf("%s/orca-%s-%s.xml", syncthingConfigDir, spec.Name, vol.Name),
			Content: xml,
			Mode:    "0644",
		})
	}
	return files, nil
}

// parseReplicateList extracts the peer list from a VolumeSpec.Source
// value of the form `replicate:peer-b,peer-c`. Returns the peer list
// and true when the Source carries a replicate directive; returns nil
// and false otherwise (the volume is not replicated).
func parseReplicateList(source string) ([]string, bool) {
	s := strings.TrimSpace(source)
	if !strings.HasPrefix(s, "replicate:") {
		return nil, false
	}
	rest := strings.TrimPrefix(s, "replicate:")
	parts := strings.Split(rest, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			out = append(out, p)
		}
	}
	return out, true
}

// buildSyncthingDevices builds the Syncthing device list for the
// rendered config. The local peer (the node the config is being
// rendered for) is first, with the local device address ("dynamic").
// Each remote peer in the replicate list follows, with a
// tcp://<peer>:22000 address. Device IDs are deterministic placeholders
// derived from the peer name (the real device IDs are discovered from
// the peer registry in a later phase; the placeholder keeps the
// rendered config byte-stable across re-runs).
func buildSyncthingDevices(node *Node, peers []string) []storage.SyncthingDevice {
	devices := make([]storage.SyncthingDevice, 0, len(peers)+1)
	devices = append(devices, storage.SyncthingDevice{
		ID:      syncthingDeviceID(node.Hostname),
		Name:    node.Hostname,
		Address: localDeviceAddress,
	})
	for _, p := range peers {
		devices = append(devices, storage.SyncthingDevice{
			ID:      syncthingDeviceID(p),
			Name:    p,
			Address: fmt.Sprintf(peerDeviceAddressTemplate, p),
		})
	}
	return devices
}

// syncthingDeviceID returns a deterministic, stable device-ID
// placeholder for the given peer name. The placeholder is a fixed
// 52-char string (Syncthing device IDs are 52-char base32) derived by
// padding the peer name. The real device ID (discovered from the peer
// registry / cluster/peers/) replaces this in a later phase; for the
// v0.9 spike the placeholder keeps the rendered config byte-stable so
// the SSH-push idempotency check works.
func syncthingDeviceID(peerName string) string {
	const idLen = 52
	name := strings.TrimSpace(peerName)
	if len(name) >= idLen {
		return name[:idLen]
	}
	pad := strings.Repeat("X", idLen-len(name))
	return name + pad
}
