import type {
  MediaAuthorizationDecision,
  MediaAuthorizationReason,
  MediaSurface,
  MediaSurfaceEvent,
  MediaSurfaceSource,
  MediaViewerContext,
} from "./types.ts";
import { parseMediaSource, parseMediaSurface, parseMediaViewerContext } from "./validation.ts";

/**
 * Produce the response a server may return to this viewer. The source locator
 * is copied only after authorization, presentation state, and explicit opt-in.
 */
export function authorizeMediaSurface(
  input: MediaSurface,
  viewer: MediaViewerContext,
): MediaAuthorizationDecision {
  const surface = parseMediaSurface(input);
  const context = parseMediaViewerContext(viewer);
  const access = audienceDecision(surface, context);
  const presenting = surface.state.status === "presenting" && surface.source !== null;
  let reason: MediaAuthorizationReason = access.reason;
  if (access.authorized && !presenting) reason = "not_presenting";
  else if (access.authorized && presenting && !context.optedIn) reason = "opt_in_required";
  else if (access.authorized && presenting && context.optedIn) reason = "ready";
  const canView = reason === "ready";
  return {
    authorized: access.authorized,
    optedIn: context.optedIn,
    canView,
    reason,
    surface: {
      screenId: surface.screenId,
      officeId: surface.officeId,
      levelId: surface.levelId,
      roomId: surface.roomId,
      status: surface.state.status,
      source: canView && surface.source ? { ...surface.source } : null,
      playback: { autoplay: false, muted: true },
    },
  };
}

/** Strict state machine for presenter stop/revoke/disconnect/reconnect flows. */
export function transitionMediaSurface(input: MediaSurface, event: MediaSurfaceEvent): MediaSurface {
  const surface = parseMediaSurface(input);
  switch (event.type) {
    case "present": {
      requirePresenter(surface, event.presenterId);
      const source = parseMediaSource(event.source);
      if (source.privacy === "private" && surface.acl.audience === "public") {
        throw new Error("a private source cannot be presented to a public audience");
      }
      return next(surface, "presenting", event.presenterId, source);
    }
    case "stop":
      requireCurrent(surface, event.presenterId);
      return next(surface, "stopped", null, null);
    case "revoke":
      if (event.actorId !== surface.state.presenterId && !surface.acl.moderatorIds.includes(event.actorId)) {
        throw new Error("only the current presenter or a moderator may revoke a surface");
      }
      return next(surface, "revoked", null, null);
    case "disconnect":
      requireCurrent(surface, event.presenterId);
      if (surface.state.status !== "presenting") throw new Error("only a presenting surface can disconnect");
      return next(surface, "reconnecting", event.presenterId, surface.source);
    case "reconnect":
      requireCurrent(surface, event.presenterId);
      if (surface.state.status !== "reconnecting") throw new Error("only a reconnecting surface can reconnect");
      return next(
        surface,
        "presenting",
        event.presenterId,
        event.source === undefined ? surface.source : parseMediaSource(event.source),
      );
  }
}

function audienceDecision(
  surface: MediaSurface,
  viewer: MediaViewerContext,
): { authorized: boolean; reason: MediaAuthorizationReason } {
  const id = viewer.viewerId;
  switch (surface.acl.audience) {
    case "public":
      return { authorized: true, reason: "ready" };
    case "authenticated":
      return id ? { authorized: true, reason: "ready" } : { authorized: false, reason: "authentication_required" };
    case "office-members":
      return id && viewer.officeIds.includes(surface.officeId)
        ? { authorized: true, reason: "ready" }
        : { authorized: false, reason: id ? "office_membership_required" : "authentication_required" };
    case "allowlist":
      return id && surface.acl.viewerIds.includes(id)
        ? { authorized: true, reason: "ready" }
        : { authorized: false, reason: id ? "not_allowlisted" : "authentication_required" };
  }
}

function requirePresenter(surface: MediaSurface, presenterId: string): void {
  if (!surface.acl.presenterIds.includes(presenterId)) throw new Error("presenter is not permitted by this surface");
}

function requireCurrent(surface: MediaSurface, presenterId: string): void {
  if (surface.state.presenterId !== presenterId) throw new Error("caller is not the current presenter");
}

function next(
  surface: MediaSurface,
  status: MediaSurface["state"]["status"],
  presenterId: string | null,
  source: MediaSurfaceSource | null,
): MediaSurface {
  return parseMediaSurface({
    ...surface,
    acl: {
      ...surface.acl,
      viewerIds: [...surface.acl.viewerIds],
      presenterIds: [...surface.acl.presenterIds],
      moderatorIds: [...surface.acl.moderatorIds],
    },
    source: source ? { ...source } : null,
    state: { status, presenterId, revision: surface.state.revision + 1 },
  });
}
