import { createRouter } from '../../create-router';
import { getUserByEmail, verifyDavAppPassword } from 'officerdb';
import { getCaldavUrl } from './sidecar-server';

// The DAV door: `/dav/*`, mounted TOP-LEVEL, outside protectedRouter.
//
// WHY IT IS NOT UNDER /api AND NOT BEHIND userMiddleware. DAVx5, iOS Calendar, macOS and Thunderbird
// speak CalDAV over HTTPS with HTTP Basic auth on every single request. They cannot log into Officer,
// cannot hold a 30-day JWT, cannot refresh one and cannot answer a passkey challenge. This is the same
// situation `/api/vault` is in — "the Bitwarden client carries its own bearer token, not a platform
// session JWT, so userMiddleware would 401 it" (hono.ts) — with a different credential type.
//
// The credential is a DAV app password (`dav_app_passwords`), scoped to this mount and nothing else.
// An app password is NOT a session: userMiddleware must never learn to accept one, or a device that
// syncs a calendar would also reach the wallet.
//
// Everything is forwarded verbatim to the caldav sidecar, which forwards it to Radicale. The platform
// authenticates and moves bytes; it does not parse a single line of iCalendar. That restraint is the
// same one create-proxy.ts documents, and for the same reason.

export const davSyncRouter = createRouter();

const unauthorized = () =>
  new Response('Unauthorized', {
    status: 401,
    // Without this header a DAV client will not prompt for credentials at all — it just fails. The
    // realm string is what the phone shows in its password dialog.
    headers: { 'WWW-Authenticate': 'Basic realm="Officer DAV", charset="UTF-8"' },
  });

type BasicCreds = { username: string; password: string };

function parseBasic(header: string | undefined): BasicCreds | null {
  if (!header?.startsWith('Basic ')) return null;
  let decoded: string;
  try {
    decoded = Buffer.from(header.slice(6), 'base64').toString('utf8');
  } catch {
    return null;
  }
  // Split on the FIRST colon only — a generated password never contains one, but an email local-part
  // legally can, and splitting greedily would break those accounts in a way that looks like a wrong
  // password.
  const at = decoded.indexOf(':');
  if (at < 0) return null;
  return { username: decoded.slice(0, at), password: decoded.slice(at + 1) };
}

// Same list the sidecar forwards, and for the same reason: `Depth` decides whether PROPFIND returns a
// collection, its children or the whole tree, so dropping it makes every calendar look empty.
const REQUEST_HEADERS = [
  'depth',
  'content-type',
  'content-length',
  'if',
  'if-match',
  'if-none-match',
  'destination',
  'overwrite',
  'lock-token',
  'timeout',
  'prefer',
  'user-agent',
] as const;

const RESPONSE_HEADERS = [
  'content-type',
  'dav',
  'allow',
  'etag',
  'last-modified',
  'location',
  'lock-token',
  'preference-applied',
  'vary',
] as const;

davSyncRouter.all('/*', async (ctx) => {
  const creds = parseBasic(ctx.req.header('authorization'));
  if (!creds) return unauthorized();

  // A credential-less request is the normal opening move — the client is asking to be challenged — so it
  // is not worth a line. A credential that is present and wrong is the interesting case, and it is
  // indistinguishable from the normal one at the client end: both just say "password incorrect".
  const user = await getUserByEmail(creds.username);
  if (!user) {
    console.warn(`[dav] auth failed: no such user ${creds.username}`);
    return unauthorized();
  }

  const userId = await verifyDavAppPassword(user.id, creds.password);
  if (!userId) {
    console.warn(`[dav] auth failed: bad app password for ${creds.username}`);
    return unauthorized();
  }

  const base = getCaldavUrl();
  if (!base) return new Response('caldav sidecar not available', { status: 503 });

  const url = new URL(ctx.req.url);
  // The sidecar mounts DAV at /dav too, so the prefix is preserved rather than stripped — Radicale
  // generates absolute hrefs in its multistatus responses, and if the path the client sees differs
  // from the path the server thinks it is at, every href points somewhere that 404s.
  const target = `${base}${url.pathname}${url.search}`;

  const headers: Record<string, string> = {};
  for (const name of REQUEST_HEADERS) {
    const value = ctx.req.header(name);
    if (value) headers[name] = value;
  }
  headers['X-Officer-User'] = String(userId);

  const method = ctx.req.method;
  const hasBody = method !== 'GET' && method !== 'HEAD';

  let upstream: Response;
  try {
    upstream = await fetch(target, {
      method,
      headers,
      body: hasBody ? await ctx.req.arrayBuffer() : undefined,
    });
  } catch (err) {
    console.error('[dav] proxy fetch failed', { path: url.pathname, error: String(err) });
    return new Response('caldav sidecar unreachable', { status: 502 });
  }

  const out = new Headers();
  for (const name of RESPONSE_HEADERS) {
    const value = upstream.headers.get(name);
    if (value) out.set(name, value);
  }

  // One line per request, with the client's own name for itself. A DAV client tells you almost nothing
  // when it fails — iOS in particular reports every setup failure as "Cannot connect using SSL" — so the
  // only way to know whether a phone ever asked for an address book is to record that it did. Cheap:
  // a sync is a handful of requests, and a body is never logged.
  console.log(
    `[dav] ${method} ${url.pathname} -> ${upstream.status} (user=${userId}, ua=${(ctx.req.header('user-agent') ?? '-').slice(0, 60)})`,
  );

  return new Response(upstream.body, { status: upstream.status, headers: out });
});
