import type { ModelRouter, ModelTier } from '../../models/router.js';
import type { ChatRequest, ChatResponseFormat, Message, ModelClient, TokenUsage } from '../../models/types.js';
import type { Session } from '../../session/index.js';
import type { ToolRegistry } from '../../tools/registry.js';
import type { ToolExecutor } from '../../tools/executor.js';
import type { MemoryStore } from '../../memory/store.js';
import type { ToolPolicyContext } from '../../tools/policy.js';
import type { Attachment } from '../../channels/types.js';
import { NativeAgent } from './agent.js';
import type { NativeAgentTurnAudioInput } from './agent.js';
import type { ToolUseEvent } from './agent.js';
import type { OutboundAttachmentCollector } from './attachments.js';
import { estimateMessageTokens, getContextWindow, shouldCompact } from '../../context/tokens.js';
import { compactHistory, type CompactionConfig, type CompactionResult, DEFAULT_COMPACTION_CONFIG } from '../../context/compaction.js';
import { estimateCost } from '../../models/costs.js';
import { auditLogger } from '../../audit/index.js';
import { buildAdaptiveMemoryContext, buildRecentMemoryContext } from '../../memory/adaptive.js';
import { buildUserMessage } from '../../models/media.js';
import { CONTEXT_CHECKPOINT_PROMPT, MEMORY_EXTRACTION_PROMPT } from './prompts.js';

// ── Public types ──────────────────────────────────────────────────────

/** A single-turn, stateless request to a sub-agent at a specific tier. */
export interface SubAgentRequest {
  tier: ModelTier;
  task?: keyof DelegationConfig;
  systemPrompt: string;
  message: string;
  maxTokens?: number;
  /** When true, include tools from the toolRegistry in the request. */
  tools?: boolean;
  /** Optional provider-level response format request (e.g., JSON schema). */
  responseFormat?: ChatResponseFormat;
}

/** Result returned from a sub-agent delegation call. */
export interface SubAgentResult {
  content: string;
  usage: TokenUsage;
  tier: ModelTier;
}

/** Maps each delegation task to the model tier that should handle it. */
export interface DelegationConfig {
  compaction: ModelTier;
  memory_extraction: ModelTier;
  classification: ModelTier;
  tool_summarisation: ModelTier;
  complex_reasoning: ModelTier;
}

/** Per-tier cumulative usage statistics. */
interface TierUsageStats {
  inputTokens: number;
  outputTokens: number;
  calls: number;
}

/** Full usage stats for an orchestrator session. */
export interface UsageReport {
  /** Primary agent (user-facing) usage. */
  primary: {
    inputTokens: number;
    outputTokens: number;
    calls: number;
  };
  /** Delegation (sub-agent) usage, broken down by tier. */
  delegation: Record<string, { inputTokens: number; outputTokens: number; calls: number }>;
  /** Combined totals. */
  total: {
    inputTokens: number;
    outputTokens: number;
    calls: number;
    estimatedCost: number;
  };
}

export type ContextAlertLevel = 'warning' | 'checkpoint' | 'critical';

export interface ContextBudget {
  estimatedTokens: number;
  contextWindow: number;
  remainingTokens: number;
  usagePct: number;
  thresholdPct: number;
  thresholdTokens: number;
  shouldCompact: boolean;
}

export interface ContextAlert {
  level: ContextAlertLevel;
  message: string;
  budget: ContextBudget;
  actions: {
    checkpointSaved: boolean;
    autoCompacted: boolean;
    checkpointNamespace?: string;
  };
}

