/**
 * Prompt layer types for system prompt assembly and compaction.
 *
 * Each layer in the system prompt has metadata that determines how it behaves
 * under token budget pressure — whether it can be expired or truncated, and
 * at what priority it should be preserved.
 */

// ── Compact strategies ──────────────────────────────────────────────────

/**
 * Determines how a prompt layer can be compacted when under token budget
 * pressure. Layers are compacted in priority order (lowest priority first).
 *
 * - "preserve":  Never compacted — core identity (profile, contract).
 * - "truncate":  Can compact the middle to fit a maxTokens cap.
 * - "expire":    Can be dropped entirely when under pressure.
 */
export type PromptLayerCompactStrategy = "preserve" | "truncate" | "expire";

// ── Prompt layer ────────────────────────────────────────────────────────

/**
 * A single layer in the assembled system prompt.
 *
 * Layers are merged in order to produce the final system prompt string.
 * Each layer carries compact metadata so the engine can make informed
 * decisions when token budgets are tight.
 */
export interface PromptLayer {
  /** Human-readable layer name (e.g. "host-profile", "evidence", "skills"). */
  name: string;

  /** The prompt text for this layer. undefined if expired by compaction. */
  value?: string;

  /** Semantic category (e.g. "profile", "context", "evidence", "repair"). */
  kind: string;

  /**
   * Priority for compaction. 0 = highest priority (never dropped).
   * Higher numbers are dropped/compacted first.
   */
  priority: number;

  /** How this layer can be compacted. */
  compactStrategy: PromptLayerCompactStrategy;

  /** Soft token cap for this layer (used by "truncate" strategy). */
  maxTokens?: number;

  /** Optional semantic compactor for layers where middle clipping loses important recency. */
  compact?: (value: string, maxTokens: number) => string;

  /** Pre-computed or cached token estimate for this layer's value. */
  estimatedTokens?: number;
}

// ── Compaction result ───────────────────────────────────────────────────

export interface PromptLayerCompactionResult {
  /** The layers after compaction (some may have value set to undefined). */
  layers: PromptLayer[];

  /** Total estimated tokens saved by the compaction. */
  tokensSaved: number;

  /** Total estimated tokens before any compaction was applied. */
  originalEstimatedTokens: number;

  /** Total estimated tokens after compaction completes. */
  finalEstimatedTokens: number;

  /** Budget used for the compaction attempt. */
  budgetTokens: number;

  /** Remaining token overflow after all compaction strategies were exhausted. */
  overBudgetTokens: number;
}

// ── Compaction function ─────────────────────────────────────────────────

/**
 * Compact system prompt layers to fit within a token budget.
 *
 * Applies compact strategies from lowest priority up:
 * - "expire" layers are dropped first.
 * - "truncate" layers are cut to their maxTokens.
 * - "preserve" layers are never touched.
 *
 * @param layers  The prompt layers to potentially compact.
 * @param budgetTokens  The remaining token budget.
 * @returns The compacted layers and tokens saved.
 */
export function compactSystemPromptLayers(
  layers: PromptLayer[],
  budgetTokens: number,
): PromptLayerCompactionResult {
  const originalEstimated = layers.reduce(
    (sum, l) => sum + (l.estimatedTokens ?? estimatePromptLayerTokens(l.value)),
    0,
  );

  if (originalEstimated <= budgetTokens) {
    return {
      layers,
      tokensSaved: 0,
      originalEstimatedTokens: originalEstimated,
      finalEstimatedTokens: originalEstimated,
      budgetTokens,
      overBudgetTokens: 0,
    };
  }

  // Work on mutable copies sorted by priority descending (compact lowest priority first)
  const indexed = layers.map((l, i) => ({ ...l, originalIndex: i }));
  const sorted = indexed.sort((a, b) => b.priority - a.priority);

  let totalEstimated = originalEstimated;

  for (const layer of sorted) {
    if (totalEstimated <= budgetTokens) break;

    const currentTokens = layer.estimatedTokens ?? estimatePromptLayerTokens(layer.value);

    if (layer.compactStrategy === "expire") {
      totalEstimated -= currentTokens;
      layer.value = undefined;
      layer.estimatedTokens = 0;
    } else if (layer.compactStrategy === "truncate" && layer.maxTokens !== undefined) {
      const remainingWithoutLayer = totalEstimated - currentTokens;
      const budgetForLayer = Math.max(0, budgetTokens - remainingWithoutLayer);
      const target = Math.min(Math.max(0, layer.maxTokens), currentTokens, budgetForLayer);
      const saved = currentTokens - target;
      if (saved > 0 && layer.value) {
        // Rough character-based truncation (~4 chars per token)
        const maxChars = target * 4;
        if (layer.value.length > maxChars) {
          const rawCompacted = layer.compact
            ? layer.compact(layer.value, target)
            : compactPromptLayerText(layer.value, maxChars);
          const compacted =
            estimatePromptLayerTokens(rawCompacted) <= target
              ? rawCompacted
              : compactPromptLayerText(rawCompacted, maxChars);
          const nextTokens = estimatePromptLayerTokens(compacted);
          layer.value = compacted;
          layer.estimatedTokens = nextTokens;
          totalEstimated -= currentTokens - nextTokens;
        }
      }
    }
    // "preserve" — never touched
  }

  // Reconstruct in original order
  const result = sorted
    .sort((a, b) => a.originalIndex - b.originalIndex)
    .map(({ originalIndex: _originalIndex, ...rest }) => rest as PromptLayer);

  return {
    layers: result,
    tokensSaved: Math.max(0, originalEstimated - totalEstimated),
    originalEstimatedTokens: originalEstimated,
    finalEstimatedTokens: totalEstimated,
    budgetTokens,
    overBudgetTokens: Math.max(0, totalEstimated - budgetTokens),
  };
}

