import { listSidecarInstalls } from 'officerdb';
import { CATALOGUE, type CatalogueEntry } from './catalogue';

// Which features actually EXIST on this server right now — as opposed to which the account is permitted
// to use.
//
// ── Why this is separate from capabilities ──
//
// They answer different questions and combining them would get the owner wrong. A capability asks "may
// this account use Photos"; the owner bypasses that entirely and always may. Installation asks "is there
// a Photos on this machine at all", and the owner is as subject to it as anyone — installing nothing
// leaves nothing to use.
//
// Without this the dock on a fresh install lists Photos, Jellyfin, Transmission and the rest for the
// owner, each leading to a screen that reports itself unavailable. The features are meant to arrive when
// they are installed, not sit there greyed out from the start.
//
// ── Why it is computed here and not in the client ──
//
// The dock already reads one list from `/capabilities`. Making it read a second and intersect the two
// puts the rule in the UI, where a member's dock and an owner's dock can drift apart, and where a
// third-party plugin would have to be taught about it. Subtracting server-side keeps one answer.

/**
 * Capability key → the sidecar that has to be installed for it to mean anything.
 *
 * Includes `alsoServes`, because one sidecar can back more than one capability: Headscale serves both the
 * owner's tailnet administration and a member enrolling their own device, and only listing the first left the
 * second looking available on a machine that had no Headscale.
 */
const CAPABILITY_TO_SIDECAR = new Map(
  CATALOGUE.flatMap((entry) =>
    [entry.capability, ...(entry.alsoServes ?? [])].filter((key): key is string => !!key).map((key) => [key, entry.id]),
  ) as Array<[string, string]>,
);

export type Availability = {
  /**
   * UI manifests of the sidecars that ARE usable — what the dock should show beyond the baseline.
   *
   * Sent with the capability answer rather than fetched separately so the dock has one source. Two
   * requests would mean two moments, and a dock rendered between them shows either a tile for something
   * uninstalled or nothing for something installed.
   */
  manifests: Array<{ sidecarId: string; capability: string | null } & NonNullable<CatalogueEntry['ui']>>;
  /** Capability keys whose sidecar is not installed, or is installed but disabled. */
  unavailable: Set<string>;
  /**
   * True when install state could not be read.
   *
   * The caller then subtracts NOTHING. Same reasoning as `useCapabilities` failing open: a member seeing
   * an icon that leads to an unavailable screen is a bad minute, while an owner whose whole dock vanished
   * because a query failed is an incident. Absence of evidence is not evidence of absence.
   */
  degraded: boolean;
};

/**
 * What is missing on this server, by capability key.
 *
 * A sidecar that is installed but DISABLED counts as unavailable, deliberately. Disable stops the
 * process and its container, so the feature genuinely does not work — leaving its icon in place would
 * make disable look broken rather than effective.
 */
export async function capabilityAvailability(): Promise<Availability> {
  const unavailable = new Set<string>();

  let installs;
  try {
    installs = await listSidecarInstalls();
  } catch {
    // Degraded: subtract nothing, and offer no manifests. The dock keeps its baseline rather than
    // guessing, which is the same fail-open posture as useCapabilities.
    return { unavailable, manifests: [], degraded: true };
  }

  const usable = new Set(
    installs.filter((row) => row.status === 'installed' && row.enabled).map((row) => row.sidecarId),
  );

  for (const [capability, sidecarId] of CAPABILITY_TO_SIDECAR) {
    if (!usable.has(sidecarId)) unavailable.add(capability);
  }

  const manifests = CATALOGUE.filter((e) => e.ui && usable.has(e.id)).map((e) => ({
    sidecarId: e.id,
    capability: e.capability,
    ...e.ui!,
  }));

  return { unavailable, manifests, degraded: false };
}