/** Full configuration for the AgentOrchestrator. */
export interface OrchestratorConfig {
  modelRouter: ModelRouter;
  systemPrompt: string;
  session?: Session;
  toolRegistry?: ToolRegistry;
  toolExecutor?: ToolExecutor;
  maxIterations?: number;
  /** The tier used by the primary NativeAgent for user-facing conversation. */
  primaryTier: ModelTier;
  /** Which tier to use for each delegation task type. */
  delegation: DelegationConfig;
  /** Optional direct provider/model overrides for specific delegation task types. */
  backgroundModelOverrides?: Partial<Record<keyof DelegationConfig, {
    client: ModelClient;
    label: string;
    fallbackTier: ModelTier;
  }>>;
  /** Maximum nesting depth for delegation calls (safety guard). */
  maxDelegationDepth: number;
  onToolUse?: (event: ToolUseEvent) => void;
  /** Context compaction settings. When provided, enables automatic compaction. */
  compaction?: CompactionConfig;
  /** Model identifier for the primary model (used for context window lookup). */
  modelName?: string;
  /** Optional override for the context window size (in tokens). */
  contextWindow?: number;
  /** Optional memory store for injecting persistent memory into the system prompt. */
  memoryStore?: MemoryStore;
  /** Enable/disable automatic memory extraction during compaction. */
  memoryAutoExtract?: boolean;
  /** Strategy for memory prompt injection. */
  memoryInjectionStrategy?: 'all' | 'recent' | 'adaptive';
  /** Maximum tokens allowed for injected memory context. */
  memoryMaxInjectionTokens?: number;
  /** Enable per-turn proactive memory extraction (beyond compaction-time extraction). */
  memoryProactiveExtractEnabled?: boolean;
  /** Minimum tool calls in a turn to trigger proactive memory extraction. */
  memoryProactiveExtractMinToolCalls?: number;
  /** Namespace to append proactive extracted facts into. */
  memoryProactiveExtractNamespace?: string;
  /** Enable daily append-only memory logs of user/assistant turns. */
  memoryDailyLogEnabled?: boolean;
  /** Namespace prefix for daily logs (full namespace: <prefix>/YYYY-MM-DD). */
  memoryDailyLogNamespacePrefix?: string;
  /** Include session/channel/sender metadata in each daily log block. */
  memoryDailyLogIncludeSessionMetadata?: boolean;
  /** Max chars stored from the user message in daily logs. */
  memoryDailyLogMaxUserChars?: number;
  /** Max chars stored from the assistant response in daily logs. */
  memoryDailyLogMaxAssistantChars?: number;
  /** Automatically retry failed primary runs on a higher tier. */
  autoEscalate?: boolean;
  /** Tier to try for auto-escalation retries. Defaults to complex. */
  autoEscalateTier?: ModelTier;
  /** Policy context for tool filtering (agent tier, provider). */
  toolPolicyContext?: ToolPolicyContext;
  /** Collector for outbound attachments queued by tools (e.g. media.send). */
  attachmentCollector?: OutboundAttachmentCollector;
}

// ── AgentOrchestrator ─────────────────────────────────────────────────

/**
 * Wraps a primary NativeAgent and adds the ability to delegate
 * single-turn sub-tasks to different model tiers via the ModelRouter.
 *
 * The primary agent handles the main conversation loop (with tools),
 * while `delegate()` enables cheap, stateless calls for tasks like
 * compaction, classification, and memory extraction.
 */
export class AgentOrchestrator {
  private _agent: NativeAgent;
  private _modelRouter: ModelRouter;
  private _delegation: DelegationConfig;
  private _backgroundModelOverrides: Partial<Record<keyof DelegationConfig, {
    client: ModelClient;
    label: string;
    fallbackTier: ModelTier;
  }>>;
  private _maxDelegationDepth: number;
  private _toolRegistry?: ToolRegistry;
  private _session?: Session;
  private _compactionConfig?: CompactionConfig;
  private _modelName?: string;
  private _contextWindow?: number;
  private _memoryStore?: MemoryStore;
  private _memoryAutoExtract: boolean;
  private _memoryInjectionStrategy: 'all' | 'recent' | 'adaptive';
  private _memoryMaxInjectionTokens: number;
  private _memoryProactiveExtractEnabled: boolean;
  private _memoryProactiveExtractMinToolCalls: number;
  private _memoryProactiveExtractNamespace: string;
  private _memoryDailyLogEnabled: boolean;
  private _memoryDailyLogNamespacePrefix: string;
  private _memoryDailyLogIncludeSessionMetadata: boolean;
  private _memoryDailyLogMaxUserChars: number;
  private _memoryDailyLogMaxAssistantChars: number;
  private _autoEscalate: boolean;
  private _autoEscalateTier: ModelTier;
  private _systemPromptBase: string;
  private _externalOnToolUse?: (event: ToolUseEvent) => void;
  private _activeRunToolStarts = 0;
  private _usageByTier: Map<string, TierUsageStats> = new Map();
  private _lastContextAlertLevel: ContextAlertLevel | null = null;
  private _pendingContextAlert?: ContextAlert;
  private _lastCheckpointAt = 0;

