'use strict';

const { textFromContent, normalizeToolCall } = require('./model-message');
const { applyProviderCompatibility } = require('./provider-compat');

function providerId(provider) {
  if (!provider) return 'unknown';
  if (typeof provider === 'string') return provider;
  return provider.type || provider.provider || provider.id || 'unknown';
}

function modelId(model, provider) {
  return model || provider?.model || '';
}

function isOpenAIStyle(provider) {
  return ['openai', 'deepseek', 'moonshot', 'lmstudio', 'ollama', 'mlx'].includes(providerId(provider));
}

function isAnthropicStyle(provider) {
  return ['anthropic', 'claude-cli'].includes(providerId(provider));
}

function normalizeMessagesForProvider(messages = [], { provider, model } = {}) {
  const pid = providerId(provider);
  const out = [];
  let lastRole = null;

  for (const raw of messages || []) {
    if (!raw || !raw.role) continue;
    const msg = { ...raw };
    if (msg.content == null) msg.content = '';

    // Some OpenAI-compatible providers reject empty text messages.
    if (isOpenAIStyle(pid) && textFromContent(msg.content).trim() === '' && !hasToolPayload(msg)) {
      msg.content = '[empty]';
    }

    // Anthropic rejects consecutive user/tool-result formatting less often than
    // OpenAI-style providers, but merging same-role plain text keeps history
    // smaller and deterministic for every provider.
    if (lastRole === msg.role && canMergeMessages(out[out.length - 1], msg)) {
      out[out.length - 1].content = mergeContent(out[out.length - 1].content, msg.content);
      continue;
    }

    if (pid === 'deepseek') {
      ensureDeepSeekAssistantReasoning(msg);
    }

    out.push(msg);
    lastRole = msg.role;
  }

  if (isOpenAIStyle(pid)) {
    ensureToolResultOrdering(out);
  }

  return out;
}

function normalizeToolsForProvider(tools = [], { provider, allowSyntheticNoop = false } = {}) {
  const pid = providerId(provider);
  const normalized = [];
  const seen = new Set();
  for (const tool of tools || []) {
    if (!tool || !tool.name || seen.has(tool.name)) continue;
    seen.add(tool.name);
    const copy = { ...tool };
    if (pid === 'anthropic' && copy.parameters && !copy.input_schema) {
      copy.input_schema = copy.parameters;
      delete copy.parameters;
    }
    if (isOpenAIStyle(pid) && copy.input_schema && !copy.parameters) {
      copy.parameters = copy.input_schema;
    }
    normalized.push(copy);
  }

  // OpenAI-compatible histories with prior tool calls can error if the next
  // request has no tools. A harmless no-op tool preserves the tool-capable shape.
  if (isOpenAIStyle(pid) && normalized.length === 0 && allowSyntheticNoop) {
    normalized.push({
      name: 'noop',
      description: 'No operation. Use only when no tool action is required.',
      input_schema: { type: 'object', properties: {} },
      parameters: { type: 'object', properties: {} },
      execute: async () => ({ ok: true }),
      _synthetic: true,
    });
  }
  return normalized;
}

function normalizeResponse(response = {}, { provider, model } = {}) {
  const toolCalls = (response.toolCalls || []).map(normalizeToolCall);
  return {
    content: response.content || '',
    reasoningContent: response.reasoningContent || '',
    toolCalls,
    stopReason: response.stopReason || (toolCalls.length > 0 ? 'tool_use' : 'end_turn'),
    usage: response.usage || { input: 0, output: 0 },
    model: response.model || modelId(model, provider),
    provider: response.provider || providerId(provider),
    latencyMs: response.latencyMs || 0,
    raw: response.raw,
  };
}

function transformRequest({ provider, model, messages, tools, system, ...rest } = {}) {
  const normalizedMessages = normalizeMessagesForProvider(messages, { provider, model });
  const request = {
    ...rest,
    provider: providerId(provider),
    model,
    system,
    messages: normalizedMessages,
    tools: normalizeToolsForProvider(tools, {
      provider,
      model,
      allowSyntheticNoop: hasToolHistory(normalizedMessages),
    }),
  };
  return applyProviderCompatibility(request, { provider, model }).request;
}

function hasToolPayload(msg) {
  if (Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0) return true;
  if (!Array.isArray(msg?.content)) return false;
  return msg.content.some((part) => part?.type === 'tool_use' || part?.type === 'tool_result');
}

function ensureDeepSeekAssistantReasoning(msg) {
  if (msg?.role !== 'assistant') return;
  if (Array.isArray(msg.content)) {
    if (!msg.content.some((part) => part?.type === 'reasoning')) {
      msg.content = [...msg.content, { type: 'reasoning', text: '' }];
    }
    return;
  }
  const text = textFromContent(msg.content);
  msg.content = [
    ...(text ? [{ type: 'text', text }] : []),
    { type: 'reasoning', text: '' },
  ];
}

function hasToolHistory(messages = []) {
  return messages.some((msg) => msg?.role === 'tool' || hasToolPayload(msg));
}

function canMergeMessages(left, right) {
  if (!left || !right) return false;
  return typeof left.content === 'string' && typeof right.content === 'string';
}

function mergeContent(left, right) {
  const a = textFromContent(left);
  const b = textFromContent(right);
  if (!a) return b;
  if (!b) return a;
  return `${a}\n\n${b}`;
}

function ensureToolResultOrdering(messages) {
  // Convert Anthropic-style tool_result user content into OpenAI-style tool
  // messages only when the provider adapter has not already done it. This is a
  // conservative transform used by tests and future stream paths; existing
  // provider adapters may still do richer conversion.
  for (let i = 0; i < messages.length; i++) {
    const msg = messages[i];
    if (msg.role !== 'user' || !Array.isArray(msg.content)) continue;
    const toolResults = msg.content.filter((part) => part?.type === 'tool_result');
    if (toolResults.length === 0) continue;
    const textParts = msg.content.filter((part) => part?.type !== 'tool_result');
    const replacements = toolResults.map((part) => ({
      role: 'tool',
      tool_call_id: part.tool_use_id,
      content: textFromContent(part.content),
    }));
    if (textParts.length > 0) {
      msg.content = textParts;
      messages.splice(i + 1, 0, ...replacements);
      i += replacements.length;
    } else {
      messages.splice(i, 1, ...replacements);
      i += replacements.length - 1;
    }
  }
}

module.exports = {
  providerId,
  isOpenAIStyle,
  isAnthropicStyle,
  normalizeMessagesForProvider,
  normalizeToolsForProvider,
  normalizeResponse,
  transformRequest,
  hasToolHistory,
};
