/**
 * Memory Manager
 * Encapsulates all memory operations with ultra-simple agent API
 * Agent provides strings, framework handles wrapping and auto-compaction
 */

import type { AgentMemory, MemoryEntry, MemoryStatus, MemoryConstraints } from './types'
import { estimateTokens } from '../../../../platform/utils'
import type { AgentScopedLogger } from '../observability/logging'
import { AgentMemoryValidationError } from '../errors'
import {
  MAX_SESSION_MEMORY_KEYS,
  MAX_MEMORY_TOKENS,
  MAX_SINGLE_ENTRY_TOKENS,
  MAX_TOOL_RESULT_TOKENS
} from '../../../../platform/constants/limits'

/**
 * Truncate tool result content if it exceeds the token limit.
 * Uses character-based estimation (1 token ~ 3.5 chars, matching estimateTokens).
 * Appends a notice so the agent knows data was cut.
 *
 * @param content - Raw tool result string
 * @param maxTokens - Maximum allowed tokens
 * @returns Original content if under limit, truncated content with notice otherwise
 */
export function truncateToolResult(content: string, maxTokens: number): string {
  const estimated = estimateTokens(content)
  if (estimated <= maxTokens) return content

  // Use same ratio as estimateTokens (3.5 chars per token)
  const maxChars = Math.floor(maxTokens * 3.5)
  const truncated = content.slice(0, maxChars)
  const omitted = estimated - maxTokens
  return (
    truncated +
    `\n\n[Response truncated — estimated ${omitted} tokens omitted. Use more specific filters to get smaller results.]`
  )
}

/**
 * Memory Manager - Agent memory orchestration
 * Provides ultra-simple API for agents (strings only)
 * Handles automatic compaction and token management
 */
export class MemoryManager {
  private cachedSnapshot?: AgentMemory

  constructor(
    private memory: AgentMemory,
    private constraints: MemoryConstraints = {},
    private logger?: AgentScopedLogger
  ) {}

  // === Agent Operations (Ultra-Simple) ===

  /**
   * Set session memory entry (agent provides string, framework wraps it)
   * @param key - Session memory key
   * @param content - String content from agent
   */
  set(key: string, content: string): void {
    // Check single entry token limit
    const entryTokens = estimateTokens(content)
    if (entryTokens > MAX_SINGLE_ENTRY_TOKENS) {
      const truncateTime = Date.now()
      this.logger?.action(
        'memory-truncate',
        `Single entry exceeds token limit (${entryTokens}/${MAX_SINGLE_ENTRY_TOKENS}): ${key}`,
        0,
        truncateTime,
        truncateTime,
        0
      )
      // Truncate content to fit limit
      const maxChars = MAX_SINGLE_ENTRY_TOKENS * 4
      content = content.slice(0, maxChars) + '... [truncated]'
    }

    this.memory.sessionMemory[key] = {
      type: 'context',
      content,
      timestamp: Date.now(),
      turnNumber: null, // Session memory entries are not turn-specific
      iterationNumber: null // Session memory entries are not iteration-specific
    }
  }

  /**
   * Get session memory entry content
   * @param key - Session memory key
   * @returns String content if exists, undefined otherwise
   */
  get(key: string): string | undefined {
    const entry = this.memory.sessionMemory[key]
    return entry?.content
  }

  /**
   * Delete session memory entry
   * @param key - Key to delete
   * @returns True if key existed and was deleted
   */
  delete(key: string): boolean {
    if (key in this.memory.sessionMemory) {
      delete this.memory.sessionMemory[key]
      return true
    }
    return false
  }

  // === Framework Operations (Automatic) ===