  constructor(config: OrchestratorConfig) {
    this._modelRouter = config.modelRouter;
    this._delegation = config.delegation;
    this._backgroundModelOverrides = config.backgroundModelOverrides ?? {};
    this._maxDelegationDepth = config.maxDelegationDepth;
    this._toolRegistry = config.toolRegistry;
    this._session = config.session;
    this._compactionConfig = config.compaction;
    this._modelName = config.modelName;
    this._contextWindow = config.contextWindow;
    this._memoryStore = config.memoryStore;
    this._memoryAutoExtract = config.memoryAutoExtract ?? true;
    this._memoryInjectionStrategy = config.memoryInjectionStrategy ?? 'all';
    this._memoryMaxInjectionTokens = config.memoryMaxInjectionTokens ?? 2000;
    this._memoryProactiveExtractEnabled = config.memoryProactiveExtractEnabled ?? false;
    this._memoryProactiveExtractMinToolCalls = Math.max(0, config.memoryProactiveExtractMinToolCalls ?? 1);
    this._memoryProactiveExtractNamespace = config.memoryProactiveExtractNamespace ?? 'global';
    this._memoryDailyLogEnabled = config.memoryDailyLogEnabled ?? false;
    this._memoryDailyLogNamespacePrefix = config.memoryDailyLogNamespacePrefix ?? 'daily';
    this._memoryDailyLogIncludeSessionMetadata = config.memoryDailyLogIncludeSessionMetadata ?? true;
    this._memoryDailyLogMaxUserChars = Math.max(100, config.memoryDailyLogMaxUserChars ?? 2000);
    this._memoryDailyLogMaxAssistantChars = Math.max(100, config.memoryDailyLogMaxAssistantChars ?? 4000);
    this._autoEscalate = config.autoEscalate ?? false;
    this._autoEscalateTier = config.autoEscalateTier ?? 'complex';
    this._systemPromptBase = config.systemPrompt;
    this._externalOnToolUse = config.onToolUse;

    // Create the primary NativeAgent for user-facing conversation
    this._agent = new NativeAgent({
      modelClient: config.modelRouter,
      systemPrompt: config.systemPrompt,
      session: config.session,
      toolRegistry: config.toolRegistry,
      toolExecutor: config.toolExecutor,
      maxIterations: config.maxIterations,
      onToolUse: (event) => this._handleToolUse(event),
      toolPolicyContext: config.toolPolicyContext,
      attachmentCollector: config.attachmentCollector,
    });

    // Set the primary tier on the agent
    this._agent.setModelTier(config.primaryTier);
  }

  // ── Delegation ────────────────────────────────────────────────────

  /**
   * Perform a single-turn, stateless call to a model at the specified tier.
   *
   * This is used for internal sub-tasks (compaction, classification, etc.)
   * that don't need the full conversation history or tool loop.
   *
   * If the requested tier is not available on the router, falls back to
   * the 'default' tier with a warning.
   */
  async delegate(request: SubAgentRequest): Promise<SubAgentResult> {
    let tier = request.tier;

    // Check if the requested tier is available; fall back to 'default' if not
    const client = this._modelRouter.getClient(tier);
    if (!client) {
      console.warn(
        `[Flynn:delegate] Tier '${tier}' not available, falling back to 'default'`,
      );
      tier = 'default';
    }

    // Build the single-turn chat request
    const messages: Message[] = [
      { role: 'user', content: request.message },
    ];

    const chatRequest: ChatRequest = {
      messages,
      system: request.systemPrompt,
      maxTokens: request.maxTokens,
      responseFormat: request.responseFormat,
    };

    // Optionally include tools from the registry (filtered by policy)
    if (request.tools && this._toolRegistry) {
      const policyContext = this._agent.getToolPolicyContext();
      chatRequest.tools = this._toolRegistry.filteredToAnthropicFormat(policyContext);
    }

    const override = request.task ? this._backgroundModelOverrides[request.task] : undefined;
    if (override) {
      try {
        const response = await override.client.chat(chatRequest);
        this._trackUsage(tier, response.usage);
        console.log(
          `[Flynn:delegate] task=${request.task} provider_model=${override.label} fallback_tier=${override.fallbackTier} ` +
          `tokens=${response.usage.inputTokens}+${response.usage.outputTokens}`,
        );
        return {
          content: response.content,
          usage: response.usage,
          tier,
        };
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        console.warn(
          `[Flynn:delegate] task=${request.task} provider/model override failed (${override.label}): ${message}; ` +
          `falling back to tier='${override.fallbackTier}'`,
        );
        tier = override.fallbackTier;
      }
    }

    const response = await this._modelRouter.chat(chatRequest, tier);

    // Track cumulative usage for this tier
    this._trackUsage(tier, response.usage);

    console.log(
      `[Flynn:delegate] tier=${tier} tokens=${response.usage.inputTokens}+${response.usage.outputTokens}`,
    );

    return {
      content: response.content,
      usage: response.usage,
      tier,
    };
  }

  // ── Primary agent proxies ─────────────────────────────────────────

