declare const Guacamole: any;

/**
 * The lifecycle of a console session as the UI needs to talk about it.
 *
 * Deliberately not Guacamole's own state enum: "waiting" and "connecting" are
 * one thing to an operator, and "disconnected because you clicked Disconnect"
 * and "disconnected because the far end vanished" are two things that Guacamole
 * reports identically.
 */
export type ConsoleState =
  | "connecting"
  | "connected"
  | "disconnected"
  | "error";

export type ConsoleFailure = {
  /** Guacamole status code, when the failure came with one. */
  code?: number;
  /** Operator-facing sentence. Always set. */
  message: string;
  /** Where the failure was reported: the tunnel or the client session. */
  source: "tunnel" | "client";
};

/**
 * Guacamole status codes, as operator-facing sentences.
 *
 * These are the only diagnosis anyone gets from a failed console: the relay,
 * guacd and the target daemon are all invisible from the browser, and guacd
 * deliberately reports upstream failures as a bare number. Leaving them
 * unmapped is what makes a broken console indistinguishable from a slow one.
 *
 * Source: Guacamole protocol status codes (guacamole-common-js).
 */
const STATUS_TEXT: Record<number, string> = {
  256: "The server does not support this operation.",
  512: "The remote desktop server encountered an error.",
  513: "The remote desktop server is busy.",
  514: "The remote host did not respond in time.",
  515: "The remote host encountered an error.",
  516: "The requested resource was not found.",
  517: "The requested resource is already in use.",
  518: "The remote connection was closed.",
  519: "The remote host could not be reached.",
  520: "The remote host is not currently available.",
  521: "The session conflicts with another session.",
  522: "The session timed out.",
  523: "The session was closed.",
  768: "The server rejected the connection request.",
  769: "Authentication failed — check the credentials or SSH key.",
  771: "Access to this connection was refused.",
  776: "The session was closed after a period of inactivity.",
  781: "The connection was closed because the client fell behind.",
  782: "The server sent data the client could not understand.",
  783: "Too many concurrent connections.",
};

/**
 * describeStatus turns a Guacamole status into a sentence, always keeping the
 * numeric code available separately. The code is what makes a support ticket
 * actionable, so it is never discarded in favour of the prose.
 */
export function describeStatus(code: number | undefined, fallback: string): string {
  if (code === undefined) return fallback;
  return STATUS_TEXT[code] ?? fallback;
}

export type ConsoleHandlers = {
  onState?: (state: ConsoleState) => void;
  onFailure?: (failure: ConsoleFailure) => void;
};

export function openConsole(
  container: HTMLElement,
  wsUrl: string,
  connectData = "",
  handlers: ConsoleHandlers = {}
): {
  disconnect: () => void;
  setScale: (scale: number) => void;
  resize: (width: number, height: number) => void;
  focus: () => void;
} {
  const tunnel = new Guacamole.WebSocketTunnel(wsUrl);
  const client = new Guacamole.Client(tunnel);

  // Set once the caller tears the session down deliberately. Guacamole reports
  // a user-initiated disconnect through exactly the same callbacks as a far-end
  // failure, so without this flag closing the tab raises an error banner.
  let closing = false;

  const fail = (source: "tunnel" | "client", status: any, fallback: string) => {
    if (closing) return;
    const code: number | undefined =
      typeof status?.code === "number" ? status.code : undefined;
    const message = describeStatus(
      code,
      typeof status?.message === "string" && status.message ? status.message : fallback
    );
    handlers.onFailure?.({ code, message, source });
    handlers.onState?.("error");
  };

  tunnel.onerror = (status: any) =>
    fail("tunnel", status, "The connection to the control plane was lost.");

  client.onerror = (status: any) =>
    fail("client", status, "The remote session ended unexpectedly.");

  // Guacamole.Client.State: 0 IDLE, 1 CONNECTING, 2 WAITING, 3 CONNECTED,
  // 4 DISCONNECTING, 5 DISCONNECTED. CONNECTING and WAITING are one state to an
  // operator — both mean "not usable yet".
  client.onstatechange = (state: number) => {
    if (closing) return;
    if (state === 1 || state === 2) handlers.onState?.("connecting");
    else if (state === 3) handlers.onState?.("connected");
    else if (state === 5) handlers.onState?.("disconnected");
  };

  container.innerHTML = "";
  container.appendChild(client.getDisplay().getElement());

  container.tabIndex = 0;
  container.style.outline = "none";

  handlers.onState?.("connecting");
  client.connect(connectData);

  const display = client.getDisplay();
  let scale = 1;

  const mouse = new Guacamole.Mouse(display.getElement());
  mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = (state: any) => {
    const s = new Guacamole.Mouse.State(
      state.x / scale,
      state.y / scale,
      state.left,
      state.middle,
      state.right,
      state.up,
      state.down
    );
    client.sendMouseState(s);
  };

  const keyboard = new Guacamole.Keyboard(container);
  keyboard.onkeydown = (k: number) => client.sendKeyEvent(1, k);
  keyboard.onkeyup = (k: number) => client.sendKeyEvent(0, k);

  const refocus = () => {
    if (document.activeElement !== container) container.focus({ preventScroll: true });
  };
  container.addEventListener("pointerdown", refocus, true);
  container.addEventListener("mousedown", refocus, true);
  container.addEventListener("touchstart", refocus, true);

  const onBlur = () => {
    if (typeof keyboard.reset === "function") keyboard.reset();
  };
  container.addEventListener("blur", onBlur);
  window.addEventListener("blur", onBlur);

  container.focus({ preventScroll: true });

  return {
    disconnect() {
      closing = true;
      container.removeEventListener("pointerdown", refocus, true);
      container.removeEventListener("mousedown", refocus, true);
      container.removeEventListener("touchstart", refocus, true);
      container.removeEventListener("blur", onBlur);
      window.removeEventListener("blur", onBlur);
      keyboard.onkeydown = null;
      keyboard.onkeyup = null;
      if (typeof keyboard.reset === "function") keyboard.reset();
      client.disconnect();
    },
    focus: refocus,
    setScale(s: number) {
      scale = s;
      display.scale(s);
    },
    resize(width: number, height: number) {
      client.sendSize(width, height);
    },
  };
}
