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

import { createHash, randomUUID } from 'node:crypto';
import { appendFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
import { getSidekickRuntimeContext } from './tool-impl/runtime-context.js';
import {
  getStoragePort,
  type ChannelMessageRecord,
  type ChannelRecord,
  type MemoryRecord,
  type RoutineRecord,
  type SidekickStores,
  type TaskRecord,
} from './tool-impl/storage.js';

const SCHEMA_VERSION_V2 = 2 as const;
const CONTENT_HASH_ALGORITHM = 'sha256';
const CONTENT_HASH_ENCODING = 'hex';
const CONTENT_HASH_PREFIX_LENGTH = 32;
const OUTBOX_RELATIVE_PATH = path.join(
  '.lumenflow',
  'state',
  'conductor',
  'outbox',
  'sidekick-events.jsonl',
);

export const SIDEKICK_CHANNEL_ID = 'sidekick' as const;

export const SIDEKICK_EVENT_KINDS = {
  TASK_CREATED: 'sidekick:task_created',
  TASK_COMPLETED: 'sidekick:task_completed',
  TASK_SCHEDULED: 'sidekick:task_scheduled',
  TASK_SNOOZED: 'sidekick:task_snoozed',
  MEMORY_STORED: 'sidekick:memory_stored',
  MEMORY_RECALLED: 'sidekick:memory_recalled',
  MEMORY_FORGOTTEN: 'sidekick:memory_forgotten',
  CHANNEL_MESSAGE_SENT: 'sidekick:channel_message_sent',
  CHANNEL_MESSAGE_RECEIVED: 'sidekick:channel_message_received',
  CHANNEL_BRIDGE_CONNECTED: 'sidekick:channel_bridge_connected',
  CHANNEL_BRIDGE_DISCONNECTED: 'sidekick:channel_bridge_disconnected',
  ROUTINE_PLANNED: 'sidekick:routine_planned',
  ROUTINE_COMMITTED: 'sidekick:routine_committed',
  ROUTINE_EXECUTED: 'sidekick:routine_executed',
  ROUTINE_STEP_FAILED: 'sidekick:routine_step_failed',
  STATE_REHYDRATED: 'sidekick:state_rehydrated',
} as const;

export const SIDEKICK_EVENT_KIND_VALUES = [
  SIDEKICK_EVENT_KINDS.TASK_CREATED,
  SIDEKICK_EVENT_KINDS.TASK_COMPLETED,
  SIDEKICK_EVENT_KINDS.TASK_SCHEDULED,
  SIDEKICK_EVENT_KINDS.TASK_SNOOZED,
  SIDEKICK_EVENT_KINDS.MEMORY_STORED,
  SIDEKICK_EVENT_KINDS.MEMORY_RECALLED,
  SIDEKICK_EVENT_KINDS.MEMORY_FORGOTTEN,
  SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_SENT,
  SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_RECEIVED,
  SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_CONNECTED,
  SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_DISCONNECTED,
  SIDEKICK_EVENT_KINDS.ROUTINE_PLANNED,
  SIDEKICK_EVENT_KINDS.ROUTINE_COMMITTED,
  SIDEKICK_EVENT_KINDS.ROUTINE_EXECUTED,
  SIDEKICK_EVENT_KINDS.ROUTINE_STEP_FAILED,
  SIDEKICK_EVENT_KINDS.STATE_REHYDRATED,
] as const;

export type SidekickEventKind = (typeof SIDEKICK_EVENT_KIND_VALUES)[number];
export type SidekickBackpressurePolicy = 'queue-with-replay' | 'ephemeral';

const QUEUE_WITH_REPLAY: SidekickBackpressurePolicy = 'queue-with-replay';
const EPHEMERAL: SidekickBackpressurePolicy = 'ephemeral';

export const SIDEKICK_EVENT_BACKPRESSURE_POLICY: Record<
  SidekickEventKind,
  SidekickBackpressurePolicy
> = {
  [SIDEKICK_EVENT_KINDS.TASK_CREATED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.TASK_COMPLETED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.TASK_SCHEDULED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.TASK_SNOOZED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.MEMORY_STORED]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.MEMORY_RECALLED]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.MEMORY_FORGOTTEN]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_SENT]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_RECEIVED]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_CONNECTED]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_DISCONNECTED]: EPHEMERAL,
  [SIDEKICK_EVENT_KINDS.ROUTINE_PLANNED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.ROUTINE_COMMITTED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.ROUTINE_EXECUTED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.ROUTINE_STEP_FAILED]: QUEUE_WITH_REPLAY,
  [SIDEKICK_EVENT_KINDS.STATE_REHYDRATED]: QUEUE_WITH_REPLAY,
};