  /**
   * Process a user message through the primary NativeAgent.
   * This is the main entry point for user-facing conversation.
   *
   * When compaction is configured, checks whether the conversation history
   * exceeds the context window threshold and compacts it before processing.
   */
  async process(
    userMessage: string,
    attachments?: Attachment[],
    turnAudioInput?: NativeAgentTurnAudioInput,
  ): Promise<string> {
    this._activeRunToolStarts = 0;
    this._injectMemoryContext(userMessage);
    await this._runProactiveContextMaintenance();
    await this.compactIfNeeded();

    // Snapshot history so we can rollback if the underlying tool loop returns an error message.
    // This avoids persisting low-level provider errors to the user-visible conversation state.
    const before = this.getHistory();
    const originalTier = this._agent.getModelTier();

    let result: string;
    try {
      result = await this._agent.process(userMessage, attachments, turnAudioInput);
    } catch {
      this._restoreHistory(before);
      const escalated = await this._retryWithEscalation(userMessage, attachments, turnAudioInput, before, originalTier);
      if (escalated) {
        await this._runPostTurnMemoryMaintenance(userMessage, escalated, this._activeRunToolStarts);
        return escalated;
      }
      const friendly =
        [
          'I ran into a model/provider error while processing that message.',
          '',
          'Try again. If it keeps happening:',
          '1. Run `/compact` or `/reset` to shrink the conversation context.',
          '2. Switch to a different model tier (e.g. `/model local`).',
        ].join('\n');
      this._appendUserAndAssistant(userMessage, attachments, friendly);
      return friendly;
    }

    // NativeAgent currently converts tool-loop exceptions into a user-visible error string.
    // Intercept a few common cases here to self-heal (context overflow) and/or degrade gracefully.
    if (this._isToolLoopErrorMessage(result)) {
      // Roll back the user message + error message inserted by the agent.
      this._restoreHistory(before);

      const underlying = this._stripToolLoopErrorPrefix(result);
      const ctx = this._extractContextWindowFromError(underlying);
      if (ctx) {
        // Attempt: compact + hard-trim to fit the discovered context window, then retry once.
        await this._compactAndTrimToFit(ctx);
        const retry = await this._agent.process(userMessage, attachments, turnAudioInput);
        if (!this._isToolLoopErrorMessage(retry)) {
          return retry;
        }
        // If we still failed, roll back again so we don't persist the error string.
        this._restoreHistory(before);
      }

      const escalated = await this._retryWithEscalation(userMessage, attachments, turnAudioInput, before, originalTier);
      if (escalated) {
        await this._runPostTurnMemoryMaintenance(userMessage, escalated, this._activeRunToolStarts);
        return escalated;
      }

      // Persist a short, user-friendly failure message (without provider internals).
      const friendly =
        [
          'I ran into a model/provider error while processing that message.',
          '',
          'Try again. If it keeps happening:',
          '1. Run `/compact` or `/reset` to shrink the conversation context.',
          '2. Switch to a different model tier (e.g. `/model local`).',
        ].join('\n');

      // Re-add the user message so the conversation state matches what the user sent.
      this._appendUserAndAssistant(userMessage, attachments, friendly);
      return friendly;
    }

    await this._runPostTurnMemoryMaintenance(userMessage, result, this._activeRunToolStarts);
    return result;
  }

  private async _retryWithEscalation(
    userMessage: string,
    attachments: Attachment[] | undefined,
    turnAudioInput: NativeAgentTurnAudioInput | undefined,
    historyBefore: Message[],
    originalTier: ModelTier,
  ): Promise<string | null> {
    if (!this._autoEscalate) {
      return null;
    }

    let targetTier = this._autoEscalateTier;
    if (targetTier === originalTier) {
      targetTier = 'complex';
    }

    if (targetTier === originalTier || !this._modelRouter.getClient(targetTier)) {
      return null;
    }

    this._agent.setModelTier(targetTier);
    try {
      const retry = await this._agent.process(userMessage, attachments, turnAudioInput);
      if (!this._isToolLoopErrorMessage(retry)) {
        return retry;
      }
      this._restoreHistory(historyBefore);
      return null;
    } finally {
      this._agent.setModelTier(originalTier);
    }
  }

  /**
   * Force-compact the current conversation history regardless of threshold.
   * Returns the compaction result, or null if there was nothing to compact
   * (e.g. no session, too few messages).
   */
  async compact(): Promise<CompactionResult | null> {
    const config = this._compactionConfig ?? DEFAULT_COMPACTION_CONFIG;
    const messages = this.getHistory();

    if (messages.length === 0) {
      return null;
    }

    const result = await compactHistory({
      messages,
      orchestrator: this,
      config,
      memoryStore: this._memoryStore,
      autoExtract: this._memoryAutoExtract,
    });

    // If nothing was actually compacted, skip the replace
    if (result.compactedCount === 0) {
      return result;
    }

    // Persist the compacted history
    if (this._session) {
      this._session.replaceHistory(result.messages);
    }

    console.log(
      `[Flynn:compact] Compacted ${result.compactedCount} messages: ` +
      `${result.tokensBefore} → ${result.tokensAfter} tokens`,
    );

    if (this._session) {
      auditLogger?.sessionCompact({
        session_id: this._session.id,
        messages_before: messages.length,
        messages_after: result.messages.length,
        tokens_before: result.tokensBefore,
        tokens_after: result.tokensAfter,
      });
    }

    return result;
  }

