import type { SlskdConfig } from './upstream';
import { deleteServiceConnection, getServiceConnection, saveServiceConnection } from 'officerdb';
import { authHeaders, getSlskdConfig, invalidateSlskdConfig, normalizeBase } from './upstream';

// `/_config` — where the slskd daemon is and what key opens it, driven from the app rather than from a
// shell on the server.
//
// The API key is WRITE-ONLY across this boundary. The GET reports the URL and whether a key is stored; it
// has no field that could carry the key itself, masked or otherwise. The only way to change one is to send
// a new one, which is the same shape the photos and headscale registries use.
//
// ONE connection, not a registry: nobody runs two Soulseek daemons. So there is no label, no id in the
// path and no activate route — a save is an upsert over the single row, and the UI is one form.
//
// A save is validated against the live daemon before it is stored: a wrong URL or a rejected key is a 400
// with the reason, not a saved row that makes every later panel fail mysteriously.

export type ProbeResult =
  | { ok: true; version: string | null; server: string | null }
  | { ok: false; version: string | null; error: string };

/**
 * Ask a daemon whether it is really there and whether the key works.
 *
 * Two calls, because they answer different questions: `/health` is unauthenticated, so a failure there means
 * the URL is wrong or slskd is down, while `/api/v0/application` failing after it succeeded means the key is
 * the problem. Collapsing them would report "daemon unreachable" for a mistyped key.
 */
export async function probe(cfg: SlskdConfig): Promise<ProbeResult> {
  try {
    const health = await fetch(`${cfg.base}/health`, { signal: AbortSignal.timeout(5000), redirect: 'manual' });
    if (!health.ok) return { ok: false, version: null, error: `daemon returned ${health.status}` };
  } catch (err) {
    return { ok: false, version: null, error: `could not reach the daemon (${String(err)})` };
  }

  try {
    const res = await fetch(`${cfg.base}/api/v0/application`, {
      headers: { Accept: 'application/json', ...authHeaders(cfg) },
      signal: AbortSignal.timeout(5000),
      redirect: 'manual',
    });
    if (!res.ok) {
      const reason = res.status === 401 || res.status === 403 ? 'API key was rejected' : 'daemon returned';
      return { ok: false, version: null, error: `${reason} (${res.status})` };
    }
    const app = (await res.json()) as {
      version?: { current?: string; full?: string };
      server?: { state?: string };
    };
    return { ok: true, version: app.version?.full ?? app.version?.current ?? null, server: app.server?.state ?? null };
  } catch (err) {
    return { ok: false, version: null, error: String(err) };
  }
}

/** What the browser is allowed to know about the connection. Never includes the key. */
async function connectionState(userId: number): Promise<Response> {
  const connection = await getServiceConnection(userId, 'slskd');
  return Response.json({ configured: !!connection, connection });
}

const bad = (error: string, status = 400) => Response.json({ error }, { status });

type ConnectionBody = { url?: unknown; apiKey?: unknown };

const isHttpUrl = (url: string): boolean => /^https?:\/\//i.test(url);

/** Save the connection: validated against the live daemon, then stored with the key encrypted. */
async function save(req: Request, userId: number): Promise<Response> {
  const body = ((await req.json().catch(() => null)) as ConnectionBody | null) ?? {};
  const url = typeof body.url === 'string' ? normalizeBase(body.url) : '';
  // A blank key on an existing connection means "keep the stored one", so changing the URL does not need
  // the key re-typed. `undefined` is what saveServiceConnection reads as leave-alone.
  const apiKey = typeof body.apiKey === 'string' && body.apiKey.trim() ? body.apiKey.trim() : undefined;

  if (!url) return bad('url is required');
  if (!isHttpUrl(url)) return bad('url must start with http:// or https://');

  const current = await getServiceConnection(userId, 'slskd');
  if (!apiKey && !current?.hasSecret) return bad('an API key is required');

  // Probing needs the actual key, so a URL-only save borrows the stored one through the normal read path
  // rather than reaching for the ciphertext here.
  const stored = apiKey ? null : await getSlskdConfig(userId);
  const result = await probe({ base: url, apiKey: apiKey ?? stored?.apiKey ?? null });
  if (!result.ok) return Response.json({ error: result.error }, { status: 400 });

  const connection = await saveServiceConnection({
    userId,
    service: 'slskd',
    url,
    secret: apiKey,
    version: result.version,
  });
  invalidateSlskdConfig(userId);
  return Response.json({ connection, server: result.server });
}

/** `/_config` — GET the connection, PUT/POST to save it, DELETE to forget it. */
export async function handleConfigRoute(req: Request, userId: number, subpath: string): Promise<Response> {
  if (subpath && subpath !== '/') return bad('not found', 404);

  if (req.method === 'GET') return connectionState(userId);
  if (req.method === 'POST' || req.method === 'PUT') return save(req, userId);

  if (req.method === 'DELETE') {
    await deleteServiceConnection(userId, 'slskd');
    invalidateSlskdConfig(userId);
    return connectionState(userId);
  }

  return bad('method not allowed', 405);
}
