import type { EventLogger } from '../../progress/event-log.js';
import type { SafetyConfig } from '../../config.js';
import { logger } from '../../logger.js';
import type { Message, ToolDef } from '../../llm/openai-compat.js';
import { ContextManager, type ContextAction } from '../context-manager.js';
import { summarizeForceTransition } from '../context/history-compactor.js';
import { guardPromptBeforeSend, parsePromptSafeLimitTokens } from '../context/prompt-guard.js';
import type { AgentLoopCallbacks, Movement, MovementResult } from './types.js';

/**
 * Codex follow-up #2: the single terminal-default policy for context-overflow
 * forced exits. Terminal defaults (COMPLETE/ASK) become a false success/ask on
 * context loss and skip the worker retry path, so they are normalized to ABORT.
 * Mid-piece movement names (verify, aggregate, …) are honored so the piece can
 * still progress. Used by every context-overflow exit so both paths agree.
 */
export function resolveContextOverflowNext(defaultNext: string | undefined): string {
  if (!defaultNext || defaultNext === 'COMPLETE' || defaultNext === 'ASK') return 'ABORT';
  return defaultNext;
}

/**
 * Build the result returned when prompt-guard cannot recover by other means.
 * Prefers force-transition to movement.defaultNext (with a last-resort LLM
 * summary handed off as the next movement's input), falling back to ABORT
 * only when defaultNext is absent or terminal.
 */
export async function buildContextOverflowResult(
  movement: Movement,
  guardMessage: string,
  messages: Message[],
  toolsUsed: string[],
  runIsolatedLlm?: (messages: Message[]) => Promise<string>,
): Promise<MovementResult> {
  const fallbackNext = resolveContextOverflowNext(movement.defaultNext);
  if (fallbackNext === 'ABORT') {
    return { next: 'ABORT', output: guardMessage, toolsUsed, abortCode: 'context_overflow' };
  }

  let handoffSummary: string | null = null;
  if (runIsolatedLlm) {
    try {
      handoffSummary = await summarizeForceTransition(messages, runIsolatedLlm);
    } catch {
      handoffSummary = null;
    }
  }
  const output = handoffSummary
    ? [
        '[Context overflow — forced handoff]',
        `Reason: ${guardMessage}`,
        '',
        '## Carried-over summary for the next step',
        handoffSummary,
      ].join('\n')
    : [
        '[Context overflow — forced handoff without summary]',
        `Reason: ${guardMessage}`,
        'The agent ran out of context budget before producing an organic transition. The next movement should re-verify state before assuming progress.',
      ].join('\n');

  return {
    next: fallbackNext,
    output,
    toolsUsed,
    lessons: 'Context overflow forced this transition. Downstream movements should re-verify file state and progress before assuming this step finished cleanly.',
  };
}

const USAGE_FALLBACK_AFTER_ITERATIONS = 3;

/**
 * After each LLM iteration, update the ContextManager with the freshly
 * reported `usage.prompt_tokens` (when present) and react to whatever
 * threshold action it returns.
 */
export function applyContextManagerUpdate(
  contextManager: ContextManager,
  lastUsage: { prompt_tokens: number; completion_tokens: number } | undefined,
  iteration: number,
  movement: Movement,
  toolsUsed: string[],
  messages: Message[],
  callbacks: AgentLoopCallbacks | undefined,
  eventLogger?: EventLogger,
): MovementResult | null {
  const buildForceTransitionResult = (reason: string): MovementResult => {
    // Codex #2: same terminal-default policy as buildContextOverflowResult —
    // a terminal default would be a false success/ask on context loss.
    const forceNext = resolveContextOverflowNext(movement.defaultNext);
    return {
      next: forceNext,
      output: `Context limit reached (${reason}). Forced transition to ${forceNext}.`,
      toolsUsed,
      ...(forceNext === 'ABORT' ? { abortCode: 'context_overflow' } : {}),
    };
  };

  const handleAction = (action: ContextAction, fallbackReason: string): MovementResult | null => {
    callbacks?.onContextAction?.(action);
    eventLogger?.emit('context_action', {
      type: action.type,
      ratio: contextManager.getRatio(),
      tokens: contextManager.getPromptTokens(),
      limit: contextManager.getContextLimit(),
      reason: fallbackReason,
    });
    if (action.type === 'prompt') {
      messages.push({ role: 'user', content: action.message });
      return null;
    }
    if (action.type === 'force_transition') {
      logger.warn(`[agent-loop] context force_transition triggered at ratio=${contextManager.getRatio().toFixed(3)}`);
      return buildForceTransitionResult(fallbackReason);
    }
    return null;
  };

  const emitContextUpdate = (): void => {
    callbacks?.onContextUpdate?.({
      promptTokens: contextManager.getPromptTokens(),
      limitTokens: contextManager.getContextLimit(),
    });
  };

  if (lastUsage) {
    const action = contextManager.update(lastUsage);
    emitContextUpdate();
    if (!action) return null;
    return handleAction(action, `${(contextManager.getRatio() * 100).toFixed(0)}%`);
  }

  if (!contextManager.hasUsageData() && iteration >= USAGE_FALLBACK_AFTER_ITERATIONS) {
    let totalChars = 0;
    for (const msg of messages) {
      totalChars += typeof msg.content === 'string' ? msg.content.length : 0;
      for (const tc of msg.tool_calls ?? []) {
        totalChars += tc.function.arguments.length;
      }
    }
    logger.info(`[agent-loop] no usage data after ${iteration} iterations, falling back to char-based estimation (${totalChars} chars)`);
    const action = contextManager.updateFromChars(totalChars);
    emitContextUpdate();
    if (!action) return null;
    return handleAction(action, 'char-based fallback');
  }

  return null;
}

