import { Schema } from "effect";

// ---------------------------------------------------------------------------
// Normalized Orb events — domain-meaningful, secret-free
// ---------------------------------------------------------------------------

export const OrbEventVariant = Schema.Literals([
  "session_opened",
  "agent_message_chunk",
  "agent_message_completed",
  "agent_thought_chunk",
  "tool_call_started",
  "tool_call_completed",
  "permission_requested",
  "permission_denied",
  "command_executed",
  "session_failed",
  "session_closed",
  "vm_booted",
  "vm_shutdown",
]);
export type OrbEventVariant = typeof OrbEventVariant.Type;

export const OrbEvent = Schema.Struct({
  command: Schema.UndefinedOr(Schema.String),
  exitCode: Schema.UndefinedOr(Schema.Int),
  sequence: Schema.Number,
  text: Schema.UndefinedOr(Schema.String),
  timestamp: Schema.String,
  toolCallId: Schema.UndefinedOr(Schema.String),
  toolName: Schema.UndefinedOr(Schema.String),
  type: OrbEventVariant,
});
export type OrbEvent = typeof OrbEvent.Type;

/** Construct an OrbEvent with auto-incrementing sequence and timestamp. */
export const makeOrbEvent = (
  sequence: number,
  type: OrbEventVariant,
  fields?: Partial<Omit<OrbEvent, "sequence" | "timestamp" | "type">>
): OrbEvent =>
  ({
    command: fields?.command ?? undefined,
    exitCode: fields?.exitCode ?? undefined,
    sequence,
    text: fields?.text ?? undefined,
    timestamp: new Date().toISOString(),
    toolCallId: fields?.toolCallId ?? undefined,
    toolName: fields?.toolName ?? undefined,
    type,
  }) as unknown as OrbEvent;

// ---------------------------------------------------------------------------
// Secret redaction — applied only to persisted/logged event copies,
// never to prompts sent to the model.
// ---------------------------------------------------------------------------

const REDACT_PATTERNS = [
  /(?:api[_-]?key|token|secret|password|credential)["'\s:=]+(?<value>[^\s"'},]+)/giu,
  /sk-(?<key>[a-zA-Z0-9]{20,})/gu,
  /Bearer\s+[a-zA-Z0-9._-]+/gu,
];

export const redactSecrets = (text: string): string => {
  let result = text;
  for (const pattern of REDACT_PATTERNS) {
    result = result.replaceAll(pattern, (match) =>
      match.toLowerCase().includes("bearer")
        ? "Bearer [REDACTED]"
        : "[REDACTED]"
    );
  }
  return result;
};

// ---------------------------------------------------------------------------
// Translation from AgentOS session stream entries to normalized OrbEvent
// ---------------------------------------------------------------------------

interface AcpSessionUpdateLike {
  readonly type?: string;
  readonly sessionUpdate?: string;
  readonly rawText?: string;
  readonly text?: string;
  readonly content?: unknown;
  readonly toolCallId?: string | null;
  readonly toolCallStatus?: string;
  readonly title?: string | null;
  readonly sessionId?: string;
}

interface SessionStreamEntryLike {
  readonly afterSequence?: number;
  readonly content?: unknown;
  readonly durability?: string;
  readonly rawText?: string;
  readonly sequence?: number;
  readonly sessionId?: string;
  readonly sessionUpdate?: string;
  readonly text?: string;
  readonly timestamp?: string;
  readonly title?: string | null;
  readonly toolCallId?: string | null;
  readonly type?: string;
}

const IGNORED_TYPES = new Set([
  "session_config",
  "agent_description",
  "agent_capability",
]);

const MESSAGE_TYPES = new Set([
  "agent_message_chunk",
  "agent_message",
  "agent_thought_chunk",
]);

const TOOL_CALL_TYPES = new Set([
  "tool_call",
  "tool_call_status",
  "tool_call_update",
]);

const extractText = (entry: AcpSessionUpdateLike): string | undefined => {
  if (entry.rawText !== undefined) {
    return entry.rawText;
  }
  if (entry.text !== undefined) {
    return entry.text;
  }
  if (typeof entry.content === "string") {
    return entry.content;
  }
  if (Array.isArray(entry.content)) {
    const texts = entry.content
      .filter(
        (block): block is { readonly type: string; readonly text?: unknown } =>
          typeof block === "object" && block !== null && "type" in block
      )
      .map((block) => (typeof block.text === "string" ? block.text : undefined))
      .filter((text): text is string => text !== undefined);
    return texts.length > 0 ? texts.join("") : undefined;
  }
  return undefined;
};

const optionalToolFields = (
  raw: SessionStreamEntryLike
): { toolCallId?: string; toolName?: string } => ({
  ...(raw.toolCallId === null || raw.toolCallId === undefined
    ? {}
    : { toolCallId: raw.toolCallId }),
  ...(raw.title === null || raw.title === undefined
    ? {}
    : { toolName: raw.title }),
});

const fromMessage = (
  type: string,
  sequence: number,
  raw: SessionStreamEntryLike
): OrbEvent | undefined => {
  const text = extractText(raw);
  if (text === undefined) {
    // agent_message completes even without text; chunk/thought do not.
    return type === "agent_message"
      ? makeOrbEvent(sequence, "agent_message_completed", {})
      : undefined;
  }
  const variant = (
    type === "agent_message" ? "agent_message_completed" : type
  ) as OrbEventVariant;
  return makeOrbEvent(sequence, variant, { text: redactSecrets(text) });
};

const fromToolCall = (
  type: string,
  sequence: number,
  raw: SessionStreamEntryLike
): OrbEvent =>
  makeOrbEvent(
    sequence,
    type === "tool_call" ? "tool_call_started" : "tool_call_completed",
    optionalToolFields(raw)
  );

/**
 * Translate one AgentOS SessionStreamEntry into zero or one normalized OrbEvent.
 * Returns undefined for event types that have no domain-meaningful mapping yet.
 * Secret redaction is applied so persisted event copies never leak credentials.
 */
export const normalizeSessionEvent = (
  raw: SessionStreamEntryLike
): OrbEvent | undefined => {
  const type = raw.type ?? raw.sessionUpdate;
  if (type === undefined) {
    return undefined;
  }
  const sequence = raw.sequence ?? 0;
  if (IGNORED_TYPES.has(type)) {
    return undefined;
  }
  if (type === "permission_request") {
    return makeOrbEvent(sequence, "permission_requested", {
      text: "Permission requested",
    });
  }
  if (MESSAGE_TYPES.has(type)) {
    return fromMessage(type, sequence, raw);
  }
  if (TOOL_CALL_TYPES.has(type)) {
    return fromToolCall(type, sequence, raw);
  }
  return undefined;
};
