import type { ToolCall, ToolDef } from '../../llm/openai-compat.js';
import { NoopEventLogger } from '../../progress/event-log.js';
import { logger } from '../../logger.js';
import { executeTool, type ToolContext } from '../tools/index.js';
import { ToolResultCache } from '../context/tool-result-cache.js';
import { extractInvalidationTrigger } from '../context/invalidation.js';
import { routeToolThroughCache } from './tool-cache-routing.js';

export interface ToolCallResult {
  toolCallId: string;
  result: string;
  countedAsRegularToolUse: boolean;
  images?: Array<{ dataUrl: string; label?: string }>;
  durationMs?: number;
  cacheHit?: boolean;
}

export const isAllowedRegularTool = (toolName: string, regularTools: ToolDef[]): boolean =>
  regularTools.some((tool) => tool.function.name === toolName);

const executeRegularToolCall = async (
  toolCall: ToolCall,
  regularTools: ToolDef[],
  toolCtx: ToolContext,
): Promise<ToolCallResult> => {
  const toolName = toolCall.function.name;

  if (!isAllowedRegularTool(toolName, regularTools)) {
    logger.warn(`[agent-loop] blocked disallowed tool call: ${toolName}`);
    try {
      toolCtx.recordToolRequest?.({
        toolName,
        category: 'blocked',
        reason: 'LLM が許可外ツールを呼び出した（自動捕捉）',
      });
    } catch {
      /* recording must never break tool dispatch */
    }
    return {
      toolCallId: toolCall.id,
      result: `Error: tool "${toolName}" is not allowed in this movement`,
      countedAsRegularToolUse: false,
    };
  }

  let input: Record<string, unknown> = {};
  try {
    input = JSON.parse(toolCall.function.arguments) as Record<string, unknown>;
  } catch {
    logger.warn(`[agent-loop] failed to parse tool arguments for ${toolName}`);
  }

  const result = await executeTool(toolName, input, toolCtx);
  const resultStr = result.isError ? `Error: ${result.output}` : result.output;
  logger.info(`[agent-loop] tool ${toolName} => isError=${result.isError} result=${resultStr.substring(0, 200)}`);

  return {
    toolCallId: toolCall.id,
    result: resultStr,
    countedAsRegularToolUse: true,
    images: result.images,
  };
};

export const executeRegularToolCallCached = async (
  toolCall: ToolCall,
  regularTools: ToolDef[],
  toolCtx: ToolContext,
  cache: ToolResultCache | undefined,
  movementName: string,
): Promise<ToolCallResult> => {
  const events = toolCtx.eventLogger ?? new NoopEventLogger();
  const correlationId = events.startCorrelation();
  let parsedArgs: unknown = {};
  try {
    parsedArgs = JSON.parse(toolCall.function.arguments);
  } catch { /* keep {} */ }
  events.emit('tool_call', {
    tool: toolCall.function.name,
    args: parsedArgs,
  }, { correlationId, llmToolCallId: toolCall.id });

  const route = routeToolThroughCache(toolCall, cache, toolCtx.workspacePath);
  const startedAt = Date.now();

  if (route?.hit) {
    const cachedResult = ToolResultCache.formatHit(route.hit, route.displayLabel);
    const durationMs = Date.now() - startedAt;
    logger.info(`[agent-loop] cache HIT ${route.displayLabel} (sourceMovement=${route.hit.sourceMovement})`);
    events.emit('cache_hit', {
      tool: toolCall.function.name,
      label: route.displayLabel,
      sourceMovement: route.hit.sourceMovement,
      ageMs: Date.now() - new Date(route.hit.createdAt).getTime(),
    }, { correlationId, llmToolCallId: toolCall.id });
    events.emit('tool_result', {
      tool: toolCall.function.name,
      isError: false,
      cacheHit: true,
      durationMs,
      outputPreview: cachedResult,
    }, { correlationId, llmToolCallId: toolCall.id });
    return {
      toolCallId: toolCall.id,
      result: cachedResult,
      countedAsRegularToolUse: true,
      durationMs,
      cacheHit: true,
    };
  }

  const result = await executeRegularToolCall(toolCall, regularTools, toolCtx);
  const isError = result.result.startsWith('Error: ');
  const durationMs = Date.now() - startedAt;
  events.emit('tool_result', {
    tool: toolCall.function.name,
    isError,
    cacheHit: false,
    durationMs,
    outputPreview: result.result,
    hasImages: (result.images?.length ?? 0) > 0,
  }, { correlationId, llmToolCallId: toolCall.id });

  if (route && cache && !route.hit) {
    const hasImages = (result.images?.length ?? 0) > 0;
    if (!isError && !hasImages) {
      cache.set({
        key: route.cacheKey,
        toolName: toolCall.function.name,
        resultText: result.result,
        createdAt: new Date().toISOString(),
        sourceMovement: movementName,
        touchedPaths: route.touchedPaths,
        volatility: route.volatility,
      });
      events.emit('cache_set', {
        tool: toolCall.function.name,
        key: route.cacheKey,
        volatility: route.volatility,
        touchedPaths: route.touchedPaths,
      }, { correlationId });
    }
  }

  if (!isError) {
    const trigger = extractInvalidationTrigger(toolCall);
    if (trigger) {
      const reason = trigger.kind === 'path'
        ? `${toolCall.function.name}(${trigger.path})`
        : `${toolCall.function.name} (all files)`;
      if (cache) {
        const evicted = trigger.kind === 'path'
          ? cache.invalidatePath(trigger.path)
          : cache.invalidateAllFiles();
        if (evicted > 0) {
          logger.info(`[agent-loop] cache invalidated ${evicted} entr${evicted === 1 ? 'y' : 'ies'} after ${reason}`);
          events.emit('cache_invalidate', { trigger: reason, kind: trigger.kind, entriesEvicted: evicted }, { correlationId });
        }
      }
    }
  }

  return { ...result, durationMs, cacheHit: false };
};
