package emitter

import (
	"context"
	"fmt"
	"os"
)

// AtomicWriter is the SSH-push transport surface that
// WriteTraefikDynamic uses to write the Traefik dynamic-config file
// atomically. It is the subset of *sshpush.Transport that the
// atomicity protocol depends on. Tests substitute a mock to assert
// the tmp+rename sequence (gate C-10) without a real SSH server.
//
// *sshpush.Transport satisfies this interface (the compile-time
// assertion lives in internal/sshpush to avoid an import cycle — the
// sshpush package imports emitter for fan-out, so this package cannot
// import sshpush).
type AtomicWriter interface {
	// WriteFileIdempotent writes content to peer:path atomically with
	// mode, returning written=true if the file was actually written
	// (content hash differed). Used by WriteTraefikDynamic to write
	// the .tmp sibling.
	WriteFileIdempotent(ctx context.Context, peer string, path string, content []byte, mode os.FileMode) (bool, error)
	// Exec runs a command on peer and returns its combined output.
	// Used by WriteTraefikDynamic to perform the atomic `mv -f
	// path.tmp path`.
	Exec(ctx context.Context, peer string, cmd string) ([]byte, error)
}

// WriteTraefikDynamic writes a Traefik dynamic-config file atomically
// (gate C-10: tmpfile + fsync + rename). The protocol is:
//
//  1. Write content to <path>.tmp via WriteFileIdempotent. The
//     underlying sshpush transport writes the tmp file in the same
//     directory as the target with mode-appended naming, fsyncs, and
//     renames — but we add an extra hop here so the *Traefik* file is
//     only ever observed at its final path after a single atomic
//     rename event that Traefik's fsnotify watcher sees.
//  2. `mv -f <path>.tmp <path>` on the peer (atomic rename on POSIX).
//     Traefik's fsnotify watcher picks up the rename → reload.
//
// On a malformed config Traefik logs an error and holds the
// last-good config (documented Traefik behavior; the C-10 test
// verifies the tmp+rename sequence so a half-written file is never
// observed by Traefik — the only window where Traefik can read the
// file is after the rename, which is atomic on POSIX).
//
// The mode is 0644 (Traefik reads the dynamic dir as root; the lead
// applier chmods after the rename).
func WriteTraefikDynamic(ctx context.Context, t AtomicWriter, peer string, path string, content []byte) error {
	if t == nil {
		return fmt.Errorf("traefik: atomic writer is nil")
	}
	if path == "" {
		return fmt.Errorf("traefik: path is empty")
	}
	tmpPath := path + ".tmp"
	if _, err := t.WriteFileIdempotent(ctx, peer, tmpPath, content, 0o644); err != nil {
		return fmt.Errorf("traefik: write tmp %s: %w", tmpPath, err)
	}
	// Atomic rename on POSIX. `mv -f` overwrites an existing target
	// without prompting. The rename is atomic; Traefik's fsnotify
	// watcher observes a single IN_MOVED_TO event.
	renameCmd := fmt.Sprintf("mv -f %s %s", shellQuoteLocal(tmpPath), shellQuoteLocal(path))
	if _, err := t.Exec(ctx, peer, renameCmd); err != nil {
		return fmt.Errorf("traefik: rename %s -> %s: %w", tmpPath, path, err)
	}
	return nil
}

// shellQuoteLocal single-quotes a path for safe shell interpolation on
// the peer. It escapes embedded single-quotes via the standard '\”
// idiom (close the single-quoted string, escape the literal single
// quote, reopen the single-quoted string). This is a local
// re-implementation (the sshpush package has its own) so the emitter
// layer does not depend on the transport package's private helpers —
// the AtomicWriter interface keeps the boundary clean for testing.
func shellQuoteLocal(s string) string {
	var b []byte
	b = append(b, '\'')
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c == '\'' {
			// close quote, escape the literal single-quote, reopen.
			b = append(b, '\'', '\\', '\'', '\'')
			continue
		}
		b = append(b, c)
	}
	b = append(b, '\'')
	return string(b)
}
