import { createRouter } from '../../create-router';
import * as errors from '../../custom-errors';
import { isSuperAdmin } from '../../super-admin';
import { listStore, install, setEnabled, uninstall } from '../../app-store/service';
import { byId, type InstallMode } from '../../app-store/catalogue';

// /api/app-store — what can be installed, what is installed, and the four verbs that change it.
//
// ── Owner only, explicitly ──
//
// Installing a sidecar starts a process on the machine, and provisioning one starts containers. That is
// an administrative act however many members share the server, so this router gates on the owner in its
// own right rather than relying on the capability layer alone. `server-admin` already covers it, and
// this is the belt to that braces — the same shape `/api/vault` uses, and for the same reason: a
// mistake here is not a leak of data, it is arbitrary process control.
//
// The per-user half lives elsewhere: `service_connections` is where a member's own credential goes, and
// members.ts is how they get one.

export const appStoreRouter = createRouter();

appStoreRouter.use(async (ctx, next) => {
  if (!(await isSuperAdmin(ctx.get('user')))) throw errors.FORBIDDEN('The app store is owner-only');
  return next();
});

/** GET /api/app-store — the catalogue joined to what has happened to each entry. */
appStoreRouter.get('/', async (ctx) => {
  return ctx.json({ items: await listStore() });
});

/**
 * POST /api/app-store/:id/install — install, or resume one that stopped.
 *
 * Resume is the same call deliberately: pressing the button after a failure and pressing it after
 * supplying the API key it was waiting for are one action to the user. What re-runs is decided by the
 * steps already recorded, not by which endpoint was hit.
 *
 * Answers with the outcome rather than a bare 200 — `blocked` is a normal result that the UI has to
 * render differently from success, and flattening it to "ok" would lose the reason and the link.
 */
appStoreRouter.post('/:id/install', async (ctx) => {
  const id = ctx.req.param('id');
  const entry = byId(id);
  if (!entry) throw errors.NOT_FOUND(`Unknown sidecar: ${id}`);

  const body = (ctx.get('body') ?? {}) as { mode?: string; values?: Record<string, string> };
  const mode = body.mode as InstallMode | undefined;
  if (!mode) throw errors.BAD_REQUEST('mode is required');
  if (!entry.modes.includes(mode)) {
    throw errors.BAD_REQUEST(`${entry.label} cannot be installed as '${mode}'`);
  }

  // The install log is collected rather than streamed for now. Streaming it into a terminal panel is
  // the intended shape — the lines are already produced one at a time — and needs a channel this route
  // does not have yet.
  const lines: string[] = [];
  const outcome = await install({ sidecarId: id, mode, values: body.values ?? {}, log: (l) => lines.push(l) });

  return ctx.json({ outcome, log: lines });
});

/** POST /api/app-store/:id/enable — start the sidecar (and, later, its container). */
appStoreRouter.post('/:id/enable', async (ctx) => {
  await setEnabled(ctx.req.param('id'), true);
  return ctx.json({ ok: true });
});

/** POST /api/app-store/:id/disable — stop it, keeping everything installed. */
appStoreRouter.post('/:id/disable', async (ctx) => {
  await setEnabled(ctx.req.param('id'), false);
  return ctx.json({ ok: true });
});

/**
 * POST /api/app-store/:id/uninstall — stop running this.
 *
 * A POST rather than a DELETE, because it is not a deletion: the sidecar's tables and the service
 * directory survive it. Calling it DELETE would suggest otherwise to the next person reading the route
 * table.
 */
appStoreRouter.post('/:id/uninstall', async (ctx) => {
  await uninstall(ctx.req.param('id'));
  return ctx.json({ ok: true });
});
