/**
 * QQBot group configuration resolution (pure logic).
 * QQBot 群配置解析（纯逻辑层）。
 *
 * Resolves per-group settings that the inbound pipeline needs to decide how
 * to gate and contextualize group messages. Reads from a raw config object
 * produced by the framework's config loader, with a `specific > wildcard
 * ("*") > default` precedence chain.
 *
 * All functions are **pure** (no I/O, no external state) — making them
 * portable to the standalone plugin build and trivially unit-testable.
 */

import { asOptionalObjectRecord as asRecord } from "../utils/string-normalize.js";
import { resolveAccountBase } from "./resolve.js";

// ============ Types ============

/**
 * Tool policy — which tool palette an agent should use in a given group.
 *
 * - `full`:       default allow everything (no engine-side restriction).
 * - `restricted`: engine returns an empty allowlist so the framework falls
 *                 back to its built-in restricted palette.
 * - `none`:       deny all tools.
 */
type GroupToolPolicy = "full" | "restricted" | "none";

/** Per-group configuration — everything that may be overridden per group. */
interface GroupConfig {
  /** Whether the bot requires @mention to respond. Defaults to true. */
  requireMention: boolean;
  /**
   * When true, group messages that @other users (but not the bot) are
   * dropped silently without reaching the AI pipeline.
   */
  ignoreOtherMentions: boolean;
  /** Tool palette policy. Defaults to "restricted". */
  toolPolicy: GroupToolPolicy;
  /** Human-readable group name. Empty string if not configured. */
  name: string;
  /** Per-group behaviour prompt appended to the system prompt. */
  prompt?: string;
  /**
   * Number of non-@ history messages buffered per group. Clamped to 0 when
   * disabled. The default matches the standalone build's `50`.
   */
  historyLimit: number;
}

// ============ Defaults ============

/** Default history limit — matches the standalone build. */
export const DEFAULT_GROUP_HISTORY_LIMIT = 50;

/** Default group behaviour prompt. Exported so the gating stage can use
 *  the same fallback when no per-group `prompt` is configured. */
export const DEFAULT_GROUP_PROMPT =
  "If the sender is a bot, respond only when they explicitly @mention you to ask a question or request assistance with a specific task; keep your replies concise and clear, avoiding the urge to race other bots to answer or engage in lengthy, unproductive exchanges. In group chats, prioritize responding to messages from human users; bots should maintain a collaborative rather than competitive dynamic to ensure the conversation remains orderly and does not result in message flooding.";

const DEFAULT_GROUP_CONFIG: Readonly<Omit<GroupConfig, "prompt">> = {
  requireMention: true,
  ignoreOtherMentions: false,
  toolPolicy: "restricted",
  name: "",
  historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
};

// ============ Helpers ============

/** Read a named account's raw `groups` map from an OpenClawConfig. */
function readGroupsMap(
  cfg: Record<string, unknown>,
  accountId?: string | null,
): Record<string, Record<string, unknown>> {
  const account = resolveAccountBase(cfg, accountId);
  const groups = asRecord(account.config.groups);
  if (!groups) {
    return {};
  }
  // Only keep sub-objects; skip scalars produced by user mistakes.
  const normalized: Record<string, Record<string, unknown>> = {};
  for (const [key, value] of Object.entries(groups)) {
    const sub = asRecord(value);
    if (sub) {
      normalized[key] = sub;
    }
  }
  return normalized;
}

function readBoolean(obj: Record<string, unknown>, key: string): boolean | undefined {
  const v = obj[key];
  return typeof v === "boolean" ? v : undefined;
}

function readString(obj: Record<string, unknown>, key: string): string | undefined {
  const v = obj[key];
  return typeof v === "string" && v.length > 0 ? v : undefined;
}

function readToolPolicy(obj: Record<string, unknown>, key: string): GroupToolPolicy | undefined {
  const v = obj[key];
  return v === "full" || v === "restricted" || v === "none" ? v : undefined;
}

function readHistoryLimit(obj: Record<string, unknown>, key: string): number | undefined {
  const v = obj[key];
  if (typeof v !== "number" || !Number.isFinite(v)) {
    return undefined;
  }
  return Math.max(0, Math.floor(v));
}

// ============ Public API ============

/**
 * Resolve per-group configuration with `specific > "*" > default` precedence.
 *
 * When `groupOpenid` is not provided, only the wildcard/default values are
 * returned. This lets callers query the "default" behaviour for new groups.
 */
export function resolveGroupConfig(
  cfg: Record<string, unknown>,
  groupOpenid?: string | null,
  accountId?: string | null,
): GroupConfig {
  const groups = readGroupsMap(cfg, accountId);
  const wildcard = groups["*"] ?? {};
  const specific = groupOpenid ? (groups[groupOpenid] ?? {}) : {};

  return {
    requireMention:
      readBoolean(specific, "requireMention") ??
      readBoolean(wildcard, "requireMention") ??
      DEFAULT_GROUP_CONFIG.requireMention,
    ignoreOtherMentions:
      readBoolean(specific, "ignoreOtherMentions") ??
      readBoolean(wildcard, "ignoreOtherMentions") ??
      DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
    toolPolicy:
      readToolPolicy(specific, "toolPolicy") ??
      readToolPolicy(wildcard, "toolPolicy") ??
      DEFAULT_GROUP_CONFIG.toolPolicy,
    name: readString(specific, "name") ?? readString(wildcard, "name") ?? DEFAULT_GROUP_CONFIG.name,
    prompt: readString(specific, "prompt") ?? readString(wildcard, "prompt"),
    historyLimit:
      readHistoryLimit(specific, "historyLimit") ??
      readHistoryLimit(wildcard, "historyLimit") ??
      DEFAULT_GROUP_CONFIG.historyLimit,
  };
}

