import { readFile } from 'node:fs/promises';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { osUserHome } from './os-user';

// The shell a member gets when they open the terminal.
//
// ── Why there is a template at all ──
//
// A brand-new Linux account opens a shell with no prompt worth the name, no history, no completion and no
// colour — `useradd` copies /etc/skel, which on Ubuntu is a bash rc for a bash user. The account's shell is
// zsh, so it gets nothing. "Their own account" should not mean "a worse terminal than the owner's".
//
// ── What it is ──
//
// `shell-skel/zshrc` → `~/.zshrc`, and the platform's own `scripts/setup/starship.toml` → `~/.config/starship.toml`
// so a member's prompt is the same one the owner's install deploys. That file is the single source for both:
// setup.sh copies it for the owner and this copies it for everybody else, so the two cannot drift.
//
// ── Never clobbering someone's edits ──
//
// Written only when the file is ABSENT. That makes this safe to re-run, which matters because the retry
// button reprovisions an account whenever the owner presses it, and losing somebody's shell configuration to
// a maintenance action would be indefensible.
//
// The cost is that improving a template reaches new accounts only. That is the right way round, and
// `~/.zshrc.local` — sourced last, never written — is the pressure valve: it is where your own configuration
// goes, so nothing a future template does can reach it.

/** Where the templates live, relative to this file. */
const SKEL_DIR = join(import.meta.dir, 'shell-skel');
/** The prompt config the owner's own install uses — one file, both audiences. */
const STARSHIP_SRC = join(import.meta.dir, '../../scripts/setup/starship.toml');

type SudoResult = { ok: boolean; out: string };

async function sudo(args: string[]): Promise<SudoResult> {
  const proc = Bun.spawn(['sudo', '-n', ...args], { stdout: 'pipe', stderr: 'pipe' });
  const [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
  return { ok: (await proc.exited) === 0, out: `${out}${err}`.trim() };
}

/** The file's current contents, or null when it does not exist. Read as root: the home is 700 and theirs. */
async function currentContents(path: string): Promise<string | null> {
  const result = await sudo(['cat', path]);
  return result.ok ? result.out : null;
}

/**
 * Write one template file into the account's home, unless they have edited it.
 *
 * Returns what happened, so the caller can report "seeded" separately from "left alone" — an owner pressing
 * retry should not be told it rewrote files it deliberately did not touch.
 */
async function installTemplate(params: {
  content: string;
  dest: string;
  uid: number;
  gid: number;
}): Promise<{ ok: true; wrote: boolean } | { ok: false; error: string }> {
  // Written only when ABSENT. Not "absent or identical to the template" — I wrote that first and it is
  // meaningless: if the file already matches there is nothing to write, and if it differs we cannot tell an
  // edit from an older template version, so the only safe reading of "differs" is "theirs". Updating a
  // template therefore reaches new accounts only, which is the right trade for never eating someone's config.
  if ((await currentContents(params.dest)) !== null) return { ok: true, wrote: false };

  const dir = await mkdtemp(join(tmpdir(), 'officer-skel-'));
  const staged = join(dir, 'staged');
  try {
    await writeFile(staged, params.content, { mode: 0o600 });

    // The parent, explicitly and with the right owner. `install -D` creates missing parents but applies
    // `-o`/`-g` only to the FILE — measured: it left `~/.config` as root:root, so the member could read their
    // own starship.toml and could not write anything else into `.config`, which is where half of a shell's
    // tools want to keep state. A single wrong-owner directory in a home is the kind of thing that surfaces
    // weeks later as one tool mysteriously failing.
    const parent = dirname(params.dest);
    const madeParent = await sudo([
      'install',
      '-d',
      '-o',
      String(params.uid),
      '-g',
      String(params.gid),
      '-m',
      '700',
      parent,
    ]);
    if (!madeParent.ok) return { ok: false, error: `could not create ${parent}: ${madeParent.out}` };

    const written = await sudo([
      'install',
      '-o',
      String(params.uid),
      '-g',
      String(params.gid),
      '-m',
      '644',
      staged,
      params.dest,
    ]);
    if (!written.ok) return { ok: false, error: `could not write ${params.dest}: ${written.out}` };
    return { ok: true, wrote: true };
  } finally {
    await rm(dir, { recursive: true, force: true });
  }
}

export type ShellSeedResult = { ok: true; wrote: string[]; kept: string[] } | { ok: false; error: string };

/**
 * Give the account the standard shell configuration.
 *
 * Reports `wrote` and `kept` separately so a reprovision can say it left someone's edited files alone rather
 * than implying it rewrote them.
 */
export async function seedShellConfig(params: { email: string; uid: number; gid: number }): Promise<ShellSeedResult> {
  const home = osUserHome(params.email);

  let zshrc: string;
  let starship: string;
  try {
    zshrc = await readFile(join(SKEL_DIR, 'zshrc'), 'utf-8');
    starship = await readFile(STARSHIP_SRC, 'utf-8');
  } catch (ex) {
    return { ok: false, error: `could not read the shell templates: ${ex instanceof Error ? ex.message : ex}` };
  }

  const wrote: string[] = [];
  const kept: string[] = [];

  for (const [content, dest] of [
    [zshrc, join(home, '.zshrc')],
    [starship, join(home, '.config/starship.toml')],
  ] as const) {
    const result = await installTemplate({ content, dest, uid: params.uid, gid: params.gid });
    if (!result.ok) return result;
    (result.wrote ? wrote : kept).push(dest.replace(`${home}/`, '~/'));
  }

  return { ok: true, wrote, kept };
}
