import crypto from "node:crypto";
import path from "node:path";
import { normalizeChatType } from "../../channels/chat-type.js";
import { normalizeAnyChannelId } from "../../channels/registry.js";
import type { OpenClawConfig } from "../../config/config.js";
import { applyMergePatch } from "../../config/merge-patch.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import {
  normalizeOptionalLowercaseString,
  normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { normalizeCommandBody } from "../commands-registry.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import type { CommandContext } from "./commands-types.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
import type { SessionInitResult } from "./session.js";

const COMPLETE_REPLY_CONFIG_SYMBOL = Symbol.for("openclaw.reply.complete-config");
const FULL_REPLY_RUNTIME_SYMBOL = Symbol.for("openclaw.reply.full-runtime");

type ReplyConfigWithMarker = OpenClawConfig & {
  [COMPLETE_REPLY_CONFIG_SYMBOL]?: true;
  [FULL_REPLY_RUNTIME_SYMBOL]?: true;
};

function isSlowReplyTestAllowed(env: NodeJS.ProcessEnv = process.env): boolean {
  return (
    env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS === "1" || env.OPENCLAW_STRICT_FAST_REPLY_CONFIG === "0"
  );
}

function resolveFastSessionKey(ctx: MsgContext): string {
  const existing = normalizeOptionalString(ctx.SessionKey);
  if (existing) {
    return existing;
  }
  const provider =
    normalizeOptionalString(ctx.Provider) ?? normalizeOptionalString(ctx.Surface) ?? "main";
  const destination =
    normalizeOptionalString(ctx.To) ?? normalizeOptionalString(ctx.From) ?? "default";
  return `agent:main:${provider}:${destination}`;
}

function markReplyConfigRuntimeMode(
  config: ReplyConfigWithMarker,
  runtimeMode: "fast" | "full" = "fast",
): void {
  Object.defineProperty(config, FULL_REPLY_RUNTIME_SYMBOL, {
    value: runtimeMode === "full" ? true : undefined,
    configurable: true,
    enumerable: false,
  });
}

export function markCompleteReplyConfig<T extends OpenClawConfig>(
  config: T,
  options?: { runtimeMode?: "fast" | "full" },
): T {
  Object.defineProperty(config as ReplyConfigWithMarker, COMPLETE_REPLY_CONFIG_SYMBOL, {
    value: true,
    configurable: true,
    enumerable: false,
  });
  markReplyConfigRuntimeMode(config as ReplyConfigWithMarker, options?.runtimeMode ?? "fast");
  return config;
}

export function withFastReplyConfig<T extends OpenClawConfig>(config: T): T {
  return markCompleteReplyConfig(config, { runtimeMode: "fast" });
}

export function withFullRuntimeReplyConfig<T extends OpenClawConfig>(config: T): T {
  return markCompleteReplyConfig(config, { runtimeMode: "full" });
}

export function isCompleteReplyConfig(config: unknown): config is OpenClawConfig {
  return Boolean(
    config &&
    typeof config === "object" &&
    (config as ReplyConfigWithMarker)[COMPLETE_REPLY_CONFIG_SYMBOL] === true,
  );
}

export function usesFullReplyRuntime(config: unknown): boolean {
  return Boolean(
    config &&
    typeof config === "object" &&
    (config as ReplyConfigWithMarker)[FULL_REPLY_RUNTIME_SYMBOL] === true,
  );
}

export function resolveGetReplyConfig(params: {
  loadConfig: () => OpenClawConfig;
  isFastTestEnv: boolean;
  configOverride?: OpenClawConfig;
}): OpenClawConfig {
  const { configOverride } = params;
  if (configOverride == null) {
    return params.loadConfig();
  }
  if (params.isFastTestEnv && !isCompleteReplyConfig(configOverride) && !isSlowReplyTestAllowed()) {
    throw new Error(
      "Fast reply tests must pass with withFastReplyConfig()/markCompleteReplyConfig(); set OPENCLAW_ALLOW_SLOW_REPLY_TESTS=1 to opt out.",
    );
  }
  if (params.isFastTestEnv && isCompleteReplyConfig(configOverride)) {
    return configOverride;
  }
  return applyMergePatch(params.loadConfig(), configOverride) as OpenClawConfig;
}

export function shouldUseReplyFastTestBootstrap(params: {
  isFastTestEnv: boolean;
  configOverride?: OpenClawConfig;
}): boolean {
  return (
    params.isFastTestEnv &&
    isCompleteReplyConfig(params.configOverride) &&
    !usesFullReplyRuntime(params.configOverride)
  );
}

export function shouldUseReplyFastTestRuntime(params: {
  cfg: OpenClawConfig;
  isFastTestEnv: boolean;
}): boolean {
  return (
    params.isFastTestEnv && isCompleteReplyConfig(params.cfg) && !usesFullReplyRuntime(params.cfg)
  );
}