  /**
   * Add entry to history (called by framework after tool results, reasoning, etc.)
   * Automatically sets timestamp to current time
   * @param entry - Memory entry to add (without timestamp - auto-generated)
   */
  addToHistory(entry: Omit<MemoryEntry, 'timestamp'>): void {
    // Validate turnNumber is provided for history entries (use null for session memory)
    if (entry.turnNumber === undefined && entry.type !== 'context') {
      throw new AgentMemoryValidationError('turnNumber required for history entries (use null for session memory)', {
        entryType: entry.type,
        missingField: 'turnNumber',
        iterationNumber: entry.iterationNumber
      })
    }

    // Truncate tool-result content if it exceeds the tool result token limit
    let content = entry.content
    if (entry.type === 'tool-result') {
      const before = content
      content = truncateToolResult(content, MAX_TOOL_RESULT_TOKENS)
      if (content !== before) {
        const truncateTime = Date.now()
        this.logger?.action(
          'memory-tool-result-truncate',
          `Tool result truncated (${estimateTokens(before)} -> ${MAX_TOOL_RESULT_TOKENS} tokens)`,
          entry.iterationNumber ?? 0,
          truncateTime,
          truncateTime,
          0
        )
      }
    }

    this.memory.history.push({
      ...entry,
      content,
      timestamp: Date.now()
    })
    // Auto-compact after adding (if needed)
    this.autoCompact()
  }

  /**
   * Auto-compact history if approaching token budget
   * Uses preserve-anchors strategy: keep first + recent entries
   */
  autoCompact(): void {
    const status = this.getStatus()

    // Auto-compact when at 100% of token budget
    if (status.historyPercent >= 100) {
      const before = this.memory.history.length

      // Preserve-anchors strategy: first entry + last 10 entries
      this.memory.history = [
        this.memory.history[0], // First (original input)
        ...this.memory.history.slice(-10) // Last 10
      ]

      const compactTime = Date.now()
      this.logger?.action(
        'memory-auto-compact',
        `Auto-compacted: ${before} -> ${this.memory.history.length} entries`,
        0,
        compactTime,
        compactTime,
        0
      )
    }
  }

  /**
   * Enforce hard limits (called before LLM request)
   * Emergency fallback if agent exceeds limits
   */
  enforceHardLimits(): void {
    const maxSessionMemoryKeys = this.constraints.maxSessionMemoryKeys || MAX_SESSION_MEMORY_KEYS

    // Check session memory count
    const sessionMemoryKeys = Object.keys(this.memory.sessionMemory)
    if (sessionMemoryKeys.length > maxSessionMemoryKeys) {
      const limitTime = Date.now()
      this.logger?.action(
        'memory-limit-exceeded',
        `Session memory exceeds hard limit (${sessionMemoryKeys.length}/${maxSessionMemoryKeys})`,
        0,
        limitTime,
        limitTime,
        0
      )

      // Remove oldest entries by timestamp
      const sorted = Object.entries(this.memory.sessionMemory).sort((a, b) => a[1].timestamp - b[1].timestamp)

      this.memory.sessionMemory = Object.fromEntries(sorted.slice(-maxSessionMemoryKeys))
    }

    // Check total token budget (emergency compaction)
    const status = this.getStatus()
    const maxTokens = this.constraints.maxMemoryTokens || MAX_MEMORY_TOKENS

    if (status.historyTokens > maxTokens) {
      const before = this.memory.history.length
      const emergencyStartTime = Date.now()
      this.logger?.action(
        'memory-emergency',
        `Total memory exceeds token budget (${status.historyTokens}/${maxTokens}), forcing emergency compaction`,
        0,
        emergencyStartTime,
        emergencyStartTime,
        0
      )

      // Emergency: Aggressively compact history
      this.memory.history = [
        this.memory.history[0],
        ...this.memory.history.slice(-5) // Keep only last 5
      ]

      const emergencyEndTime = Date.now()
      this.logger?.action(
        'memory-emergency-compact',
        `Emergency compaction: ${before} -> ${this.memory.history.length} entries`,
        0,
        emergencyStartTime,
        emergencyEndTime,
        emergencyEndTime - emergencyStartTime
      )
    }
  }

  /**
   * Get history length (for logging and introspection)
   * @returns Number of entries in history
   */
  getHistoryLength(): number {
    return this.memory.history.length
  }