  /** Reset the primary agent's conversation history and usage stats. */
  reset(): void {
    this._agent.reset();
    this._usageByTier.clear();
    this._lastContextAlertLevel = null;
    this._pendingContextAlert = undefined;
    this._lastCheckpointAt = 0;
  }

  /** Get the primary agent's conversation history. */
  getHistory(): Message[] {
    return this._agent.getHistory();
  }

  /** Set the model tier on the primary agent. */
  setModelTier(tier: ModelTier): void {
    this._agent.setModelTier(tier);
  }

  /** Get the current model tier of the primary agent. */
  getModelTier(): ModelTier {
    return this._agent.getModelTier();
  }

  /** Set the tool-use callback on the primary agent. */
  setOnToolUse(callback: ((event: ToolUseEvent) => void) | undefined): void {
    this._externalOnToolUse = callback;
  }

  /**
   * Get the effective system prompt for the current turn.
   * When a user message is provided, applies memory-context injection first
   * so external backends can receive the same prompt context as native runs.
   */
  getSystemPrompt(userMessage?: string): string {
    if (typeof userMessage === 'string') {
      this._injectMemoryContext(userMessage);
    }
    return this._agent.getSystemPrompt();
  }

  /** Request cancellation for the current primary-agent operation. */
  cancel(): void {
    this._agent.cancel();
  }

  /** Whether the primary agent currently has an in-flight operation. */
  isCancellable(): boolean {
    return this._agent.isCancellable();
  }

  // ── Usage & config accessors ──────────────────────────────────────

  /**
   * Returns cumulative delegation usage stats per tier.
   * Useful for cost tracking and visibility into sub-agent calls.
   */
  getDelegationUsage(): Record<string, TierUsageStats> {
    const result: Record<string, TierUsageStats> = {};
    for (const [tier, stats] of this._usageByTier) {
      result[tier] = { ...stats };
    }
    return result;
  }

  /**
   * Returns comprehensive usage stats combining primary agent and delegation usage.
   * Includes estimated cost based on the primary model's pricing.
   */
  getUsage(): UsageReport {
    const primary = this._agent.getUsage();
    const delegation = this.getDelegationUsage();

    let totalInput = primary.inputTokens;
    let totalOutput = primary.outputTokens;
    let totalCalls = primary.calls;

    for (const stats of Object.values(delegation)) {
      totalInput += stats.inputTokens;
      totalOutput += stats.outputTokens;
      totalCalls += stats.calls;
    }

    return {
      primary,
      delegation,
      total: {
        inputTokens: totalInput,
        outputTokens: totalOutput,
        calls: totalCalls,
        estimatedCost: estimateCost(totalInput, totalOutput, this._modelName),
      },
    };
  }

  /**
   * Returns current context usage budget for this session.
   * Uses estimator-based history token counting and configured context window.
   */
  getContextBudget(): ContextBudget {
    const messages = this.getHistory();
    const model = this._modelName ?? 'unknown';
    const thresholdPct = this._compactionConfig?.thresholdPct ?? DEFAULT_COMPACTION_CONFIG.thresholdPct;
    const window = getContextWindow(model, this._contextWindow);
    const estimated = estimateMessageTokens(messages);
    const thresholdTokens = Math.floor((thresholdPct / 100) * window);
    const usagePct = window > 0 ? Math.min(100, (estimated / window) * 100) : 0;
    return {
      estimatedTokens: estimated,
      contextWindow: window,
      remainingTokens: Math.max(0, window - estimated),
      usagePct,
      thresholdPct,
      thresholdTokens,
      shouldCompact: estimated > thresholdTokens,
    };
  }

  /** Returns and clears the pending proactive context alert, if any. */
  consumeContextAlert(): ContextAlert | undefined {
    const alert = this._pendingContextAlert;
    this._pendingContextAlert = undefined;
    return alert;
  }

  /**
   * Look up which model tier is configured for a given delegation task.
   * Convenience method so callers don't need to access the config directly.
   */
  getDelegationTier(task: keyof DelegationConfig): ModelTier {
    return this._delegation[task];
  }

  // ── Private helpers ───────────────────────────────────────────────

