import type { AudioTranscriptionConfig } from '../models/media.js';
import type { Attachment } from '../channels/types.js';
import { isSupportedAudio, transcribeAudio } from '../models/media.js';
import { synthesizeSpeechAttachment } from '../models/tts.js';
import { supportsAudioInput } from '../models/capabilities.js';
import { AgentOrchestrator, type DelegationConfig } from '../backends/index.js';
import { OutboundAttachmentCollector } from '../backends/native/attachments.js';
import type { ExternalBackend, ExternalBackendName } from '../backends/index.js';
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
import { MemoryStore } from '../memory/index.js';
import type { Tool } from '../tools/types.js';
import { createMediaSendTool, createAgentDelegateTool, createCouncilRunTool } from '../tools/index.js';
import type { AgentDelegateDeps } from '../tools/index.js';
import { createSandboxedShellTool, createSandboxedProcessStartTool, SandboxManager } from '../sandbox/index.js';
import { MODEL_PROVIDERS, type Config, type CouncilsConfig, type ModelConfig, type ModelProvider } from '../config/index.js';
import { ModelRouter, type ModelClient, type ModelTier } from '../models/index.js';
import { ToolRegistry, ToolExecutor } from '../tools/index.js';
import { SessionManager } from '../session/index.js';
import { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
import type { CommandRegistry } from '../commands/index.js';
import {
  executeRuntimeBackendModeCommand,
  formatRuntimeBackendStatusLine,
  type RuntimeBackendMode,
} from '../commands/index.js';
import type { ComponentRegistry } from '../intents/index.js';
import type { RoutingPolicy } from '../routing/index.js';
import type { HookEngine } from '../hooks/index.js';
import { createClientFromConfig } from './models.js';
import { matchReactionPrompt } from '../automation/reactions.js';
import { loadSkillRegistryCatalog } from '../skills/index.js';
import type { SkillInstaller, SkillRegistry, SkillRegistryEntry, SkillRegistrySource } from '../skills/index.js';
import { auditLogger } from '../audit/index.js';
import { getElevationStatusMessage, setElevationFromInput } from '../security/elevation.js';
import { dirname, resolve } from 'path';
import { loadCouncilScaffoldSafe } from '../councils/scaffold.js';
import { buildCouncilPreflightReport, shouldRunCouncilPreflight } from '../councils/preflight.js';

export type BackendRuntimeMode = RuntimeBackendMode;

function buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
  const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
  const modelConfigs: ModelConfig[] = [
    config.models.default,
    ...(config.models.fast ? [config.models.fast] : []),
    ...(config.models.complex ? [config.models.complex] : []),
    ...(config.models.local ? [config.models.local] : []),
    ...Object.values(config.models.local_providers ?? {}),
  ];

  for (const modelConfig of modelConfigs) {
    providerConfigs[modelConfig.provider] = modelConfig;
    if (modelConfig.fallback) {
      providerConfigs[modelConfig.fallback.provider] = modelConfig.fallback;
    }
  }

  return providerConfigs;
}

function tierFromUseCase(config: Config, useCaseRaw: unknown): ModelTier | undefined {
  if (typeof useCaseRaw !== 'string') {
    return undefined;
  }
  const normalized = useCaseRaw.trim().toLowerCase();
  if (!normalized) {
    return undefined;
  }

  const mappings: Array<{ tier: ModelTier; tags: string[] | undefined }> = [
    { tier: 'fast', tags: config.models.fast?.for },
    { tier: 'default', tags: config.models.default.for },
    { tier: 'complex', tags: config.models.complex?.for },
    { tier: 'local', tags: config.models.local?.for },
  ];

  for (const { tier, tags } of mappings) {
    if (!tags || tags.length === 0) {
      continue;
    }
    if (tags.some((tag) => tag.trim().toLowerCase() === normalized)) {
      return tier;
    }
  }

  return undefined;
}