  /**
   * Get memory status for agent awareness
   * @returns Memory status with token usage and key counts
   */
  getStatus(): MemoryStatus {
    const sessionMemoryKeys = Object.keys(this.memory.sessionMemory)

    // Calculate tokens
    const sessionMemoryContent = Object.values(this.memory.sessionMemory)
      .map((entry) => entry.content)
      .join('')
    const historyContent = this.memory.history.map((entry) => entry.content).join('')
    const sessionMemoryTokens = estimateTokens(sessionMemoryContent)
    const historyTokens = estimateTokens(historyContent)
    const totalTokens = sessionMemoryTokens + historyTokens

    // Token budget
    const tokenBudget = this.constraints.maxMemoryTokens || MAX_MEMORY_TOKENS
    const sessionMemoryLimit = this.constraints.maxSessionMemoryKeys || MAX_SESSION_MEMORY_KEYS

    return {
      sessionMemoryKeys: sessionMemoryKeys.length,
      sessionMemoryLimit,
      currentKeys: sessionMemoryKeys,
      historyPercent: Math.round((totalTokens / tokenBudget) * 100),
      historyTokens: totalTokens,
      tokenBudget
    }
  }

  /**
   * Create memory snapshot for persistence
   * Caches snapshot internally for later retrieval
   * @returns Deep copy of current memory state
   */
  toSnapshot(): AgentMemory {
    this.cachedSnapshot = structuredClone(this.memory)
    return this.cachedSnapshot
  }

  /**
   * Get cached memory snapshot
   * Returns snapshot created by toSnapshot()
   * @returns Cached snapshot (undefined if toSnapshot() not called yet)
   */
  getSnapshot(): AgentMemory | undefined {
    return this.cachedSnapshot
  }

  /**
   * Build context string for LLM
   * Serializes sessionmemory + history memory with clear sections
   * Shows current iteration entries FIRST (reverse chronological) for LLM attention
   * @param currentIteration - Current iteration number (0 = pre-iteration)
   * @param currentTurn - Current turn number (optional, for session context filtering)
   * @returns Formatted memory context for LLM prompt
   */
  toContext(currentIteration: number, currentTurn?: number): string {
    const status = this.getStatus()

    // Split by turn and iteration number
    // DUAL FILTERING: Turn scope first (if provided), then iteration scope
    // null iterations are session memory entries (excluded from history context)
    // undefined/null turnNumbers are treated as legacy entries (pre-migration) or one-off executions
    const currentContext = this.memory.history
      .filter(
        (entry) =>
          (!currentTurn || entry.turnNumber === currentTurn || entry.turnNumber === undefined) &&
          entry.iterationNumber === currentIteration
      )
      .reverse() // Most recent first (LLM positional bias)

    const earlierContext = this.memory.history.filter(
      (entry) =>
        (!currentTurn || entry.turnNumber === currentTurn || entry.turnNumber === undefined) &&
        entry.iterationNumber !== null &&
        entry.iterationNumber < currentIteration
    )
    // Earlier entries stay chronological

    // Format entry with simple label
    const formatEntry = (entry: MemoryEntry): string => {
      const label = `[${entry.type.toUpperCase()}]`
      return `${label}\n${entry.content}`
    }

    // Serialize session memory
    const sessionMemoryContext = Object.entries(this.memory.sessionMemory)
      .map(([key, entry]) => `[SESSION:${key}]\n${entry.content}`)
      .join('\n\n')

    const currentSection = currentContext.map(formatEntry).join('\n\n')
    const earlierSection =
      earlierContext.length > 0 ? earlierContext.map(formatEntry).join('\n\n') : '(no earlier context)'

    return `
=== MEMORY STATUS ===
${status.sessionMemoryKeys}/${status.sessionMemoryLimit} session keys
${status.historyPercent}% of token budget

=== SESSION MEMORY (Persists for conversation) ===
${sessionMemoryContext || '(empty)'}

=== ITERATION ${currentIteration} - CURRENT CONTEXT ===

${currentSection}

=== EARLIER CONTEXT ===

${earlierSection}
`.trim()
  }
}
