import type { Message, ToolDef } from '../../llm/openai-compat.js';
import type { ContextManager } from '../context-manager.js';
import type { HistorySummarizationConfig } from '../../config.js';
import { dedupeFileReads } from './file-read-dedup.js';
import { summarizeHistory } from './history-compactor.js';
import {
  estimateTokensFromText,
  estimateMessagesTokens,
  estimateToolsTokens,
} from './token-estimate.js';
import { logger } from '../../logger.js';

export const PROMPT_GUARD_RATIO_DEFAULT = 0.8;
/**
 * Fallback used only when the LLM client's preflight error message can't be
 * parsed for a safe-limit value. Conservative on purpose: kicks in only in
 * degraded paths.
 */
export const PROMPT_GUARD_FALLBACK_TOKENS = 24_000;
/** Tool result messages above this size become candidates for compaction. */
export const LARGE_TOOL_RESULT_TOKENS = 8_000;

export type GuardResult =
  | {
      ok: true;
      estimatedTokens: number;
      compacted: boolean;
      deduped: boolean;
      summarized: boolean;
      feedback?: string;
    }
  | {
      ok: false;
      estimatedTokens: number;
      limitTokens: number;
      message: string;
    };

export interface GuardOptions {
  promptGuardRatio?: number;
  historySummarization?: HistorySummarizationConfig;
  runIsolatedLlm?: (messages: Message[]) => Promise<string>;
}