/** Resolve the effective `historyLimit` (>= 0) for a given group. */
export function resolveHistoryLimit(
  cfg: Record<string, unknown>,
  groupOpenid?: string | null,
  accountId?: string | null,
): number {
  return resolveGroupConfig(cfg, groupOpenid, accountId).historyLimit;
}

/** Resolve `requireMention` for a given group. */
export function resolveRequireMention(
  cfg: Record<string, unknown>,
  groupOpenid?: string | null,
  accountId?: string | null,
): boolean {
  return resolveGroupConfig(cfg, groupOpenid, accountId).requireMention;
}

/** Resolve `ignoreOtherMentions` for a given group. */
export function resolveIgnoreOtherMentions(
  cfg: Record<string, unknown>,
  groupOpenid?: string | null,
  accountId?: string | null,
): boolean {
  return resolveGroupConfig(cfg, groupOpenid, accountId).ignoreOtherMentions;
}

/** Resolve tool policy for a given group. */
export function resolveGroupToolPolicy(
  cfg: Record<string, unknown>,
  groupOpenid?: string | null,
  accountId?: string | null,
): GroupToolPolicy {
  return resolveGroupConfig(cfg, groupOpenid, accountId).toolPolicy;
}

/**
 * Resolve the behaviour prompt (PE) for a group. Falls back to the built-in
 * default when neither specific nor wildcard configuration provides one.
 */
export function resolveGroupPrompt(
  cfg: Record<string, unknown>,
  groupOpenid?: string | null,
  accountId?: string | null,
): string {
  return resolveGroupConfig(cfg, groupOpenid, accountId).prompt ?? DEFAULT_GROUP_PROMPT;
}

/**
 * Resolve the display name for a group.
 *
 * When no name is configured, the first 8 characters of the openid are used
 * as a short identifier so log lines stay compact.
 */
export function resolveGroupName(
  cfg: Record<string, unknown>,
  groupOpenid: string,
  accountId?: string | null,
): string {
  const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
  return name || groupOpenid.slice(0, 8);
}

// ============ GroupSettings (aggregate) ============

/**
 * Per-inbound aggregate of everything the pipeline needs about a group.
 *
 * Built once at the top of the group-gate stage so downstream consumers
 * don't repeatedly re-parse the same `cfg` tree. Superset of
 * {@link GroupConfig}: also includes the effective `mentionPatterns`
 * (which depend on `agentId`, not on the group itself) and a
 * pre-computed display name for logging.
 */
interface GroupSettings {
  /** Merged group config (specific > wildcard > defaults). */
  config: GroupConfig;
  /** Display name — `config.name` or the first 8 chars of the openid. */
  name: string;
  /** Raw mentionPatterns (agent > global > []). */
  mentionPatterns: string[];
}

/**
 * Resolve all per-inbound group-related settings in one pass.
 *
 * Prefer this over calling `resolveHistoryLimit` / `resolveRequireMention`
 * / etc. individually in hot paths — each of those currently re-walks
 * the config tree on its own.
 */
export function resolveGroupSettings(params: {
  cfg: Record<string, unknown>;
  groupOpenid: string;
  accountId?: string | null;
  agentId?: string | null;
}): GroupSettings {
  const config = resolveGroupConfig(params.cfg, params.groupOpenid, params.accountId);
  const name = config.name || params.groupOpenid.slice(0, 8);
  const mentionPatterns = resolveMentionPatterns(params.cfg, params.agentId);
  return { config, name, mentionPatterns };
}

interface AgentEntry {
  id?: unknown;
  groupChat?: { mentionPatterns?: unknown };
}

/**
 * Resolve mentionPatterns with `agent > global > []` precedence.
 *
 * Mirrors the framework's `messages.groupChat.mentionPatterns` / per-agent
 * `agents.list[].groupChat.mentionPatterns` chain.
 */
export function resolveMentionPatterns(
  cfg: Record<string, unknown>,
  agentId?: string | null,
): string[] {
  // ---- 1. Agent-level ----
  if (agentId) {
    const agents = asRecord(cfg.agents);
    const list = Array.isArray(agents?.list) ? (agents?.list as AgentEntry[]) : [];
    const entry = list.find(
      (a) => typeof a.id === "string" && a.id.trim().toLowerCase() === agentId.trim().toLowerCase(),
    );
    const agentGroupChat = entry?.groupChat;
    if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
      const patterns = agentGroupChat.mentionPatterns;
      return Array.isArray(patterns)
        ? patterns.filter((p): p is string => typeof p === "string")
        : [];
    }
  }

  // ---- 2. Global level ----
  const messages = asRecord(cfg.messages);
  const globalGroupChat = asRecord(messages?.groupChat);
  if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
    const patterns = globalGroupChat.mentionPatterns;
    return Array.isArray(patterns)
      ? patterns.filter((p): p is string => typeof p === "string")
      : [];
  }

  // ---- 3. Default ----
  return [];
}
