/**
 * Fleet onboarding-injection (#620).
 *
 * Fleet agents are born not knowing how to reach their peers — the root cause of
 * a spawned agent's failed first send. When an agent boots via `mosaic yolo
 * <runtime>` (→ composeContract → system prompt), we append a comms cheat-sheet
 * + peer roster so it can talk to the orchestrator and other agents immediately.
 *
 * Cross-host aware: a peer may carry `host`/`ssh` (a deliberate pre-federation
 * stopgap — manual cross-host listing; federation/W1 auto-discovers later), so a
 * w-jarvis agent is born knowing the exact `-H` command to reach a dragon-lin
 * peer. Same-host peers render the short form.
 *
 * Standalone (no fleet.ts import) to keep launch.ts's prompt path free of the
 * heavy fleet command module. The roster is parsed leniently — the cheat-sheet
 * is best-effort onboarding, never a hard dependency.
 */

import { existsSync, readFileSync } from 'node:fs';
import { homedir, hostname } from 'node:os';
import { join } from 'node:path';

export interface CommsPeer {
  name: string;
  /** Roster `class` (orchestrator | enhancer | implementer | worker | …). */
  className: string;
  /** Host the peer runs on; absent ⇒ the fleet host (same host). */
  host?: string;
  /** SSH target (user@host) for a cross-host peer; renders the `-H` form. */
  ssh?: string;
  /** tmux socket the peer's session lives on; absent ⇒ default socket (no `-L`). */
  socket?: string;
}

/**
 * Lenient parse of a fleet `roster.yaml` for agent name/class/host/ssh. Avoids a
 * dependency on the full fleet roster parser; the format is `- name:` list items
 * with `class:`/`host:`/`ssh:` siblings under `agents:`.
 */
