import type { ClawdbotConfig } from "../config/config.js";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { listBindings } from "./bindings.js";
import {
  buildAgentMainSessionKey,
  buildAgentPeerSessionKey,
  DEFAULT_ACCOUNT_ID,
  DEFAULT_MAIN_KEY,
  normalizeAgentId,
  sanitizeAgentId,
} from "./session-key.js";

export type RoutePeerKind = "dm" | "direct" | "group" | "channel";

export type RoutePeer = {
  kind: RoutePeerKind;
  id: string;
};

export type ResolveAgentRouteInput = {
  cfg: ClawdbotConfig;
  channel: string;
  accountId?: string | null;
  peer?: RoutePeer | null;
  /** Parent peer for threads — used for binding inheritance when peer doesn't match directly. */
  parentPeer?: RoutePeer | null;
  guildId?: string | null;
  teamId?: string | null;
  /**
   * Raw inbound message text. Used for content-based DM multi-agent routing:
   * when a DM has no binding match, the agent whose dmChat.mentionPatterns
   * matches this text is selected instead of the default agent.
   */
  messageText?: string | null;
};

export type ResolvedAgentRoute = {
  agentId: string;
  channel: string;
  accountId: string;
  /** Internal session key used for persistence + concurrency. */
  sessionKey: string;
  /** Convenience alias for direct-chat collapse. */
  mainSessionKey: string;
  /** Match description for debugging/logging. */
  matchedBy:
    | "binding.peer"
    | "binding.peer.parent"
    | "binding.guild"
    | "binding.team"
    | "binding.account"
    | "binding.channel"
    | "dm.mention"
    | "default";
};

export { DEFAULT_ACCOUNT_ID, DEFAULT_AGENT_ID } from "./session-key.js";

function normalizeToken(value: string | undefined | null): string {
  return (value ?? "").trim().toLowerCase();
}

/** Normalize and lowercase unicode-invisible chars for DM pattern matching. */
function normalizeDmText(text: string): string {
  return (text ?? "").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "").toLowerCase();
}

/** Returns the first agentId whose dmChat.mentionPatterns matches the text, or null. */
function resolveAgentByDmContent(cfg: ClawdbotConfig, text: string): string | null {
  const cleaned = normalizeDmText(text);
  if (!cleaned) return null;
  const agents = listAgents(cfg);
  for (const agent of agents) {
    const patterns = agent?.dmChat?.mentionPatterns;
    if (!Array.isArray(patterns) || patterns.length === 0) continue;
    const matched = patterns.some((pattern) => {
      if (!pattern) return false;
      try {
        return new RegExp(pattern, "i").test(cleaned);
      } catch {
        return false;
      }
    });
    if (matched && agent?.id?.trim()) return agent.id.trim();
  }
  return null;
}

function normalizeId(value: string | undefined | null): string {
  return (value ?? "").trim();
}

function normalizeAccountId(value: string | undefined | null): string {
  const trimmed = (value ?? "").trim();
  return trimmed ? trimmed : DEFAULT_ACCOUNT_ID;
}

function matchesAccountId(match: string | undefined, actual: string): boolean {
  const trimmed = (match ?? "").trim();
  if (!trimmed) {
    return actual === DEFAULT_ACCOUNT_ID;
  }
  if (trimmed === "*") {
    return true;
  }
  return trimmed === actual;
}

export function buildAgentSessionKey(params: {
  agentId: string;
  channel: string;
  accountId?: string | null;
  peer?: RoutePeer | null;
  /** DM session scope. */
  dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
  identityLinks?: Record<string, string[]>;
}): string {
  const channel = normalizeToken(params.channel) || "unknown";
  const peer = params.peer;
  return buildAgentPeerSessionKey({
    agentId: params.agentId,
    mainKey: DEFAULT_MAIN_KEY,
    channel,
    accountId: params.accountId,
    peerKind: peer?.kind ?? "dm",
    peerId: peer ? normalizeId(peer.id) || "unknown" : null,
    dmScope: params.dmScope,
    identityLinks: params.identityLinks,
  });
}

function listAgents(cfg: ClawdbotConfig) {
  const agents = cfg.agents?.list;
  return Array.isArray(agents) ? agents : [];
}

function pickFirstExistingAgentId(cfg: ClawdbotConfig, agentId: string): string {
  const trimmed = (agentId ?? "").trim();
  if (!trimmed) {
    return sanitizeAgentId(resolveDefaultAgentId(cfg));
  }
  const normalized = normalizeAgentId(trimmed);
  const agents = listAgents(cfg);
  if (agents.length === 0) {
    return sanitizeAgentId(trimmed);
  }
  const match = agents.find((agent) => normalizeAgentId(agent.id) === normalized);
  if (match?.id?.trim()) {
    return sanitizeAgentId(match.id.trim());
  }
  return sanitizeAgentId(resolveDefaultAgentId(cfg));
}

