/**
 * Exact roster-resolved fleet communications contract (#766).
 *
 * The runtime composer and `mosaic fleet` command surface share the canonical
 * v1 roster resolver. This module never probes tmux, guesses an SSH target, or
 * mutates an active session.
 */

import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { homedir, hostname } from 'node:os';
import { join, resolve } from 'node:path';
import { readRegularFileSecure } from './secure-file.js';
import { resolveBrainHome } from './brain-home.js';
import {
  parseFleetRosterV1,
  resolveInstalledFleetRosterPath,
  getRosterAgent,
  type FleetAgent,
  type FleetRoster,
} from './fleet-roster-v1.js';

export interface FleetCommsOptions {
  /** Exact current roster member. */
  selfName: string;
  /** Canonically resolved roster. */
  roster: FleetRoster;
  /** Stable fleet-host baseline for members whose roster host is absent. */
  localHost: string;
  /** Absolute helper path in this installation. */
  agentSendPath: string;
}

export interface CommsBlockResult {
  ok: boolean;
  output: string;
  error?: string;
}

export interface ResolvedFleetIdentity {
  readonly roster: FleetRoster;
  readonly member: FleetAgent;
  readonly requestedName: string;
  readonly agentSendPath: string;
  readonly localHost: string;
}

export interface FleetIdentityResult {
  ok: boolean;
  identity?: ResolvedFleetIdentity;
  error?: string;
}

export interface PeerCommandResult {
  ok: boolean;
  command: string;
  error?: string;
}

export const FLEET_COMMS_TOOLS_CONTRACT = 'fleet-comms-contract: 1';
const MAX_TOOLS_CONTRACT_BYTES = 256 * 1024;

function shortHostname(): string {
  return hostname().split('.')[0] || 'localhost';
}

function resolvedHost(agent: FleetAgent, fleetHost: string): string {
  return agent.host ?? fleetHost;
}

function displaySocket(socket: string): string {
  return socket || '(default)';
}

function knownNames(roster: FleetRoster, except?: string): string {
  return roster.agents
    .filter((agent) => agent.name !== except)
    .map((agent) => agent.name)
    .join(', ');
}

function missingMemberError(roster: FleetRoster, selfName: string): string {
  return `Agent "${selfName}" is not in the fleet roster. Known exact names: ${knownNames(roster)}. Select an exact roster name; do not infer or fuzzy-match a tmux session.`;
}

/** Render one shell argument without changing already-safe exact values. */
function shellArg(value: string): string {
  if (/^[A-Za-z0-9_./:@=+-]+$/.test(value)) return value;
  return `'${value.replaceAll("'", `'"'"'`)}'`;
}

/** Render the exact command for one peer. Throws rather than guessing. */
export function renderPeerReach(
  peer: FleetAgent,
  selfHost: string,
  fleetHost: string,
  rosterSocket: string,
  agentSendPath: string,
): string {
  const parts = [shellArg(agentSendPath)];
  if (rosterSocket) parts.push('-L', shellArg(rosterSocket));

  const peerHost = resolvedHost(peer, fleetHost);
  if (peerHost !== selfHost) {
    if (!peer.ssh) {
      throw new Error(
        `Cross-host peer "${peer.name}" (${peerHost}) requires an explicit roster ssh target; refusing to substitute its host value.`,
      );
    }
    parts.push('-H', shellArg(peer.ssh));
  }
  parts.push('-s', shellArg(peer.name), '-m', '"…"');
  return parts.join(' ');
}

