// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

import { createHash, randomUUID } from 'node:crypto';

const SCHEMA_VERSION_V2 = 2 as const;
const CONTENT_HASH_ALGORITHM = 'sha256';
const CONTENT_HASH_ENCODING = 'hex';
const CONTENT_HASH_PREFIX_LENGTH = 32;

export const AGENT_RUNTIME_CHANNEL_ID = 'agent-runtime' as const;

export const AGENT_RUNTIME_EVENT_KINDS = {
  TURN_STARTED: 'agent-runtime:turn_started',
  TURN_COMPLETED: 'agent-runtime:turn_completed',
  TURN_ABORTED: 'agent-runtime:turn_aborted',
  TOOL_CALLED: 'agent-runtime:tool_called',
  APPROVAL_REQUIRED: 'agent-runtime:approval_required',
  WORKFLOW_NODE_STARTED: 'agent-runtime:workflow_node_started',
  WORKFLOW_NODE_COMPLETED: 'agent-runtime:workflow_node_completed',
  WORKFLOW_NODE_FAILED: 'agent-runtime:workflow_node_failed',
  WORKFLOW_PAUSED: 'agent-runtime:workflow_paused',
  SCHEDULED_WAKEUP_SET: 'agent-runtime:scheduled_wakeup_set',
  BUDGET_THRESHOLD: 'agent-runtime:budget_threshold',
  AGENT_SESSION_ENROLLED: 'agent-runtime:agent_session_enrolled',
  AGENT_SESSION_ENDED: 'agent-runtime:agent_session_ended',
  AGENT_SESSION_STALLED: 'agent-runtime:agent_session_stalled',
  // WU-2733 (INIT-060 Phase 3, ADR-013 §6 governance):
  // autonomy_changed emitted by elevate_autonomy / lower_autonomy real
  // remote-control impls. Cloud conductor subscribes to render the new
  // autonomy band in the session UI.
  AUTONOMY_CHANGED: 'agent-runtime:autonomy_changed',
} as const;

export const AGENT_RUNTIME_EVENT_KIND_VALUES = [
  AGENT_RUNTIME_EVENT_KINDS.TURN_STARTED,
  AGENT_RUNTIME_EVENT_KINDS.TURN_COMPLETED,
  AGENT_RUNTIME_EVENT_KINDS.TURN_ABORTED,
  AGENT_RUNTIME_EVENT_KINDS.TOOL_CALLED,
  AGENT_RUNTIME_EVENT_KINDS.APPROVAL_REQUIRED,
  AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_STARTED,
  AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_COMPLETED,
  AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_FAILED,
  AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_PAUSED,
  AGENT_RUNTIME_EVENT_KINDS.SCHEDULED_WAKEUP_SET,
  AGENT_RUNTIME_EVENT_KINDS.BUDGET_THRESHOLD,
  AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_ENROLLED,
  AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_ENDED,
  AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_STALLED,
  AGENT_RUNTIME_EVENT_KINDS.AUTONOMY_CHANGED,
] as const;

export type AgentRuntimeEventKind = (typeof AGENT_RUNTIME_EVENT_KIND_VALUES)[number];

interface AgentRuntimeEventEnvelope {
  schema_version: typeof SCHEMA_VERSION_V2;
  timestamp: string;
  event_id: string;
  channel_id: typeof AGENT_RUNTIME_CHANNEL_ID;
  seq: number;
}

export interface AgentRuntimeTurnCostBreakdown {
  input_tokens_usd: number;
  output_tokens_usd: number;
  tool_calls_usd: number;
  total_usd: number;
}

export type AgentRuntimeCleanupStatus = 'clean' | 'partial' | 'needs_recovery';

export interface TurnStartedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.TURN_STARTED;
  session_id: string;
  turn_index: number;
  model_profile: string;
}

export interface TurnCompletedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.TURN_COMPLETED;
  session_id: string;
  turn_index: number;
  status: string;
  cost_breakdown: AgentRuntimeTurnCostBreakdown;
}

export interface TurnAbortedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.TURN_ABORTED;
  session_id: string;
  turn_index: number;
  cleanup_status: AgentRuntimeCleanupStatus;
  recovery_action: string | null;
  reason: string;
}

export interface ToolCalledEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.TOOL_CALLED;
  session_id: string;
  turn_index: number;
  tool_name: string;
  tool_call_id: string;
}

export interface ApprovalRequiredEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.APPROVAL_REQUIRED;
  session_id: string;
  turn_index: number;
  tool_name: string;
  request_id: string;
}