function matchesChannel(
  match: { channel?: string | undefined } | undefined,
  channel: string,
): boolean {
  const key = normalizeToken(match?.channel);
  if (!key) {
    return false;
  }
  return key === channel;
}

function matchesPeer(
  match: { peer?: { kind?: string; id?: string } | undefined } | undefined,
  peer: RoutePeer,
): boolean {
  const m = match?.peer;
  if (!m) {
    return false;
  }
  const kind = normalizeToken(m.kind);
  const id = normalizeId(m.id);
  if (!kind || !id) {
    return false;
  }
  return kind === peer.kind && id === peer.id;
}

function matchesGuild(
  match: { guildId?: string | undefined } | undefined,
  guildId: string,
): boolean {
  const id = normalizeId(match?.guildId);
  if (!id) {
    return false;
  }
  return id === guildId;
}

function matchesTeam(match: { teamId?: string | undefined } | undefined, teamId: string): boolean {
  const id = normalizeId(match?.teamId);
  if (!id) {
    return false;
  }
  return id === teamId;
}

export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentRoute {
  const channel = normalizeToken(input.channel);
  const accountId = normalizeAccountId(input.accountId);
  const peer = input.peer ? { kind: input.peer.kind, id: normalizeId(input.peer.id) } : null;
  const guildId = normalizeId(input.guildId);
  const teamId = normalizeId(input.teamId);

  const bindings = listBindings(input.cfg).filter((binding) => {
    if (!binding || typeof binding !== "object") {
      return false;
    }
    if (!matchesChannel(binding.match, channel)) {
      return false;
    }
    return matchesAccountId(binding.match?.accountId, accountId);
  });

  const dmScope = input.cfg.session?.dmScope ?? "main";
  const identityLinks = input.cfg.session?.identityLinks;

  const choose = (agentId: string, matchedBy: ResolvedAgentRoute["matchedBy"]) => {
    const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId);
    const sessionKey = buildAgentSessionKey({
      agentId: resolvedAgentId,
      channel,
      accountId,
      peer,
      dmScope,
      identityLinks,
    }).toLowerCase();
    const mainSessionKey = buildAgentMainSessionKey({
      agentId: resolvedAgentId,
      mainKey: DEFAULT_MAIN_KEY,
    }).toLowerCase();
    return {
      agentId: resolvedAgentId,
      channel,
      accountId,
      sessionKey,
      mainSessionKey,
      matchedBy,
    };
  };

  if (peer) {
    const peerMatch = bindings.find((b) => matchesPeer(b.match, peer));
    if (peerMatch) {
      return choose(peerMatch.agentId, "binding.peer");
    }
  }

  // Thread parent inheritance: if peer (thread) didn't match, check parent peer binding
  const parentPeer = input.parentPeer
    ? { kind: input.parentPeer.kind, id: normalizeId(input.parentPeer.id) }
    : null;
  if (parentPeer && parentPeer.id) {
    const parentPeerMatch = bindings.find((b) => matchesPeer(b.match, parentPeer));
    if (parentPeerMatch) return choose(parentPeerMatch.agentId, "binding.peer.parent");
  }

  if (guildId) {
    const guildMatch = bindings.find((b) => matchesGuild(b.match, guildId));
    if (guildMatch) {
      return choose(guildMatch.agentId, "binding.guild");
    }
  }

  if (teamId) {
    const teamMatch = bindings.find((b) => matchesTeam(b.match, teamId));
    if (teamMatch) {
      return choose(teamMatch.agentId, "binding.team");
    }
  }

  const accountMatch = bindings.find(
    (b) =>
      b.match?.accountId?.trim() !== "*" && !b.match?.peer && !b.match?.guildId && !b.match?.teamId,
  );
  if (accountMatch) {
    return choose(accountMatch.agentId, "binding.account");
  }

  const anyAccountMatch = bindings.find(
    (b) =>
      b.match?.accountId?.trim() === "*" && !b.match?.peer && !b.match?.guildId && !b.match?.teamId,
  );
  if (anyAccountMatch) {
    return choose(anyAccountMatch.agentId, "binding.channel");
  }

  // Content-based DM routing: if this is a DM and messageText was provided,
  // check each agent's dmChat.mentionPatterns for a match before falling back to default.
  const isDm = peer?.kind === "dm" || peer?.kind === "direct";
  if (isDm && input.messageText) {
    const dmAgentId = resolveAgentByDmContent(input.cfg, input.messageText);
    if (dmAgentId) {
      return choose(dmAgentId, "dm.mention");
    }
  }

  return choose(resolveDefaultAgentId(input.cfg), "default");
}