  /**
   * Inject persistent memory context into the primary agent's system prompt.
   * Reads from the memory store and appends relevant context to the base
   * system prompt. If no memory store is configured or no memory content
   * exists, restores the original base prompt.
   */
  private _injectMemoryContext(userMessage: string): void {
    if (!this._memoryStore) {
      return;
    }

    let memoryContext = '';
    try {
      if (this._memoryInjectionStrategy === 'recent') {
        memoryContext = buildRecentMemoryContext(this._memoryStore, this._memoryMaxInjectionTokens);
      } else if (this._memoryInjectionStrategy === 'adaptive') {
        memoryContext = buildAdaptiveMemoryContext({
          store: this._memoryStore,
          userMessage,
          recentMessages: this.getHistory(),
          config: {
            maxTokens: this._memoryMaxInjectionTokens,
          },
        });
      } else {
        memoryContext = this._memoryStore.getContextForPrompt();
      }
    } catch (error) {
      console.warn('[Flynn:memory] Adaptive memory injection failed, falling back to default context:', error);
      memoryContext = this._memoryStore.getContextForPrompt();
    }

    memoryContext = this._clipMemoryContext(memoryContext);

    if (!memoryContext) {
      this._agent.setSystemPrompt(this._systemPromptBase);
      return;
    }

    const enrichedPrompt = `${this._systemPromptBase}\n\n# Memory Context\n\nThe following is your persistent memory. Use it to maintain continuity across sessions.\n\n${memoryContext}`;
    this._agent.setSystemPrompt(enrichedPrompt);
  }

  private _clipMemoryContext(context: string): string {
    if (!context) {
      return context;
    }
    const maxChars = this._memoryMaxInjectionTokens * 4;
    if (context.length <= maxChars) {
      return context;
    }
    return context.slice(0, maxChars);
  }

  private _handleToolUse(event: ToolUseEvent): void {
    if (event.type === 'start') {
      this._activeRunToolStarts += 1;
    }
    this._externalOnToolUse?.(event);
  }

  private async _runPostTurnMemoryMaintenance(userMessage: string, assistantText: string, toolCallsInRun: number): Promise<void> {
    if (!this._memoryStore) {
      return;
    }

    if (this._memoryDailyLogEnabled) {
      this._appendDailyLog(userMessage, assistantText, toolCallsInRun);
    }

    if (!this._memoryProactiveExtractEnabled || toolCallsInRun < this._memoryProactiveExtractMinToolCalls) {
      return;
    }

    try {
      const extractionTier = this.getDelegationTier('memory_extraction');
      const extraction = await this.delegate({
        task: 'memory_extraction',
        tier: extractionTier,
        systemPrompt: MEMORY_EXTRACTION_PROMPT,
        message: this._buildExtractionInput(userMessage, assistantText, toolCallsInRun),
        maxTokens: 512,
      });
      const extractedContent = extraction.content.trim();
      if (!extractedContent || extractedContent.toLowerCase().includes('no facts')) {
        return;
      }
      this._memoryStore.write(this._memoryProactiveExtractNamespace, extractedContent, 'append');
    } catch (error) {
      console.warn('[Flynn:memory] Proactive per-turn extraction failed:', error);
    }
  }

  private _appendDailyLog(userMessage: string, assistantText: string, toolCallsInRun: number): void {
    if (!this._memoryStore) {
      return;
    }
    const date = new Date().toISOString().slice(0, 10);
    const timestamp = new Date().toISOString();
    const namespace = `${this._memoryDailyLogNamespacePrefix}/${date}`;
    const user = this._truncateForMemory(userMessage, this._memoryDailyLogMaxUserChars);
    const assistant = this._truncateForMemory(assistantText, this._memoryDailyLogMaxAssistantChars);
    const metadataLines = this._memoryDailyLogIncludeSessionMetadata
      ? [
        `- session: ${this._session?.id ?? 'unknown'}`,
        `- channel: ${this._agent.getToolPolicyContext()?.channel ?? 'unknown'}`,
        `- sender: ${this._agent.getToolPolicyContext()?.sender ?? 'unknown'}`,
      ]
      : [];
    const block = [
      `## ${timestamp}`,
      '',
      `- tool_calls: ${toolCallsInRun}`,
      ...metadataLines,
      '',
      '### User',
      user,
      '',
      '### Assistant',
      assistant,
      '',
    ].join('\n');
    this._memoryStore.write(namespace, block, 'append');
  }

  private _buildExtractionInput(userMessage: string, assistantText: string, toolCallsInRun: number): string {
    const user = this._truncateForMemory(userMessage, 4000);
    const assistant = this._truncateForMemory(assistantText, 6000);
    return [
      `Tool calls in this turn: ${toolCallsInRun}`,
      '',
      'User message:',
      user,
      '',
      'Assistant response:',
      assistant,
    ].join('\n');
  }

  private _truncateForMemory(text: string, maxChars: number): string {
    if (text.length <= maxChars) {
      return text;
    }
    return `${text.slice(0, maxChars)}...[truncated]`;
  }

