import { stat } from 'node:fs/promises';
import { join } from 'node:path';

// Starting, stopping and removing the containers behind a provisioned sidecar.
//
// ── The one rule ──
//
// `down`, never `down -v`. Uninstall removes containers; it does not remove data, and there is no
// option here that does. A user uninstalling Photos is saying "stop running this", not "delete my
// library", and for Immich or Jellyfin getting that wrong once is unrecoverable.
//
// The bind-mount convention makes that structural rather than a rule to remember: the data lives on the
// host inside the service directory, so `-v` — which only removes NAMED volumes — could not delete it
// even if someone added the flag. This module simply never gives them the chance.
//
// ── Why every function tolerates a missing directory ──
//
// Three of the four call sites can legitimately arrive with nothing there: an `existing` install never
// provisioned anything, a failed install may have died before writing the compose file, and a resumed
// uninstall may be re-running a step that already succeeded. Treating those as errors would make a row
// impossible to uninstall, which is the one state a user cannot get themselves out of.

export type ComposeResult = { ok: true; ran: boolean } | { ok: false; error: string };

async function hasCompose(serviceDir: string): Promise<boolean> {
  try {
    await stat(join(serviceDir, 'docker-compose.yaml'));
    return true;
  } catch {
    return false;
  }
}

async function compose(serviceDir: string, args: string[]): Promise<ComposeResult> {
  if (!(await hasCompose(serviceDir))) return { ok: true, ran: false };

  const proc = Bun.spawn(['docker', 'compose', '--project-directory', serviceDir, ...args], {
    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;

  if (code !== 0) return { ok: false, error: `${err || out}`.trim() || `docker compose ${args[0]} exited ${code}` };
  return { ok: true, ran: true };
}

/**
 * Bring the containers up. Used by enable, and by a resumed install whose containers were stopped.
 *
 * `up -d` rather than `start`: `start` only works on containers that already exist, and after a
 * `down` they do not. `up -d` covers both, and is a no-op against an unchanged compose file.
 */
export const composeUp = (serviceDir: string): Promise<ComposeResult> => compose(serviceDir, ['up', '-d']);

/**
 * Stop them, leaving them defined.
 *
 * Disable stops the container as well as the sidecar — there is no reason to leave Immich holding
 * memory while Photos is switched off — and `stop` rather than `down` is what makes re-enabling
 * instant rather than a fresh `up`.
 */
export const composeStop = (serviceDir: string): Promise<ComposeResult> => compose(serviceDir, ['stop']);

/**
 * Remove containers and networks. Uninstall only.
 *
 * No `-v`, and no `--rmi`: the images are shared and expensive to re-pull, and the volumes are the
 * user's data. Both are deliberate omissions rather than oversights.
 */
export const composeDown = (serviceDir: string): Promise<ComposeResult> => compose(serviceDir, ['down']);
