import { getServiceCredentials } from 'officerdb';

// slskd upstream config + header hygiene for the officer-slskd sidecar.
//
// All knowledge of the slskd instance — its URL and its API key — lives here. The platform API is a thin
// auth+forward proxy and holds NO slskd credentials.
//
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `service_connections` (see
// databases/officer_db/src/service-connections/queries.ts). It is deliberately no longer read from the
// environment: Bun auto-loads `.env` into every process started in the platform directory, so an
// `SLSKD_API_KEY` there was also sitting in `officer`'s own process.env — a credential that drives the whole
// Soulseek daemon, held by the one process with no code to use it. Nothing in this file reads process.env.

/** Everything needed to reach the daemon. A candidate being validated has this and nothing else yet. */
export type SlskdConfig = { base: string; apiKey: string | null };

/** Trailing slashes off, so `${base}/api/v0/...` never doubles the separator. */
export const normalizeBase = (url: string): string => url.trim().replace(/\/+$/, '');

// The transfers view polls every couple of seconds and every request needs the key, so the row is cached
// rather than re-read per request. Writes invalidate immediately; the TTL only covers someone editing the
// row in psql, which then takes effect within a minute instead of needing a restart.
const TTL_MS = 60_000;
const cache = new Map<number, { cfg: SlskdConfig | null; at: number }>();

/**
 * The owner's slskd connection, or null when nothing is configured — the sidecar then answers 503, and the
 * UI turns that into the setup form rather than a wall of empty panels.
 */
export async function getSlskdConfig(userId: number): Promise<SlskdConfig | null> {
  const hit = cache.get(userId);
  if (hit && Date.now() - hit.at < TTL_MS) return hit.cfg;

  const creds = await getServiceCredentials(userId, 'slskd');
  // `url` became nullable when services that inherit one instance landed (see the schema column note).
  // slskd fronts a single daemon and always stores its own, so null here is a malformed row, not an
  // inheriting one — treat it as unconfigured rather than reaching for somebody else's URL.
  const cfg = creds?.url ? { base: normalizeBase(creds.url), apiKey: creds.secret } : null;
  cache.set(userId, { cfg, at: Date.now() });
  return cfg;
}

/** Drop the cached row — called by the config routes after any save or removal. */
export function invalidateSlskdConfig(userId: number): void {
  cache.delete(userId);
}

/** The auth header slskd expects, ready to spread. Empty when the daemon has no key configured. */
export const authHeaders = (cfg: SlskdConfig): Record<string, string> =>
  cfg.apiKey ? { 'X-API-Key': cfg.apiKey } : {};

// Hop-by-hop headers must not cross a proxy hop (RFC 7230 §6.1). `host` is dropped so the outgoing
// fetch sets the upstream authority itself; everything else — including our injected X-API-Key — passes.
const HOP_BY_HOP = new Set([
  'connection',
  'keep-alive',
  'proxy-authenticate',
  'proxy-authorization',
  'te',
  'trailer',
  'transfer-encoding',
  'upgrade',
  'host',
]);

/** Copy headers, dropping only hop-by-hop / proxy-* ones. Used for both request and response directions. */
export function stripHopByHop(src: Headers): Headers {
  const out = new Headers();
  src.forEach((value, key) => {
    const k = key.toLowerCase();
    if (HOP_BY_HOP.has(k) || k.startsWith('proxy-')) return;
    out.set(key, value);
  });
  return out;
}