  /**
   * Check whether automatic compaction should run, and if so, compact.
   * Called before each `process()` call when compaction is configured.
   */
  private async compactIfNeeded(): Promise<void> {
    if (!this._compactionConfig) {return;}

    const messages = this.getHistory();
    if (messages.length === 0) {return;}

    const model = this._modelName ?? 'unknown';
    const needs = shouldCompact({
      messages,
      model,
      contextWindow: this._contextWindow,
      thresholdPct: this._compactionConfig.thresholdPct,
    });

    if (!needs) {return;}

    await this.compact();
  }

  private async _runProactiveContextMaintenance(): Promise<void> {
    const proactive = this._compactionConfig?.proactive;
    if (!proactive?.enabled) {
      return;
    }

    let budget = this.getContextBudget();
    let level = this._resolveContextAlertLevel(budget.usagePct);
    const levelBeforeMaintenance = level;
    let checkpointSaved = false;
    let autoCompacted = false;
    let checkpointNamespace: string | undefined;

    if (level !== null && this._shouldCheckpoint(level)) {
      const checkpointResult = await this._writeProactiveCheckpoint();
      checkpointSaved = checkpointResult.saved;
      checkpointNamespace = checkpointResult.namespace;
    }

    if (level === 'critical') {
      const beforeAuto = budget;
      try {
        const result = await this.compact();
        autoCompacted = Boolean(result && result.compactedCount > 0);
      } catch (error) {
        console.warn('[Flynn:compact] Proactive auto-compaction failed:', error);
      }
      budget = this.getContextBudget();
      if (this._session) {
        auditLogger?.sessionAutoCompact({
          session_id: this._session.id,
          usage_pct_before: beforeAuto.usagePct,
          usage_pct_after: budget.usagePct,
          compacted: autoCompacted,
          tokens_before: beforeAuto.estimatedTokens,
          tokens_after: budget.estimatedTokens,
        });
      }
      level = this._resolveContextAlertLevel(budget.usagePct);
    }

    if (!level && (autoCompacted || checkpointSaved)) {
      level = levelBeforeMaintenance ?? 'warning';
    }

    if (!level) {
      this._lastContextAlertLevel = null;
      return;
    }

    const shouldEmit = (
      this._lastContextAlertLevel !== level ||
      checkpointSaved ||
      autoCompacted
    );
    this._lastContextAlertLevel = level;
    if (!shouldEmit) {
      return;
    }

    const pct = budget.usagePct.toFixed(1);
    const message = [
      `Context usage is ${pct}% (${budget.estimatedTokens.toLocaleString()}/${budget.contextWindow.toLocaleString()} estimated tokens).`,
      checkpointSaved && checkpointNamespace ? `Checkpoint saved to memory namespace \`${checkpointNamespace}\`.` : '',
      autoCompacted ? 'Auto-compaction ran proactively to preserve headroom.' : '',
    ].filter(Boolean).join(' ');

    this._pendingContextAlert = {
      level,
      message,
      budget,
      actions: {
        checkpointSaved,
        autoCompacted,
        checkpointNamespace,
      },
    };
  }

  private _resolveContextAlertLevel(usagePct: number): ContextAlertLevel | null {
    const proactive = this._compactionConfig?.proactive ?? DEFAULT_COMPACTION_CONFIG.proactive;
    if (!proactive) {
      return null;
    }
    const warnPct = proactive.warnPct;
    const checkpointPct = Math.max(proactive.checkpointPct, warnPct);
    const autoCompactPct = Math.max(proactive.autoCompactPct, checkpointPct);

    if (usagePct >= autoCompactPct) {
      return 'critical';
    }
    if (usagePct >= checkpointPct) {
      return 'checkpoint';
    }
    if (usagePct >= warnPct) {
      return 'warning';
    }
    return null;
  }

  private _shouldCheckpoint(level: ContextAlertLevel): boolean {
    if (level === 'warning') {
      return false;
    }
    const proactive = this._compactionConfig?.proactive ?? DEFAULT_COMPACTION_CONFIG.proactive;
    if (!proactive) {
      return false;
    }
    const now = Date.now();
    if (now - this._lastCheckpointAt < proactive.checkpointCooldownMs) {
      return false;
    }
    return true;
  }

