/**
 * Permission event channel — public contract.
 *
 * Exports channel name constants, TypeScript types for all emitted events,
 * and thin emit helpers.
 *
 * Stability guarantee: fields may be added, but existing fields will not be
 * removed or renamed without a semver-major version bump.
 */

import type { PromptRequestFacts } from "#src/presentation/prompt-payload";

/** Minimal event bus interface required by the emit helpers. */
export interface PermissionEventBus {
  emit(channel: string, data: unknown): void;
  on(channel: string, handler: (data: unknown) => void): () => void;
}

// ── Channel name constants ─────────────────────────────────────────────────

/** Emitted at `session_start`, after the service is published. */
export const PERMISSIONS_READY_CHANNEL = "permissions:ready";

/** Emitted when a permission request is committed to the active UI prompt path. */
export const PERMISSIONS_UI_PROMPT_CHANNEL = "permissions:ui_prompt";

/** Emitted after every permission gate resolution. */
export const PERMISSIONS_DECISION_CHANNEL = "permissions:decision";

// ── permissions:ready ──────────────────────────────────────────────────────

/**
 * Payload emitted on `permissions:ready`.
 *
 * Intentionally empty: the channel is a readiness signal. There is no
 * `protocolVersion` — the published types plus package semver define the
 * broadcast contract.
 */
export type PermissionsReadyEvent = Record<string, never>;

// ── permissions:ui_prompt ──────────────────────────────────────────────────

/**
 * Origin of a UI prompt.
 *
 * Forwarding is orthogonal to origin: a forwarded subagent prompt keeps its
 * original source and is identified by a non-null `forwarding` field, not by a
 * dedicated source value.
 */
export type PermissionUiPromptSource =
  | "tool_call"
  | "skill_input"
  | "skill_read";

/** Forwarding context, present only when a prompt was forwarded from a non-UI subagent. */
export interface ForwardedPromptContext {
  /** Requesting subagent's display name, when known. */
  requesterAgentName: string | null;
  /** Requesting subagent's session id, when known. */
  requesterSessionId: string | null;
}

/**
 * Payload emitted on `permissions:ui_prompt`, immediately before the active
 * user-facing permission UI is shown.
 *
 * Lean by design: `surface`/`value` are the normalized display projection a
 * notification consumer reads; `source` is the origin; `forwarding` is non-null
 * only for forwarded subagent prompts. There is no `protocolVersion` — the
 * published types plus package semver define the broadcast contract, and
 * consumers should read defensively.
 */
export interface PermissionUiPromptEvent {
  /** Unique ID for the permission request being prompted. */
  requestId: string;
  /** Prompt origin. */
  source: PermissionUiPromptSource;
  /** Normalized display surface (e.g. "bash", "skill"), when known. */
  surface: string | null;
  /** Normalized display value (command, path, skill name, etc.), when known. */
  value: string | null;
  /** Agent name (when known). */
  agentName: string | null;
  /**
   * The ask's invariant core (ADR 0011 §3), verbatim from the prompt payload.
   *
   * Nested rather than flattened so the event and the payload share one shape:
   * a fact added to `PromptRequestFacts` reaches the bus without a second
   * hand-maintained declaration. Carries no evidence and no annotations — the
   * bus is the narrowest renderer (ADR 0011 §6), observable by any loaded
   * extension without the operator having named it.
   *
   * `request.surface` is the *gate* surface the rule fired on; the top-level
   * `surface` is the display projection. Both are here on purpose.
   */
  request: PromptRequestFacts;
  /** Forwarding context, or null for a direct prompt. */
  forwarding: ForwardedPromptContext | null;
}

// ── permissions:decision ───────────────────────────────────────────────────

/** How a permission decision was reached. */
export type PermissionDecisionResolution =
  | "policy_allow"
  | "policy_deny"
  | "session_approved"
  | "infrastructure_auto_allowed"
  | "user_approved"
  | "user_approved_for_session"
  | "user_denied"
  | "auto_approved"
  | "confirmation_unavailable";

/** Payload emitted on `permissions:decision`. */
export interface PermissionDecisionEvent {
  /**
   * Identifies the permission request this decision resolves, minted when the
   * request was created. Distinct from the host's tool-call id: one tool call
   * runs several gates and so raises several requests.
   */
  requestId: string;
  /** Permission surface: "bash", "read", "mcp", "skill", "external_directory", etc. */
  surface: string;
  /** The value that was evaluated (command, tool name, skill name, path). */
  value: string;
  /** Final decision. */
  result: "allow" | "deny";
  /** How the decision was reached. */
  resolution: PermissionDecisionResolution;
  /** Which config scope contributed the winning rule (when available). */
  origin: string | null;
  /** Agent name (when known). */
  agentName: string | null;
  /** Matched pattern from the winning rule (when available). */
  matchedPattern: string | null;
}

// ── Emit helpers ───────────────────────────────────────────────────────────

/**
 * Emit the `permissions:ready` broadcast.
 * Call at `session_start`, after the service is published, so a consumer
 * reacting to ready can immediately resolve `getPermissionsService()`.
 */
export function emitReadyEvent(events: PermissionEventBus): void {
  const payload: PermissionsReadyEvent = {};
  try {
    events.emit(PERMISSIONS_READY_CHANNEL, payload);
  } catch {
    // Broadcasts are best-effort. A throwing listener must not block the
    // permission system from completing session startup.
  }
}

/**
 * Emit a `permissions:ui_prompt` broadcast.
 * Call immediately before invoking the active user-facing permission UI.
 */
export function emitUiPromptEvent(
  events: PermissionEventBus,
  event: PermissionUiPromptEvent,
): void {
  try {
    events.emit(PERMISSIONS_UI_PROMPT_CHANNEL, event);
  } catch {
    // UI-prompt broadcasts are observational. A consumer failure must not block
    // the permission dialog itself.
  }
}

/**
 * Emit a `permissions:decision` broadcast.
 * Call after every permission gate resolution.
 */
export function emitDecisionEvent(
  events: PermissionEventBus,
  event: PermissionDecisionEvent,
): void {
  try {
    events.emit(PERMISSIONS_DECISION_CHANNEL, event);
  } catch {
    // Broadcasts are best-effort. A throwing listener must not block the
    // permission gate from resolving.
  }
}