export function parseRosterAgents(yamlText: string): CommsPeer[] {
  const peers: CommsPeer[] = [];
  let current: CommsPeer | null = null;
  let inAgents = false;
  const scalar = (line: string, key: string): string | null => {
    const m = line.match(new RegExp(`^\\s*${key}:\\s*["']?([^"'#]+?)["']?\\s*$`));
    return m ? (m[1] as string).trim() : null;
  };
  for (const rawLine of yamlText.split('\n')) {
    const line = rawLine.replace(/\s+$/, '');
    if (/^agents:\s*$/.test(line)) {
      inAgents = true;
      continue;
    }
    if (!inAgents) continue;
    // A new top-level key (no leading space) ends the agents block.
    if (/^\S/.test(line)) break;

    const nameMatch = line.match(/^\s*-\s*name:\s*["']?([A-Za-z0-9._-]+)["']?\s*$/);
    if (nameMatch) {
      if (current) peers.push(current);
      current = { name: nameMatch[1] as string, className: 'worker' };
      continue;
    }
    if (!current) continue;
    const cls = scalar(line, 'class');
    if (cls) current.className = cls;
    const host = scalar(line, 'host');
    if (host) current.host = host;
    const ssh = scalar(line, 'ssh');
    if (ssh) current.ssh = ssh;
    const socket = scalar(line, 'socket');
    if (socket) current.socket = socket;
  }
  if (current) peers.push(current);
  return peers;
}

export interface FleetCommsOptions {
  /** This agent's name (it is excluded from its own peer list). */
  selfName: string;
  /** All roster agents (including self; filtered out internally). */
  agents: CommsPeer[];
  /** Host the fleet runs on (short hostname) — the same-host baseline. */
  fleetHost: string;
  /** Absolute path to agent-send.sh in this install. */
  agentSendPath: string;
}

/** Is this peer on a different host than the fleet baseline? */
function isRemote(peer: CommsPeer, fleetHost: string): boolean {
  return peer.host !== undefined && peer.host !== fleetHost;
}

/**
 * Render the exact agent-send command to reach a peer (session = agent name).
 * Data-driven per peer: a named `socket` → `-L <socket>`; an unset socket → the
 * default tmux socket (no `-L`). A cross-host peer adds `-H <ssh|host>`.
 */
export function renderPeerReach(peer: CommsPeer, fleetHost: string, agentSendPath: string): string {
  const parts = [agentSendPath];
  if (peer.socket) parts.push('-L', peer.socket); // unset ⇒ default socket, no -L
  if (isRemote(peer, fleetHost)) parts.push('-H', peer.ssh ?? (peer.host as string));
  parts.push('-s', peer.name, '-m', '"…"');
  return parts.join(' ');
}

/**
 * Build the `# Fleet Comms` onboarding block (pure markdown). Returns '' when
 * the agent has no peers (a single-agent roster has no one to talk to).
 */
export function buildFleetCommsBlock(opts: FleetCommsOptions): string {
  const peers = opts.agents.filter((a) => a.name !== opts.selfName);
  if (peers.length === 0) return '';

  const orchestrator = peers.find((p) => p.className === 'orchestrator');
  const rows = peers
    .map((p) => {
      const where = isRemote(p, opts.fleetHost)
        ? `${p.className} · host \`${p.host}\``
        : p.className;
      const role = p.className === 'orchestrator' ? `${where} ← point of contact` : where;
      return `| ${p.name} | ${role} | \`${renderPeerReach(p, opts.fleetHost, opts.agentSendPath)}\` |`;
    })
    .join('\n');

  const orchLine = orchestrator
    ? `Your point of contact is **${orchestrator.name}** (the orchestrator) — route questions, ` +
      `status, and decisions there.`
    : `This fleet has no orchestrator in its roster; coordinate with your peers directly.`;

  return `# Fleet Comms — reach your peers

You are **${opts.selfName}** in this fleet. Your comms identity is \`[${opts.fleetHost}:${opts.selfName}]\` —
that is the \`<src>\` other agents see and reply to. Reach other agents (durable tmux sessions) with the
Mosaic comms tool at \`${opts.agentSendPath}\`. The **Reach** column below is the exact command per peer:
same-host peers use the short form (no \`-H\`); cross-host peers include \`-H <user@host>\`.

## Peers

| Agent | Role | Reach (session = agent name) |
| ----- | ---- | ---------------------------- |
${rows}

${orchLine}

## Conventions

- Every message carries a self-identifying preamble \`[<src_host>:<src_session> -> <dst_host>:<dst_session>]\` — \`agent-send.sh\` adds it automatically.
- **To reply, FLIP the preamble:** address your reply to the sender's \`src\` (their host:session becomes your \`-s\`/\`-H\`).
- \`agent-send.sh\` (a.k.a. \`agent send --verify\`) confirms the message was **ACCEPTED** at the destination prompt — not merely injected. Prefer it for anything that matters.`;
}

/**
 * Read the fleet roster from `mosaicHome` and build the comms block for
 * `selfName`. Returns '' when there is no roster, the agent is not in it, or
 * there are no peers — onboarding is best-effort and never throws.
 */
export function readFleetCommsBlock(
  mosaicHome: string,
  selfName: string | undefined,
  fleetHost: string = hostname().split('.')[0] || 'localhost',
): string {
  if (!selfName) return '';
  const rosterPath = join(mosaicHome, 'fleet', 'roster.yaml');
  if (!existsSync(rosterPath)) return '';
  let text: string;
  try {
    text = readFileSync(rosterPath, 'utf-8');
  } catch {
    return '';
  }
  const agents = parseRosterAgents(text);
  if (!agents.some((a) => a.name === selfName)) return ''; // not a member of this fleet
  return buildFleetCommsBlock({
    selfName,
    agents,
    fleetHost,
    agentSendPath: join(mosaicHome, 'tools', 'tmux', 'agent-send.sh'),
  });
}

/** Result of resolving a comms-block emit request — see `mosaic fleet comms-block`. */
export interface CommsBlockResult {
  /** True when a cheat-sheet was produced; false maps to stderr + non-zero exit. */
  ok: boolean;
  /** The Fleet-Comms cheat-sheet (empty unless ok). */
  output: string;
  /** Operator-facing reason when !ok. */
  error?: string;
}

/**
 * Resolve the Fleet-Comms cheat-sheet for an explicit <role>, backing the
 * `mosaic fleet comms-block <role>` command. Unlike readFleetCommsBlock — which
 * returns '' on any miss so composeContract can no-op silently during a launch —
 * this NEVER silently emits empty: an unknown role or missing roster yields
 * ok:false + an operator-facing reason, so the CLI surfaces it (stderr + exit 1)
 * rather than printing nothing. That makes it safe to preview any peer's view,
 * e.g. `mosaic fleet comms-block coder0-0`.
 */
export function resolveCommsBlock(
  mosaicHome: string,
  role: string | undefined,
  fleetHost?: string,
): CommsBlockResult {
  if (!role) {
    return { ok: false, output: '', error: 'comms-block requires a <role> argument' };
  }
  const block = fleetHost
    ? readFleetCommsBlock(mosaicHome, role, fleetHost)
    : readFleetCommsBlock(mosaicHome, role);
  if (!block) {
    const rosterPath = join(mosaicHome, 'fleet', 'roster.yaml');
    return {
      ok: false,
      output: '',
      error: existsSync(rosterPath)
        ? `role "${role}" is not a member of the fleet roster at ${rosterPath}`
        : `no fleet roster at ${rosterPath}`,
    };
  }
  return { ok: true, output: block };
}

/** Default mosaic home (mirrors launch.ts), for callers that don't pass one. */
export const DEFAULT_MOSAIC_HOME_FOR_COMMS = join(homedir(), '.config', 'mosaic');