/** Resolve one requested peer without fuzzy lookup. */
export function resolvePeerCommand(
  roster: FleetRoster,
  selfName: string,
  peerName: string,
  localHost: string,
  agentSendPath: string,
): PeerCommandResult {
  const self = roster.agents.find((agent) => agent.name === selfName);
  if (!self) return { ok: false, command: '', error: missingMemberError(roster, selfName) };
  const peer = roster.agents.find((agent) => agent.name === peerName && agent.name !== selfName);
  if (!peer) {
    return {
      ok: false,
      command: '',
      error:
        `Peer "${peerName}" is absent from the fleet roster. Known exact peer names: ${knownNames(roster, selfName)}. ` +
        `Run \`mosaic agent comms-block ${selfName}\` to rediscover exact rendered rows; do not infer or fuzzy-match a tmux session.`,
    };
  }
  try {
    return {
      ok: true,
      command: renderPeerReach(
        peer,
        resolvedHost(self, localHost),
        localHost,
        roster.tmux.socketName,
        agentSendPath,
      ),
    };
  } catch (error) {
    return {
      ok: false,
      command: '',
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

interface ResolvedRow {
  readonly peer: FleetAgent;
  readonly host: string;
  readonly socket: string;
  readonly command: string;
}

function resolveRows(opts: FleetCommsOptions, self: FleetAgent): readonly ResolvedRow[] {
  const selfHost = resolvedHost(self, opts.localHost);
  return opts.roster.agents
    .filter((agent) => agent.name !== opts.selfName)
    .map(
      (peer): ResolvedRow => ({
        peer,
        host: resolvedHost(peer, opts.localHost),
        socket: opts.roster.tmux.socketName,
        command: renderPeerReach(
          peer,
          selfHost,
          opts.localHost,
          opts.roster.tmux.socketName,
          opts.agentSendPath,
        ),
      }),
    );
}

function commsGeneration(
  self: FleetAgent,
  selfHost: string,
  selfSocket: string,
  helper: string,
  rows: readonly ResolvedRow[],
): string {
  const canonical = JSON.stringify({
    self: { ...self, resolvedHost: selfHost, resolvedSocket: selfSocket, helper },
    peers: rows.map((row) => ({
      ...row.peer,
      resolvedHost: row.host,
      resolvedSocket: row.socket,
      exactCommand: row.command,
    })),
  });
  return createHash('sha256').update(canonical).digest('hex');
}

/** Build the authoritative Markdown contract for one exact roster member. */
export function buildFleetCommsBlock(opts: FleetCommsOptions): string {
  const self = opts.roster.agents.find((agent) => agent.name === opts.selfName);
  if (!self) throw new Error(missingMemberError(opts.roster, opts.selfName));
  const rows = resolveRows(opts, self);
  const selfHost = resolvedHost(self, opts.localHost);
  const selfSocket = opts.roster.tmux.socketName;
  const generation = commsGeneration(self, selfHost, selfSocket, opts.agentSendPath, rows);
  const orchestrator = rows.find((row) => row.peer.className === 'orchestrator');
  const peerSection =
    rows.length === 0
      ? 'This roster has no peers. Do not invent a target.'
      : `| Agent | Role | Host | Socket | Exact command |
| ----- | ---- | ---- | ------ | ------------- |
${rows
  .map((row) => {
    const pointOfContact = row.peer.className === 'orchestrator' ? ' ← point of contact' : '';
    return `| ${row.peer.name} | ${row.peer.className}${pointOfContact} | ${row.host} | ${displaySocket(row.socket)} | \`${row.command}\` |`;
  })
  .join('\n')}`;
  const contact = orchestrator
    ? `Your point of contact is **${orchestrator.peer.name}**. Select that exact peer row for status, questions, and decisions.`
    : rows.length === 0
      ? 'No peer coordination target exists in this roster.'
      : 'This roster has no orchestrator. Select an exact peer row for coordination.';
  const soloAuthority =
    rows.length === 0
      ? `\n## Solo authority boundaries\n\nThis member is normalized as role/class **${self.className}**. The roster grants no peer, orchestrator, or remote communication authority. Do not send, infer a target, or claim fleet coordination until an exact peer is added to the canonical roster and this block is recomposed.\n`
      : '';

  return `# Fleet Comms — authoritative exact targets

## Local identity

- Host: \`${selfHost}\`
- Agent/session: \`${self.name}\`
- Role/class: \`${self.className}\`
- tmux socket: \`${displaySocket(selfSocket)}\`
- Helper: \`${opts.agentSendPath}\`
- Comms generation: \`${generation}\`

The roster-resolved rows below are the only valid operational targets. Select the row whose Agent value
exactly matches the requested peer. Never invent, substitute, or fuzzy-match host, session, socket, SSH,
or helper-path values. If the peer is absent, stop and run \`mosaic agent comms-block ${self.name}\` to
rediscover this exact member's rows; if it is still absent, report the unknown peer.

## Peers

${peerSection}

${contact}
${soloAuthority}
## Context freshness

This block is a snapshot; Mosaic does not rewrite an active agent's context. Compare its Comms generation
with fresh output from \`mosaic agent comms-block ${self.name}\`. If they differ, report stale composed
context and have an authorized operator relaunch only this exact roster member with
\`mosaic fleet restart ${self.name}\`. Do not restart or mutate a session automatically.`;
}