export interface WorkflowNodeStartedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_STARTED;
  session_id: string;
  node_id: string;
}

export interface WorkflowNodeCompletedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_COMPLETED;
  session_id: string;
  node_id: string;
}

export interface WorkflowNodeFailedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_FAILED;
  session_id: string;
  node_id: string;
  error_code: string;
  error_message: string;
}

export interface WorkflowPausedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_PAUSED;
  session_id: string;
  node_id: string;
  reason: string;
}

export interface ScheduledWakeupSetEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.SCHEDULED_WAKEUP_SET;
  session_id: string;
  node_id: string;
  wake_at: string;
}

export interface BudgetThresholdEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.BUDGET_THRESHOLD;
  session_id: string;
  budget_name: string;
  threshold: number;
  observed_value: number;
}

export interface AgentSessionEnrolledEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_ENROLLED;
  session_id: string;
  wu_id: string;
  lane: string;
  client_type: string;
}

export interface AgentSessionEndedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_ENDED;
  session_id: string;
  wu_id: string;
  lane: string;
  incidents_logged: number;
  incidents_major: number;
}

export interface AgentSessionStalledEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_STALLED;
  session_id: string;
  wu_id: string;
  lane: string;
  stage: string;
  reason: string;
}

/**
 * WU-2733 (INIT-060 Phase 3, ADR-013 §6 governance): autonomy_changed
 * carries the transition between autonomy bands ("manual" ↔ "assisted"
 * ↔ "auto") so cloud renders the shift and operators see the change
 * reflected in the evidence feed. `reason` surfaces the operator-supplied
 * rationale when provided.
 */
export interface AutonomyChangedEvent extends AgentRuntimeEventEnvelope {
  kind: typeof AGENT_RUNTIME_EVENT_KINDS.AUTONOMY_CHANGED;
  session_id: string;
  previous_level: string;
  new_level: string;
  direction: 'elevate' | 'lower';
  reason: string | null;
}

export type AgentRuntimeEvent =
  | TurnStartedEvent
  | TurnCompletedEvent
  | TurnAbortedEvent
  | ToolCalledEvent
  | ApprovalRequiredEvent
  | WorkflowNodeStartedEvent
  | WorkflowNodeCompletedEvent
  | WorkflowNodeFailedEvent
  | WorkflowPausedEvent
  | ScheduledWakeupSetEvent
  | BudgetThresholdEvent
  | AgentSessionEnrolledEvent
  | AgentSessionEndedEvent
  | AgentSessionStalledEvent
  | AutonomyChangedEvent;

export type AgentRuntimeEventSink = (event: AgentRuntimeEvent) => void;

export interface AgentRuntimeEventBuildOptions {
  timestamp?: string;
  idempotencyKey?: string;
}

export function resetAgentRuntimeSeqCounter(): void {
  channelSeqCounters.clear();
}

export function createZeroTurnCostBreakdown(): AgentRuntimeTurnCostBreakdown {
  return {
    input_tokens_usd: 0,
    output_tokens_usd: 0,
    tool_calls_usd: 0,
    total_usd: 0,
  };
}

export function buildTurnStartedEvent(
  input: Omit<TurnStartedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): TurnStartedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.TURN_STARTED,
      ...input,
    },
    options,
  );
}

export function buildTurnCompletedEvent(
  input: Omit<TurnCompletedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): TurnCompletedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.TURN_COMPLETED,
      ...input,
    },
    options,
  );
}

export function buildTurnAbortedEvent(
  input: Omit<TurnAbortedEvent, keyof AgentRuntimeEventEnvelope | 'kind' | 'recovery_action'> & {
    recovery_action?: string | null;
  },
  options?: AgentRuntimeEventBuildOptions,
): TurnAbortedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.TURN_ABORTED,
      ...input,
      recovery_action: resolveRecoveryAction(input.cleanup_status, input.recovery_action),
    },
    options,
  );
}

export function buildToolCalledEvent(
  input: Omit<ToolCalledEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): ToolCalledEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.TOOL_CALLED,
      ...input,
    },
    options,
  );
}

export function buildApprovalRequiredEvent(
  input: Omit<ApprovalRequiredEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): ApprovalRequiredEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.APPROVAL_REQUIRED,
      ...input,
    },
    options,
  );
}

export function buildWorkflowNodeStartedEvent(
  input: Omit<WorkflowNodeStartedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): WorkflowNodeStartedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_STARTED,
      ...input,
    },
    options,
  );
}