interface SidekickEventEnvelope {
  schema_version: typeof SCHEMA_VERSION_V2;
  timestamp: string;
  event_id: string;
  channel_id: typeof SIDEKICK_CHANNEL_ID;
  seq: number;
}

export interface TaskCreatedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.TASK_CREATED;
  task: TaskRecord;
}

export interface TaskCompletedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.TASK_COMPLETED;
  task: TaskRecord;
}

export interface TaskScheduledEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.TASK_SCHEDULED;
  task: TaskRecord;
}

export interface TaskSnoozedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.TASK_SNOOZED;
  task_id: string;
  previous_due_at: string | null;
  due_at: string;
}

export interface MemoryStoredEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.MEMORY_STORED;
  memory: MemoryRecord;
}

export interface MemoryRecalledEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.MEMORY_RECALLED;
  query: string | null;
  count: number;
  memory_ids: string[];
}

export interface MemoryForgottenEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.MEMORY_FORGOTTEN;
  memory_id: string;
}

export interface ChannelMessageSentEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_SENT;
  message: Record<string, unknown>;
}

export interface ChannelMessageReceivedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_RECEIVED;
  channel: string;
  count: number;
  provider?: string;
}

export interface ChannelBridgeConnectedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_CONNECTED;
  channel: ChannelRecord;
}

export interface ChannelBridgeDisconnectedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_DISCONNECTED;
  bridge_id: string;
  deleted_message_count: number;
}

export interface RoutinePlannedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.ROUTINE_PLANNED;
  routine: RoutineRecord;
}

export interface RoutineCommittedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.ROUTINE_COMMITTED;
  routine: RoutineRecord;
  /**
   * WU-2738 (INIT-060, ADR-013 §6 governance): content-hash of the commit
   * envelope carried when the event is emitted by the governed
   * `sidekick:routine:commit_plan` tool. PII stays inside the local
   * envelope; cloud correlates on the hash. Optional so the existing
   * update-triggered emission (routine:update) stays PII-free.
   */
  envelope_content_hash?: string;
}

export interface RoutineExecutedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.ROUTINE_EXECUTED;
  routine_id: string;
  name: string;
  step_count: number;
  plan_only: true;
}

export interface RoutineStepFailedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.ROUTINE_STEP_FAILED;
  step_index: number;
  reason: string;
  routine_id?: string;
  routine_name?: string;
}

export interface StateRehydratedEvent extends SidekickEventEnvelope {
  kind: typeof SIDEKICK_EVENT_KINDS.STATE_REHYDRATED;
  snapshot: SidekickStores;
}

export type SidekickEvent =
  | TaskCreatedEvent
  | TaskCompletedEvent
  | TaskScheduledEvent
  | TaskSnoozedEvent
  | MemoryStoredEvent
  | MemoryRecalledEvent
  | MemoryForgottenEvent
  | ChannelMessageSentEvent
  | ChannelMessageReceivedEvent
  | ChannelBridgeConnectedEvent
  | ChannelBridgeDisconnectedEvent
  | RoutinePlannedEvent
  | RoutineCommittedEvent
  | RoutineExecutedEvent
  | RoutineStepFailedEvent
  | StateRehydratedEvent;

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

type EventPayload = Record<string, unknown> & { kind: SidekickEventKind };

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

function resolveTimestamp(timestamp?: string): string {
  return timestamp ?? new Date().toISOString();
}

function resolveEventId(
  kind: SidekickEventKind,
  payload: Record<string, unknown>,
  timestamp: string,
  idempotencyKey?: string,
): string {
  if (!idempotencyKey) {
    return randomUUID();
  }
  return createHash(CONTENT_HASH_ALGORITHM)
    .update(
      JSON.stringify({
        kind,
        payload,
        timestamp,
        idempotencyKey,
      }),
    )
    .digest(CONTENT_HASH_ENCODING)
    .slice(0, CONTENT_HASH_PREFIX_LENGTH);
}