function validateAgentSendHelper(path: string, mosaicHome: string): string | undefined {
  try {
    readRegularFileSecure(path, {
      root: mosaicHome,
      executable: true,
      // The helper tree may live under the framework config home while this
      // caller's mosaicHome is the brain; both are framework-owned roots.
      symlinkTargetRoots: [resolveBrainHome(mosaicHome)],
    });
    return undefined;
  } catch (error) {
    const reason = error instanceof Error ? error.message : String(error);
    return `helper is unavailable or unsafe: ${path} (${reason})`;
  }
}

/**
 * Framework install homes probed for tools/tmux/agent-send.sh (stack#1380 M2).
 * The helper ships with the FRAMEWORK install, which on split-home layouts is
 * the config home — not the brain (~/.mosaic carries fleet state, no tools).
 */
function frameworkHelperHomes(mosaicHome: string): string[] {
  const homes = [resolve(mosaicHome)];
  const envHome = process.env['MOSAIC_HOME'];
  if (envHome && envHome.trim() !== '' && resolve(envHome) !== resolve(mosaicHome)) {
    homes.push(resolve(envHome));
  }
  const configDefault = join(homedir(), '.config', 'mosaic');
  if (resolve(configDefault) !== resolve(mosaicHome)) homes.push(configDefault);
  return homes;
}

function resolveAgentSendHelper(mosaicHome: string): { path: string; error?: string } {
  const homes = frameworkHelperHomes(mosaicHome);
  for (const home of homes) {
    const helper = join(home, 'tools', 'tmux', 'agent-send.sh');
    if (!existsSync(helper)) continue;
    const error = validateAgentSendHelper(helper, home);
    if (!error) return { path: helper };
    // Present but unsafe: surface that verdict instead of silently probing on.
    return { path: helper, error };
  }
  return {
    path: join(resolve(mosaicHome), 'tools', 'tmux', 'agent-send.sh'),
    error:
      `fleet helper agent-send.sh was not found under any framework install home ` +
      `(${homes.map((h) => join(h, 'tools', 'tmux', 'agent-send.sh')).join('; ')}). ` +
      `Verify the framework install for this host (the helper ships with the framework ` +
      `config home; the brain home carries fleet state, not tools) and have an authorized ` +
      `operator restore it if missing.`,
  };
}

function helperFailureGuidance(reason: string): string {
  // stack#1380 M5a: `mosaic update --repair-tools` is a forbidden remedy on the
  // affected estate (and wrong for a layout/missing-helper failure). Name the
  // actual recovery shape instead.
  return `${reason}. Verify the framework install provides tools/tmux/agent-send.sh under the framework config home and that the roster resolves (split-home layouts symlink the roster into the brain); contact the operator if it persists; no active context or session was rewritten.`;
}

export function resolveFleetIdentity(
  mosaicHome: string,
  requestedName: string | undefined,
  localHost: string = shortHostname(),
): FleetIdentityResult {
  if (!requestedName) return { ok: true };
  const helper = resolveAgentSendHelper(mosaicHome);
  if (helper.error) return { ok: false, error: helperFailureGuidance(helper.error) };
  const agentSendPath = helper.path;

  // Split-home layouts unify the roster by symlinking
  // <configHome>/fleet/roster.yaml -> <brain>/fleet/roster.yaml. The secure
  // read resolves that framework-created symlink when the brain is a
  // sanctioned target root (stack#1380 M1).
  const rosterSymlinkRoots = [resolveBrainHome(mosaicHome)];

  let rosterPath: string;
  try {
    rosterPath = resolveInstalledFleetRosterPath(mosaicHome);
  } catch (error) {
    return {
      ok: false,
      error: `cannot inspect fleet roster.yaml: ${error instanceof Error ? error.message : String(error)}; refusing JSON fallback because fallback is allowed only when YAML is absent`,
    };
  }
  if (!existsSync(rosterPath)) {
    return {
      ok: false,
      error: `no fleet roster at ${join(mosaicHome, 'fleet', 'roster.yaml')} or ${join(mosaicHome, 'fleet', 'roster.json')}`,
    };
  }

  let roster: FleetRoster;
  try {
    roster = parseFleetRosterV1(
      readRegularFileSecure(rosterPath, {
        root: mosaicHome,
        symlinkTargetRoots: rosterSymlinkRoots,
      }).content.toString('utf8'),
      rosterPath.endsWith('.json') ? 'json' : 'yaml',
    );
  } catch (error) {
    return {
      ok: false,
      error: `invalid fleet roster at ${rosterPath}: ${error instanceof Error ? error.message : String(error)}`,
    };
  }

  try {
    return {
      ok: true,
      identity: {
        roster,
        member: getRosterAgent(roster, requestedName),
        requestedName,
        agentSendPath,
        localHost,
      },
    };
  } catch {
    return { ok: false, error: missingMemberError(roster, requestedName) };
  }
}

