// Starting and stopping a sidecar's PM2 process.
//
// ── Why shell out rather than use the pm2 API ──
//
// PM2 is already the supervisor for every process here and the ecosystem file is already the definition
// of how each one runs. Importing pm2 as a library would put a second thing in charge of that, and the
// failure mode is two supervisors disagreeing about what should be running. The CLI is the same
// interface a human uses, which also means an owner can undo anything the app store did with a command
// they already know.
//
// ── The one PM2 fact that matters here ──
//
// `pm2 start <name>` only works for a process PM2 has already seen. A sidecar that has never run is not
// in PM2's list, and starting it by name fails with "process or namespace not found". So a first
// install has to start it FROM THE ECOSYSTEM FILE, and every start after that can go by name.
//
// `startProcess` handles both without the caller having to know which case it is in, because the caller
// genuinely cannot know: a resumed install, a re-enable after a restart, and a first install all arrive
// at the same line.

export type Pm2Result = { ok: true } | { ok: false; error: string };

// The file officer-setup GENERATES at $OFFICER_ROOT/platform. It is not in the repository.
//
// `[open]` As of 2026-08-13 it contains the SIX core processes and nothing else, so
// `pm2 start ecosystem.config.cjs --only officer-jellyfin` finds no such app and does nothing. Installing
// a plugin has to append its entry here before starting it — that is the plugin system's job and it is
// not built. Until it is, the app store can stop a running sidecar but cannot start one that was never
// in the file.
const ECOSYSTEM = 'ecosystem.config.cjs';

async function pm2(args: string[], cwd: string): Promise<{ code: number; out: string }> {
  const proc = Bun.spawn(['pm2', ...args], { cwd, stdout: 'pipe', stderr: 'pipe' });
  const [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
  const code = await proc.exited;
  return { code, out: `${out}${err}`.trim() };
}

/** Is this process known to PM2 at all — running, stopped or errored? */
export async function isKnownToPm2(name: string, cwd: string): Promise<boolean> {
  const { code, out } = await pm2(['jlist'], cwd);
  if (code !== 0) return false;
  try {
    const list = JSON.parse(out) as Array<{ name?: string }>;
    return list.some((p) => p.name === name);
  } catch {
    // jlist printing something unparseable means PM2 is in a state we should not guess about. Saying
    // "not known" makes the caller start from the ecosystem file, which is the safe direction — it
    // works whether or not the process exists.
    return false;
  }
}

/**
 * Start a sidecar, whether or not PM2 has seen it before.
 *
 * `--only` is what keeps this surgical: starting the ecosystem file without it would bring up every
 * process in the estate, which on a light install is precisely the thing the user opted out of.
 */
export async function startProcess(name: string, cwd: string): Promise<Pm2Result> {
  const known = await isKnownToPm2(name, cwd);
  const args = known ? ['start', name] : ['start', ECOSYSTEM, '--only', name];

  const { code, out } = await pm2(args, cwd);
  if (code !== 0) return { ok: false, error: out || `pm2 ${args.join(' ')} exited ${code}` };
  return { ok: true };
}

/**
 * Stop it, leaving it in PM2's list.
 *
 * Stop rather than delete, deliberately. A stopped process still appears in `pm2 list` as stopped,
 * which is the honest picture for a disabled sidecar — deleting it would make a disabled service
 * indistinguishable from one that was never installed, both to PM2 and to anyone looking.
 */
export async function stopProcess(name: string, cwd: string): Promise<Pm2Result> {
  const { code, out } = await pm2(['stop', name], cwd);
  // Stopping something that is not there is the desired end state, not an error. This happens on a
  // resumed uninstall, and treating it as a failure would leave the row un-uninstallable.
  if (code !== 0 && !/not found|doesn't exist/i.test(out)) {
    return { ok: false, error: out || `pm2 stop ${name} exited ${code}` };
  }
  return { ok: true };
}

/** Remove it from PM2 entirely. Uninstall only — see `stopProcess` for why disable does not do this. */
export async function deleteProcess(name: string, cwd: string): Promise<Pm2Result> {
  const { code, out } = await pm2(['delete', name], cwd);
  if (code !== 0 && !/not found|doesn't exist/i.test(out)) {
    return { ok: false, error: out || `pm2 delete ${name} exited ${code}` };
  }
  return { ok: true };
}

/** 'online' | 'stopped' | 'errored' | … , or null when PM2 has never heard of it. */
export async function processStatus(name: string, cwd: string): Promise<string | null> {
  const { code, out } = await pm2(['jlist'], cwd);
  if (code !== 0) return null;
  try {
    const list = JSON.parse(out) as Array<{ name?: string; pm2_env?: { status?: string } }>;
    return list.find((p) => p.name === name)?.pm2_env?.status ?? null;
  } catch {
    return null;
  }
}
