import type { Message, ToolCall, ToolDef } from '../../llm/openai-compat.js';
import { toolResultMessage } from '../../llm/openai-compat.js';
import { logger } from '../../logger.js';
import type { MovementResult } from './types.js';

export const TRANSITION_TOOL_NAME = 'transition';
export const COMPLETE_TOOL_NAME = 'complete';

type CompleteStatus = 'success' | 'aborted' | 'needs_user_input';

const COMPLETE_STATUS_TO_NEXT = {
  success: 'COMPLETE',
  aborted: 'ABORT',
  needs_user_input: 'ASK',
} as const satisfies Record<CompleteStatus, string>;

export function buildTransitionTool(rules: Array<{ condition: string; next: string }>): ToolDef {
  const validNextValues = rules.map(r => r.next);
  const conditionsDesc = rules.map(r => `- ${r.condition} → "${r.next}"`).join('\n');

  return {
    type: 'function',
    function: {
      name: TRANSITION_TOOL_NAME,
      description: `現在のステップから次の movement へ遷移します。\nタスクを終了する場合 (success/aborted/needs_user_input) は \`complete\` ツールを使ってください。\n遷移先の選択肢:\n${conditionsDesc}`,
      parameters: {
        type: 'object',
        properties: {
          next_step: {
            type: 'string',
            description: '遷移先の movement 名 (rules で定義されたもののみ)',
            enum: validNextValues,
          },
          summary: {
            type: 'string',
            description: '現在のステップで行った作業の要約。ツール結果に [[embed:xxx]] マーカーが含まれていた場合は、summary 内にもそのマーカーをそのまま含めること（リッチUI 表示に使用される）。',
          },
          lessons: {
            type: 'string',
            description: 'このステップで得た教訓・発見をログに記録する。例: 有効だったアプローチ、失敗して別の方法が必要だったこと、データの特徴や注意点、成果物の概要など。',
          },
        },
        required: ['next_step', 'summary'],
      },
    },
  };
}

export function buildCompleteTool(): ToolDef {
  return {
    type: 'function',
    function: {
      name: COMPLETE_TOOL_NAME,
      description: [
        'タスクを終了します。中間 movement への遷移には使わず、必ず transition を使ってください。',
        '- status="success": タスク完了。result にユーザー向け最終出力を 1 文以上で記述',
        '- status="aborted": ユーザーに聞いても解決しない技術的失敗。abort_reason に理由を記述',
        '- status="needs_user_input": 指示が曖昧で確認が必要。missing_info と why_no_default を記述',
      ].join('\n'),
      parameters: {
        type: 'object',
        properties: {
          status: {
            type: 'string',
            enum: ['success', 'aborted', 'needs_user_input'],
            description: '終了ステータス',
          },
          result: {
            type: 'string',
            description: 'status="success" 時に必須。ユーザーに表示される最終出力。[[embed:xxx]] マーカーをそのまま含めて良い。',
          },
          abort_reason: {
            type: 'string',
            description: 'status="aborted" 時に必須。例: "pptxgenjs ライブラリのロード失敗でスライド生成不可"',
          },
          missing_info: {
            type: 'string',
            description: 'status="needs_user_input" 時に必須。不足している具体的情報。',
          },
          why_no_default: {
            type: 'string',
            description: 'status="needs_user_input" 時に必須。なぜデフォルト値で進められないか。',
          },
          lessons: {
            type: 'string',
            description: '任意。デバッグ・改善ログ用 (transition.lessons と同義)。',
          },
        },
        required: ['status'],
      },
    },
  };
}

interface ParsedCompleteArgs {
  status: CompleteStatus;
  result?: string;
  abort_reason?: string;
  missing_info?: string;
  why_no_default?: string;
  lessons?: string;
}

interface ClassifiedTerminals {
  nativeCompletes: ToolCall[];
  nonTerminalTransitions: ToolCall[];
}

type TerminalWinnerOutcome =
  | { kind: 'native_winner'; toolCall: ToolCall; args: ParsedCompleteArgs; ignoredCalls: ToolCall[] }
  | { kind: 'retry'; reason: string; failingCalls: ToolCall[]; ignoredCalls: ToolCall[] }
  | { kind: 'no_terminal' };

