import { updateUser } from 'officerdb';
import { OS_USERS_ENABLED, ensureOsUser, osUserHome } from '@@/os-user';
import { provisionSshAccess } from '@@/os-user-ssh';
import { seedShellConfig } from '@@/os-user-shell';
import { provisionClaudeCli } from '@@/os-user-claude';
import { provisionRootlessDocker } from '@@/os-user-docker';
import { provisionUserDirs } from '@@/data-path';

// Giving an account its Linux side: the directory skeleton, the Linux user, the confinement, the keys.
//
// Shared by account creation and the retry route, because they are the same work and there are two moments
// it has to happen at — the same reasoning as app-store/members.ts. The list of reasons a retry is needed is
// not exotic:
//
//   - the host was not set up for it when the account was made (`OFFICER_OS_USERS` off, no sudoers entry)
//   - an ancestor directory was not traversable, which is the one everybody hits once
//   - the owner wants to replace the inbound SSH key
//
// Before this existed the answer to all three was "delete the account and make it again", which loses the
// password, the dashboards and anything else keyed to the row — for what is really a retryable side effect.

export type OsProvisionOutcome = {
  osUser: string | null;
  sshPublicKey: string | null;
  /** Null on success. Everything here is non-fatal — the platform account works regardless. */
  error: string | null;
};

/**
 * Idempotent. Every step underneath adopts what already exists: `useradd` is skipped for an account whose
 * home already matches, the confinement re-applies mode bits, and an existing `id_ed25519` is kept rather
 * than rotated (it has been added to Gitea by then).
 *
 * Never throws. A failure here must not undo or block a platform account that otherwise works, so the error
 * is returned and the row keeps `osUser: null` — which is exactly what an account created before this
 * feature looks like, and which every consumer already handles.
 */
export async function provisionOsAccount(params: {
  userId: number;
  email: string;
  username: string;
  /** Inbound SSH key for `authorized_keys`. Already validated by the caller. */
  inboundKey?: string | null;
}): Promise<OsProvisionOutcome> {
  if (!OS_USERS_ENABLED) {
    return { osUser: null, sshPublicKey: null, error: 'per-user Linux accounts are not enabled on this server' };
  }

  // First, because a missing skeleton is the reason `useradd --home-dir … -M` would have nothing to point at.
  try {
    provisionUserDirs(params.email);
  } catch (ex) {
    return {
      osUser: null,
      sshPublicKey: null,
      error: `could not provision data directories: ${ex instanceof Error ? ex.message : String(ex)}`,
    };
  }

  const account = await ensureOsUser({ email: params.email, username: params.username });
  if (!account.ok) return { osUser: null, sshPublicKey: null, error: account.error };

  // SSH after the account, because everything it writes lives inside a home that is not ours until
  // `ensureOsUser` has chowned it away.
  const ssh = await provisionSshAccess({
    email: params.email,
    osUser: account.osUser,
    uid: account.uid,
    gid: account.gid,
    authorizedKey: params.inboundKey,
  });

  // The shell configuration. Late because its failure leaves nothing broken — the account works, the keys
  // work, the terminal opens; it just opens with zsh's bare defaults.
  const shell = await seedShellConfig({ email: params.email, uid: account.uid, gid: account.gid });

  // Their own `claude`, in their own home. After the shell because it installs into a home that is not ours
  // until `ensureOsUser` has chowned it away, and because the installer wants a working HOME.
  //
  // Only the binary. Logging in is the member's own act against their own Anthropic account — the platform
  // cannot do it for them and must not try, because the alternative is lending them the owner's credential.
  const claude = await provisionClaudeCli({ email: params.email, osUser: account.osUser });

  // Their own rootless Docker daemon. Last, and the most tolerant of failure: a host without the uidmap
  // package or a kernel that will not do rootless still gets a perfectly good account, minus containers.
  //
  // Provisioned for every member rather than behind a toggle, because "can I run a database to develop
  // against" should not be an administrative request. The cost — one daemon and one image cache per member —
  // is real and is written down in os-user-docker.ts.
  const docker = await provisionRootlessDocker({
    osUser: account.osUser,
    uid: account.uid,
    gid: account.gid,
    home: osUserHome(params.email),
  });

  // The Linux account is recorded either way: it exists, it is confined, and a member's terminal can run as
  // it. Only the keys are missing, and that is what the error says.
  const sshPublicKey = ssh.ok ? ssh.publicKey : null;
  await updateUser(params.userId, { osUser: account.osUser, osSshPublicKey: sshPublicKey });

  // Reported in order of consequence, not in order of execution: no keys matters more than a plain prompt,
  // which matters more than no containers. Only one is surfaced because the UI shows one line — the rest are
  // in the log.
  for (const step of [claude, shell, docker] as const) {
    if (!step.ok) console.warn(`[users] ${params.email}: ${step.error}`);
  }
  const error = !ssh.ok
    ? ssh.error
    : !claude.ok
      ? claude.error
      : !shell.ok
        ? shell.error
        : !docker.ok
          ? docker.error
          : null;
  return { osUser: account.osUser, sshPublicKey, error };
}