export function buildWorkflowNodeCompletedEvent(
  input: Omit<WorkflowNodeCompletedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): WorkflowNodeCompletedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_COMPLETED,
      ...input,
    },
    options,
  );
}

export function buildWorkflowNodeFailedEvent(
  input: Omit<WorkflowNodeFailedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): WorkflowNodeFailedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_NODE_FAILED,
      ...input,
    },
    options,
  );
}

export function buildWorkflowPausedEvent(
  input: Omit<WorkflowPausedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): WorkflowPausedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.WORKFLOW_PAUSED,
      ...input,
    },
    options,
  );
}

export function buildScheduledWakeupSetEvent(
  input: Omit<ScheduledWakeupSetEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): ScheduledWakeupSetEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.SCHEDULED_WAKEUP_SET,
      ...input,
    },
    options,
  );
}

export function buildBudgetThresholdEvent(
  input: Omit<BudgetThresholdEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): BudgetThresholdEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.BUDGET_THRESHOLD,
      ...input,
    },
    options,
  );
}

export function buildAgentSessionEnrolledEvent(
  input: Omit<AgentSessionEnrolledEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): AgentSessionEnrolledEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_ENROLLED,
      ...input,
    },
    options,
  );
}

export function buildAgentSessionEndedEvent(
  input: Omit<AgentSessionEndedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): AgentSessionEndedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_ENDED,
      ...input,
    },
    options,
  );
}

export function buildAgentSessionStalledEvent(
  input: Omit<AgentSessionStalledEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): AgentSessionStalledEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.AGENT_SESSION_STALLED,
      ...input,
    },
    options,
  );
}

export function buildAutonomyChangedEvent(
  input: Omit<AutonomyChangedEvent, keyof AgentRuntimeEventEnvelope | 'kind'>,
  options?: AgentRuntimeEventBuildOptions,
): AutonomyChangedEvent {
  return buildAgentRuntimeEvent(
    {
      kind: AGENT_RUNTIME_EVENT_KINDS.AUTONOMY_CHANGED,
      ...input,
    },
    options,
  );
}

export function emitAgentRuntimeEvent(
  sink: AgentRuntimeEventSink | undefined,
  event: AgentRuntimeEvent,
): void {
  if (!sink) {
    return;
  }
  try {
    sink(event);
  } catch {
    // Ephemeral telemetry must never break the host/session workflow.
  }
}

const channelSeqCounters = new Map<string, number>();

function buildAgentRuntimeEvent<T extends { kind: AgentRuntimeEventKind }>(
  payload: T,
  options?: AgentRuntimeEventBuildOptions,
): T & AgentRuntimeEventEnvelope {
  const timestamp = options?.timestamp ?? new Date().toISOString();
  const payloadRecord = payload as unknown as Record<string, unknown>;
  return {
    ...payload,
    schema_version: SCHEMA_VERSION_V2,
    timestamp,
    event_id: resolveEventId(payload.kind, payloadRecord, timestamp, options),
    channel_id: AGENT_RUNTIME_CHANNEL_ID,
    seq: nextSeq(AGENT_RUNTIME_CHANNEL_ID),
  };
}

function resolveEventId(
  kind: string,
  payload: Record<string, unknown>,
  timestamp: string,
  options?: AgentRuntimeEventBuildOptions,
): string {
  const key = options?.idempotencyKey;
  if (!key) {
    return randomUUID();
  }
  const canonicalPayload = JSON.stringify(payload, Object.keys(payload).sort());
  return createHash(CONTENT_HASH_ALGORITHM)
    .update(`${key}|${kind}|${timestamp}|${canonicalPayload}`)
    .digest(CONTENT_HASH_ENCODING)
    .slice(0, CONTENT_HASH_PREFIX_LENGTH);
}

function nextSeq(channelId: string): number {
  const current = channelSeqCounters.get(channelId) ?? 0;
  const next = current + 1;
  channelSeqCounters.set(channelId, next);
  return next;
}

function resolveRecoveryAction(
  cleanupStatus: AgentRuntimeCleanupStatus,
  recoveryAction?: string | null,
): string | null {
  if (cleanupStatus === 'clean') {
    return null;
  }
  if (recoveryAction && recoveryAction.trim().length > 0) {
    return recoveryAction.trim();
  }
  if (cleanupStatus === 'partial') {
    return 'Review the partially applied state and re-run wu:prep before resuming.';
  }
  return 'Run wu:recover after reviewing the current session state.';
}
