/**
 * remote/events — the SessionEvent schema for remote agent control.
 *
 * A running agent emits a stream of `SessionEvent`s; a remote client (a phone
 * app, a browser, another terminal) consumes them to observe — and intervene
 * on — the work in progress.
 *
 * The schema is aligned with the **AG-UI Protocol** (Agent-User Interaction
 * Protocol — the emerging standard adopted by AWS Bedrock AgentCore, Google,
 * CopilotKit). Event `type` strings mirror AG-UI's vocabulary so any
 * AG-UI-aware client interoperates. Two Vauban extensions go beyond AG-UI:
 *   - `tool.intent` — the agent announces what it is ABOUT to do before doing
 *     it (the "veto window" — transparency before action, not after).
 *   - every event carries an optional `sig` field — an Ed25519 signature of
 *     the canonical event, so the remote-control transcript is itself a
 *     tamper-evident audit trail (EU AI Act Art. 12).
 *
 * @public @since 2.11.0 — preste remote-control T1
 */

import { z } from "zod";

import { isKnownEventType } from "./event-name-map.js";

// ─── Event payloads ──────────────────────────────────────────────────────────

/** The run began. Mirrors AG-UI RUN_STARTED. */
export const RunStartPayloadSchema = z
  .object({
    runId: z.string(),
    agentId: z.string(),
    agentVersion: z.string().optional(),
    /** The user message / task that started the run. */
    task: z.string().optional(),
    startedAt: z.string(),
  })
  .strict();

/** A loop step completed. Vauban granularity on top of AG-UI. */
export const RunStepPayloadSchema = z
  .object({
    stepIndex: z.number().int().nonnegative(),
    inputTokens: z.number().int().nonnegative(),
    outputTokens: z.number().int().nonnegative(),
    costUsd: z.number().nonnegative(),
  })
  .strict();

/** A streamed assistant token delta. Mirrors AG-UI TEXT_MESSAGE_CONTENT. */
export const AssistantDeltaPayloadSchema = z
  .object({ text: z.string() })
  .strict();

/** A complete assistant message for a step. Mirrors AG-UI TEXT_MESSAGE_END. */
export const AssistantMessagePayloadSchema = z
  .object({ content: z.string() })
  .strict();

/**
 * The agent ANNOUNCES a tool call before executing it — the veto window.
 * Vauban extension: no AG-UI equivalent. A remote client gets a chance to
 * reject during `vetoWindowMs` before the call runs.
 */
export const ToolIntentPayloadSchema = z
  .object({
    callId: z.string(),
    toolName: z.string(),
    /** Canonical, possibly-truncated args preview. */
    argsPreview: z.string(),
    /** One-line rationale: why the agent intends this call. */
    rationale: z.string().optional(),
    /** Milliseconds the client has to veto before the call proceeds. */
    vetoWindowMs: z.number().int().nonnegative(),
  })
  .strict();

/** A tool call started. Mirrors AG-UI TOOL_CALL_START. */
export const ToolCallStartPayloadSchema = z
  .object({
    callId: z.string(),
    toolName: z.string(),
    argsPreview: z.string(),
  })
  .strict();

/** A tool call finished. Mirrors AG-UI TOOL_CALL_END. */
export const ToolCallEndPayloadSchema = z
  .object({
    callId: z.string(),
    toolName: z.string(),
    ok: z.boolean(),
    /** Truncated result/output summary. */
    resultPreview: z.string(),
    durationMs: z.number().int().nonnegative().optional(),
  })
  .strict();

/** A HITL approval is pending. Mirrors AG-UI RUN_FINISHED(interrupt). */
export const HitlRequestPayloadSchema = z
  .object({
    requestId: z.string(),
    action: z.string(),
    context: z.string(),
    /** Deadline after which the timeout policy fires (ISO8601). */
    expiresAt: z.string().optional(),
  })
  .strict();

/** A HITL request was resolved. */
export const HitlResolvedPayloadSchema = z
  .object({
    requestId: z.string(),
    approved: z.boolean(),
    by: z.string(),
  })
  .strict();

/** A remote client injected an instruction into the running session. */
export const InstructionInjectedPayloadSchema = z
  .object({
    text: z.string(),
    source: z.string(),
    /** When true, the instruction steers but is not shown as a user turn. */
    whisper: z.boolean().default(false),
  })
  .strict();

/** The run finished. Mirrors AG-UI RUN_FINISHED. */
export const RunFinishedPayloadSchema = z
  .object({
    runId: z.string(),
    stopReason: z.string(),
    stepCount: z.number().int().nonnegative(),
    costUsd: z.number().nonnegative(),
    finishedAt: z.string(),
  })
  .strict();

/** A periodic snapshot of session state. Mirrors AG-UI STATE_SNAPSHOT. */
export const StatePayloadSchema = z
  .object({
    runId: z.string(),
    stepCount: z.number().int().nonnegative(),
    costUsd: z.number().nonnegative(),
    elapsedMs: z.number().int().nonnegative(),
    pendingHitl: z.number().int().nonnegative(),
  })
  .strict();

// ─── The discriminated union ─────────────────────────────────────────────────

/**
 * Every event shares an envelope: a stable id, a sequence number (monotonic
 * per session — lets a reconnecting client resume without gaps), an ISO
 * timestamp, and an optional Ed25519 signature over the canonical event.
 */