export function looksLikeLargeEncodedPayload(text: string): boolean {
  if (text.length < 8_000) return false;
  if (/data:[^;,\s]+;base64,[A-Za-z0-9+/=\s]{2000,}/.test(text)) return true;
  if (/base64[,:"'\s]+[A-Za-z0-9+/=\s]{2000,}/i.test(text)) return true;
  return false;
}

export function buildPromptLimitAgentInstruction(estimatedTokens: number, maxPromptTokens: number): string {
  return [
    '前回のツール結果または会話履歴が大きすぎるため、一部の内容は LLM コンテキストに入れられませんでした。',
    `推定 prompt サイズ: ${estimatedTokens.toLocaleString()} tokens / 安全上限: ${maxPromptTokens.toLocaleString()} tokens。`,
    '全文を再読込しようとせず、必要な箇所を絞って調査を続けてください。',
    '推奨行動: Read(offset/limit), Read(byte_offset/byte_length), Grep, または対象を絞った Bash で必要範囲だけ確認してください。',
    'ユーザーに確認する前に、まず自分で範囲指定や検索に切り替えて続行してください。',
  ].join('\n');
}

export function parsePromptSafeLimitTokens(errorMessage: string): number | null {
  const match = /safe limit ([\d,]+) tokens/i.exec(errorMessage);
  if (!match) return null;
  const parsed = Number.parseInt(match[1]!.replace(/,/g, ''), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

/**
 * Replace `role: 'tool'` messages whose content exceeds LARGE_TOOL_RESULT_TOKENS
 * with a short placeholder, in descending size order, until the prompt fits or
 * candidates are exhausted. Mutates `messages` in place.
 *
 * Tracks the size delta directly instead of re-walking messages each iteration —
 * O(candidates) vs the previous O(messages × candidates).
 */
export function compactOversizedToolResults(
  messages: Message[],
  toolTokens: number,
  maxPromptTokens: number,
): { changed: boolean; estimatedTokens: number; omittedCount: number } {
  let estimatedTokens = estimateMessagesTokens(messages) + toolTokens;
  let changed = false;
  let omittedCount = 0;
  if (estimatedTokens <= maxPromptTokens) return { changed, estimatedTokens, omittedCount };

  const candidates = messages
    .map((message, index) => ({
      message,
      index,
      tokens: typeof message.content === 'string' ? estimateTokensFromText(message.content) : 0,
    }))
    .filter(({ message, tokens }) => message.role === 'tool' && tokens >= LARGE_TOOL_RESULT_TOKENS)
    .sort((a, b) => b.tokens - a.tokens);

  for (const candidate of candidates) {
    if (estimatedTokens <= maxPromptTokens) break;
    const content = typeof candidate.message.content === 'string' ? candidate.message.content : '';
    const encodedHint = looksLikeLargeEncodedPayload(content)
      ? ' The omitted content appears to contain base64/data URLs.'
      : '';
    const placeholder = [
      '[Tool result omitted before LLM request]',
      `The previous tool result was too large to fit safely in the model context.${encodedHint}`,
      'Use a narrower Read(offset/limit), Read(byte_offset/byte_length), Grep, or a targeted Bash command to inspect only the needed range.',
    ].join('\n');
    const placeholderTokens = estimateTokensFromText(placeholder);
    estimatedTokens = estimatedTokens - candidate.tokens + placeholderTokens;
    candidate.message.content = placeholder;
    changed = true;
    omittedCount++;
  }

  return { changed, estimatedTokens, omittedCount };
}

/**
 * Three-stage prompt-overflow defense, run before every LLM request.
 *
 *   Stage 1 — dedupe duplicate file Reads (cheap, no LLM call, no info loss
 *             since the latest Read of each file is preserved).
 *   Stage 2 — compact oversized tool results (prune large tool messages).
 *   Stage 3 — anchored Markdown history summarization via runIsolatedLlm
 *             (Opencode-style); skipped if disabled in config or no LLM hook.
 *
 * Returns ok:false only when all three stages fail to bring the prompt under
 * `promptGuardRatio` of the model context limit. The caller (executeMovement)
 * decides whether to ABORT or force-transition.
 */
export async function guardPromptBeforeSend(
  messages: Message[],
  tools: ToolDef[],
  contextManager?: ContextManager,
  options: GuardOptions = {},
): Promise<GuardResult> {
  const promptGuardRatio = options.promptGuardRatio ?? PROMPT_GUARD_RATIO_DEFAULT;
  // tools is built once per movement and never mutated, so JSON.stringify it
  // exactly once instead of recomputing inside every estimate call.
  const toolTokens = estimateToolsTokens(tools);
  if (!contextManager) {
    return {
      ok: true,
      estimatedTokens: estimateMessagesTokens(messages) + toolTokens,
      compacted: false,
      deduped: false,
      summarized: false,
    };
  }
  const limitTokens = contextManager.getContextLimit();
  const maxPromptTokens = Math.floor(limitTokens * promptGuardRatio);

  let estimated = estimateMessagesTokens(messages) + toolTokens;
  if (estimated <= maxPromptTokens) {
    return { ok: true, estimatedTokens: estimated, compacted: false, deduped: false, summarized: false };
  }

  const dedup = dedupeFileReads(messages);
  if (dedup.changed) {
    estimated = estimateMessagesTokens(messages) + toolTokens;
    logger.info(`[prompt-guard] file-read dedup replaced=${dedup.replacedCount} freedChars=${dedup.freedChars} estimated=${estimated}`);
  }
  if (estimated <= maxPromptTokens) {
    return { ok: true, estimatedTokens: estimated, compacted: false, deduped: dedup.changed, summarized: false };
  }

  const compacted = compactOversizedToolResults(messages, toolTokens, maxPromptTokens);
  let summarized = false;
  if (compacted.changed) {
    const feedback = buildPromptLimitAgentInstruction(compacted.estimatedTokens, maxPromptTokens);
    // Pop on overshoot: the feedback instruction is only valuable when it
    // actually fits — otherwise we'd carry redundant guidance into stage 3.
    messages.push({ role: 'user', content: feedback });
    const estimatedWithFeedback = compacted.estimatedTokens + estimateTokensFromText(feedback);
    if (estimatedWithFeedback <= maxPromptTokens) {
      return {
        ok: true,
        estimatedTokens: estimatedWithFeedback,
        compacted: true,
        deduped: dedup.changed,
        summarized: false,
        feedback,
      };
    }
    messages.pop();
  }
  estimated = compacted.estimatedTokens;
  if (estimated <= maxPromptTokens) {
    return {
      ok: true,
      estimatedTokens: estimated,
      compacted: compacted.changed,
      deduped: dedup.changed,
      summarized: false,
    };
  }

  const summarizationEnabled = options.historySummarization?.enabled !== false;
  if (summarizationEnabled && options.runIsolatedLlm) {
    const tailTurns = options.historySummarization?.tailTurns ?? 2;
    const preserveRecentBudget = options.historySummarization?.preserveRecentBudget
      ?? Math.min(Math.floor(limitTokens * 0.25), 8_000);
    const summary = await summarizeHistory(messages, {
      tailTurns,
      preserveRecentBudget,
      runIsolatedLlm: options.runIsolatedLlm,
    });
    if (summary.summarized) {
      summarized = true;
      estimated = estimateMessagesTokens(messages) + toolTokens;
      logger.info(`[prompt-guard] history summarization complete freedChars=${summary.freedChars} estimated=${estimated}`);
      if (estimated <= maxPromptTokens) {
        return {
          ok: true,
          estimatedTokens: estimated,
          compacted: compacted.changed,
          deduped: dedup.changed,
          summarized: true,
        };
      }
    } else {
      logger.warn(`[prompt-guard] history summarization skipped: ${summary.reason}`);
    }
  }

  return {
    ok: false,
    estimatedTokens: estimated,
    limitTokens,
    message: `LLM request blocked before send: estimated prompt size ${estimated.toLocaleString()} tokens exceeds safe limit ${maxPromptTokens.toLocaleString()} tokens (${Math.round(promptGuardRatio * 100)}% of context ${limitTokens.toLocaleString()})${summarized ? ' (after dedup, compaction, and history summarization)' : ''}. Narrow the requested content with Read(offset/limit), Read(byte_offset/byte_length), Grep, or targeted Bash before continuing.`,
  };
}