interface LLMErrorContext {
  movement: Movement;
  messages: Message[];
  tools: ToolDef[];
  toolsUsed: string[];
  contextManager?: ContextManager;
  promptGuardRatio: number;
  safetyConfig?: SafetyConfig;
  runIsolatedLlm: (messages: Message[]) => Promise<string>;
}

const NO_TOOLS_SUPPORT_RE = /does not support tools|tool.*not.*support|tool_use.*not.*support/i;
const NO_TOOLS_MODEL_NAME_RE = /library\/([^\s"]+)|model[`'" ]+([^\s"'`]+)/i;

/**
 * Translate an LLM stream error into either a recovery (return null, caller
 * continues the loop) or a terminal MovementResult.
 */
export async function handleLLMError(
  errorMessage: string,
  ctx: LLMErrorContext,
): Promise<MovementResult | null> {
  if (errorMessage.startsWith('LLM request blocked before send:')) {
    const parsedSafeLimit = parsePromptSafeLimitTokens(errorMessage);
    const limitTokens = ctx.contextManager?.getContextLimit();
    const impliedRatio = parsedSafeLimit && limitTokens
      ? parsedSafeLimit / limitTokens
      : ctx.promptGuardRatio;
    // Recovery must target the safe limit the client actually enforced. The
    // guard normally re-derives its threshold as `limit - min(ratioReserve,
    // reserveCap)`, which on large-context models is LOOSER than the client's
    // limit (e.g. 230,144 vs 209,715 on a 262k model) — the guard would then
    // see the blocked prompt as "within limits" and shrink nothing. Pinning
    // reserveCapTokens to `limit - parsedSafeLimit` makes the re-derived
    // threshold equal the client's limit exactly.
    const historySummarization = parsedSafeLimit && limitTokens
      ? { ...ctx.safetyConfig?.historySummarization, reserveCapTokens: limitTokens - parsedSafeLimit }
      : ctx.safetyConfig?.historySummarization;
    const recoveredGuard = await guardPromptBeforeSend(ctx.messages, ctx.tools, ctx.contextManager, {
      promptGuardRatio: impliedRatio,
      historySummarization,
      runIsolatedLlm: ctx.runIsolatedLlm,
    });
    if (recoveredGuard.ok) {
      const changedAnything = recoveredGuard.deduped || recoveredGuard.compacted || recoveredGuard.summarized;
      if (changedAnything) {
        logger.warn(`[agent-loop] movement=${ctx.movement.name} recovered from client prompt preflight block (deduped=${recoveredGuard.deduped} compacted=${recoveredGuard.compacted} summarized=${recoveredGuard.summarized}) estimated=${recoveredGuard.estimatedTokens}`);
        return null;
      }
      // The client blocked the request but the local guard sees the prompt as
      // within limits AND changed nothing — the estimators disagree. Resending
      // the byte-identical request would be blocked again on every iteration
      // (observed in production: 195 consecutive ~14ms failures straight to
      // maxIterations). A recovery that changed nothing is not a recovery.
      logger.warn(`[agent-loop] movement=${ctx.movement.name} client preflight blocked but local guard found nothing to shrink (estimated=${recoveredGuard.estimatedTokens}) — ending movement instead of resending an identical request`);
      return await buildContextOverflowResult(
        ctx.movement,
        `${errorMessage}\n\nThe local prompt guard found nothing further to shrink; resending the identical request would spin until max iterations.`,
        ctx.messages,
        ctx.toolsUsed,
        ctx.runIsolatedLlm,
      );
    }
    return await buildContextOverflowResult(
      ctx.movement,
      `${errorMessage}\n\nRecovery via dedup, compaction, and summarization could not bring the prompt under the safe limit.`,
      ctx.messages,
      ctx.toolsUsed,
      ctx.runIsolatedLlm,
    );
  }

  if (errorMessage && NO_TOOLS_SUPPORT_RE.test(errorMessage)) {
    const modelMatch = errorMessage.match(NO_TOOLS_MODEL_NAME_RE);
    const modelName = modelMatch?.[1] ?? modelMatch?.[2] ?? '使用中のモデル';
    return {
      next: 'ABORT',
      output: `モデル "${modelName}" はツール使用に対応していません。config.yaml の model 設定をツール対応モデル（例: qwen2.5:7b、llama3.1:8b）に変更してください。`,
      toolsUsed: ctx.toolsUsed,
      abortCode: 'llm_unsupported_tools',
    };
  }

  return { next: 'ABORT', output: `LLM error: ${errorMessage}`, toolsUsed: ctx.toolsUsed, abortCode: 'llm_error' };
}