function buildBackgroundModelOverrides(config: Config): Partial<Record<keyof DelegationConfig, {
  client: ModelClient;
  label: string;
  fallbackTier: ModelTier;
}>> {
  const overrides: Partial<Record<keyof DelegationConfig, {
    client: ModelClient;
    label: string;
    fallbackTier: ModelTier;
  }>> = {};
  const configured = config.agents?.background_models ?? {};
  const providerConfigs = buildProviderConfigMap(config);
  const tasks: Array<keyof DelegationConfig> = [
    'compaction',
    'memory_extraction',
    'classification',
    'tool_summarisation',
    'complex_reasoning',
  ];

  for (const task of tasks) {
    const entry = configured[task];
    if (!entry || entry.enabled === false) {
      continue;
    }

    const template = providerConfigs[entry.provider];
    try {
      const client = createClientFromConfig(
        template
          ? { ...template, provider: entry.provider, model: entry.model }
          : { provider: entry.provider, model: entry.model },
      );
      overrides[task] = {
        client,
        label: `${entry.provider}/${entry.model}`,
        fallbackTier: entry.fallback_tier,
      };
    } catch (error) {
      console.warn(
        `[Flynn:routing] Failed to initialize background model override for ${task} ` +
        `(${entry.provider}/${entry.model}): ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  return overrides;
}

function parseResearchPrefix(text: string): string | undefined {
  const trimmed = text.trim();
  const researchMatch = trimmed.match(/^research(?:\s*[:,-])?\s+(.+)$/i);
  if (researchMatch?.[1]) {
    return researchMatch[1].trim();
  }
  const lookupMatch = trimmed.match(/^(?:look\s+up|lookup)(?:\s*[:,-])?\s+(.+)$/i);
  if (lookupMatch?.[1]) {
    return lookupMatch[1].trim();
  }
  return undefined;
}

function shouldForceNativeForCapabilityQuery(text: string): boolean {
  const normalized = text.trim().toLowerCase();
  if (!normalized) {
    return false;
  }
  if (
    normalized.includes('available tools')
    || normalized.includes('what tools')
    || normalized.includes('which tools')
    || normalized.includes('tool list')
    || normalized.includes('list tools')
    || normalized.includes('what can you do')
    || normalized.includes('full access')
    || normalized.includes('do you have access')
    || normalized.includes('what access')
  ) {
    return true;
  }
  return (
    /\b(?:show|list|check)\s+(?:me\s+)?(?:your\s+)?(?:available\s+|new\s+)?tools?\b/.test(normalized)
    || /\b(?:what|which)\s+tools?\b/.test(normalized)
    || /\btools?\s+(?:do\s+you\s+have|are\s+available)\b/.test(normalized)
    || /\b(?:show|list|what\s+are)\s+(?:your\s+)?capabilities\b/.test(normalized)
    || /\bdo\s+you\s+have\s+(?:full\s+)?access\b/.test(normalized)
  );
}

function shouldForceNativeForPiNoTools(text: string): boolean {
  const normalized = text.trim().toLowerCase();
  if (!normalized) {
    return false;
  }

  if (
    /`(?:shell\.exec|file\.(?:read|write|edit|patch|list)|web\.(?:fetch|search)|browser\.)/.test(normalized)
    || /\b(?:gmail|calendar|docs|drive|tasks|k8s|docker|minio)\b/.test(normalized)
  ) {
    return true;
  }

  return (
    /\b(?:run|execute)\s+(?:a\s+)?(?:shell|bash|command)\b/.test(normalized)
    || /\b(?:run|execute)\s+(?:a\s+)?(?:quick\s+)?check\b/.test(normalized)
    || /\b(?:quick\s+)?check\s+(?:access|status|logs?|health|config|setup)\b/.test(normalized)
    || /\b(?:verify|confirm)\s+(?:access|setup|status|config)\b/.test(normalized)
    || /\b(?:read|open|show|edit|write|patch|delete|list)\s+(?:the\s+)?(?:file|files|directory|repo|code)\b/.test(normalized)
    || /\b(?:search|fetch|browse|scrape)\s+(?:the\s+)?(?:web|internet|url|site)\b/.test(normalized)
    || /\b(?:use|call)\s+(?:a\s+)?tool\b/.test(normalized)
  );
}

function providerAcceptsNativeAudioContentParts(provider: string): boolean {
  return (
    provider === 'openai'
    || provider === 'github'
    || provider === 'gemini'
    || provider === 'openrouter'
    || provider === 'zhipuai'
    || provider === 'xai'
    || provider === 'minimax'
    || provider === 'moonshot'
    || provider === 'vercel'
  );
}

const LAST_AUDIO_ATTACHMENT_CONFIG_KEY = 'lastAudioAttachment';

function persistLatestAudioAttachment(
  session: { setConfig(key: string, value: string): void },
  audioAttachments: Attachment[],
): void {
  const latest = [...audioAttachments].reverse().find((att) => (
    (typeof att.data === 'string' && att.data.length > 0)
      || (typeof att.url === 'string' && att.url.length > 0)
  ));
  if (!latest) {
    return;
  }

  const payload: { data?: string; url?: string; mimeType?: string } = {
    mimeType: latest.mimeType,
  };
  if (typeof latest.data === 'string' && latest.data.length > 0) {
    payload.data = latest.data;
  } else if (typeof latest.url === 'string' && latest.url.length > 0) {
    payload.url = latest.url;
  }

  if (!payload.data && !payload.url) {
    return;
  }

  try {
    session.setConfig(LAST_AUDIO_ATTACHMENT_CONFIG_KEY, JSON.stringify(payload));
  } catch (error) {
    console.warn(
      'Failed to persist latest audio attachment for tool hydration:',
      error instanceof Error ? error.message : String(error),
    );
  }
}

function extractLatestAudioToolInput(audioAttachments: Attachment[]): { data?: string; url?: string; mime_type?: string } | undefined {
  const latest = [...audioAttachments].reverse().find((att) => (
    (typeof att.data === 'string' && att.data.length > 0)
      || (typeof att.url === 'string' && att.url.length > 0)
  ));
  if (!latest) {
    return undefined;
  }
  const data = typeof latest.data === 'string' && latest.data.length > 0 ? latest.data : undefined;
  const url = typeof latest.url === 'string' && latest.url.length > 0 ? latest.url : undefined;
  if (!data && !url) {
    return undefined;
  }
  return {
    ...(data ? { data } : {}),
    ...(url ? { url } : {}),
    mime_type: latest.mimeType,
  };
}

function isTtsEnabledForChannel(config: Config, channel: string): boolean {
  if (!config.tts?.enabled) {
    return false;
  }
  const enabledChannels = config.tts.enabled_channels ?? [];
  if (enabledChannels.length === 0) {
    return true;
  }
  return enabledChannels.includes(channel);
}

function resolveRegistrySource(config: Config): { source?: SkillRegistrySource; error?: string } {
  const raw = config.skills.registry_source?.trim() || process.env.FLYNN_SKILLS_REGISTRY_SOURCE?.trim();
  if (!raw) {
    return {
      error: 'Skills registry is not configured. Set `skills.registry_source` (or FLYNN_SKILLS_REGISTRY_SOURCE).',
    };
  }
  if (raw.startsWith('http://')) {
    return { error: `Registry URL must use https:// (${raw})` };
  }
  if (raw.startsWith('https://')) {
    return { source: { type: 'url', url: raw } };
  }
  return { source: { type: 'file', path: raw } };
}

function resolveRegistryEntryLocalPath(entry: SkillRegistryEntry, registrySource: SkillRegistrySource): string | null {
  const source = entry.source.trim();
  if (!source) {
    return null;
  }

  if (source.startsWith('http://') || source.startsWith('https://') || source.startsWith('git+https://')) {
    return null;
  }

  if (source.startsWith('file://')) {
    try {
      return decodeURIComponent(new URL(source).pathname);
    } catch {
      return null;
    }
  }

  if (registrySource.type === 'file' && (source.startsWith('./') || source.startsWith('../'))) {
    return resolve(dirname(resolve(registrySource.path)), source);
  }

  return resolve(source);
}
/**
 * Create the unified message handler for the channel registry.
 * Each channel+sender pair gets its own AgentOrchestrator backed by a persistent session.
 * The orchestrator wraps a NativeAgent and adds delegation to different model tiers.
 *
 * Returns both the message handler function and the agents map for usage tracking.
 */
export function createMessageRouter(deps: {
  sessionManager: SessionManager;
  modelRouter: ModelRouter;
  systemPrompt: string;
  toolRegistry: ToolRegistry;
  toolExecutor: ToolExecutor;
  config: Config;
  memoryStore?: MemoryStore;
  agentConfigRegistry?: AgentConfigRegistry;
  agentRouter?: AgentRouter;
  sandboxManager?: SandboxManager;
  commandRegistry?: CommandRegistry;
  hookEngine?: HookEngine;
  intentRegistry?: ComponentRegistry;
  routingPolicy?: RoutingPolicy;
  skillRegistry?: SkillRegistry;
  skillInstaller?: SkillInstaller;
  externalBackends?: Partial<Record<ExternalBackendName, ExternalBackend>>;
  defaultName?: ExternalBackendName;
  getBackendMode?: () => BackendRuntimeMode;
  setBackendMode?: (mode: BackendRuntimeMode) => void;
}): {
  handler: (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>) => Promise<void>;
  agents: Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>;
} {
  // Cache agents by session ID + agent config name to avoid recreating on every message
  const agents = new Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>();
  const talkModeUntil = new Map<string, number>();
  const activeRuns = new Map<string, AgentOrchestrator>();

  function getBackendMode(): BackendRuntimeMode {
    return deps.getBackendMode?.() ?? 'config_default';
  }

  function getConfiguredOrFallbackDefaultBackend(): ExternalBackendName | 'native' {
    return deps.defaultName ?? 'native';
  }

  function getEffectiveDefaultBackend(): ExternalBackendName | 'native' {
    const mode = getBackendMode();
    if (mode === 'force_native') {
      return 'native';
    }
    if (mode === 'force_pi_embedded') {
      return 'pi_embedded';
    }
    return getConfiguredOrFallbackDefaultBackend();
  }

  function resolveRoutableBackend(
    requestedBackend: ExternalBackendName | 'native' | undefined,
  ): ExternalBackendName | 'native' {
    if (!requestedBackend || requestedBackend === 'native') {
      return 'native';
    }
    return deps.externalBackends?.[requestedBackend] ? requestedBackend : 'native';
  }

  function applyBackendModeOverride(
    requestedBackend: ExternalBackendName | 'native' | undefined,
  ): ExternalBackendName | 'native' | undefined {
    if (requestedBackend !== 'pi_embedded') {
      return requestedBackend;
    }
    if (getBackendMode() === 'force_native') {
      return 'native';
    }
    return requestedBackend;
  }

  function listAvailableExternalBackends(): string[] {
    return Object.keys(deps.externalBackends ?? {});
  }

  function formatBackendStatus(activeTier: string): string {
    return formatRuntimeBackendStatusLine({
      getActiveTier: () => activeTier,
      getBackendMode,
      getConfiguredDefaultBackend: getConfiguredOrFallbackDefaultBackend,
      getEffectiveDefaultBackend: () => resolveRoutableBackend(getEffectiveDefaultBackend()),
      getAvailableExternalBackends: listAvailableExternalBackends,
    });
  }

  function executeBackendCommand(inputRaw: string, activeTier: string): string {
    return executeRuntimeBackendModeCommand(inputRaw, {
      getActiveTier: () => activeTier,
      getBackendMode,
      setBackendMode: deps.setBackendMode,
      getConfiguredDefaultBackend: getConfiguredOrFallbackDefaultBackend,
      getEffectiveDefaultBackend: () => resolveRoutableBackend(getEffectiveDefaultBackend()),
      getAvailableExternalBackends: listAvailableExternalBackends,
    });
  }

  async function maybeBuildTtsAttachment(responseText: string, channel: string) {
    if (!isTtsEnabledForChannel(deps.config, channel)) {
      return undefined;
    }

    const provider = deps.config.tts?.provider;
    const endpoint = provider?.endpoint ?? (provider?.type === 'openai' ? 'https://api.openai.com/v1/audio/speech' : undefined);
    if (!endpoint) {
      return undefined;
    }

    try {
      return await synthesizeSpeechAttachment(responseText, {
        endpoint,
        apiKey: provider?.api_key,
        model: provider?.model,
        voice: provider?.voice,
        format: provider?.format,
      });
    } catch (error) {
      console.warn(`TTS synthesis failed for channel ${channel}:`, error instanceof Error ? error.message : 'Unknown error');
      return undefined;
    }
  }

  function getOrCreateAgent(channel: string, senderId: string, metadata?: Record<string, unknown>, agentOverride?: string): { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector } {
    // Resolve agent config name via routing (sender → channel → default fallback)
    const agentConfigName = agentOverride ?? deps.agentRouter?.resolve(channel, senderId);
    const agentConfig = agentConfigName ? deps.agentConfigRegistry?.get(agentConfigName) : undefined;

    // Cron job tier wins over agent config tier
    const tierFromMetadata = metadata?.modelTier as ModelTier | undefined;
    const tierFromUseCaseMetadata = tierFromUseCase(deps.config, metadata?.modelFor);

    // Include agent config name in cache key so different agents aren't shared
    let skillOverride = metadata?.skillOverride as string | undefined;
    if (skillOverride && deps.skillRegistry) {
      const s = deps.skillRegistry.get(skillOverride);
      if (!s || !s.available) {
        skillOverride = undefined;
      }
    }
    const baseSid = agentConfigName || skillOverride
      ? `${channel}:${senderId}:${agentConfigName ?? 'default'}:${skillOverride ?? 'none'}`
      : `${channel}:${senderId}`;
    const session = deps.sessionManager.getSession(channel, senderId);

    // Read per-session model tier override (persisted in SQLite)
    const sessionTierOverride = session.getConfig('modelTier') as ModelTier | undefined;

    // Resolution chain: metadata (explicit tier) → metadata modelFor -> session override -> agent config -> global default
    const effectiveTier = tierFromMetadata
      ?? tierFromUseCaseMetadata
      ?? sessionTierOverride
      ?? agentConfig?.modelTier
      ?? deps.config.agents.primary_tier
      ?? 'default';

    // Cache agents by tier too so switching tiers updates context-window heuristics.
    const sessionId = `${baseSid}:${effectiveTier}`;

    let entry = agents.get(sessionId);
    if (!entry) {
      // Use agent config overrides where available, falling back to global config
      let effectiveSystemPrompt = agentConfig?.systemPrompt ?? deps.systemPrompt;

      // If an active skill is specified, annotate the system prompt for clarity.
      const activeSkillName = skillOverride;
      const activeSkill = activeSkillName ? deps.skillRegistry?.get(activeSkillName) : undefined;
      if (activeSkillName) {
        effectiveSystemPrompt += `\n\n[Active skill: ${activeSkillName}. Tool access is capability-restricted and may be sandboxed.]`;
      }

      const modelsConfig = deps.config.models as Record<string, { provider?: string; model?: string; context_window?: number } | undefined>;
      const tierConfig = modelsConfig[effectiveTier] ?? deps.config.models.default;
      const effectiveProvider = tierConfig?.provider ?? deps.config.models.default.provider;
      const effectiveModelName = tierConfig?.model ?? deps.config.models.default.model;
      const effectiveContextWindow = tierConfig?.context_window ?? deps.config.models.default.context_window;

      const delegationConfig: DelegationConfig = {
        compaction: deps.config.agents.delegation.compaction ?? 'fast',
        memory_extraction: deps.config.agents.delegation.memory_extraction ?? 'fast',
        classification: deps.config.agents.delegation.classification ?? 'fast',
        tool_summarisation: deps.config.agents.delegation.tool_summarisation ?? 'fast',
        complex_reasoning: deps.config.agents.delegation.complex_reasoning ?? 'complex',
      };
      const backgroundModelOverrides = buildBackgroundModelOverrides(deps.config);

      // Clone the tool registry and replace high-risk tools with sandboxed versions if configured.
      let effectiveToolRegistry = deps.toolRegistry;

      const skillEnvPreference = activeSkill?.manifest.permissions?.execution_environment;
      const executionEnvironment: 'host' | 'sandbox' = skillOverride
        ? (skillEnvPreference === 'host'
          ? 'host'
          : (deps.sandboxManager && deps.config.sandbox.enabled ? 'sandbox' : 'host'))
        : 'host';

      const useSandboxTools = executionEnvironment === 'sandbox' && deps.sandboxManager && deps.config.sandbox.enabled;

      if ((agentConfig?.sandbox || Boolean(skillOverride)) && useSandboxTools) {
        effectiveToolRegistry = deps.toolRegistry.clone();
        // Lazy sandbox: create the sandboxed tools with a deferred sandbox reference
        // The sandbox is created on first use via SandboxManager.getOrCreate()
        const sandboxSessionId = sessionId;
        const sandboxManager = deps.sandboxManager;
        if (!sandboxManager) {
          throw new Error('Sandbox manager unavailable for sandboxed agent execution');
        }

        // Create a proxy sandbox that lazily initializes
        const lazySandboxShell: Tool = {
          name: 'shell.exec',
          description: 'Execute a shell command inside a sandboxed container and return stdout/stderr.',
          inputSchema: {
            type: 'object',
            properties: {
              command: { type: 'string', description: 'The shell command to execute' },
              cwd: { type: 'string', description: 'Working directory inside the container (optional)' },
              timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000)' },
            },
            required: ['command'],
          },
          execute: async (rawArgs: unknown) => {
            const sandbox = await sandboxManager.getOrCreate(sandboxSessionId);
            const tool = createSandboxedShellTool(sandbox);
            return tool.execute(rawArgs);
          },
        };

        const lazySandboxProcess: Tool = {
          name: 'process.start',
          description: 'Start a command in the background inside a sandboxed container.',
          inputSchema: {
            type: 'object',
            properties: {
              command: { type: 'string', description: 'The shell command to run in the background' },
              cwd: { type: 'string', description: 'Working directory inside the container (optional)' },
            },
            required: ['command'],
          },
          execute: async (rawArgs: unknown) => {
            const sandbox = await sandboxManager.getOrCreate(sandboxSessionId);
            const tool = createSandboxedProcessStartTool(sandbox);
            return tool.execute(rawArgs);
          },
        };

        effectiveToolRegistry.replace(lazySandboxShell);
        effectiveToolRegistry.replace(lazySandboxProcess);
      }

      // Create an attachment collector for this agent session
      const collector = new OutboundAttachmentCollector();

      // Clone the tool registry to register the media.send tool bound to this collector
      effectiveToolRegistry = effectiveToolRegistry.clone();
      effectiveToolRegistry.register(createMediaSendTool(collector));

      // Register delegation tools with lazy orchestrator reference (resolved after construction)
      let resolveOrchestrator: ((o: AgentOrchestrator) => void) | undefined;
      if (deps.agentConfigRegistry && deps.agentConfigRegistry.list().length > 0) {
        let lazyOrchestrator: AgentOrchestrator | null = null;
        resolveOrchestrator = (o: AgentOrchestrator) => { lazyOrchestrator = o; };
        effectiveToolRegistry.register(createAgentDelegateTool({
          registry: deps.agentConfigRegistry,
          get orchestrator(): AgentOrchestrator {
            if (!lazyOrchestrator) {
              throw new Error('Agent orchestrator not yet initialized');
            }
            return lazyOrchestrator;
          },
        } as AgentDelegateDeps));

        if (deps.config.councils?.enabled) {
          const scaffold = loadCouncilScaffoldSafe(deps.config.councils.scaffold_path);
          effectiveToolRegistry.register(createCouncilRunTool({
            registry: deps.agentConfigRegistry,
            config: deps.config.councils as CouncilsConfig,
            scaffold,
            get orchestrator(): AgentOrchestrator {
              if (!lazyOrchestrator) {
                throw new Error('Agent orchestrator not yet initialized');
              }
              return lazyOrchestrator;
            },
          }));
        }
      }

      const toolPolicyContext = {
        agent: effectiveTier,
        provider: effectiveProvider,
        sessionId: session.id,
        channel,
        sender: senderId,
        tier: effectiveTier,
        autonomyLevel: deps.config.agents.autonomy_level ?? 'standard',
        sensitiveMode: deps.config.agents.sensitive_mode,
        immutableDenylist: (deps.config.agents.immutable_denylist ?? []).map((rule) => ({
          tool: rule.tool,
          argsPattern: rule.args_pattern,
          reason: rule.reason,
        })),
        skillName: activeSkillName,
        skillPermissions: activeSkill?.manifest.permissions,
        allowedSecretScopes: activeSkill?.manifest.permissions?.secrets,
        executionEnvironment,
      };

      const orchestrator = new AgentOrchestrator({
        modelRouter: deps.modelRouter,
        systemPrompt: effectiveSystemPrompt,
        session,
        toolRegistry: effectiveToolRegistry,
        toolExecutor: deps.toolExecutor,
        primaryTier: effectiveTier,
        delegation: delegationConfig,
        backgroundModelOverrides,
        maxDelegationDepth: deps.config.agents.max_delegation_depth ?? 3,
        maxIterations: deps.config.agents.max_iterations,
        compaction: deps.config.compaction.enabled ? {
          thresholdPct: deps.config.compaction.threshold_pct,
          keepTurns: deps.config.compaction.keep_turns,
          summaryMaxTokens: deps.config.compaction.summary_max_tokens,
          importanceThreshold: deps.config.compaction.importance_threshold,
          proactive: {
            enabled: deps.config.compaction.proactive.enabled,
            warnPct: deps.config.compaction.proactive.warn_pct,
            checkpointPct: deps.config.compaction.proactive.checkpoint_pct,
            autoCompactPct: deps.config.compaction.proactive.auto_compact_pct,
            checkpointCooldownMs: deps.config.compaction.proactive.checkpoint_cooldown_ms,
            memoryNamespace: deps.config.compaction.proactive.memory_namespace,
          },
        } : undefined,
        modelName: effectiveModelName,
        contextWindow: effectiveContextWindow,
        memoryStore: deps.memoryStore,
        memoryAutoExtract: deps.config.memory?.auto_extract,
        memoryInjectionStrategy: deps.config.memory?.injection_strategy,
        memoryMaxInjectionTokens: deps.config.memory?.max_injection_tokens,
        memoryProactiveExtractEnabled: deps.config.memory?.proactive_extract?.enabled,
        memoryProactiveExtractMinToolCalls: deps.config.memory?.proactive_extract?.min_tool_calls,
        memoryProactiveExtractNamespace: deps.config.memory?.proactive_extract?.namespace,
        memoryDailyLogEnabled: deps.config.memory?.daily_log?.enabled,
        memoryDailyLogNamespacePrefix: deps.config.memory?.daily_log?.namespace_prefix,
        memoryDailyLogIncludeSessionMetadata: deps.config.memory?.daily_log?.include_session_metadata,
        memoryDailyLogMaxUserChars: deps.config.memory?.daily_log?.max_user_chars,
        memoryDailyLogMaxAssistantChars: deps.config.memory?.daily_log?.max_assistant_chars,
        autoEscalate: deps.config.agents.auto_escalate,
        autoEscalateTier: 'complex',
        toolPolicyContext,
        attachmentCollector: collector,
      });
      // Resolve the lazy orchestrator reference for agent.delegate
      resolveOrchestrator?.(orchestrator);

      entry = { orchestrator, collector };
      agents.set(sessionId, entry);
    }
    return entry;
  }

  const handler = async (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>): Promise<void> => {
    const sessionIdForRun = `${msg.channel}:${msg.senderId}`;
    let incomingText = msg.text;
    let matchedReactionName: string | undefined;
    const talkMode = deps.config.audio?.talk_mode;
    if (talkMode?.enabled && incomingText.trim().length > 0) {
      const key = `${msg.channel}:${msg.senderId}`;
      const now = Date.now();
      const timeoutMs = talkMode.timeout_ms;
      const currentUntil = talkModeUntil.get(key) ?? 0;
      const lower = incomingText.trim().toLowerCase();

      if (talkMode.allow_manual_toggle) {
        if (lower === '/talk on') {
          talkModeUntil.set(key, now + timeoutMs);
          await reply({ text: `Talk mode enabled for ${Math.ceil(timeoutMs / 1000)}s.`, replyTo: msg.id });
          return;
        }
        if (lower === '/talk off') {
          talkModeUntil.delete(key);
          await reply({ text: 'Talk mode disabled.', replyTo: msg.id });
          return;
        }
        if (lower === '/talk status') {
          if (currentUntil <= now) {
            await reply({ text: 'Talk mode is idle (wake phrase required).', replyTo: msg.id });
          } else {
            await reply({ text: `Talk mode active for ${Math.ceil((currentUntil - now) / 1000)}s.`, replyTo: msg.id });
          }
          return;
        }
      }

      const phrase = talkMode.wake_phrase.trim();
      const wakeRegex = phrase
        ? new RegExp(`^\\s*${escapeRegex(phrase)}(?:[\\s,:!.-]+)?`, 'i')
        : null;
      const wakeMatched = Boolean(wakeRegex && wakeRegex.test(incomingText));

      if (wakeMatched && wakeRegex) {
        talkModeUntil.set(key, now + timeoutMs);
        incomingText = incomingText.replace(wakeRegex, '').trim();
        if (!incomingText) {
          await reply({ text: `Listening. Talk mode active for ${Math.ceil(timeoutMs / 1000)}s.`, replyTo: msg.id });
          return;
        }
      } else if (currentUntil > now) {
        talkModeUntil.set(key, now + timeoutMs);
      } else {
        return;
      }
    }

    const automationReactions = deps.config.automation?.reactions ?? [];
    if (!msg.metadata?.isCommand && automationReactions.length > 0) {
      const reactionMatch = matchReactionPrompt(automationReactions, {
        channel: msg.channel,
        senderId: msg.senderId,
        text: incomingText,
        metadata: msg.metadata,
      });
      if (reactionMatch) {
        matchedReactionName = reactionMatch.name;
        incomingText = reactionMatch.prompt;
      }
    }

    let intentAgentOverride: string | undefined;
    let intentSkillOverride: string | undefined;
    if (!deps.config.intents?.enabled && deps.agentConfigRegistry?.get('research')) {
      const researchTask = parseResearchPrefix(incomingText);
      if (researchTask) {
        intentAgentOverride = 'research';
        incomingText = researchTask;
      }
    }
    if (deps.config.intents?.enabled && deps.intentRegistry) {
      const intentMatch = deps.intentRegistry.match(incomingText);

      if (intentMatch?.rule.target.type === 'agent') {
        let confidence = intentMatch.score;
        if (deps.config.history_index?.enabled) {
          const historySessionId = `${msg.channel}:${msg.senderId}`;
          const historyHits = deps.sessionManager.searchHistory(msg.text, {
            sessionId: historySessionId,
            limit: 1,
          });
          if (historyHits.length > 0 && historyHits[0].score >= (deps.config.history_index.min_score ?? 0.15)) {
            confidence = Math.min(1, confidence + (deps.config.history_index.routing_boost ?? 0.05));
          }
        }

        const decision = deps.routingPolicy
          ? deps.routingPolicy.decide({ confidence })
          : { path: 'fast' as const, reason: 'high_confidence' as const };

        console.log(`[routing] intent=${intentMatch.rule.name} confidence=${confidence.toFixed(3)} path=${decision.path} reason=${decision.reason}`);

        if (decision.path === 'fast') {
          intentAgentOverride = intentMatch.rule.target.name;
        }
      }

      if (intentMatch?.rule.target.type === 'skill') {
        let confidence = intentMatch.score;
        const decision = deps.routingPolicy
          ? deps.routingPolicy.decide({ confidence })
          : { path: 'fast' as const, reason: 'high_confidence' as const };

        console.log(`[routing] intent=${intentMatch.rule.name} confidence=${confidence.toFixed(3)} path=${decision.path} reason=${decision.reason}`);

        if (decision.path === 'fast') {
          intentSkillOverride = intentMatch.rule.target.name;
        }
      }
    }

    const effectiveMetadata = {
      ...(msg.metadata ?? {}),
      ...(intentSkillOverride ? { skillOverride: intentSkillOverride } : {}),
      ...(matchedReactionName ? { automationReaction: matchedReactionName } : {}),
    };

    const agentConfigName = intentAgentOverride ?? deps.agentRouter?.resolve(msg.channel, msg.senderId);
    const agentConfig = agentConfigName ? deps.agentConfigRegistry?.get(agentConfigName) : undefined;
    const { orchestrator: agent, collector } = getOrCreateAgent(msg.channel, msg.senderId, effectiveMetadata, agentConfigName);

    const commandInput = msg.metadata?.isCommand && typeof msg.metadata.command === 'string'
      ? `/${msg.metadata.command}${msg.metadata.commandArgs ? ` ${msg.metadata.commandArgs}` : ''}`
      : incomingText;
    const metadataCommand = typeof msg.metadata?.command === 'string' ? msg.metadata.command : undefined;
    const parsedCommand = msg.metadata?.isCommand
      ? metadataCommand
      : commandInput.startsWith('/') ? commandInput.slice(1).split(/\s+/, 1)[0] : undefined;
    auditLogger?.userAction({
      session_id: `${msg.channel}:${msg.senderId}`,
      channel: msg.channel,
      sender: msg.senderId,
      source: 'channel',
      action_type: parsedCommand ? 'command' : 'message',
      content_length: commandInput.length,
      attachments_count: msg.attachments?.length ?? 0,
      command: parsedCommand,
    });

    if (deps.commandRegistry && deps.commandRegistry.isCommand(commandInput)) {
      const session = deps.sessionManager.getSession(msg.channel, msg.senderId);
      const commandResult = await deps.commandRegistry.execute(commandInput, {
        channel: msg.channel,
        senderId: msg.senderId,
        sessionId: session.id,
        rawInput: commandInput,
        services: {
          getStatus: () => {
            return formatBackendStatus(agent.getModelTier());
          },
          getTools: () => {
            const names = new Set(deps.toolRegistry.list().map((tool: Tool) => tool.name));
            names.add('media.send');
            if (deps.agentConfigRegistry && deps.agentConfigRegistry.list().length > 0) {
              names.add('agent.delegate');
              if (deps.config.councils?.enabled) {
                names.add('council.run');
              }
            }
            const sorted = [...names].sort();
            return [
              `Available tools (${sorted.length}):`,
              ...sorted.map((name) => `- ${name}`),
            ].join('\n');
          },
          getUsage: () => {
            const usage = agent.getUsage();
            const lines = [
              '**Token Usage**',
              '',
              `Primary: ${usage.primary.inputTokens.toLocaleString()} in / ${usage.primary.outputTokens.toLocaleString()} out (${usage.primary.calls} calls)`,
            ];

            const delegationEntries = Object.entries(usage.delegation);
            if (delegationEntries.length > 0) {
              lines.push('');
              lines.push('Delegation:');
              for (const [tier, stats] of delegationEntries) {
                lines.push(`  ${tier}: ${stats.inputTokens.toLocaleString()} in / ${stats.outputTokens.toLocaleString()} out (${stats.calls} calls)`);
              }
            }

            lines.push('');
            lines.push(`**Total:** ${usage.total.inputTokens.toLocaleString()} in / ${usage.total.outputTokens.toLocaleString()} out (${usage.total.calls} calls)`);

            if (usage.total.estimatedCost > 0) {
              lines.push(`**Estimated cost:** $${usage.total.estimatedCost.toFixed(4)}`);
            }

            return lines.join('\n');
          },
          getModel: () => {
            const currentTier = agent.getModelTier();
            const sessionOverride = session.getConfig('modelTier');
            const available = deps.modelRouter.getAvailableTiers();
            const labels = deps.modelRouter.getAllLabels();
            const lines = [`Active tier: ${currentTier}${sessionOverride ? ' (session override)' : ''}`];
            for (const tier of available) {
              const label = labels[tier] ?? 'unknown';
              const marker = tier === currentTier ? ' ←' : '';
              lines.push(`  ${tier}: ${label}${marker}`);
            }
            return lines.join('\n');
          },
          setModel: (tier) => {
            const raw = tier.trim();
            if (!raw) {
              return 'Usage: /model <tier>  OR  /model <tier> <provider/model>  OR  /model <tier> reset';
            }

            const parts = raw.split(/\s+/);
            const requestedTier = parts[0];

            const validTiers = deps.modelRouter.getAvailableTiers();
            if (!validTiers.includes(requestedTier as ModelTier)) {
              return `Model tier not available: ${requestedTier}`;
            }

            const modelTier = requestedTier as ModelTier;

            // /model <tier>
            if (parts.length === 1) {
              session.setConfig('modelTier', modelTier);
              agent.setModelTier(modelTier);
              const label = deps.modelRouter.getLabel(modelTier);
              return `Switched to model: ${modelTier} (${label})`;
            }

            const arg2 = parts[1];

            // /model <tier> reset — restore configured provider/model and re-enable fallbacks
            if (arg2.toLowerCase() === 'reset') {
              const configured: ModelConfig | undefined = modelTier === 'default'
                ? deps.config.models.default
                : modelTier === 'fast'
                  ? deps.config.models.fast
                  : modelTier === 'complex'
                    ? deps.config.models.complex
                    : modelTier === 'local'
                      ? deps.config.models.local
                      : undefined;
              if (!configured) {
                return `No configured model for tier: ${modelTier}`;
              }

              const client = createClientFromConfig(configured);
              const label = `${configured.provider}/${configured.model}`;
              deps.modelRouter.setClient(modelTier, client, label);
              deps.modelRouter.setTierStrict(modelTier, false);
              session.setConfig('modelTier', modelTier);
              agent.setModelTier(modelTier);
              return `Reset ${modelTier} to: ${label}`;
            }

            // /model <tier> <provider/model>
            const providerModel = arg2;
            if (!providerModel.includes('/')) {
              return 'Invalid format. Use: /model <tier> <provider/model> (e.g. /model default github/gpt-5-mini)';
            }

            const slashIdx = providerModel.indexOf('/');
            const provider = providerModel.slice(0, slashIdx);
            const model = providerModel.slice(slashIdx + 1);

            if (!MODEL_PROVIDERS.includes(provider as ModelProvider)) {
              return `Unknown provider "${provider}". Known providers: ${MODEL_PROVIDERS.join(', ')}`;
            }

            const providerType = provider as ModelProvider;
            const providerConfigs = buildProviderConfigMap(deps.config);
            const template = providerConfigs[providerType];

            try {
              const client = createClientFromConfig(
                template
                  ? { ...template, provider: providerType, model }
                  : { provider: providerType, model },
              );

              deps.modelRouter.setClient(modelTier, client, providerModel);
              deps.modelRouter.setTierStrict(modelTier, true);
              session.setConfig('modelTier', modelTier);
              agent.setModelTier(modelTier);

              const lines = [
                `Set ${modelTier} to: ${providerModel}`,
                `Fallbacks for ${modelTier} disabled (strict tier mode).`,
              ];
              if (parts.length > 2) {
                lines.push(`Note: ignored extra args: ${parts.slice(2).join(' ')}`);
              }
              return lines.join('\n');
            } catch (error) {
              const message = error instanceof Error ? error.message : String(error);
              return `Failed to switch ${modelTier} to ${providerModel}: ${message}`;
            }
          },
          compact: async () => {
            const result = await agent.compact();
            if (result && result.compactedCount > 0) {
              return `Compacted ${result.compactedCount} messages: ${result.tokensBefore} → ${result.tokensAfter} tokens`;
            }
            return 'Nothing to compact.';
          },
          reset: () => {
            agent.reset();
            session.deleteConfig('modelTier');
            return '';
          },
          cancelRun: () => {
            const run = activeRuns.get(session.id);
            if (!run || !run.isCancellable()) {
              return 'No active operation to cancel.';
            }
            run.cancel();
            return 'Cancellation requested. The active operation will stop at the next safe point.';
          },

          delegateAgent: async (agentName: string, task: string) => {
            const target = agentName.trim();
            const message = task.trim();
            if (!target || !message) {
              return 'Usage: /research <question or task>';
            }
            if (!deps.agentConfigRegistry) {
              return 'No agent configurations are registered. Add agent_configs.research in config.';
            }
            const agentConfig = deps.agentConfigRegistry.get(target);
            if (!agentConfig) {
              const available = deps.agentConfigRegistry.list().map((c) => c.name);
              return `Agent "${target}" not found. Available agents: ${available.length > 0 ? available.join(', ') : 'none'}`;
            }

            const tier: ModelTier = agentConfig.modelTier ?? 'default';
            const systemPrompt = agentConfig.systemPrompt
              ?? `You are a sub-agent named "${target}". Complete the assigned task concisely and accurately.`;

            const result = await agent.delegate({
              tier,
              systemPrompt,
              message,
              maxTokens: 4096,
            });

            return `[Agent: ${target} | Tier: ${result.tier} | Tokens: ${result.usage.inputTokens}+${result.usage.outputTokens}]\n\n${result.content}`;
          },
          runCouncil: async (task: string) => {
            const message = task.trim();
            if (!message) {
              return 'Usage: /council <question or task> | /council preflight';
            }
            if (!deps.config.councils?.enabled) {
              return 'Councils are disabled. Set councils.enabled: true in config.';
            }
            if (!deps.agentConfigRegistry || deps.agentConfigRegistry.list().length === 0) {
              return 'No agent configurations are registered. Add council_* agent_configs first.';
            }
            if (shouldRunCouncilPreflight(message)) {
              return buildCouncilPreflightReport({
                config: deps.config,
                registry: deps.agentConfigRegistry,
                delegateRunner: agent,
                activeTier: agent.getModelTier(),
                includeLiveProbe: true,
              });
            }
            const tool = createCouncilRunTool({
              registry: deps.agentConfigRegistry,
              orchestrator: agent,
              config: deps.config.councils as CouncilsConfig,
              scaffold: loadCouncilScaffoldSafe(deps.config.councils.scaffold_path),
            });
            const result = await tool.execute({ task: message });
            if (!result.success) {
              return `Council run failed: ${result.error ?? 'unknown error'}`;
            }
            return result.output;
          },

          getElevation: () => {
            return getElevationStatusMessage({
              get: (key) => session.getConfig(key),
              set: (key, value) => session.setConfig(key, value),
              delete: (key) => session.deleteConfig(key),
            }, {
              showExpiredSuffix: true,
              auditContext: {
                sessionId: session.id,
                channel: msg.channel,
                sender: msg.senderId,
              },
            });
          },

          setElevation: (input: string) => {
            return setElevationFromInput({
              get: (key) => session.getConfig(key),
              set: (key, value) => session.setConfig(key, value),
              delete: (key) => session.deleteConfig(key),
            }, input, {
              auditContext: {
                sessionId: session.id,
                channel: msg.channel,
                sender: msg.senderId,
              },
            });
          },

          getQueue: () => {
            const mode = session.getConfig('queue.mode') ?? deps.config.server.queue.mode;
            const cap = session.getConfig('queue.cap') ?? String(deps.config.server.queue.cap);
            const overflow = session.getConfig('queue.overflow') ?? deps.config.server.queue.overflow;
            const debounceMs = session.getConfig('queue.debounce_ms') ?? String(deps.config.server.queue.debounce_ms);
            const summarizeOverflow = session.getConfig('queue.summarize_overflow') ?? String(deps.config.server.queue.summarize_overflow);
            const source = session.getConfig('queue.mode')
              || session.getConfig('queue.cap')
              || session.getConfig('queue.overflow')
              || session.getConfig('queue.debounce_ms')
              || session.getConfig('queue.summarize_overflow')
              ? 'session override'
              : 'default config';
            return [
              '**Queue policy**',
              `mode: ${mode}`,
              `cap: ${cap}`,
              `overflow: ${overflow}`,
              `debounce_ms: ${debounceMs}`,
              `summarize_overflow: ${summarizeOverflow}`,
              `source: ${source}`,
            ].join('\n');
          },

          setQueue: (input: string) => {
            const [rawKey, ...rest] = input.trim().split(/\s+/);
            const value = rest.join(' ').trim();
            if (!rawKey || !value) {
              return 'Usage: /queue <mode|cap|overflow|debounce_ms|summarize_overflow> <value>';
            }
            const key = rawKey.toLowerCase();
            if (key === 'mode') {
              if (!['collect', 'followup', 'steer', 'steer_backlog', 'interrupt'].includes(value)) {
                return 'Invalid mode. Use one of: collect, followup, steer, steer_backlog, interrupt';
              }
              session.setConfig('queue.mode', value);
              return `Set queue.mode=${value} for this session`;
            }
            if (key === 'cap') {
              const cap = Number.parseInt(value, 10);
              if (!Number.isFinite(cap) || cap < 1 || cap > 1000) {
                return 'Invalid cap. Use an integer between 1 and 1000';
              }
              session.setConfig('queue.cap', String(cap));
              return `Set queue.cap=${cap} for this session`;
            }
            if (key === 'overflow') {
              if (value !== 'drop_old' && value !== 'drop_new') {
                return 'Invalid overflow. Use drop_old or drop_new';
              }
              session.setConfig('queue.overflow', value);
              return `Set queue.overflow=${value} for this session`;
            }
            if (key === 'debounce_ms') {
              const debounceMs = Number.parseInt(value, 10);
              if (!Number.isFinite(debounceMs) || debounceMs < 0 || debounceMs > 60_000) {
                return 'Invalid debounce_ms. Use an integer between 0 and 60000';
              }
              session.setConfig('queue.debounce_ms', String(debounceMs));
              return `Set queue.debounce_ms=${debounceMs} for this session`;
            }
            if (key === 'summarize_overflow') {
              const normalized = value.toLowerCase();
              if (normalized !== 'true' && normalized !== 'false') {
                return 'Invalid summarize_overflow. Use true or false';
              }
              session.setConfig('queue.summarize_overflow', normalized);
              return `Set queue.summarize_overflow=${normalized} for this session`;
            }
            return 'Unknown queue key. Use one of: mode, cap, overflow, debounce_ms, summarize_overflow';
          },

          resetQueue: () => {
            session.deleteConfig('queue.mode');
            session.deleteConfig('queue.cap');
            session.deleteConfig('queue.overflow');
            session.deleteConfig('queue.debounce_ms');
            session.deleteConfig('queue.summarize_overflow');
            return 'Reset session queue overrides.';
          },

          transferSession: (targetRaw: string) => {
            const target = targetRaw.trim().toLowerCase();
            if (!target) {
              return 'Usage: /transfer <tui|telegram>';
            }

            let toFrontend: string;
            let toUserId: string;
            let destinationLabel: string;

            if (target === 'tui') {
              toFrontend = 'tui';
              toUserId = 'local';
              destinationLabel = 'TUI (local)';
            } else if (target === 'telegram') {
              if (msg.channel === 'telegram') {
                toFrontend = 'telegram';
                toUserId = msg.senderId;
              } else {
                const chatId = deps.config.telegram?.allowed_chat_ids?.[0];
                if (chatId === undefined) {
                  return 'Telegram not configured';
                }
                toFrontend = 'telegram';
                toUserId = String(chatId);
              }
              destinationLabel = `Telegram (${toUserId})`;
            } else {
              return `Unknown transfer target: ${target}. Supported targets: tui, telegram`;
            }

            if (msg.channel === toFrontend && msg.senderId === toUserId) {
              return `Session is already active on ${destinationLabel}`;
            }

            deps.sessionManager.transferSession(msg.channel, msg.senderId, toFrontend, toUserId);
            return `Session transferred to ${destinationLabel}`;
          },

          backendCommand: (inputRaw: string) => executeBackendCommand(inputRaw, agent.getModelTier()),

          getApprovals: () => {
            if (!deps.hookEngine) {
              return 'Approval gates are not enabled in this runtime.';
            }
            const pending = deps.hookEngine.getPendingConfirmations({ sessionId: session.id });
            if (pending.length === 0) {
              return 'No pending approvals for this session.';
            }
            const lines = ['Pending approvals:'];
            for (const item of pending) {
              const ageSec = Math.max(0, Math.round((Date.now() - item.createdAt.getTime()) / 1000));
              lines.push(`- ${item.id} | ${item.tool} | ${ageSec}s old`);
            }
            lines.push('');
            lines.push('Use `/approve <id>` or `/deny <id> <reason>` (id optional: latest is used).');
            return lines.join('\n');
          },

          approvePending: (inputRaw: string) => {
            if (!deps.hookEngine) {
              return 'Approval gates are not enabled in this runtime.';
            }
            const pending = deps.hookEngine.getPendingConfirmations({ sessionId: session.id });
            if (pending.length === 0) {
              return 'No pending approvals for this session.';
            }

            const input = inputRaw.trim();
            const selected = input
              ? pending.find((item) => item.id === input)
              : pending[pending.length - 1];

            if (!selected) {
              return `Approval id not found in this session: ${input}`;
            }

            const resolved = deps.hookEngine.resolveConfirmation(selected.id, { approved: true });
            return resolved
              ? `Approved: ${selected.tool} (${selected.id})`
              : `Approval request is no longer pending: ${selected.id}`;
          },

          denyPending: (inputRaw: string) => {
            if (!deps.hookEngine) {
              return 'Approval gates are not enabled in this runtime.';
            }
            const pending = deps.hookEngine.getPendingConfirmations({ sessionId: session.id });
            if (pending.length === 0) {
              return 'No pending approvals for this session.';
            }

            const input = inputRaw.trim();
            let targetId: string | undefined;
            let reason = 'Denied by user';

            if (input) {
              const [first, ...rest] = input.split(/\s+/);
              const matched = pending.find((item) => item.id === first);
              if (matched) {
                targetId = matched.id;
                reason = rest.join(' ').trim() || reason;
              } else {
                targetId = pending[pending.length - 1].id;
                reason = input;
              }
            } else {
              targetId = pending[pending.length - 1].id;
            }

            const selected = pending.find((item) => item.id === targetId);
            if (!selected) {
              return `Approval request is no longer pending: ${targetId}`;
            }

            const resolved = deps.hookEngine.resolveConfirmation(selected.id, { approved: false, reason });
            return resolved
              ? `Denied: ${selected.tool} (${selected.id}) — ${reason}`
              : `Approval request is no longer pending: ${selected.id}`;
          },

          skillCommand: async (inputRaw: string) => {
            const input = inputRaw.trim();
            const [actionRaw, ...rest] = input.split(/\s+/).filter(Boolean);
            const action = actionRaw?.toLowerCase();

            if (!action || action === 'help') {
              return [
                'Usage: /skill <list|search|install>',
                '/skill list',
                '/skill search <term>',
                '/skill install <registry-id>',
              ].join('\n');
            }

            if (action === 'list') {
              const skills = deps.skillRegistry?.listAvailable() ?? [];
              if (skills.length === 0) {
                return 'No available skills are currently loaded.';
              }
              return [
                `Available skills (${skills.length}):`,
                ...skills.map((skill) => `- ${skill.manifest.name} (${skill.manifest.tier}, v${skill.manifest.version})`),
              ].join('\n');
            }

            if (action === 'search') {
              const term = rest.join(' ').trim().toLowerCase();
              if (!term) {
                return 'Usage: /skill search <term>';
              }
              const sourceResolved = resolveRegistrySource(deps.config);
              if (!sourceResolved.source) {
                return sourceResolved.error ?? 'Failed to resolve registry source.';
              }
              try {
                const catalog = await loadSkillRegistryCatalog(sourceResolved.source);
                const matches = catalog.skills.filter((entry) => {
                  const haystack = `${entry.id} ${entry.name} ${entry.summary} ${entry.publisher ?? ''}`.toLowerCase();
                  return haystack.includes(term);
                }).slice(0, 12);
                if (matches.length === 0) {
                  return `No registry skills found for "${term}".`;
                }
                return [
                  `Registry matches (${matches.length}):`,
                  ...matches.map((entry) => `- ${entry.id} (${entry.version}) — ${entry.summary}`),
                ].join('\n');
              } catch (error) {
                return `Registry lookup failed: ${error instanceof Error ? error.message : String(error)}`;
              }
            }

            if (action === 'install') {
              const registryId = rest.join(' ').trim().toLowerCase();
              if (!registryId) {
                return 'Usage: /skill install <registry-id>';
              }
              if (!deps.skillInstaller || !deps.skillRegistry) {
                return 'Skill installation is unavailable in this runtime.';
              }

              const sourceResolved = resolveRegistrySource(deps.config);
              if (!sourceResolved.source) {
                return sourceResolved.error ?? 'Failed to resolve registry source.';
              }

              try {
                const catalog = await loadSkillRegistryCatalog(sourceResolved.source);
                const entry = catalog.skills.find((item) => item.id.toLowerCase() === registryId);
                if (!entry) {
                  return `Registry skill not found: ${registryId}`;
                }
                const localPath = resolveRegistryEntryLocalPath(entry, sourceResolved.source);
                if (!localPath) {
                  return `Registry entry '${entry.id}' points to a remote source. Use CLI install for remote sources.`;
                }

                const installed = deps.skillInstaller.install(localPath);
                if (!installed) {
                  return `Failed to install skill from ${entry.source}`;
                }
                deps.skillRegistry.register(installed);
                return `Installed skill '${installed.manifest.name}' (${installed.manifest.version}) from registry id '${entry.id}'.`;
              } catch (error) {
                return `Skill install failed: ${error instanceof Error ? error.message : String(error)}`;
              }
            }

            return 'Unknown skill action. Use: list, search, install';
          },
        },
      });

      if (commandResult.handled) {
        if (commandResult.text.trim()) {
          await reply({ text: commandResult.text, replyTo: msg.id });
        }
        return;
      }
    }

    try {
      // Determine if the active model supports native audio input
      let effectiveTier: string = deps.config.agents.primary_tier ?? 'default';
      const session = deps.sessionManager.getSession(msg.channel, msg.senderId);
      const sessionTierOverride = session.getConfig('modelTier');
      const tierFromUseCaseMetadata = tierFromUseCase(deps.config, msg.metadata?.modelFor);

      if (msg.metadata?.modelTier) {
        effectiveTier = msg.metadata.modelTier as string;
      } else if (tierFromUseCaseMetadata) {
        effectiveTier = tierFromUseCaseMetadata;
      } else if (sessionTierOverride) {
        effectiveTier = sessionTierOverride;
      } else if (deps.agentRouter && deps.agentConfigRegistry) {
        const agentName = deps.agentRouter.resolve(msg.channel, msg.senderId);
        if (agentName) {
          const agentCfg = deps.agentConfigRegistry.get(agentName);
          if (agentCfg?.modelTier) {
            effectiveTier = agentCfg.modelTier;
          }
        }
      }

      // Look up provider/model for the effective tier
      const modelsConfig = deps.config.models as Record<string, { provider?: string; model?: string } | undefined>;
      const tierConfig = modelsConfig[effectiveTier] ?? deps.config.models.default;
      const modelProvider = tierConfig?.provider ?? deps.config.models.default.provider;
      const modelName = tierConfig?.model ?? deps.config.models.default.model;
      const supportsAudioOverride = (tierConfig as Record<string, unknown> | undefined)?.supports_audio as boolean | undefined;
      const nativeAudioSupported = supportsAudioInput(modelProvider, modelName, supportsAudioOverride);

      let messageText = incomingText;
      let attachments = msg.attachments;
      const audioAttachments = (msg.attachments ?? []).filter((a: Attachment) => isSupportedAudio(a));
      const turnAudioToolInput = extractLatestAudioToolInput(audioAttachments);
      if (audioAttachments.length > 0) {
        persistLatestAudioAttachment(session, audioAttachments);
      }

      if (audioAttachments.length > 0 && !nativeAudioSupported) {
        // Model doesn't support native audio — transcribe via Whisper and strip audio attachments
        const audioConfig: AudioTranscriptionConfig | undefined = deps.config.audio?.enabled && deps.config.audio.provider
          ? {
            endpoint: deps.config.audio.provider.endpoint,
            apiKey: deps.config.audio.provider.api_key,
            model: deps.config.audio.provider.model,
          }
          : undefined;

        if (!audioConfig?.endpoint) {
          // Without transcription, we cannot safely send audio to a non-audio-capable model.
          // Fast-path a deterministic, user-friendly reply instead of invoking the agent loop.
          await reply({
            text:
              [
                'I received your voice message, but I cannot transcribe it yet because audio transcription is not configured.',
                '',
                'To enable voice messages, set `audio.enabled: true` and configure an `audio.provider` in `config.yaml` (OpenAI/Groq/custom Whisper-compatible `/v1/audio/transcriptions`).',
                '',
                'Workarounds:',
                '1. Paste the transcription text.',
                '2. Upload the audio file somewhere and send me a direct URL.',
              ].join('\n'),
            replyTo: msg.id,
          });
          return;
        }

        for (const att of audioAttachments) {
          const transcript = await transcribeAudio(att, audioConfig);
          messageText = `[Voice message]: ${transcript}\n\n${messageText}`;
        }
        // For providers that cannot ingest native audio content parts (e.g. Anthropic),
        // keep the original audio attachment available in the tool loop so
        // audio.transcribe can still be hydrated from bytes if the model requests it.
        // For providers that do accept native audio parts (OpenAI-compatible/Gemini),
        // strip audio to avoid sending raw audio to a model tier that was marked as non-audio.
        if (providerAcceptsNativeAudioContentParts(modelProvider)) {
          attachments = (msg.attachments ?? []).filter((a: Attachment) => !isSupportedAudio(a));
          if (attachments.length === 0) { attachments = undefined; }
        }
      }
      // If native audio IS supported, we pass attachments through unchanged —
      // buildUserMessage() in the agent will create native audio content parts

      const requestedBackend = applyBackendModeOverride(agentConfig?.backend ?? getEffectiveDefaultBackend());
      const forceNativeForCapabilityQuery = shouldForceNativeForCapabilityQuery(messageText);
      const hasAttachmentsForExternalBackend = Boolean(attachments && attachments.length > 0);
      const selectedBackend = requestedBackend && requestedBackend !== 'native'
        ? deps.externalBackends?.[requestedBackend]
        : undefined;
      const externalBackendRequested = Boolean(selectedBackend && requestedBackend && requestedBackend !== 'native');
      const forceNativeForPiNoTools = requestedBackend === 'pi_embedded'
        && deps.config.backends.pi_embedded.no_tools_mode
        && shouldForceNativeForPiNoTools(messageText);
      let forcedNativeGuardReason: 'capability_query' | 'pi_no_tools_mode' | 'attachments_present' | undefined;
      if (externalBackendRequested) {
        if (forceNativeForCapabilityQuery) {
          forcedNativeGuardReason = 'capability_query';
        } else if (forceNativeForPiNoTools) {
          forcedNativeGuardReason = 'pi_no_tools_mode';
        } else if (hasAttachmentsForExternalBackend) {
          forcedNativeGuardReason = 'attachments_present';
        }
      }
      const sessionIdForAudit = `${msg.channel}:${msg.senderId}`;
      const selectedBackendForAudit: 'native' | ExternalBackendName = selectedBackend && requestedBackend && !forcedNativeGuardReason
        ? requestedBackend
        : 'native';

      auditLogger?.backendRoute?.({
        session_id: sessionIdForAudit,
        channel: msg.channel,
        sender: msg.senderId,
        selected_backend: selectedBackendForAudit,
        source: forcedNativeGuardReason
          ? 'forced_native_guard'
          : agentConfig?.backend
            ? 'agent_override'
            : selectedBackend
              ? 'default_external'
              : 'native',
        ...(forcedNativeGuardReason ? { guard_reason: forcedNativeGuardReason } : {}),
      });

      if (selectedBackend && !hasAttachmentsForExternalBackend && !forceNativeForCapabilityQuery && !forceNativeForPiNoTools) {
        const backendStartedAt = Date.now();
        try {
          const history = toExternalHistory(session.getHistory());
          session.addMessage({ role: 'user', content: messageText });
          const externalSystemPrompt = requestedBackend === 'pi_embedded'
            ? agent.getSystemPrompt(messageText)
            : undefined;
          const response = await selectedBackend.process({
            prompt: messageText,
            history,
            ...(externalSystemPrompt ? { systemPrompt: externalSystemPrompt } : {}),
          });
          auditLogger?.backendSuccess?.({
            session_id: sessionIdForAudit,
            channel: msg.channel,
            sender: msg.senderId,
            backend: selectedBackend.name,
            duration_ms: Date.now() - backendStartedAt,
            response_length: response.length,
          });
          session.addMessage({ role: 'assistant', content: response });
          const ttsAttachment = await maybeBuildTtsAttachment(response, msg.channel);
          await reply({
            text: response,
            replyTo: msg.id,
            attachments: ttsAttachment ? [ttsAttachment] : undefined,
          });
          return;
        } catch (error) {
          const detail = error instanceof Error ? error.message : String(error);
          console.warn(`External backend "${selectedBackend.name}" failed, falling back to native: ${detail}`);
          auditLogger?.backendFallback?.({
            session_id: sessionIdForAudit,
            channel: msg.channel,
            sender: msg.senderId,
            from_backend: (requestedBackend && requestedBackend !== 'native')
              ? requestedBackend
              : (selectedBackend.name as ExternalBackendName),
            to_backend: 'native',
            reason: detail,
            duration_ms: Date.now() - backendStartedAt,
          });
        }
      }

      let response: string;
      activeRuns.set(sessionIdForRun, agent);
      try {
        response = await agent.process(messageText, attachments, turnAudioToolInput);
      } catch (error) {
        const currentTier = agent.getModelTier();
        const canEscalate = deps.config.agents.auto_escalate && currentTier !== 'complex';
        if (!canEscalate) {
          throw error;
        }

        console.warn(`Auto-escalating session ${msg.channel}:${msg.senderId} from ${currentTier} to complex after processing failure.`);
        agent.setModelTier('complex');
        response = await agent.process(messageText, attachments, turnAudioToolInput);
      }
      const outboundAttachments = collector.drain();
      const ttsAttachment = await maybeBuildTtsAttachment(response, msg.channel);
      const mergedAttachments = ttsAttachment
        ? [...outboundAttachments, ttsAttachment]
        : outboundAttachments;
      await reply({
        text: response,
        replyTo: msg.id,
        attachments: mergedAttachments.length > 0 ? mergedAttachments : undefined,
      });
    } catch (error) {
      console.error(`Error processing message from ${msg.channel}:${msg.senderId}:`, error);
      await reply({
        text: 'Sorry, an error occurred while processing your message.',
        replyTo: msg.id,
      });
    } finally {
      activeRuns.delete(sessionIdForRun);
    }
  };

  return { handler, agents };
}

function escapeRegex(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function toExternalHistory(history: Array<{ role: string; content: unknown }>): Array<{ role: 'user' | 'assistant'; content: string }> {
  return history
    .filter((message): message is { role: 'user' | 'assistant'; content: unknown } => (
      message.role === 'user' || message.role === 'assistant'
    ))
    .map((message) => ({
      role: message.role,
      content: messageContentToText(message.content),
    }))
    .filter((message) => message.content.trim().length > 0);
}

function messageContentToText(content: unknown): string {
  if (typeof content === 'string') {
    return content;
  }
  if (!Array.isArray(content)) {
    return '';
  }
  return content
    .map((part) => {
      if (!part || typeof part !== 'object') {
        return '';
      }
      const partObj = part as { type?: string; text?: string };
      if (partObj.type === 'text' && typeof partObj.text === 'string') {
        return partObj.text;
      }
      return '';
    })
    .filter(Boolean)
    .join('\n');
}
