import { createRouter } from '../../create-router';
import { getVaultServerUrl } from './sidecar-server';
import { stripHopByHop, redactPath } from './proxy-util';

// Vaultwarden, reachable at Officer's own hostname with NO Officer authentication.
//
// ── Why this exists ──
//
// The Bitwarden browser extension speaks Vaultwarden's protocol and nothing else: it obtains its own
// bearer token from `/identity/connect/token` and has nowhere to put a platform JWT. `/api/vault` cannot
// serve it — that router requires an Officer session and REPLACES the caller's Authorization header with
// a server-held Vaultwarden token, which is the right design for our own clients (the device then holds
// no vault credential at all) and an impossible one for a third-party client.
//
// So this mount exists to retire a second public hostname. Andre pointed the extension straight at
// Vaultwarden; that URL can now go away, and the extension can use the platform's.
//
// ── Why it is acceptable to leave unauthenticated ──
//
// It is not a new exposure, it is the same exposure through one door instead of two: everything here was
// already reachable at the Vaultwarden URL this replaces, guarded by the same thing that guards it now —
// the vault's own master password and Vaultwarden's own auth. Officer adds no security to a Bitwarden
// client today because it cannot: it has no credential to check.
//
// It is also going behind Tailscale, so the surface is a private network rather than the internet.
//
// ── TEMPORARY ──
//
// The intended end state is a browser extension of our own, reusing `@officer/vault` (already proven to
// run as a plain JS bundle outside React Native — the iOS autofill extension hosts it in JavaScriptCore)
// against the `/api/vault/session/login` broker. Then the client authenticates to OFFICER, Vaultwarden is
// never addressed directly by anything, and this mount is deleted rather than adjusted.
//
// ── The rule that keeps it safe ──
//
// DUMB PASS-THROUGH, and more so than `/api/vault`: that one rewrites the Authorization header, this one
// touches nothing. Method, path, query, headers, status and both body streams go through verbatim. The
// vault is end-to-end encrypted and no part of this may parse, decrypt, buffer, cache or log a body —
// paths are redacted in logs for the same reason. Every line of "cleverness" added here is reachable
// without an Officer session, which is the whole reason it must stay boring.

export const publicVaultRouter = createRouter();

const PREFIX = '/vaultwarden';

/**
 * Paths that belong to Vaultwarden and to nothing else in this platform.
 *
 * These are served at the ROOT so the extension can be configured with the bare Officer URL — no
 * `/vaultwarden` suffix — which is the point: one hostname, one URL, nothing to explain. None of them
 * collide with an Officer route, so no sniffing is involved; the path alone is the answer.
 */
export const VAULT_ONLY_PREFIXES = ['/identity', '/notifications', '/icons', '/events'];

/**
 * Is this a Bitwarden client talking to us?
 *
 * Needed only for `/api/*`, which is the one namespace both servers claim — Vaultwarden has
 * `/api/sync`, `/api/ciphers`, `/api/settings/domains`; Officer has `/api/chat`, `/api/settings` and
 * everything else. The path cannot decide it, so the client says who it is: every Bitwarden client
 * stamps `Bitwarden-Client-Name` (`browser`, `desktop`, `cli`, `web`), and older ones `Device-Type`.
 *
 * This is ROUTING, not authentication, which is why trusting a client-supplied header is fine here: the
 * worst a forged header achieves is reaching Vaultwarden, which then demands its own credential exactly
 * as it would have. Nothing is authorised by this function.
 */
export const isBitwardenClient = (headers: Headers): boolean =>
  headers.has('bitwarden-client-name') || headers.has('bitwarden-client-version') || headers.has('device-type');

/** Everything Vaultwarden serves, whichever door it arrived through. */
export const isVaultwardenPath = (path: string, headers: Headers): boolean => {
  if (VAULT_ONLY_PREFIXES.some((p) => path === p || path.startsWith(`${p}/`))) return true;
  return path.startsWith('/api/') && isBitwardenClient(headers);
};

publicVaultRouter.all('/*', async (ctx) => {
  const base = getVaultServerUrl();
  if (!base) return ctx.text('Vault sidecar not available', 503);

  const url = new URL(ctx.req.url);
  // Mounted at `/vaultwarden` AND at the root prefixes above. Strip the prefix only when it is there,
  // so `/vaultwarden/identity/...` and a bare `/identity/...` both reach Vaultwarden's own path.
  const subpath = (url.pathname.startsWith(PREFIX) ? url.pathname.slice(PREFIX.length) : url.pathname) || '/';
  const target = `${base}${subpath}${url.search}`;
  const method = ctx.req.method;
  const hasBody = method !== 'GET' && method !== 'HEAD';

  // Authorization is FORWARDED, not replaced — the opposite of `/api/vault`. The client's Vaultwarden
  // token is the only credential in play, and Vaultwarden is the only thing entitled to judge it.
  const headers = stripHopByHop(ctx.req.raw.headers);

  const init: RequestInit & { duplex?: 'half' } = { method, headers, redirect: 'manual' };
  if (hasBody) {
    init.body = ctx.req.raw.body;
    init.duplex = 'half';
  }

  let upstream: Response;
  try {
    upstream = await fetch(target, init);
  } catch {
    console.error(`[vault:public] ${method} ${redactPath(subpath + url.search)} -> sidecar unreachable`);
    return ctx.text('Vault sidecar unreachable', 502);
  }

  // No 401-refresh-and-retry here, deliberately. That belongs to `/api/vault`, which owns the token it
  // injected; this router holds no token and a 401 is Vaultwarden's answer to the CLIENT, which the
  // client knows how to handle — it re-authenticates with the master password.
  return new Response(upstream.body, { status: upstream.status, headers: stripHopByHop(upstream.headers) });
});
