/** Reply threading policy helpers for channel replies and status notices. */
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import { normalizeChatType } from "../../channels/chat-type.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import type { ChannelThreadingAdapter } from "../../channels/plugins/types.core.js";
import { normalizeAnyChannelId } from "../../channels/registry.js";
import type { ReplyToMode } from "../../config/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/account-id.js";
import {
  copyReplyPayloadMetadata,
  isReplyPayloadStatusNotice,
  type ReplyDeliveryContext,
} from "../reply-payload.js";
import type { OriginatingChannelType } from "../templating.js";
import type { ReplyPayload, ReplyThreadingPolicy } from "../types.js";
import { isSingleUseReplyToMode } from "./reply-reference.js";

type ReplyToModeChannelConfig = {
  replyToMode?: ReplyToMode;
  replyToModeByChatType?: Partial<Record<"direct" | "group" | "channel", ReplyToMode>>;
  dm?: {
    replyToMode?: ReplyToMode;
  };
};

function normalizeReplyToModeChatType(
  chatType?: string | null,
): "direct" | "group" | "channel" | undefined {
  return chatType === "direct" || chatType === "group" || chatType === "channel"
    ? chatType
    : undefined;
}

/** Resolve configured reply-to mode from channel and chat-type config. */
export function resolveConfiguredReplyToMode(
  cfg: OpenClawConfig,
  channel?: OriginatingChannelType,
  chatType?: string | null,
): ReplyToMode {
  const provider = normalizeAnyChannelId(channel) ?? normalizeOptionalLowercaseString(channel);
  if (!provider) {
    return "all";
  }
  const channelConfig = (cfg.channels as Record<string, ReplyToModeChannelConfig> | undefined)?.[
    provider
  ];
  const normalizedChatType = normalizeReplyToModeChatType(chatType);
  if (normalizedChatType) {
    const scopedMode = channelConfig?.replyToModeByChatType?.[normalizedChatType];
    if (scopedMode !== undefined) {
      return scopedMode;
    }
  }
  if (normalizedChatType === "direct") {
    const legacyDirectMode = channelConfig?.dm?.replyToMode;
    if (legacyDirectMode !== undefined) {
      return legacyDirectMode;
    }
  }
  return channelConfig?.replyToMode ?? "all";
}

/** Resolve reply-to mode using channel threading adapter override when present. */
export function resolveReplyToModeWithThreading(
  cfg: OpenClawConfig,
  threading: ChannelThreadingAdapter | undefined,
  params: {
    channel?: OriginatingChannelType;
    accountId?: string | null;
    chatType?: string | null;
  } = {},
): ReplyToMode {
  const resolved = threading?.resolveReplyToMode?.({
    cfg,
    accountId: params.accountId,
    chatType: params.chatType,
  });
  return resolved ?? resolveConfiguredReplyToMode(cfg, params.channel, params.chatType);
}

/** Resolve effective reply-to mode for a channel/account/chat tuple. */
export function resolveReplyToMode(
  cfg: OpenClawConfig,
  channel?: OriginatingChannelType,
  accountId?: string | null,
  chatType?: string | null,
): ReplyToMode {
  const normalizedAccountId = normalizeOptionalLowercaseString(accountId);
  if (!normalizedAccountId) {
    return resolveConfiguredReplyToMode(cfg, channel, chatType);
  }
  const provider = normalizeAnyChannelId(channel) ?? normalizeOptionalLowercaseString(channel);
  const threading = provider ? getChannelPlugin(provider)?.threading : undefined;
  return resolveReplyToModeWithThreading(cfg, threading, {
    channel,
    accountId: normalizedAccountId,
    chatType,
  });
}

/** Resolve the account that routed reply delivery will use when none is explicit. */
export function resolveReplyDeliveryAccountId(
  cfg: OpenClawConfig,
  channel?: OriginatingChannelType,
  accountId?: string | null,
): string | undefined {
  const explicitAccountId = normalizeOptionalLowercaseString(accountId);
  if (explicitAccountId) {
    return explicitAccountId;
  }
  const provider = normalizeAnyChannelId(channel) ?? normalizeOptionalLowercaseString(channel);
  if (!provider) {
    return undefined;
  }
  const plugin = getChannelPlugin(provider);
  if (!plugin) {
    return undefined;
  }
  const configuredDefault = normalizeOptionalLowercaseString(plugin.config.defaultAccountId?.(cfg));
  if (configuredDefault) {
    return configuredDefault;
  }
  const channelConfiguredDefault = normalizeOptionalLowercaseString(
    (cfg.channels as Record<string, { defaultAccount?: string | null } | undefined> | undefined)?.[
      provider
    ]?.defaultAccount,
  );
  if (channelConfiguredDefault) {
    return channelConfiguredDefault;
  }
  const listedDefault = plugin.config
    .listAccountIds(cfg)
    .map((listedAccountId) => normalizeOptionalLowercaseString(listedAccountId))
    .find((listedAccountId): listedAccountId is string => Boolean(listedAccountId));
  return listedDefault ?? DEFAULT_ACCOUNT_ID;
}