export function classifyTerminalCalls(toolCalls: ToolCall[]): ClassifiedTerminals {
  const nativeCompletes: ToolCall[] = [];
  const nonTerminalTransitions: ToolCall[] = [];

  for (const tc of toolCalls) {
    if (tc.function.name === COMPLETE_TOOL_NAME) {
      nativeCompletes.push(tc);
    } else if (tc.function.name === TRANSITION_TOOL_NAME) {
      nonTerminalTransitions.push(tc);
    }
  }

  return { nativeCompletes, nonTerminalTransitions };
}

function parseCompleteArgs(toolCall: ToolCall): { ok: true; args: ParsedCompleteArgs } | { ok: false; reason: string } {
  let raw: Record<string, unknown>;
  try {
    raw = JSON.parse(toolCall.function.arguments) as Record<string, unknown>;
  } catch (e) {
    return { ok: false, reason: `failed to parse complete arguments: ${(e as Error).message}` };
  }
  const status = raw['status'];
  if (status !== 'success' && status !== 'aborted' && status !== 'needs_user_input') {
    return { ok: false, reason: `status must be one of "success" | "aborted" | "needs_user_input", got "${String(status)}"` };
  }
  const args: ParsedCompleteArgs = { status };
  if (typeof raw['result'] === 'string') args.result = raw['result'];
  if (typeof raw['abort_reason'] === 'string') args.abort_reason = raw['abort_reason'];
  if (typeof raw['missing_info'] === 'string') args.missing_info = raw['missing_info'];
  if (typeof raw['why_no_default'] === 'string') args.why_no_default = raw['why_no_default'];
  if (typeof raw['lessons'] === 'string') args.lessons = raw['lessons'];
  return { ok: true, args };
}

function validateCompleteArgs(args: ParsedCompleteArgs): { ok: true } | { ok: false; reason: string } {
  if (args.status === 'success') {
    if (!args.result || args.result.trim() === '') {
      return { ok: false, reason: 'status="success" requires a non-empty `result` (the user-facing final output)' };
    }
  } else if (args.status === 'aborted') {
    if (!args.abort_reason || args.abort_reason.trim() === '') {
      return { ok: false, reason: 'status="aborted" requires `abort_reason`' };
    }
  } else {
    if (!args.missing_info || args.missing_info.trim() === '') {
      return { ok: false, reason: 'status="needs_user_input" requires `missing_info`' };
    }
    // Codex follow-up #3: the prompt and tool description both demand
    // `why_no_default`, but validation previously ignored it. Enforce it so the
    // agent must justify asking instead of choosing a reasonable default.
    if (!args.why_no_default || args.why_no_default.trim() === '') {
      return { ok: false, reason: 'status="needs_user_input" requires `why_no_default` (explain why no reasonable default could be chosen)' };
    }
  }
  return { ok: true };
}

export function selectTerminalWinner(classified: ClassifiedTerminals): TerminalWinnerOutcome {
  const { nativeCompletes, nonTerminalTransitions } = classified;

  if (nativeCompletes.length === 0) {
    return { kind: 'no_terminal' };
  }

  const ignoredOnSuccess = [
    ...nativeCompletes.slice(1),
    ...nonTerminalTransitions,
  ];

  if (nativeCompletes.length > 1) {
    const firstArgs = nativeCompletes[0]!.function.arguments;
    const allMatch = nativeCompletes.every((c) => c.function.arguments === firstArgs);
    if (!allMatch) {
      return {
        kind: 'retry',
        reason: 'Multiple `complete` calls with conflicting arguments. Issue exactly one `complete` with consistent args.',
        failingCalls: nativeCompletes,
        ignoredCalls: nonTerminalTransitions,
      };
    }
  }

  const firstNative = nativeCompletes[0]!;
  const parsed = parseCompleteArgs(firstNative);
  if (!parsed.ok) {
    return {
      kind: 'retry',
      reason: `Invalid \`complete\` args: ${parsed.reason}.`,
      failingCalls: nativeCompletes,
      ignoredCalls: nonTerminalTransitions,
    };
  }
  const validated = validateCompleteArgs(parsed.args);
  if (!validated.ok) {
    return {
      kind: 'retry',
      reason: `Invalid \`complete\` args: ${validated.reason}.`,
      failingCalls: nativeCompletes,
      ignoredCalls: nonTerminalTransitions,
    };
  }
  return { kind: 'native_winner', toolCall: firstNative, args: parsed.args, ignoredCalls: ignoredOnSuccess };
}

