import type { Role } from "@slackwsh/contracts";

/**
 * Central authorisation module (§4.5): "Authorisation is centralised in one
 * module — core/policy.can(actor, action, resource). Controllers and socket
 * handlers call it; they never re-implement checks."
 *
 * The full roles × actions × resources permission matrix is a §8.1 spec
 * deliverable owed before Phase 2 opens. This is the MVP subset needed for
 * Phase 1 (workspace/channel/membership actions) — extend the ACTION_TABLE,
 * not the call sites, as new resource types are added.
 */

export type Action =
  | "workspace:read"
  | "workspace:update_settings"
  | "workspace:delete"
  | "member:invite"
  | "member:remove"
  | "member:change_role"
  | "channel:create"
  | "channel:read"
  | "channel:update"
  | "channel:archive"
  | "channel:post"
  | "message:edit_own"
  | "message:edit_any"
  | "message:delete_own"
  | "message:delete_any"
  | "message:pin"
  | "task:create"
  | "task:read"
  | "task:update"
  | "task:assign"
  | "task:update_status"
  | "task:delete"
  | "event:create"
  | "event:read"
  | "event:update"
  | "event:invite"
  | "event:rsvp"
  | "event:delete"
  | "call:start"
  | "call:read"
  | "call:join"
  | "call:end";

export interface Actor {
  userId: string | number;
  role: Role;
}

export interface ResourceContext {
  /** Present when the check concerns a specific message/channel author. */
  isOwnResource?: boolean;
  /** True when the actor is a single-channel guest scoped outside this channel. */
  isOutsideGuestScope?: boolean;
}

const ROLE_RANK: Record<Role, number> = {
  owner: 5,
  admin: 4,
  member: 3,
  multi_channel_guest: 2,
  single_channel_guest: 1,
  bot: 0,
};

function atLeast(role: Role, min: Role): boolean {
  return ROLE_RANK[role] >= ROLE_RANK[min];
}

const ACTION_TABLE: Record<Action, (actor: Actor, ctx: ResourceContext) => boolean> = {
  "workspace:read": (actor, ctx) => !ctx.isOutsideGuestScope && atLeast(actor.role, "single_channel_guest"),
  "workspace:update_settings": (actor) => atLeast(actor.role, "admin"),
  "workspace:delete": (actor) => actor.role === "owner",
  "member:invite": (actor) => atLeast(actor.role, "admin"),
  "member:remove": (actor) => atLeast(actor.role, "admin"),
  "member:change_role": (actor) => atLeast(actor.role, "admin"),
  "channel:create": (actor) => atLeast(actor.role, "member"),
  "channel:read": (actor, ctx) => !ctx.isOutsideGuestScope && atLeast(actor.role, "single_channel_guest"),
  "channel:update": (actor) => atLeast(actor.role, "member"),
  "channel:archive": (actor) => atLeast(actor.role, "admin"),
  "channel:post": (actor, ctx) => !ctx.isOutsideGuestScope && atLeast(actor.role, "single_channel_guest"),
  "message:edit_own": (actor, ctx) => Boolean(ctx.isOwnResource) && atLeast(actor.role, "single_channel_guest"),
  "message:edit_any": (actor) => atLeast(actor.role, "admin"),
  "message:delete_own": (actor, ctx) => Boolean(ctx.isOwnResource) && atLeast(actor.role, "single_channel_guest"),
  "message:delete_any": (actor) => atLeast(actor.role, "admin"),
  "message:pin": (actor) => atLeast(actor.role, "member"),
  // Tasks (§ Tasks feature): create/read is as open as posting a message —
  // any active member, including guests, can add or see a task. Editing
  // content or deleting is restricted to the creator (isOwnResource) or an
  // admin, since a task carries per-person accountability that "any member
  // can post" doesn't. Assigning and moving status are looser than that —
  // any member can triage/hand off a task or move it between columns, and
  // update_status additionally lets the assignee self-serve mark their own
  // task done even below the "member" rank.
  "task:create": (actor) => atLeast(actor.role, "single_channel_guest"),
  "task:read": (actor) => atLeast(actor.role, "single_channel_guest"),
  "task:update": (actor, ctx) => Boolean(ctx.isOwnResource) || atLeast(actor.role, "admin"),
  "task:assign": (actor) => atLeast(actor.role, "member"),
  "task:update_status": (actor, ctx) => Boolean(ctx.isOwnResource) || atLeast(actor.role, "member"),
  // Any member can delete any workspace task; guests may only delete their own.
  // Deleting someone else's task notifies the creator (see messaging deleteTask).
  "task:delete": (actor, ctx) => Boolean(ctx.isOwnResource) || atLeast(actor.role, "member"),
  // Calendar (§ Calendar feature): reading the workspace calendar is as open
  // as reading a task, but *scheduling* is member-and-up — an event puts a
  // claim on other people's time and can carry invitations, which isn't
  // something a single-channel guest should be able to do. Mutating an
  // existing event is the organiser's (isOwnResource) or an admin's, since a
  // series edit rewrites every occurrence for every attendee. RSVP is the
  // exception: any member may answer, and "only for yourself" is enforced in
  // the service rather than here, because the role table has no way to
  // express "the subject must equal the actor".
  "event:create": (actor) => atLeast(actor.role, "member"),
  "event:read": (actor) => atLeast(actor.role, "single_channel_guest"),
  "event:update": (actor, ctx) => Boolean(ctx.isOwnResource) || atLeast(actor.role, "admin"),
  "event:invite": (actor, ctx) => Boolean(ctx.isOwnResource) || atLeast(actor.role, "admin"),
  "event:rsvp": (actor) => atLeast(actor.role, "single_channel_guest"),
  "event:delete": (actor, ctx) => Boolean(ctx.isOwnResource) || atLeast(actor.role, "admin"),
  // Calls (§ Calls feature): ringing someone interrupts them, so starting a
  // call is member-and-up like scheduling an event rather than as open as
  // posting a message. Reading history is guest-level, but note that the
  // service additionally scopes the read to calls the actor took part in —
  // one member's call log is not workspace-public, which is a property the
  // role table cannot express. Joining is guest-level for the same reason
  // RSVP is: you are answering something addressed to you, and "you must be
  // an invited participant" is enforced in the service by construction.
  // Ending is not restricted to the initiator — anyone on a call can hang up,
  // and the service ends the call outright only when the last person leaves.
  "call:start": (actor) => atLeast(actor.role, "member"),
  "call:read": (actor) => atLeast(actor.role, "single_channel_guest"),
  "call:join": (actor) => atLeast(actor.role, "single_channel_guest"),
  "call:end": (actor) => atLeast(actor.role, "single_channel_guest"),
};

export function can(actor: Actor, action: Action, ctx: ResourceContext = {}): boolean {
  return ACTION_TABLE[action](actor, ctx);
}
