/**
 * Office packs, off disk.
 *
 * `TERA_OFFICES_DIR` holds one `<id>.json` per office, each one an `OfficeDoc`
 * wrapping the `Office` that `src/interiors/types.ts` defines — the same bytes a
 * self-hoster hand-writes and drops in the directory. There is no database and
 * no build step, because the format's whole claim is that a pack written by hand
 * and a pack arriving over HTTP are the same thing.
 *
 * A box with no offices directory configured has no offices. That is the
 * default, and it answers 404 to everything.
 */

import { readFile } from "node:fs/promises";
import { join } from "node:path";
import type { OfficeDoc } from "../../../src/server/wire.ts";

/**
 * Ids are the filename, so this is a path-traversal boundary and not a style
 * preference. Lowercase, digits and hyphens; nothing that can climb out of the
 * directory and nothing that means something different on a case-insensitive
 * filesystem.
 */
const ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;

export interface OfficeStore {
  get(id: string): Promise<OfficeDoc | null>;
}

export function createOfficeStore(dir: string): OfficeStore {
  return {
    async get(id: string): Promise<OfficeDoc | null> {
      if (dir === "" || !ID_PATTERN.test(id)) return null;
      try {
        const parsed: unknown = JSON.parse(await readFile(join(dir, `${id}.json`), "utf8"));
        return normalise(parsed, id);
      } catch {
        // Missing, unreadable and unparseable are the same answer to a caller:
        // there is no office here. Distinguishing them out loud would leak the
        // directory listing one status code at a time.
        return null;
      }
    },
  };
}

function normalise(parsed: unknown, id: string): OfficeDoc | null {
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
  const doc = parsed as Partial<OfficeDoc>;
  if (doc.floor === undefined || typeof doc.floor !== "object") return null;

  // Visibility defaults to `private`. A pack that forgot to say is a pack whose
  // author has not thought about it yet, and the fail-closed reading of that is
  // the only one that cannot embarrass anybody.
  const visibility =
    doc.visibility === "public" || doc.visibility === "unlisted" ? doc.visibility : "private";

  return {
    id: typeof doc.id === "string" ? doc.id : id,
    name: typeof doc.name === "string" ? doc.name : id,
    floor: doc.floor,
    visibility,
    ...(typeof doc.updated === "string" ? { updated: doc.updated } : {}),
  };
}
