import type { Message, ToolCall } from '../../llm/openai-compat.js';
import type { EventLogger } from '../../progress/event-log.js';
import { logger } from '../../logger.js';
import type { Movement, MovementResult } from './types.js';

export function buildMaxIterationsAbortMessage(
  movementName: string,
  maxIterations: number,
  toolsUsed: string[],
): string {
  const toolSummary = toolsUsed.length > 0 ? toolsUsed.join(', ') : 'none';
  return [
    `Aborted: movement "${movementName}" exceeded max iterations (${maxIterations}).`,
    `Tools used in this movement: ${toolSummary}.`,
    'Likely causes: too many files inspected in one movement, repeated review loops, or overly large tool outputs.',
  ].join(' ');
}

/**
 * Stable, order-insensitive fingerprint of a batch of regular tool calls,
 * used by the tool-call loop detector.
 */
export function fingerprintToolCalls(calls: ToolCall[]): string {
  const stable = (raw: string): string => {
    try {
      const parsed = JSON.parse(raw);
      const keys = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
        ? Object.keys(parsed).sort()
        : undefined;
      return JSON.stringify(parsed, keys);
    } catch {
      return raw;
    }
  };
  return calls.map((c) => `${c.function.name}(${stable(c.function.arguments)})`).join('|');
}

export function buildToolLoopAbortMessage(
  movementName: string,
  repeats: number,
  limit: number,
  calls: ToolCall[],
): string {
  const toolNames = calls.map((c) => c.function.name).join(', ');
  return [
    `Aborted: movement "${movementName}" repeated the identical tool call(s) [${toolNames}] ${repeats} times in a row (limit: ${limit}) without making progress.`,
    'This is a tool-call loop: the same action produced no change in state. Adjust safety.maxToolLoopRepeats to tune the threshold.',
  ].join(' ');
}

export type ToolLoopTrackerOutcome =
  | { kind: 'none' }
  | { kind: 'warn'; repeats: number; remaining: number }
  | { kind: 'abort'; result: MovementResult };

export interface ToolLoopTracker {
  evaluate: (args: {
    regularCalls: ToolCall[];
    hasFlowControlCalls: boolean;
    iteration: number;
    toolsUsed: string[];
    eventLogger: EventLogger;
  }) => ToolLoopTrackerOutcome;
}

export const createToolLoopTracker = (opts: {
  movementName: string;
  limit: number;
}): ToolLoopTracker => {
  let lastRegularFingerprint: string | null = null;
  let consecutiveToolRepeats = 0;
  let warned = false;

  const evaluate: ToolLoopTracker['evaluate'] = ({
    regularCalls,
    hasFlowControlCalls,
    iteration,
    toolsUsed,
    eventLogger,
  }) => {
    if (regularCalls.length === 0 || hasFlowControlCalls) {
      return { kind: 'none' };
    }

    const fingerprint = fingerprintToolCalls(regularCalls);
    if (fingerprint === lastRegularFingerprint) {
      consecutiveToolRepeats += 1;
    } else {
      lastRegularFingerprint = fingerprint;
      consecutiveToolRepeats = 1;
      warned = false;
    }

    if (consecutiveToolRepeats >= opts.limit) {
      logger.warn(`[agent-loop] movement=${opts.movementName} tool-call loop detected: identical batch repeated ${consecutiveToolRepeats}x (limit=${opts.limit}) tools=[${regularCalls.map((c) => c.function.name).join(',')}]`);
      eventLogger.emit('tool_loop_detected', {
        repeats: consecutiveToolRepeats,
        limit: opts.limit,
        tools: regularCalls.map((c) => c.function.name),
        fingerprint: fingerprint.substring(0, 500),
      }, { iteration });
      return {
        kind: 'abort',
        result: {
          next: 'ABORT',
          output: buildToolLoopAbortMessage(opts.movementName, consecutiveToolRepeats, opts.limit, regularCalls),
          toolsUsed,
          abortCode: 'tool_loop_detected',
        },
      };
    }

    const warnAt = Math.max(2, opts.limit - 2);
    if (consecutiveToolRepeats >= warnAt && !warned) {
      warned = true;
      return {
        kind: 'warn',
        repeats: consecutiveToolRepeats,
        remaining: opts.limit - consecutiveToolRepeats,
      };
    }

    return { kind: 'none' };
  };

  return { evaluate };
};

const TEXT_ONLY_REMIND_EMPTY = 'ステップの作業を続けてください。完了したら transition ツールを呼んで次のステップに遷移してください。';
const MAX_TEXT_ONLY_RETRIES = 3;

/**
 * Handle an LLM iteration that returned text but no tool calls (no
 * transition either).
 */
export function handleTextOnlyResponse(
  accumulatedText: string,
  movement: Movement,
  toolsUsed: string[],
  messages: Message[],
  retryCount: { value: number },
): { kind: 'continue' } | { kind: 'abort'; result: MovementResult } {
  if (!accumulatedText.trim()) {
    logger.info(`[agent-loop] movement=${movement.name} empty response, reminding to use transition tool`);
    messages.push({ role: 'user', content: TEXT_ONLY_REMIND_EMPTY });
    return { kind: 'continue' };
  }

  retryCount.value++;
  if (retryCount.value >= MAX_TEXT_ONLY_RETRIES) {
    // Salvage: in a strictly terminal movement a text-only response IS the
    // deliverable — the model answered in prose but skipped the `complete` call.
    if (movement.defaultNext === 'COMPLETE' && movement.rules.length === 0) {
      logger.warn(`[agent-loop] movement=${movement.name} text-only after ${MAX_TEXT_ONLY_RETRIES} reminders in a strictly-terminal movement; salvaging prose as completion`);
      return {
        kind: 'abort',
        result: { next: 'COMPLETE', output: accumulatedText, toolsUsed },
      };
    }
    logger.warn(`[agent-loop] movement=${movement.name} transition not called after ${MAX_TEXT_ONLY_RETRIES} reminders, aborting`);
    return {
      kind: 'abort',
      result: { next: 'ABORT', output: accumulatedText, toolsUsed, abortCode: 'text_only_limit' },
    };
  }

  const validTargets = movement.rules.map((r) => `"${r.next}"`).join(' / ');
  logger.info(`[agent-loop] movement=${movement.name} text-only response, reminding to use transition (${retryCount.value}/${MAX_TEXT_ONLY_RETRIES})`);
  messages.push({
    role: 'user',
    content: `transition ツールを呼んで next_step を指定してください。テキストだけで終了することはできません。有効な遷移先: ${validTargets} / "ASK"（リマインド ${retryCount.value}/${MAX_TEXT_ONLY_RETRIES}回目）`,
  });
  return { kind: 'continue' };
}
