import type {
  MediaSurface,
  MediaSurfaceAcl,
  MediaSurfaceSource,
  MediaSurfaceState,
  MediaViewerContext,
} from "./types.ts";

const ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
const AUDIENCES = new Set(["public", "authenticated", "office-members", "allowlist"]);
const SOURCE_KINDS = new Set(["live-stream", "recording"]);
const PRIVACY = new Set(["public", "private"]);
const STATUSES = new Set(["idle", "presenting", "reconnecting", "stopped", "revoked"]);

export function parseMediaSurface(value: unknown): MediaSurface {
  const surface = record(value, "surface", [
    "screenId", "officeId", "levelId", "roomId", "acl", "source", "state",
  ]);
  const parsed: MediaSurface = {
    screenId: id(surface.screenId, "screenId"),
    officeId: id(surface.officeId, "officeId"),
    levelId: id(surface.levelId, "levelId"),
    roomId: surface.roomId === null ? null : id(surface.roomId, "roomId"),
    acl: parseAcl(surface.acl),
    source: surface.source === null ? null : parseSource(surface.source),
    state: parseState(surface.state),
  };
  const live = parsed.state.status === "presenting" || parsed.state.status === "reconnecting";
  if (live !== (parsed.source !== null)) {
    throw new TypeError("presenting/reconnecting surfaces require a source; all other states forbid one");
  }
  if (parsed.source?.privacy === "private" && parsed.acl.audience === "public") {
    throw new TypeError("a private source cannot have a public audience");
  }
  if (live !== (parsed.state.presenterId !== null)) {
    throw new TypeError("presenting/reconnecting surfaces require a presenter; all other states forbid one");
  }
  if (parsed.state.presenterId && !parsed.acl.presenterIds.includes(parsed.state.presenterId)) {
    throw new TypeError("current presenter is not permitted by acl.presenterIds");
  }
  return parsed;
}

export function isMediaSurface(value: unknown): value is MediaSurface {
  try {
    parseMediaSurface(value);
    return true;
  } catch {
    return false;
  }
}

export function parseMediaSource(value: unknown): MediaSurfaceSource {
  return parseSource(value);
}

/** Validate the trusted context assembled by a server/session boundary. */
export function parseMediaViewerContext(value: unknown): MediaViewerContext {
  const viewer = record(value, "viewer", ["viewerId", "officeIds", "optedIn"]);
  if (viewer.viewerId !== null && typeof viewer.viewerId !== "string") {
    throw new TypeError("viewer.viewerId is invalid");
  }
  if (typeof viewer.optedIn !== "boolean") throw new TypeError("viewer.optedIn must be boolean");
  return {
    viewerId: viewer.viewerId === null ? null : id(viewer.viewerId, "viewer.viewerId"),
    officeIds: ids(viewer.officeIds, "viewer.officeIds"),
    optedIn: viewer.optedIn,
  };
}

function parseAcl(value: unknown): MediaSurfaceAcl {
  const acl = record(value, "acl", ["audience", "viewerIds", "presenterIds", "moderatorIds"]);
  if (typeof acl.audience !== "string" || !AUDIENCES.has(acl.audience)) {
    throw new TypeError("acl.audience is invalid");
  }
  const parsed: MediaSurfaceAcl = {
    audience: acl.audience as MediaSurfaceAcl["audience"],
    viewerIds: ids(acl.viewerIds, "acl.viewerIds"),
    presenterIds: ids(acl.presenterIds, "acl.presenterIds"),
    moderatorIds: ids(acl.moderatorIds, "acl.moderatorIds"),
  };
  if (parsed.audience === "allowlist" && parsed.viewerIds.length === 0) {
    throw new TypeError("an allowlist audience requires acl.viewerIds");
  }
  return parsed;
}

function parseSource(value: unknown): MediaSurfaceSource {
  const source = record(value, "source", ["kind", "locator", "privacy", "hasAudio", "autoplay", "muted"]);
  if (typeof source.kind !== "string" || !SOURCE_KINDS.has(source.kind)) throw new TypeError("source.kind is invalid");
  if (typeof source.privacy !== "string" || !PRIVACY.has(source.privacy)) throw new TypeError("source.privacy is invalid");
  if (typeof source.locator !== "string" || source.locator.length < 1 || source.locator.length > 2048) {
    throw new TypeError("source.locator must be a non-empty string of at most 2048 characters");
  }
  if (typeof source.hasAudio !== "boolean") throw new TypeError("source.hasAudio must be boolean");
  if (source.autoplay !== false) throw new TypeError("media sources must set autoplay to false");
  if (source.muted !== true) throw new TypeError("media sources must start muted");
  return {
    kind: source.kind as MediaSurfaceSource["kind"],
    locator: source.locator,
    privacy: source.privacy as MediaSurfaceSource["privacy"],
    hasAudio: source.hasAudio,
    autoplay: false,
    muted: true,
  };
}

function parseState(value: unknown): MediaSurfaceState {
  const state = record(value, "state", ["status", "presenterId", "revision"]);
  if (typeof state.status !== "string" || !STATUSES.has(state.status)) throw new TypeError("state.status is invalid");
  if (state.presenterId !== null && typeof state.presenterId !== "string") throw new TypeError("state.presenterId is invalid");
  if (!Number.isSafeInteger(state.revision) || (state.revision as number) < 0) {
    throw new TypeError("state.revision must be a non-negative safe integer");
  }
  return {
    status: state.status as MediaSurfaceState["status"],
    presenterId: state.presenterId === null ? null : id(state.presenterId, "state.presenterId"),
    revision: state.revision as number,
  };
}

function record(value: unknown, name: string, keys: readonly string[]): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
    throw new TypeError(`${name} must be a plain JSON object`);
  }
  const result = value as Record<string, unknown>;
  const actual = Object.keys(result);
  if (actual.length !== keys.length || actual.some((key) => !keys.includes(key))) {
    throw new TypeError(`${name} must contain exactly: ${keys.join(", ")}`);
  }
  return result;
}

function id(value: unknown, name: string): string {
  if (typeof value !== "string" || !ID.test(value)) throw new TypeError(`${name} is not a valid id`);
  return value;
}

function ids(value: unknown, name: string): string[] {
  if (!Array.isArray(value) || value.length > 1024) throw new TypeError(`${name} must be an array`);
  const result = value.map((item, index) => id(item, `${name}[${index}]`));
  if (new Set(result).size !== result.length) throw new TypeError(`${name} contains duplicates`);
  return result;
}
