// Headscale version detection and the supported floor.
//
// Officer targets Headscale >= 0.29 and nothing older. That is a deliberate, narrow floor: the admin API
// changed shape repeatedly below it — identifiers went name→numeric at 0.26, `/api/v1/routes` was removed at
// 0.26 in favour of node-owned route sets, `forcedTags`/`validTags` collapsed into `tags` at 0.28, pre-auth
// key expiry became id-based at 0.28, and MoveNode was removed at 0.28. Supporting 0.23–0.28 would mean
// carrying several incompatible data models; refusing them at registration time costs one probe.
//
// Detection uses the server's own unauthenticated `GET /version`, which exists in 0.28 and 0.29 and sits at
// the root — NOT under /api/v1, and not behind the bearer middleware. Do not confuse it with the three
// other similarly-named endpoints: `GET /health` (root, unauthenticated, `{status:'pass'}`) and
// `GET /api/v1/health` (authenticated, `{databaseConnectivity:true}`) carry no version at all.

export const MIN_MAJOR = 0;
export const MIN_MINOR = 29;
export const MIN_VERSION_LABEL = '0.29';

const PROBE_TIMEOUT_MS = 8000;

export type VersionProbe =
  | { ok: true; version: string; supported: true }
  /** Reached the server but can't judge the version — self-built images report the literal 'dev'. */
  | { ok: true; version: string; supported: 'unknown' }
  | { ok: true; version: string; supported: false }
  | { ok: false; error: string };

/** `major.minor` from a Headscale version string, or null when it isn't semver (e.g. the literal 'dev'). */
export function parseVersion(raw: string): { major: number; minor: number } | null {
  const m = raw
    .trim()
    .replace(/^v/, '')
    .match(/^(\d+)\.(\d+)/);
  if (!m) return null;
  return { major: Number(m[1]), minor: Number(m[2]) };
}

/** Whether a parsed version is at or above the supported floor. */
export function meetsFloor(v: { major: number; minor: number }): boolean {
  if (v.major !== MIN_MAJOR) return v.major > MIN_MAJOR;
  return v.minor >= MIN_MINOR;
}

/**
 * Probe a base URL's `GET /version`. Unauthenticated, so this also doubles as the reachability check during
 * registration — it tells us "is there a Headscale here at all" before we bother validating a key.
 *
 * An unparseable version is reported as `supported: 'unknown'` rather than rejected: a server built without
 * VCS build info reports 'dev', and refusing those would lock out legitimately self-built deployments.
 */
export async function probeVersion(baseUrl: string): Promise<VersionProbe> {
  let res: Response;
  try {
    res = await fetch(`${baseUrl}/version`, {
      headers: { accept: 'application/json' },
      signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
    });
  } catch {
    return { ok: false, error: 'server unreachable' };
  }

  if (!res.ok) {
    // A Headscale that answers /version with a non-2xx isn't one we can identify. Most often this is a URL
    // pointing at a reverse proxy or an unrelated service rather than at Headscale itself.
    return { ok: false, error: `GET /version returned ${res.status} — is this a Headscale server?` };
  }

  let version: string;
  try {
    const body = (await res.json()) as { version?: unknown };
    if (typeof body.version !== 'string' || !body.version) return { ok: false, error: 'no version in response' };
    version = body.version;
  } catch {
    return { ok: false, error: 'GET /version did not return JSON' };
  }

  const parsed = parseVersion(version);
  if (!parsed) return { ok: true, version, supported: 'unknown' };
  return { ok: true, version, supported: meetsFloor(parsed) };
}
