import type { Handler } from 'hono';
import { createUser, getUserByEmail, getUserByUsername, USER_ROLES } from 'officerdb';
import type { UserRole } from 'officerdb';
import argon2 from 'argon2';
import * as errors from '@@/custom-errors';
import { validatePublicKey } from '@@/os-user-ssh';
import { provisionOsAccount } from './provision-os';
import { validatePassword } from '../auth/validate-password';
import { validateUsername } from '../auth/validate-username';
import { toPublicUser } from './manage-users';

// The owner creating a second account. Until this existed, `createUser` had exactly one call site —
// `auth/bootstrap.ts`, gated on an empty user table — so every non-owner account on any instance had
// been inserted into Postgres by hand.
//
// ── Why the owner sets the password ──
//
// The alternative is an invite: a token emailed to the person, who then sets their own. That is the
// better shape and it needs a mail path, a token table and an expiry policy. This is the honest
// intermediate: the owner types a password and tells the person, the same way they would hand over a
// wifi key. `passwordChangedAt` stays null, so nothing pretends the person chose it.
//
// ── Status is 'Active', deliberately ──
//
// The column defaults to 'Unverified' and `signin.ts` refuses anything that is not 'Active' with a bare
// UNAUTHORIZED. So an account created at the default would be indistinguishable from a wrong password,
// which is precisely the trap the hand-INSERT route fell into. An account the owner created in the admin
// UI is verified by definition — the owner is the verification.

/** Roles this route may assign. Never 'Super Admin' — see below. */
const ASSIGNABLE_ROLES = USER_ROLES.filter((r) => r !== 'Super Admin');

export const createUserHandler: Handler = async function (ctx) {
  const body = (ctx.get('body') ?? {}) as Record<string, unknown>;

  const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : '';
  const name = typeof body.name === 'string' ? body.name.trim() : '';
  const password = typeof body.password === 'string' ? body.password : '';
  const role = typeof body.role === 'string' ? body.role : 'Member';

  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw errors.BAD_REQUEST('Invalid email address');
  if (!name) throw errors.BAD_REQUEST('Name is required');

  // The same validators bootstrap uses. A member's password protects the same surface a member can
  // reach, so there is no argument for a weaker rule here — and two different rules would mean the
  // owner could create an account that could not then change its own password to something similar.
  const username = validateUsername(typeof body.username === 'string' ? body.username : undefined);
  validatePassword(password);

  // Refused rather than filtered, so the owner is told instead of quietly getting a Member.
  //
  // There is exactly one owner. The database only pins user 1's role — a row-level CHECK cannot say
  // "no OTHER row may hold this" — so a second Super Admin is storable, and `getOwnerUser()` would then
  // return whichever the query reached first. That answer decides the identity the agent sidecar runs
  // as, who reaches the vault and which origin is privileged, so it is not a thing to leave to a query
  // plan. If the owner ever needs to hand the server over, that is a deliberate transfer, not a dropdown.
  if (!(ASSIGNABLE_ROLES as readonly string[]).includes(role)) {
    throw errors.BAD_REQUEST(
      role === 'Super Admin'
        ? 'There is one server owner and it cannot be created here.'
        : `Role must be one of: ${ASSIGNABLE_ROLES.join(', ')}`,
    );
  }

  // The inbound SSH key, if the owner supplied one. Validated HERE rather than at use, so a bad paste is a
  // 400 on the form instead of an account that exists with a confusing warning attached.
  //
  // Optional by design: an account with no inbound key is platform-only, which is a perfectly good state.
  // The OUTBOUND key is generated regardless — see os-user-ssh.ts for why those are not alternatives.
  const rawKey = typeof body.sshPublicKey === 'string' ? body.sshPublicKey.trim() : '';
  let inboundKey: string | null = null;
  if (rawKey) {
    const checked = validatePublicKey(rawKey);
    if (!checked.ok) throw errors.BAD_REQUEST(`SSH public key: ${checked.error}`);
    inboundKey = checked.key;
  }

  // Checked before the insert purely for the message — both columns are unique, so the database is the
  // real guard and this is a race it can lose harmlessly (the insert then throws).
  if (await getUserByEmail(email)) throw errors.CONFLICT('An account with that email already exists');
  if (await getUserByUsername(username)) throw errors.CONFLICT('That username is taken');

  const user = await createUser({
    email,
    password: await argon2.hash(password),
    name,
    username,
    status: 'Active',
    role: role as UserRole,
  });

  // The Linux side: directories, the Linux user, the confinement, the keys. Non-fatal and reported rather
  // than thrown — this is a side effect of creating a platform account, and a failed `useradd` must not undo
  // an account that otherwise exists and can sign in. The row simply keeps `osUser: null`, which is what an
  // account created before this feature looks like, and which every consumer already handles.
  //
  // Retryable in place afterwards via POST /users/:id/provision-linux, so a host that was not ready when the
  // account was made does not cost anybody their password and dashboards.
  const os = await provisionOsAccount({ userId: user.id, email, username, role: role as UserRole, inboundKey });

  if (os.error) console.warn(`[users] created ${email} but its Linux side did not finish: ${os.error}`);

  return ctx.json(
    {
      user: { ...toPublicUser(user), osUser: os.osUser, osSshPublicKey: os.sshPublicKey },
      osUserError: os.error,
    },
    201,
  );
};