/** Build the canonical reply policy context consumed by delivery adapters. */
export function createReplyDeliveryContext(
  replyToMode: ReplyToMode,
  chatType?: string | null,
): ReplyDeliveryContext {
  const normalizedChatType = normalizeChatType(chatType ?? undefined);
  return {
    ...(normalizedChatType ? { chatType: normalizedChatType } : {}),
    replyToMode,
  };
}

/** Create a payload filter that strips reply targets according to reply-to mode. */
export function createReplyToModeFilter(
  mode: ReplyToMode,
  opts: { allowExplicitReplyTagsWhenOff?: boolean } = {},
) {
  let hasThreaded = false;
  return (payload: ReplyPayload): ReplyPayload => {
    const isStatusNotice = isReplyPayloadStatusNotice(payload);
    if (!payload.replyToId) {
      return payload;
    }
    if (mode === "off") {
      const isExplicit = Boolean(payload.replyToTag) || Boolean(payload.replyToCurrent);
      // Status notices must never be threaded when replyToMode=off — even
      // if they carry explicit reply tags (replyToCurrent).  Honouring the
      // explicit tag here would make status notices appear in-thread while
      // normal assistant replies stay off-thread, contradicting the off-mode
      // expectation.  Strip replyToId unconditionally for compaction payloads.
      if (opts.allowExplicitReplyTagsWhenOff && isExplicit && !isStatusNotice) {
        return payload;
      }
      return copyReplyPayloadMetadata(payload, { ...payload, replyToId: undefined });
    }
    if (mode === "all") {
      return payload;
    }
    if (isSingleUseReplyToMode(mode) && hasThreaded) {
      // Status notices are transient messages that should always
      // appear in-thread, even after the first assistant block has already
      // consumed the "first" slot.  Let them keep their replyToId.
      if (isStatusNotice) {
        return payload;
      }
      return copyReplyPayloadMetadata(payload, { ...payload, replyToId: undefined });
    }
    // Status notices are transient messages — they should be
    // threaded (so they appear in-context), but they must not consume the
    // "first" slot of the replyToMode=first|batched filter.  Skip advancing
    // hasThreaded so the real assistant reply still gets replyToId.
    if (isSingleUseReplyToMode(mode) && !isStatusNotice) {
      hasThreaded = true;
    }
    return payload;
  };
}

/** Resolve whether implicit current-message replies are allowed under threading policy. */
export function resolveImplicitCurrentMessageReplyAllowance(
  mode: ReplyToMode | undefined,
  policy?: ReplyThreadingPolicy,
): boolean {
  const implicitCurrentMessage = policy?.implicitCurrentMessage ?? "default";
  if (implicitCurrentMessage === "allow") {
    return true;
  }
  if (implicitCurrentMessage === "deny") {
    return false;
  }
  return mode !== "batched";
}

/** Build threading policy for batched reply-to mode. */
export function resolveBatchedReplyThreadingPolicy(
  mode: ReplyToMode,
  isBatched: boolean,
): ReplyThreadingPolicy | undefined {
  if (mode !== "batched") {
    return undefined;
  }
  return {
    implicitCurrentMessage: isBatched ? "allow" : "deny",
  };
}

/** Create a reply-to filter using channel-specific explicit-tag defaults. */
export function createReplyToModeFilterForChannel(
  mode: ReplyToMode,
  channel?: OriginatingChannelType,
) {
  const normalized = normalizeOptionalLowercaseString(channel);
  const isWebchat = normalized === "webchat";
  // Default: allow explicit reply tags/directives even when replyToMode is "off".
  // Unknown channels fail closed; internal webchat stays allowed.
  const allowExplicitReplyTagsWhenOff = normalized ? true : isWebchat;
  return createReplyToModeFilter(mode, {
    allowExplicitReplyTagsWhenOff,
  });
}