function stampSidekickEvent<TEvent extends SidekickEvent>(
  payload: EventPayload,
  options?: { timestamp?: string; idempotencyKey?: string },
): TEvent {
  const timestamp = resolveTimestamp(options?.timestamp);
  const event_id = resolveEventId(payload.kind, payload, timestamp, options?.idempotencyKey);
  return {
    ...payload,
    schema_version: SCHEMA_VERSION_V2,
    timestamp,
    event_id,
    channel_id: SIDEKICK_CHANNEL_ID,
    seq: nextSeq(SIDEKICK_CHANNEL_ID),
  } as TEvent;
}

async function queueEvent(event: SidekickEvent, workspaceRoot: string): Promise<void> {
  const outboxPath = path.join(workspaceRoot, OUTBOX_RELATIVE_PATH);
  await mkdir(path.dirname(outboxPath), { recursive: true });
  await appendFile(outboxPath, `${JSON.stringify(event)}\n`, 'utf8');
}

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

export function readSidekickOrgId(config: unknown): string | undefined {
  if (!config || typeof config !== 'object') {
    return undefined;
  }
  const controlPlane = (config as { control_plane?: unknown }).control_plane;
  if (!controlPlane || typeof controlPlane !== 'object') {
    return undefined;
  }
  const raw = (controlPlane as { org_id?: unknown }).org_id;
  if (typeof raw !== 'string') {
    return undefined;
  }
  const trimmed = raw.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

export async function emitSidekickEvent<TEvent extends SidekickEvent>(
  event: TEvent,
): Promise<TEvent> {
  const runtimeContext = getSidekickRuntimeContext();
  const policy = SIDEKICK_EVENT_BACKPRESSURE_POLICY[event.kind];
  const orgId = readSidekickOrgId(runtimeContext?.workspaceConfig);
  const sink = runtimeContext?.eventSink;

  if (policy === 'ephemeral') {
    if (!orgId || !sink) {
      return event;
    }
    try {
      await sink(event);
    } catch {
      // Ephemeral events fail silent when the control plane is unavailable.
    }
    return event;
  }

  if (orgId && sink) {
    try {
      await sink(event);
      return event;
    } catch {
      // Fall through to queued replay on transport failure.
    }
  }

  await queueEvent(event, runtimeContext?.workspaceRoot ?? process.cwd());
  return event;
}

export async function snapshotSidekickState(): Promise<SidekickStores> {
  const storage = getStoragePort();
  const [tasks, memories, channels, messages, routines] = await Promise.all([
    storage.readStore('tasks'),
    storage.readStore('memories'),
    storage.readStore('channels'),
    storage.readStore('messages'),
    storage.readStore('routines'),
  ]);

  return { tasks, memories, channels, messages, routines };
}

export function buildTaskCreatedEvent(task: TaskRecord): TaskCreatedEvent {
  return stampSidekickEvent<TaskCreatedEvent>({
    kind: SIDEKICK_EVENT_KINDS.TASK_CREATED,
    task,
  });
}

export function buildTaskCompletedEvent(task: TaskRecord): TaskCompletedEvent {
  return stampSidekickEvent<TaskCompletedEvent>({
    kind: SIDEKICK_EVENT_KINDS.TASK_COMPLETED,
    task,
  });
}

export function buildTaskScheduledEvent(task: TaskRecord): TaskScheduledEvent {
  return stampSidekickEvent<TaskScheduledEvent>({
    kind: SIDEKICK_EVENT_KINDS.TASK_SCHEDULED,
    task,
  });
}

export function buildTaskSnoozedEvent(input: {
  task_id: string;
  previous_due_at: string | null;
  due_at: string;
}): TaskSnoozedEvent {
  return stampSidekickEvent<TaskSnoozedEvent>({
    kind: SIDEKICK_EVENT_KINDS.TASK_SNOOZED,
    ...input,
  });
}

export function buildMemoryStoredEvent(memory: MemoryRecord): MemoryStoredEvent {
  return stampSidekickEvent<MemoryStoredEvent>({
    kind: SIDEKICK_EVENT_KINDS.MEMORY_STORED,
    memory,
  });
}

export function buildMemoryRecalledEvent(input: {
  query: string | null;
  memories: MemoryRecord[];
}): MemoryRecalledEvent {
  return stampSidekickEvent<MemoryRecalledEvent>({
    kind: SIDEKICK_EVENT_KINDS.MEMORY_RECALLED,
    query: input.query,
    count: input.memories.length,
    memory_ids: input.memories.map((memory) => memory.id),
  });
}

export function buildMemoryForgottenEvent(memory_id: string): MemoryForgottenEvent {
  return stampSidekickEvent<MemoryForgottenEvent>({
    kind: SIDEKICK_EVENT_KINDS.MEMORY_FORGOTTEN,
    memory_id,
  });
}

export function buildChannelMessageSentEvent(
  message: Record<string, unknown>,
): ChannelMessageSentEvent {
  return stampSidekickEvent<ChannelMessageSentEvent>({
    kind: SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_SENT,
    message,
  });
}

export function buildChannelMessageReceivedEvent(input: {
  channel: string;
  count: number;
  provider?: string;
}): ChannelMessageReceivedEvent {
  return stampSidekickEvent<ChannelMessageReceivedEvent>({
    kind: SIDEKICK_EVENT_KINDS.CHANNEL_MESSAGE_RECEIVED,
    channel: input.channel,
    count: input.count,
    ...(input.provider ? { provider: input.provider } : {}),
  });
}

export function buildChannelBridgeConnectedEvent(
  channel: ChannelRecord,
): ChannelBridgeConnectedEvent {
  return stampSidekickEvent<ChannelBridgeConnectedEvent>({
    kind: SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_CONNECTED,
    channel,
  });
}

export function buildChannelBridgeDisconnectedEvent(input: {
  bridge_id: string;
  deleted_message_count: number;
}): ChannelBridgeDisconnectedEvent {
  return stampSidekickEvent<ChannelBridgeDisconnectedEvent>({
    kind: SIDEKICK_EVENT_KINDS.CHANNEL_BRIDGE_DISCONNECTED,
    ...input,
  });
}

export function buildRoutinePlannedEvent(routine: RoutineRecord): RoutinePlannedEvent {
  return stampSidekickEvent<RoutinePlannedEvent>({
    kind: SIDEKICK_EVENT_KINDS.ROUTINE_PLANNED,
    routine,
  });
}

export function buildRoutineCommittedEvent(
  routine: RoutineRecord,
  options?: { envelope_content_hash?: string },
): RoutineCommittedEvent {
  return stampSidekickEvent<RoutineCommittedEvent>({
    kind: SIDEKICK_EVENT_KINDS.ROUTINE_COMMITTED,
    routine,
    ...(options?.envelope_content_hash
      ? { envelope_content_hash: options.envelope_content_hash }
      : {}),
  });
}

export function buildRoutineExecutedEvent(input: {
  routine_id: string;
  name: string;
  step_count: number;
}): RoutineExecutedEvent {
  return stampSidekickEvent<RoutineExecutedEvent>({
    kind: SIDEKICK_EVENT_KINDS.ROUTINE_EXECUTED,
    routine_id: input.routine_id,
    name: input.name,
    step_count: input.step_count,
    plan_only: true,
  });
}

export function buildRoutineStepFailedEvent(input: {
  step_index: number;
  reason: string;
  routine_id?: string;
  routine_name?: string;
}): RoutineStepFailedEvent {
  return stampSidekickEvent<RoutineStepFailedEvent>({
    kind: SIDEKICK_EVENT_KINDS.ROUTINE_STEP_FAILED,
    step_index: input.step_index,
    reason: input.reason,
    ...(input.routine_id ? { routine_id: input.routine_id } : {}),
    ...(input.routine_name ? { routine_name: input.routine_name } : {}),
  });
}

export function buildStateRehydratedEvent(snapshot: SidekickStores): StateRehydratedEvent {
  return stampSidekickEvent<StateRehydratedEvent>({
    kind: SIDEKICK_EVENT_KINDS.STATE_REHYDRATED,
    snapshot,
  });
}

export function serializeLocalChannelMessage(
  message: ChannelMessageRecord,
  channelName?: string,
): Record<string, unknown> {
  return {
    id: message.id,
    channel_id: message.channel_id,
    ...(channelName ? { channel: channelName } : {}),
    sender: message.sender,
    content: message.content,
    created_at: message.created_at,
  };
}