  private async _writeProactiveCheckpoint(): Promise<{ saved: boolean; namespace?: string }> {
    if (!this._memoryStore || !this._session) {
      return { saved: false };
    }

    const messages = this.getHistory();
    if (messages.length === 0) {
      return { saved: false };
    }

    const conversation = this._formatCheckpointConversation(messages, 20_000);
    if (!conversation.trim()) {
      return { saved: false };
    }

    try {
      const tier = this.getDelegationTier('compaction');
      const result = await this.delegate({
        task: 'compaction',
        tier,
        systemPrompt: CONTEXT_CHECKPOINT_PROMPT,
        message: conversation,
        maxTokens: Math.min(this._compactionConfig?.summaryMaxTokens ?? 512, 1024),
      });
      const summary = result.content.trim();
      if (!summary) {
        return { saved: false };
      }

      const namespaceBase = this._compactionConfig?.proactive?.memoryNamespace
        ?? DEFAULT_COMPACTION_CONFIG.proactive?.memoryNamespace
        ?? 'session/checkpoints';
      const namespace = `${namespaceBase}/${this._sanitizeSessionId(this._session.id)}`;
      const block = `## ${new Date().toISOString()}\n\n${summary}\n\n`;
      this._memoryStore.write(namespace, block, 'append');
      auditLogger?.sessionCheckpoint({
        session_id: this._session.id,
        namespace,
        chars_written: block.length,
        usage_pct: this.getContextBudget().usagePct,
      });
      this._lastCheckpointAt = Date.now();
      return { saved: true, namespace };
    } catch (error) {
      console.warn('[Flynn:memory] Proactive checkpoint write failed:', error);
      return { saved: false };
    }
  }

  private _formatCheckpointConversation(messages: Message[], maxChars: number): string {
    const text = messages
      .map((msg) => `${msg.role}: ${typeof msg.content === 'string' ? msg.content : '[multimodal content]'}`)
      .join('\n\n');
    if (text.length <= maxChars) {
      return text;
    }
    return text.slice(text.length - maxChars);
  }

  private _sanitizeSessionId(sessionId: string): string {
    return sessionId
      .replace(/:/g, '/')
      .replace(/[^a-zA-Z0-9/_-]+/g, '_')
      .replace(/_+/g, '_');
  }

  private _isToolLoopErrorMessage(text: string): boolean {
    return text.startsWith('Error in tool loop (iteration ');
  }

  private _stripToolLoopErrorPrefix(text: string): string {
    const m = text.match(/^Error in tool loop \(iteration \d+\):\s*(.*)$/s);
    return m ? m[1] : text;
  }

  private _restoreHistory(messages: Message[]): void {
    if (this._session) {
      this._session.replaceHistory(messages);

    }
    // No session available; nothing safe to do here.
  }

  private _appendUserAndAssistant(userMessage: string, attachments: Attachment[] | undefined, assistantText: string): void {
    if (!this._session) {
      return;
    }
    const userMsg = buildUserMessage(userMessage, attachments);
    this._session.addMessage(userMsg);
    this._session.addMessage({ role: 'assistant', content: assistantText });
  }

  private _extractContextWindowFromError(errorText: string): number | undefined {
    // Try a few common patterns and pick the smallest plausible context window.
    // Example llama.cpp error:
    //   exceeds the available context size (4096 tokens) ... "nctx":4096
    const candidates: number[] = [];
    const jsonNctx = errorText.match(/"nctx"\s*:\s*(\d{3,7})/);
    if (jsonNctx) { candidates.push(Number(jsonNctx[1])); }
    const paren = errorText.match(/context size \((\d{3,7}) tokens\)/);
    if (paren) { candidates.push(Number(paren[1])); }
    const maxContext = errorText.match(/maximum context length is (\d{3,7}) tokens/i);
    if (maxContext) { candidates.push(Number(maxContext[1])); }

    const valid = candidates.filter(n => Number.isFinite(n) && n >= 256);
    if (valid.length === 0) {
      return undefined;
    }
    return Math.min(...valid);
  }

  private async _compactAndTrimToFit(contextWindow: number): Promise<void> {
    // Compaction is best-effort; if it fails (e.g., providers down), fall back to a hard trim.
    try {
      await this.compact();
    } catch (error) {
      console.warn('[Flynn:compact] Emergency compaction failed:', error);
    }

    if (!this._session) {
      return;
    }

    const threshold = Math.floor((this._compactionConfig?.thresholdPct ?? 80) / 100 * contextWindow);
    let messages = this.getHistory();
    let estimated = estimateMessageTokens(messages);

    // Drop oldest messages until we're under budget.
    // This is intentionally blunt; it only triggers after a real provider context overflow.
    while (messages.length > 1 && estimated > threshold) {
      messages = messages.slice(1);
      estimated = estimateMessageTokens(messages);
    }

    this._session.replaceHistory(messages);
  }

  /** Accumulate usage stats for a given tier. */
  private _trackUsage(tier: ModelTier, usage: TokenUsage): void {
    const existing = this._usageByTier.get(tier);
    if (existing) {
      existing.inputTokens += usage.inputTokens;
      existing.outputTokens += usage.outputTokens;
      existing.calls += 1;
    } else {
      this._usageByTier.set(tier, {
        inputTokens: usage.inputTokens,
        outputTokens: usage.outputTokens,
        calls: 1,
      });
    }
  }
}