export function buildMovementResultFromComplete(
  args: ParsedCompleteArgs,
  toolsUsed: string[],
): MovementResult {
  const next = COMPLETE_STATUS_TO_NEXT[args.status];

  let output: string;
  if (args.status === 'success') {
    output = args.result ?? '';
  } else if (args.status === 'aborted') {
    output = args.abort_reason ?? '';
  } else {
    output = args.missing_info ?? '';
  }

  return {
    next,
    output,
    toolsUsed,
    lessons: args.lessons ?? null,
    ...(args.status === 'aborted' ? { abortCode: 'agent_self_abort' } : {}),
  };
}

interface TransitionMovement {
  name: string;
  rules: Array<{ condition: string; next: string }>;
}

function validateTransition(next: string, rules: TransitionMovement['rules']): boolean {
  return rules.some(r => r.next === next);
}

/**
 * Codex follow-up #1: a winning control call (transition/complete) exits the
 * movement without recording a tool result for its tool_call. Because the live
 * Conversation is shared across movements, that leaves a dangling assistant
 * tool_call in the next model request — strict OpenAI-compatible providers
 * require every assistant tool_call to have a matching `tool` message. Record a
 * result for the winner and for any control calls it superseded.
 */
export function recordControlToolResults(
  messages: Message[],
  winner: ToolCall,
  superseded: ToolCall[],
  ack: string,
): void {
  messages.push(toolResultMessage(winner.id, ack));
  for (const tc of superseded) {
    messages.push(toolResultMessage(tc.id, 'Ignored: superseded by the winning control call.'));
  }
}

export function processTransitionCalls(
  transitionCalls: ToolCall[],
  movement: TransitionMovement,
  accumulatedText: string,
  toolsUsed: string[],
  messages: Message[],
): MovementResult | null {
  for (let i = 0; i < transitionCalls.length; i++) {
    const tc = transitionCalls[i]!;
    let input: Record<string, unknown> = {};
    try {
      input = JSON.parse(tc.function.arguments) as Record<string, unknown>;
    } catch {
      logger.warn('[agent-loop] failed to parse transition arguments');
    }

    const nextStep = String(input['next_step'] ?? '');
    const summary = String(input['summary'] ?? '');
    const lessons = input['lessons'] ? String(input['lessons']) : null;
    logger.info(`[agent-loop] transition tool called: next_step="${nextStep}" summary="${summary}" lessons="${lessons ?? ''}"`);

    if (!validateTransition(nextStep, movement.rules)) {
      logger.warn(`[agent-loop] invalid transition target: "${nextStep}", allowed: ${movement.rules.map((r) => r.next).join(',')}`);
      messages.push(toolResultMessage(tc.id, `Error: "${nextStep}" is not a valid transition target. Valid targets: ${movement.rules.map((r) => r.next).join(', ')}`));
      continue;
    }

    const outputText = summary || accumulatedText;

    // Codex #1: resolve the winning transition (and any later transition calls it
    // supersedes) so the shared conversation carries no dangling control tool_call.
    recordControlToolResults(messages, tc, transitionCalls.slice(i + 1), `Transitioned to ${nextStep}.`);
    logger.info(`[agent-loop] movement=${movement.name} transition to ${nextStep}: ${outputText}`);
    return { next: nextStep, output: outputText, toolsUsed, lessons };
  }
  return null;
}

export function pushRetryToolResults(
  messages: Message[],
  failing: ToolCall[],
  ignored: ToolCall[],
  reason: string,
): void {
  for (const tc of failing) {
    messages.push(toolResultMessage(tc.id, `Error: ${reason}`));
  }
  for (const tc of ignored) {
    messages.push(toolResultMessage(tc.id, `Ignored: superseded by another tool call in this iteration. Reason: ${reason}`));
  }
}
