import { loadDevices, isDialableDevice } from '../devices/registry.js';
import { machineId, normalizeHost } from '../machine-id.js';
import { RUSH_CHANNELS } from './providers/rush.js';
import { resolvePeerTarget, sshCapture } from '../session/remote/remote-list.js';
import { buildRemoteAgentsInvocation, stripClixml } from '../hosts/remote-cmd.js';
/**
 * Env marker set on the forwarded `agents send` so a box that received a
 * forwarded owner notify never forwards it onward. `agents send` does not route
 * through this module today, so this is defense-in-depth against a future
 * consumer wiring forwarding into the send path and creating a fan-out loop.
 */
export const OWNER_FORWARD_GUARD_ENV = 'AGENTS_OWNER_NO_FORWARD';
/** Per-peer SSH deadline for a one-shot owner delivery. */
const PEER_SEND_TIMEOUT_MS = 15_000;
/**
 * True when the resolved owner transport is the macOS-only rush family — the
 * one case a Linux/headless box structurally cannot deliver and a peer can.
 * `openclaw-telegram` and the local `desktop`/`mailbox` providers are NOT
 * rush-backed, so a failure there is not a wrong-OS problem and is left as-is.
 * Mirrors the same `RUSH_CHANNELS.includes(transport)` gate in owner-sink.ts.
 */
export function isRushBackedTransport(channel, meta) {
    const transport = meta.notify?.transports?.[channel] ?? channel;
    return RUSH_CHANNELS.includes(transport);
}
/**
 * Decide which peers can deliver the owner notification, in try order. Pure —
 * no I/O — so the channel gate, the recursion guard, self-exclusion, and the
 * capability/ordering rules are unit-testable without a live tailnet.
 *
 * Only macOS peers are candidates: the rush owner transport is macOS-only, so a
 * Linux/Windows peer could not deliver it either. The configured
 * `interactive.host` (the box the operator sits at, where rush is signed in) is
 * tried first when it is among the candidates.
 */
export function planOwnerForward(channel, meta, devices, self, opts = {}) {
    if (opts.guarded)
        return { candidates: [], skip: 'guarded' };
    if (!isRushBackedTransport(channel, meta))
        return { candidates: [], skip: 'not-rush-backed' };
    const selfId = normalizeHost(self);
    const capable = devices.filter((d) => d.platform === 'macos' && isDialableDevice(d) && normalizeHost(d.name) !== selfId);
    const interactiveHost = typeof meta.config?.interactiveHost === 'string'
        ? normalizeHost(meta.config.interactiveHost)
        : undefined;
    const rank = (name) => (interactiveHost && normalizeHost(name) === interactiveHost ? 0 : 1);
    const candidates = capable
        .map((d) => normalizeHost(d.name))
        .sort((a, b) => rank(a) - rank(b));
    if (candidates.length === 0)
        return { candidates: [], skip: 'no-capable-peer' };
    return { candidates };
}
async function sendOnPeer(machine, text, channel, target, envelope = {}) {
    const peer = await resolvePeerTarget(machine);
    if (!peer)
        return undefined;
    const args = ['send', '--channel', channel, '--to', target, '--text', text, '--json'];
    if (envelope.thread)
        args.push('--thread', envelope.thread);
    if (envelope.from)
        args.push('--from', envelope.from);
    // Reuse the one injection-tested remote-command builder every `--device`
    // dispatch uses (posix `bash -lc` / Windows `-EncodedCommand`), rather than a
    // second hand-rolled quoting path on a security-sensitive seam. The env map is
    // the loop guard, exported the same way every remote invocation exports env.
    const remoteCmd = buildRemoteAgentsInvocation(args, undefined, peer.os, { [OWNER_FORWARD_GUARD_ENV]: '1' });
    const capture = await sshCapture(peer.target, remoteCmd, PEER_SEND_TIMEOUT_MS);
    if (capture.code !== 0)
        return undefined;
    try {
        const parsed = JSON.parse(stripClixml(capture.stdout));
        if (parsed && typeof parsed === 'object' && typeof parsed.ok === 'boolean')
            return parsed;
    }
    catch {
        return undefined;
    }
    return undefined;
}
/**
 * Try each capable peer in order and return the first successful delivery. A
 * peer that is unreachable or reports its own delivery failure is skipped and
 * the next is tried; the first `ok:true` wins and stops the sweep so the owner's
 * phone rings once. Resolves `undefined` when forwarding does not apply or no
 * peer delivered — the caller then keeps its original local error.
 *
 * The transport (`send`) is injectable so the try-order / first-success / stop
 * orchestration is testable without a live SSH host; the default runs the real
 * explicit-destination `agents send` over SSH.
 */
export async function forwardOwnerNotifyToPeer(text, channel, target, meta, opts = {}) {
    // Cheap, I/O-free gate first: a box that already received a forward, or an
    // owner channel that isn't the macOS-only rush family, can never forward — so
    // a normal local success/failure never pays a device-registry disk read.
    if (process.env[OWNER_FORWARD_GUARD_ENV] === '1')
        return undefined;
    if (!isRushBackedTransport(channel, meta))
        return undefined;
    const self = opts.self ?? machineId();
    let devices = opts.devices;
    if (!devices) {
        try {
            devices = Object.values(await loadDevices());
        }
        catch {
            return undefined; // no registry, nothing to forward to
        }
    }
    const plan = planOwnerForward(channel, meta, devices, self);
    if (plan.candidates.length === 0)
        return undefined;
    const send = opts.send ?? sendOnPeer;
    for (const machine of plan.candidates) {
        const result = await send(machine, text, channel, target, opts.envelope);
        if (result?.ok)
            return result;
    }
    return undefined;
}
