import type { UpstreamConfig } from './upstream';
import { callUpstream } from './upstream';

// The Officer-owned route surface. This is an allow-list, not a passthrough.
//
// Immich's API is already REST and already shaped the way a photo UI wants it, so the value this sidecar
// adds is (a) holding the API key, (b) streaming the binary routes through without buffering them, and
// (c) refusing to expose the administrative half of the API at all.
//
// The gate is the FIRST path segment plus the method. Anything whose first segment is not listed here is a
// 404 before a request is made, so the deny side is closed by construction rather than by enumeration.
//
// DELIBERATELY NOT EXPOSED: admin/* (user creation, deletion, quotas), auth/* and oauth/* (session and
// password machinery — the key is the credential here, and nothing should be minting sessions through a
// dashboard), api-keys/* (a proxy that can mint its own credentials is not a proxy), sessions/*, jobs/*
// (queue control), system-config/* and system-metadata/* (rewrites the deployment), libraries/* (external
// library paths and scans) and sync/*. Those either mutate the deployment or can lock the owner out of it.
// Adding one should be a decision, not an accident.

const RESOURCES: Record<string, readonly string[]> = {
  // The library itself.
  assets: ['GET', 'POST', 'PUT', 'DELETE'],
  albums: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  timeline: ['GET'],
  memories: ['GET', 'POST', 'PUT', 'DELETE'],
  people: ['GET', 'POST', 'PUT'],
  faces: ['GET', 'PUT'],
  tags: ['GET', 'POST', 'PUT', 'DELETE'],
  stacks: ['GET', 'POST', 'PUT', 'DELETE'],
  // Finding things. `search/metadata`, `search/smart` and `search/random` are POSTs with a JSON body.
  search: ['GET', 'POST'],
  duplicates: ['GET'],
  map: ['GET'],
  view: ['GET'],
  // Getting things out. `download/info` then `download/archive` — both POST, the archive streams a zip.
  download: ['POST'],
  // Recoverable deletes. `trash/empty` is the one irreversible route in this list; it is here because it is
  // the counterpart of a delete the UI can already perform, not because it is safe.
  trash: ['GET', 'POST'],
  // Sharing and social. Read-mostly, but a shared link is the point of them.
  'shared-links': ['GET', 'POST', 'PATCH', 'DELETE'],
  activities: ['GET', 'POST', 'DELETE'],
  partners: ['GET'],
  notifications: ['GET', 'PUT', 'DELETE'],
  // Read-only context: who the key acts as, and what the server is.
  users: ['GET'],
  server: ['GET'],
};

// Response headers worth carrying back. Content-Type and Content-Length for every response; the rest so a
// thumbnail can be cached and revalidated and a video can be seeked without the proxy understanding either.
const PASSTHROUGH_HEADERS = [
  'content-type',
  'content-length',
  'content-disposition',
  'content-range',
  'accept-ranges',
  'etag',
  'last-modified',
  'cache-control',
] as const;

/**
 * Forward one request to Immich and stream the answer back.
 *
 * The body is a stream, never an ArrayBuffer: a full-resolution original or a `download/archive` zip is
 * hundreds of megabytes and buffering it would hold all of it in the sidecar's heap for no reason.
 */
export async function handleOfficerRoute(cfg: UpstreamConfig, req: Request, url: URL): Promise<Response | null> {
  const rest = url.pathname.slice('/_officer/'.length);
  if (!rest) return null;

  const [resource] = rest.split('/');
  const allowedMethods = resource ? RESOURCES[resource] : undefined;
  if (!allowedMethods) return null;
  if (!allowedMethods.includes(req.method)) {
    return Response.json({ error: `${req.method} not allowed on ${resource}` }, { status: 405 });
  }

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

  // An <img>/<video> cannot send an Authorization header, so the browser puts Officer's JWT in `?token=`
  // (userMiddleware accepts it there). That token is Officer's, not Immich's: forwarding it would write the
  // owner's session credential into Immich's access log for every thumbnail. Drop it at the boundary.
  const query = new URLSearchParams(url.search);
  query.delete('token');
  const search = query.toString();

  const res = await callUpstream(cfg, {
    path: `/api/${rest}`,
    method: req.method,
    query: search ? `?${search}` : '',
    body: hasBody ? await req.arrayBuffer() : null,
    contentType: req.headers.get('content-type'),
    range: req.headers.get('range'),
    ifNoneMatch: req.headers.get('if-none-match'),
  });

  const headers = new Headers();
  for (const name of PASSTHROUGH_HEADERS) {
    const value = res.headers.get(name);
    if (value) headers.set(name, value);
  }

  // 204 and 304 must not carry a body, and Bun throws if one is attached.
  const body = res.status === 204 || res.status === 304 ? null : res.body;
  return new Response(body, { status: res.status, headers });
}
