import type { OfficerContext } from './routes';
import {
  listHeadscaleServers,
  createHeadscaleServer,
  updateHeadscaleServer,
  setActiveHeadscaleServer,
  deleteHeadscaleServer,
  getHeadscaleCredentials,
  recordHeadscaleProbe,
} from '../db/queries';
import { createClient, HeadscaleError } from './client';
import { probeVersion, MIN_VERSION_LABEL } from './version';
import { badRequest, notFound, methodNotAllowed } from './routes';
import { normalizeSshHost } from './ssh';

// Server registry routes — /_officer/servers/*. Officer manages any number of Headscale servers; the owner
// registers each with a URL and an admin API key generated on that server, and one is active at a time.
//
// Registration VALIDATES before it saves, in two steps, because a bad registration is otherwise only
// discovered later as a confusing failure on some unrelated screen:
//   1. unauthenticated GET /version — proves something Headscale-shaped is there and enforces the >=0.29 floor
//   2. an authenticated call — proves the key actually works
// Neither step is skippable, and a rejected registration is never written.

/** Normalize a user-supplied base URL, or null if it isn't a usable http(s) origin. */
function normalizeUrl(raw: unknown): string | null {
  if (typeof raw !== 'string' || !raw.trim()) return null;
  let candidate = raw.trim();
  // Bare host/port is the most common paste; assume https rather than rejecting it.
  if (!/^https?:\/\//i.test(candidate)) candidate = `https://${candidate}`;
  let parsed: URL;
  try {
    parsed = new URL(candidate);
  } catch {
    return null;
  }
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
  // Trailing slash would produce `//api/v1/...`; query/hash are meaningless on a base URL.
  return `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}`;
}

function requireString(value: unknown, field: string): string | Response {
  if (typeof value !== 'string' || !value.trim()) return badRequest(`${field} is required`);
  return value.trim();
}

/**
 * Confirm a URL+key pair is a supported, reachable Headscale we can authenticate against.
 * Returns the observed version on success, or a ready-to-send error Response.
 */
async function validateServer(url: string, apiKey: string): Promise<string | Response> {
  const probe = await probeVersion(url);
  if (!probe.ok) return badRequest(probe.error);
  if (probe.supported === false) {
    return badRequest(`Headscale ${probe.version} is not supported — Officer requires ${MIN_VERSION_LABEL} or newer`);
  }

  // Cheapest authenticated GET whose path is stable across releases, and the same call Headscale's own
  // clients use to test a key. A wrong key surfaces here as HeadscaleError(502, 'rejected the stored key').
  const client = createClient({ id: 0, name: 'probe', url, apiKey });
  try {
    await client.call('/api/v1/apikey');
  } catch (err) {
    if (err instanceof HeadscaleError) {
      return badRequest(err.status === 502 ? 'the API key was rejected by that server' : err.message);
    }
    throw err;
  }
  return probe.version;
}

async function handleCollection(ctx: OfficerContext): Promise<Response> {
  const { req, userId } = ctx;

  if (req.method === 'GET') {
    return Response.json({ servers: await listHeadscaleServers(userId) });
  }

  if (req.method === 'POST') {
    const body = (await req.json().catch(() => null)) as Record<string, unknown> | null;
    if (!body) return badRequest('expected a JSON body');

    const url = normalizeUrl(body.url);
    if (!url) return badRequest('url must be a valid http(s) URL');
    const apiKey = requireString(body.apiKey, 'apiKey');
    if (apiKey instanceof Response) return apiKey;
    // The name is a label only; default it to the host so registration needs just a URL and a key.
    const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : new URL(url).host;
    // Optional, and never validated by connecting: registration should not fail because a box is rebooting.
    const sshHost = normalizeSshHost(body.sshHost);
    if (sshHost instanceof Response) return sshHost;

    const validated = await validateServer(url, apiKey);
    if (validated instanceof Response) return validated;

    // First registration becomes active, so the owner is never left with servers but none selected.
    const existing = await listHeadscaleServers(userId);
    const server = await createHeadscaleServer({
      userId,
      name,
      url,
      apiKey,
      version: validated,
      sshHost,
      activate: existing.length === 0,
    });
    return Response.json({ server }, { status: 201 });
  }

  return methodNotAllowed();
}

async function handleOne(ctx: OfficerContext, id: number, action: string | undefined): Promise<Response> {
  const { req, userId } = ctx;

  if (action === 'activate') {
    if (req.method !== 'POST') return methodNotAllowed();
    const server = await setActiveHeadscaleServer(userId, id);
    return server ? Response.json({ server }) : notFound('no such server');
  }

  if (action === 'health') {
    if (req.method !== 'GET') return methodNotAllowed();
    const creds = await getHeadscaleCredentials(userId, id);
    if (!creds) return notFound('no such server');

    const started = Date.now();
    const probe = await probeVersion(creds.url);
    if (!probe.ok) return Response.json({ ok: false, error: probe.error, ms: Date.now() - started });

    // Reachable — confirm the key too, so "healthy" means "we can actually use this server".
    try {
      await createClient(creds).call('/api/v1/apikey');
    } catch (err) {
      const message = err instanceof HeadscaleError ? err.message : 'upstream error';
      return Response.json({ ok: false, version: probe.version, error: message, ms: Date.now() - started });
    }

    await recordHeadscaleProbe(userId, id, probe.version);
    return Response.json({ ok: true, version: probe.version, supported: probe.supported, ms: Date.now() - started });
  }

  if (action !== undefined) return notFound();

  if (req.method === 'PATCH') {
    const body = (await req.json().catch(() => null)) as Record<string, unknown> | null;
    if (!body) return badRequest('expected a JSON body');

    const current = await getHeadscaleCredentials(userId, id);
    if (!current) return notFound('no such server');

    let url: string | undefined;
    if (body.url !== undefined) {
      const normalized = normalizeUrl(body.url);
      if (!normalized) return badRequest('url must be a valid http(s) URL');
      url = normalized;
    }
    let apiKey: string | undefined;
    if (body.apiKey !== undefined) {
      const parsed = requireString(body.apiKey, 'apiKey');
      if (parsed instanceof Response) return parsed;
      apiKey = parsed;
    }
    const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : undefined;
    // Absent = leave it; '' or null = clear the console target. normalizeSshHost collapses both to null.
    let sshHost: string | null | undefined;
    if (body.sshHost !== undefined) {
      const parsed = normalizeSshHost(body.sshHost);
      if (parsed instanceof Response) return parsed;
      sshHost = parsed;
    }

    // Re-validate whenever either half of the credentials moves — a saved-but-broken server is the exact
    // state registration works hard to prevent, and an edit can reintroduce it.
    if (url !== undefined || apiKey !== undefined) {
      const validated = await validateServer(url ?? current.url, apiKey ?? current.apiKey);
      if (validated instanceof Response) return validated;
    }

    const server = await updateHeadscaleServer(userId, id, { name, url, apiKey, sshHost });
    return server ? Response.json({ server }) : notFound('no such server');
  }

  if (req.method === 'DELETE') {
    const deleted = await deleteHeadscaleServer(userId, id);
    return deleted ? new Response(null, { status: 204 }) : notFound('no such server');
  }

  return methodNotAllowed();
}

/** Dispatch `/_officer/servers/...`. `rest` is the path after `servers`. */
export async function handleServersRoute(ctx: OfficerContext, rest: string[]): Promise<Response> {
  if (rest.length === 0) return handleCollection(ctx);

  const id = Number(rest[0]);
  if (!Number.isInteger(id) || id <= 0) return badRequest('server id must be a positive integer');
  if (rest.length > 2) return notFound();

  return handleOne(ctx, id, rest[1]);
}
