import type { OfficerContext } from './routes';
import { badRequest, notFound, methodNotAllowed, readJson } from './routes';
import { activeClient } from './active';
import { toPreAuthKey, arrayField } from './normalize';

// Pre-auth key routes — /_officer/keys/*. These are the tokens a machine uses to join the tailnet.
//
// The one thing that matters here: since 0.28 Headscale stores pre-auth keys HASHED and returns the real
// secret ONLY in the create response. Every later list returns it masked as `hskey-auth-<prefix>-***`. A
// creation response that the UI drops is a key the owner can never recover — it has to be shown once, with
// a copy affordance, and the API has to make the difference legible. `key` is non-null exactly once.
//
// That "exactly once" is enforced by call path, not by inspecting the value: keys created before 0.28 are
// still plaintext upstream and Headscale hands them back in full from the LIST endpoint for backwards
// compatibility. So listing passes reveal:false and drops the secret unconditionally; only createKey
// reveals. A server with history in it would otherwise leak live keys into the browser's query cache.
//
// Also note the shape of the delete/expire pair: expire takes the id in a POST BODY, delete takes it in a
// query STRING, and neither is a REST-shaped path. Both are hidden behind ordinary Officer routes.

/** Default lifetime when the caller doesn't pick one; matches Headscale's own CLI default. */
const DEFAULT_EXPIRY_DAYS = 90;
const MAX_EXPIRY_DAYS = 3650;

async function listKeys(ctx: OfficerContext): Promise<Response> {
  const client = await activeClient(ctx.userId);
  if (client instanceof Response) return client;

  // 0.29 lists every user's keys in one call (pre-0.29 required a ?user= filter and one call per user).
  const body = await client.call('/api/v1/preauthkey');
  const keys = arrayField(body, 'preAuthKeys').map((raw) => toPreAuthKey(raw, { reveal: false }));

  // Usable keys first, then by newest — a spent key is history, an active one is the thing you came for.
  const rank = { active: 0, used: 1, expired: 2 } as const;
  keys.sort((a, b) => rank[a.status] - rank[b.status] || (b.createdAt ?? '').localeCompare(a.createdAt ?? ''));

  return Response.json({ keys });
}

async function createKey(ctx: OfficerContext): Promise<Response> {
  const client = await activeClient(ctx.userId);
  if (client instanceof Response) return client;

  const body = await readJson(ctx.req);
  if (!body) return badRequest('expected a JSON body');

  // CreatePreAuthKey takes a numeric user ID — unlike the node list filter, which takes a username. The
  // two are easy to confuse and the failure is a confusing upstream error, so it's validated here.
  const userId = typeof body.userId === 'string' ? body.userId.trim() : '';
  if (!/^\d+$/.test(userId)) return badRequest('userId must be the numeric id of a Headscale user');

  const days = body.expirationDays === undefined ? DEFAULT_EXPIRY_DAYS : Number(body.expirationDays);
  if (!Number.isFinite(days) || days <= 0 || days > MAX_EXPIRY_DAYS) {
    return badRequest(`expirationDays must be between 1 and ${MAX_EXPIRY_DAYS}`);
  }

  const aclTags = Array.isArray(body.aclTags)
    ? body.aclTags
        .filter((t): t is string => typeof t === 'string')
        .map((t) => t.trim())
        .filter(Boolean)
        .map((t) => (t.startsWith('tag:') ? t : `tag:${t}`))
    : [];

  const created = await client.call<{ preAuthKey?: Record<string, unknown> }>('/api/v1/preauthkey', {
    method: 'POST',
    body: {
      user: userId,
      reusable: body.reusable === true,
      ephemeral: body.ephemeral === true,
      expiration: new Date(Date.now() + days * 86_400_000).toISOString(),
      aclTags,
    },
  });

  if (!created.preAuthKey) return Response.json({ error: 'headscale returned no key' }, { status: 502 });

  const key = toPreAuthKey(created.preAuthKey, { reveal: true });
  // Stated explicitly rather than left for the client to infer from `key !== null`: this response is the
  // only time the secret exists anywhere outside the joining machine.
  return Response.json({ key, secretShownOnce: true }, { status: 201 });
}

type KeyActionParams = { ctx: OfficerContext; id: string; action: string | undefined };

async function handleKeyAction({ ctx, id, action }: KeyActionParams): Promise<Response> {
  const client = await activeClient(ctx.userId);
  if (client instanceof Response) return client;

  if (action === 'expire') {
    if (ctx.req.method !== 'POST') return methodNotAllowed();
    await client.call('/api/v1/preauthkey/expire', { method: 'POST', body: { id } });
    return new Response(null, { status: 204 });
  }

  if (action !== undefined) return notFound();

  if (ctx.req.method === 'DELETE') {
    await client.call(`/api/v1/preauthkey?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
    return new Response(null, { status: 204 });
  }

  return methodNotAllowed();
}

/** Dispatch `/_officer/keys/...`. `rest` is the path after `keys`. */
export async function handleKeysRoute(ctx: OfficerContext, rest: string[]): Promise<Response> {
  if (rest.length === 0) {
    if (ctx.req.method === 'GET') return listKeys(ctx);
    if (ctx.req.method === 'POST') return createKey(ctx);
    return methodNotAllowed();
  }
  if (rest.length > 2) return notFound();

  const id = rest[0];
  if (!id || !/^\d+$/.test(id)) return badRequest('key id must be numeric');

  return handleKeyAction({ ctx, id, action: rest[1] });
}