const ENVELOPE = {
  /** Unique event id. */
  id: z.string(),
  /** Monotonic per-session sequence number — for gap-free reconnection. */
  seq: z.number().int().nonnegative(),
  ts: z.string(),
  /** Ed25519 signature of the canonical event (hex). Added by `signing.ts`. */
  sig: z.string().optional(),
} as const;

/**
 * @deprecated Legacy dotted-lowercase `type: "run.start"` is renamed to
 *   canonical AG-UI `"RUN_STARTED"`. Will be removed in
 *   `@vauban-org/agent-sdk@3.0.0`. Opt-in early via
 *   `createRemoteControlHub({ eventNaming: "canonical" })`.
 */
export const SessionEventSchema = z.discriminatedUnion("type", [
  /**
   * @deprecated renamed to `RUN_STARTED`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("run.start"),
    ...ENVELOPE,
    data: RunStartPayloadSchema,
  }),
  /**
   * @deprecated renamed to `STEP_STARTED`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("run.step"),
    ...ENVELOPE,
    data: RunStepPayloadSchema,
  }),
  /**
   * @deprecated renamed to `TEXT_MESSAGE_CONTENT`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("assistant.delta"),
    ...ENVELOPE,
    data: AssistantDeltaPayloadSchema,
  }),
  /**
   * @deprecated renamed to `TEXT_MESSAGE_END`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("assistant.message"),
    ...ENVELOPE,
    data: AssistantMessagePayloadSchema,
  }),
  /**
   * @deprecated renamed to `CUSTOM_TOOL_INTENT`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("tool.intent"),
    ...ENVELOPE,
    data: ToolIntentPayloadSchema,
  }),
  /**
   * @deprecated renamed to `TOOL_CALL_START`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("tool.call.start"),
    ...ENVELOPE,
    data: ToolCallStartPayloadSchema,
  }),
  /**
   * @deprecated renamed to `TOOL_CALL_END`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("tool.call.end"),
    ...ENVELOPE,
    data: ToolCallEndPayloadSchema,
  }),
  /**
   * @deprecated renamed to `CUSTOM_HITL_REQUEST`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("hitl.request"),
    ...ENVELOPE,
    data: HitlRequestPayloadSchema,
  }),
  /**
   * @deprecated renamed to `CUSTOM_HITL_RESOLVED`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("hitl.resolved"),
    ...ENVELOPE,
    data: HitlResolvedPayloadSchema,
  }),
  /**
   * @deprecated renamed to `CUSTOM_INSTRUCTION_INJECTED`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("instruction.injected"),
    ...ENVELOPE,
    data: InstructionInjectedPayloadSchema,
  }),
  /**
   * @deprecated renamed to `RUN_FINISHED`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({
    type: z.literal("run.finished"),
    ...ENVELOPE,
    data: RunFinishedPayloadSchema,
  }),
  /**
   * @deprecated renamed to `STATE_SNAPSHOT`, will be removed in `@vauban-org/agent-sdk@3.0.0`.
   */
  z.object({ type: z.literal("state"), ...ENVELOPE, data: StatePayloadSchema }),
]);

export type SessionEvent = z.infer<typeof SessionEventSchema>;
export type SessionEventType = SessionEvent["type"];

// ─── Permissive type guard ───────────────────────────────────────────────────

/**
 * Permissive runtime guard ; true when `input` looks like a SessionEvent
 * (legacy OR canonical AG-UI form). Used by relay-client + PWA + conformance
 * suite so the same handler can accept events emitted in either mode.
 *
 * The guard checks only the envelope ; it does NOT validate the payload
 * shape against `SessionEventSchema`. For full validation, use
 * `SessionEventSchema.safeParse(input)` separately.
 *
 * @public @since 2.27.0
 */
export function looksLikeSessionEvent(input: unknown): boolean {
  if (typeof input !== "object" || input === null) return false;
  const obj = input as Record<string, unknown>;
  if (typeof obj.type !== "string") return false;
  if (typeof obj.id !== "string") return false;
  if (typeof obj.seq !== "number" || !Number.isFinite(obj.seq)) return false;
  if (typeof obj.ts !== "string") return false;
  if (typeof obj.data !== "object" || obj.data === null) return false;
  return isKnownEventType(obj.type);
}

// ─── Construction helper ─────────────────────────────────────────────────────

let monotonicSeq = 0;

/**
 * Build a well-formed SessionEvent envelope around a payload.
 *
 * `seq` is monotonic process-wide; `id` is unique. The event is NOT signed
 * here — `signing.ts` adds `sig` as a separate, optional step so the SDK
 * stays free of a hard crypto dependency.
 */
export function makeEvent<T extends SessionEvent["type"]>(
  type: T,
  data: Extract<SessionEvent, { type: T }>["data"]
): SessionEvent {
  return {
    type,
    id: `evt_${Date.now().toString(36)}_${monotonicSeq.toString(36)}`,
    seq: monotonicSeq++,
    ts: new Date().toISOString(),
    data,
  } as SessionEvent;
}

/** Reset the monotonic sequence — test-only. */
export function __resetEventSeq(): void {
  monotonicSeq = 0;
}
