import { getServiceCredentials } from 'officerdb';

// Transmission upstream config for the officer-transmission sidecar.
//
// All knowledge of the daemon — its URL, its RPC path and its credentials — lives here. The platform API is
// a thin auth+forward proxy and holds NO Transmission credentials.
//
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored in `service_connections` (see
// databases/officer_db/src/service-connections/queries.ts), no longer read from the environment: Bun
// auto-loads `.env` into every process started in the platform directory, so TRANSMISSION_* was also
// sitting in `officer`'s own process.env, and pointing Officer at a daemon meant editing a file on the
// server. Nothing in this file reads process.env.

/** Everything needed to make one RPC call. A candidate being validated has this and nothing else yet. */
export type TransmissionConfig = { base: string; rpcPath: string; auth: string | null };

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

/**
 * The RPC endpoint path. Transmission serves it at /transmission/rpc by default, but a reverse proxy can
 * mount it anywhere, so it is stored per connection rather than hardcoded.
 */
export const normalizeRpcPath = (raw: string | null | undefined): string => {
  const path = raw?.trim();
  if (!path) return '/transmission/rpc';
  return path.startsWith('/') ? path : `/${path}`;
};

/**
 * The `Authorization: Basic …` header value, or null when the daemon has no auth configured.
 *
 * Transmission treats an empty username as "no authentication required" — sending an empty Basic header in
 * that case is not merely useless, it makes the daemon reject the request. So this returns null unless a
 * username is actually set, which is why the stored username is nullable rather than defaulting to ''.
 */
export function basicAuth(username: string | null, password: string | null): string | null {
  const user = username?.trim();
  if (!user) return null;
  return `Basic ${Buffer.from(`${user}:${password ?? ''}`).toString('base64')}`;
}

// The torrent list polls every couple of seconds, 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: TransmissionConfig | null; at: number }>();

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

  const creds = await getServiceCredentials(userId, 'transmission');
  // `url` became nullable when services that inherit one instance landed (see the schema column note).
  // Transmission fronts a single daemon and always stores its own, so null here is a malformed row.
  const cfg = creds?.url
    ? {
        base: normalizeBase(creds.url),
        rpcPath: normalizeRpcPath(creds.path),
        auth: basicAuth(creds.username, 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 invalidateTransmissionConfig(userId: number): void {
  cache.delete(userId);
}