// ── Helpers ─────────────────────────────────────────────────────────────

/**
 * Rough token estimate: ~4 characters per token.
 * This matches the existing estimateTextTokens heuristic used elsewhere.
 */
export function estimatePromptLayerTokens(value: string | undefined): number {
  if (!value) return 0;
  return Math.ceil(value.length / 4);
}

export function compactRecentPromptLayerLines(value: string, maxTokens: number): string {
  const maxChars = Math.max(0, maxTokens * 4);
  if (value.length <= maxChars) {
    return value;
  }
  if (maxChars <= 0) {
    return "";
  }
  const lines = value
    .split("\n")
    .map((line) => line.trimEnd())
    .filter((line) => line.length > 0);
  if (lines.length === 0) {
    return value.slice(0, maxChars);
  }
  const marker = "[...older entries compacted...]";
  if (maxChars <= marker.length + 2) {
    return value.slice(value.length - maxChars);
  }
  const kept: string[] = [];
  let used = marker.length + 1;
  for (let index = lines.length - 1; index >= 0; index -= 1) {
    const line = lines[index]!;
    const nextUsed = used + line.length + 1;
    if (nextUsed > maxChars) {
      break;
    }
    kept.unshift(line);
    used = nextUsed;
  }
  if (kept.length === 0) {
    return value.slice(value.length - maxChars);
  }
  return [marker, ...kept].join("\n");
}

export function compactConversationMemoryLayer(value: string, maxTokens: number): string {
  const maxChars = Math.max(0, maxTokens * 4);
  if (value.length <= maxChars) {
    return value;
  }
  if (maxChars <= 0) {
    return "";
  }

  const lines = value
    .split("\n")
    .map((line) => line.trimEnd())
    .filter((line) => line.length > 0);
  if (lines.length === 0) {
    return value.slice(0, maxChars);
  }

  const importantLines = lines.filter(isHighValueMemoryLine);
  const recentText = compactRecentPromptLayerLines(value, maxTokens);
  const candidates = [
    ...importantLines,
    importantLines.length > 0 ? "[...recent memory...]" : undefined,
    ...lines.slice(-4),
    ...recentText.split("\n"),
  ].filter((line): line is string => Boolean(line && line.length > 0));
  const selected: string[] = [];
  const seen = new Set<string>();
  for (const line of candidates) {
    if (seen.has(line)) {
      continue;
    }
    const nextText = [...selected, line].join("\n");
    if (nextText.length > maxChars) {
      continue;
    }
    selected.push(line);
    seen.add(line);
  }

  if (selected.length > 0) {
    return selected.join("\n");
  }
  return compactRecentPromptLayerLines(value, maxTokens);
}

function isHighValueMemoryLine(line: string): boolean {
  const normalized = line.replace(/^- /u, "").toLowerCase();
  return (
    normalized.startsWith("objective:") ||
    normalized.startsWith("current objective:") ||
    normalized.startsWith("latest user intent:") ||
    normalized.startsWith("user constraints:") ||
    normalized.startsWith("active constraints:") ||
    normalized.startsWith("open obligations:") ||
    normalized.startsWith("active obligations:") ||
    normalized.startsWith("pending obligations:") ||
    normalized.startsWith("unresolved tasks:") ||
    normalized.startsWith("open blockers:") ||
    normalized.startsWith("open questions:") ||
    normalized.startsWith("do not repeat:")
  );
}

function compactPromptLayerText(value: string, maxChars: number): string {
  if (maxChars <= 0) {
    return "";
  }

  if (value.length <= maxChars) {
    return value;
  }

  const marker = "\n[...middle compacted...]\n";
  if (maxChars <= marker.length + 8) {
    return value.slice(0, maxChars);
  }

  const preservedChars = maxChars - marker.length;
  const headChars = Math.ceil(preservedChars / 2);
  const tailChars = Math.floor(preservedChars / 2);
  return `${value.slice(0, headChars)}${marker}${value.slice(value.length - tailChars)}`;
}