/** Render Fleet Comms from one already-resolved canonical member identity. */
export function buildResolvedFleetCommsBlock(identity: ResolvedFleetIdentity): string {
  return buildFleetCommsBlock({
    selfName: identity.member.name,
    roster: identity.roster,
    localHost: identity.localHost,
    agentSendPath: identity.agentSendPath,
  });
}

/**
 * Read and resolve the installed roster for runtime composition. A requested
 * fleet identity fails closed; only a genuinely non-fleet launch (no selfName)
 * is a quiet no-op.
 */
export function readFleetCommsBlock(
  mosaicHome: string,
  selfName: string | undefined,
  localHost: string = shortHostname(),
): CommsBlockResult {
  const resolved = resolveFleetIdentity(mosaicHome, selfName, localHost);
  if (!resolved.ok) return { ok: false, output: '', error: resolved.error };
  if (!resolved.identity) return { ok: true, output: '' };
  try {
    return { ok: true, output: buildResolvedFleetCommsBlock(resolved.identity) };
  } catch (error) {
    return {
      ok: false,
      output: '',
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

/** Backing resolver for `mosaic agent comms-block <exact-member>`. */
export function resolveCommsBlock(
  mosaicHome: string,
  exactMember: string | undefined,
): CommsBlockResult {
  if (!exactMember) {
    return {
      ok: false,
      output: '',
      error: 'comms-block requires an exact <exact-member> argument',
    };
  }
  return readFleetCommsBlock(mosaicHome, exactMember);
}

function expectedContractVersion(content: Buffer | string): boolean {
  return content.toString().includes(`<!-- ${FLEET_COMMS_TOOLS_CONTRACT} -->`);
}

function boundedContractDigest(
  path: string,
  mosaicHome: string,
): { digest?: string; versionOk: boolean } {
  try {
    const content = readRegularFileSecure(path, {
      root: mosaicHome,
      maxBytes: MAX_TOOLS_CONTRACT_BYTES,
    }).content;
    return {
      digest: createHash('sha256').update(content).digest('hex'),
      versionOk: expectedContractVersion(content),
    };
  } catch {
    return { versionOk: false };
  }
}

function replacementGuidance(): string {
  // stack#1380 M5a: never recommend the forbidden --repair-tools remedy from
  // error text; name the operator-verified recovery shape instead.
  return `Verify the installed TOOLS contract against the framework source with an authorized operator (the installed file must byte-match the supported current version) and have the operator explicitly relaunch the exact roster member. The active context was not rewritten.`;
}

/** Detect preserved installed TOOLS.md drift without changing it. */
export function renderToolsContractStatus(mosaicHome: string): string {
  const installedPath = join(mosaicHome, 'TOOLS.md');
  const sourcePath = join(mosaicHome, 'defaults', 'TOOLS.md');
  if (!existsSync(installedPath)) {
    return `# Fleet Comms Installation Status\n\nInstalled TOOLS.md is missing at \`${installedPath}\`. ${replacementGuidance()}`;
  }

  const installed = boundedContractDigest(installedPath, mosaicHome);
  const source = boundedContractDigest(sourcePath, mosaicHome);
  if (!source.digest || !source.versionOk) {
    return `# Fleet Comms Installation Status\n\nThe bounded framework source contract at \`${sourcePath}\` is unavailable or does not declare the expected \`${FLEET_COMMS_TOOLS_CONTRACT}\` version. Run \`mosaic update\` to restore framework source data, verify again, then have an authorized operator explicitly relaunch the exact roster member. The installed file and active context were not rewritten.`;
  }
  if (installed.versionOk && installed.digest === source.digest) return '';

  return `# Fleet Comms Installation Status\n\nInstalled TOOLS.md is unavailable, has the wrong contract version, or does not byte-match the bounded framework source contract \`${FLEET_COMMS_TOOLS_CONTRACT}\`. ${replacementGuidance()}`;
}

export const DEFAULT_MOSAIC_HOME_FOR_COMMS = join(homedir(), '.config', 'mosaic');
