import { getServiceCredentials } from 'officerdb';

// Where the Memos instance is and what it takes to talk to it. The credential lives here and only here —
// the platform forwards to this sidecar and holds nothing.
//
// Memos authenticates with a personal access token (`Authorization: Bearer …`), minted from Memos' own
// Settings → Access Tokens. That is the OWNER'S to create and paste in; there is no way to derive one
// from a platform session, and no reason to want one.

export type MemosConfig = { id: number; base: string; token: string | null };

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

const cache = new Map<number, MemosConfig | null>();

export function invalidateMemosConfig(userId: number): void {
  cache.delete(userId);
}

export async function getMemosConfig(userId: number): Promise<MemosConfig | null> {
  if (cache.has(userId)) return cache.get(userId) ?? null;
  const row = await getServiceCredentials(userId, 'memos');
  // `url` is nullable now that some services inherit an instance (see the schema). memos is a single
  // daemon and always stores its own, so a null here means a malformed row, not an inheriting one.
  const config = row?.url ? { id: row.id, base: normalizeBase(row.url), token: row.secret } : null;
  cache.set(userId, config);
  return config;
}

export const authHeaders = (config: MemosConfig): Record<string, string> =>
  config.token ? { Authorization: `Bearer ${config.token}` } : {};

/**
 * One call against the instance. Returns the raw Response so callers can stream or inspect status —
 * nothing here parses a memo, because a memo is the owner's note and this layer has no business
 * reading it.
 */
export async function callMemos(config: MemosConfig, path: string, init?: RequestInit): Promise<Response> {
  const headers = { ...authHeaders(config), ...((init?.headers as Record<string, string>) ?? {}) };
  return fetch(`${config.base}${path}`, { ...init, headers, signal: init?.signal ?? AbortSignal.timeout(20_000) });
}

export type ProbeResult = { ok: boolean; version: string | null; user: string | null; error?: string };

/**
 * Is the instance up, and does the stored token actually work?
 *
 * Two checks, because they fail differently and the UI needs to tell them apart: `/healthz` answers
 * unauthenticated so a bad URL is distinguishable from a bad token, and `/api/v1/users/me` is the
 * cheapest endpoint that genuinely requires auth. Memos returns 200 with an empty list for
 * unauthenticated reads rather than 401, so "the list came back" proves nothing on its own.
 */
export async function probe(config: MemosConfig): Promise<ProbeResult> {
  try {
    const health = await fetch(`${config.base}/healthz`, { signal: AbortSignal.timeout(8000) });
    if (!health.ok) return { ok: false, version: null, user: null, error: `instance returned ${health.status}` };
  } catch (err) {
    return { ok: false, version: null, user: null, error: `unreachable: ${String(err)}` };
  }

  if (!config.token) return { ok: false, version: null, user: null, error: 'no access token stored' };

  try {
    const me = await callMemos(config, '/api/v1/users/me');
    if (!me.ok) return { ok: false, version: null, user: null, error: `token rejected (${me.status})` };
    const body = (await me.json().catch(() => ({}))) as { name?: string; username?: string; nickname?: string };
    return { ok: true, version: null, user: body.nickname || body.username || body.name || null };
  } catch (err) {
    return { ok: false, version: null, user: null, error: String(err) };
  }
}