export function shouldUseReplyFastDirectiveExecution(params: {
  isFastTestBootstrap: boolean;
  isGroup: boolean;
  isHeartbeat: boolean;
  resetTriggered: boolean;
  triggerBodyNormalized: string;
}): boolean {
  if (
    !params.isFastTestBootstrap ||
    params.isGroup ||
    params.isHeartbeat ||
    params.resetTriggered
  ) {
    return false;
  }
  return !params.triggerBodyNormalized.includes("/");
}

export function buildFastReplyCommandContext(params: {
  ctx: MsgContext;
  cfg: OpenClawConfig;
  agentId?: string;
  sessionKey?: string;
  isGroup: boolean;
  triggerBodyNormalized: string;
  commandAuthorized: boolean;
}): CommandContext {
  const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized, commandAuthorized } =
    params;
  const surface = normalizeOptionalLowercaseString(ctx.Surface ?? ctx.Provider) ?? "";
  const channel = normalizeOptionalLowercaseString(ctx.Provider ?? surface) ?? "";
  const from = normalizeOptionalString(ctx.From);
  const to = normalizeOptionalString(ctx.To);
  return {
    surface,
    channel,
    channelId: normalizeAnyChannelId(channel) ?? normalizeAnyChannelId(surface) ?? undefined,
    ownerList: [],
    senderIsOwner: false,
    isAuthorizedSender: commandAuthorized,
    senderId: from,
    abortKey: sessionKey ?? from ?? to,
    rawBodyNormalized: triggerBodyNormalized,
    commandBodyNormalized: normalizeCommandBody(
      isGroup ? stripMentions(triggerBodyNormalized, ctx, cfg, agentId) : triggerBodyNormalized,
      { botUsername: ctx.BotUsername },
    ),
    from,
    to,
  };
}

export function shouldHandleFastReplyTextCommands(params: {
  cfg: OpenClawConfig;
  commandSource?: string;
}): boolean {
  return params.commandSource === "native" || params.cfg.commands?.text !== false;
}

export function initFastReplySessionState(params: {
  ctx: MsgContext;
  cfg: OpenClawConfig;
  agentId: string;
  commandAuthorized: boolean;
  workspaceDir: string;
}): SessionInitResult {
  const { ctx, cfg, agentId, commandAuthorized, workspaceDir } = params;
  const sessionScope = cfg.session?.scope ?? "per-sender";
  const sessionKey = resolveFastSessionKey(ctx);
  const sessionId = crypto.randomUUID();
  const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
  const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim();
  const normalizedChatType = normalizeChatType(ctx.ChatType);
  const isGroup = normalizedChatType != null && normalizedChatType !== "direct";
  const strippedForReset = isGroup
    ? stripMentions(triggerBodyNormalized, ctx, cfg, agentId)
    : triggerBodyNormalized;
  const resetMatch = strippedForReset.match(/^\/(new|reset)(?:\s|$)/i);
  const resetTriggered = Boolean(resetMatch);
  const bodyStripped = resetTriggered
    ? strippedForReset.slice(resetMatch?.[0].length ?? 0).trimStart()
    : (ctx.BodyForAgent ?? ctx.Body ?? "");
  const now = Date.now();
  const sessionFile = path.join(workspaceDir, ".openclaw", "sessions", `${sessionId}.jsonl`);
  const sessionEntry: SessionEntry = {
    sessionId,
    sessionFile,
    updatedAt: now,
    ...(normalizedChatType ? { chatType: normalizedChatType } : {}),
    ...(normalizeOptionalString(ctx.Provider)
      ? { channel: normalizeOptionalString(ctx.Provider) }
      : {}),
    ...(normalizeOptionalString(ctx.GroupSubject)
      ? { subject: normalizeOptionalString(ctx.GroupSubject) }
      : {}),
    ...(normalizeOptionalString(ctx.GroupChannel)
      ? { groupChannel: normalizeOptionalString(ctx.GroupChannel) }
      : {}),
  };
  const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
  const sessionCtx: TemplateContext = {
    ...ctx,
    SessionKey: sessionKey,
    CommandAuthorized: commandAuthorized,
    BodyStripped: bodyStripped,
    ...(normalizedChatType ? { ChatType: normalizedChatType } : {}),
  };
  return {
    sessionCtx,
    sessionEntry,
    previousSessionEntry: undefined,
    sessionStore,
    sessionKey,
    sessionId,
    isNewSession: resetTriggered || !ctx.SessionKey,
    resetTriggered,
    systemSent: false,
    abortedLastRun: false,
    storePath: normalizeOptionalString(cfg.session?.store) ?? "",
    sessionScope,
    groupResolution: undefined,
    isGroup,
    bodyStripped,
    triggerBodyNormalized,
  };
}
