/**
 * OpenAI Chat Completions-compatible backend (DeepSeek, etc.).
 *
 * DeepSeek: base URL + `/chat/completions`, Bearer key — see
 * https://api-docs.deepseek.com/
 *
 * Tool schemas in Vibn are authored for Gemini (uppercase type enums).
 * We normalize them to JSON Schema before sending.
 */

import type { ChatMessage, ToolCall, ToolDefinition } from "./gemini-chat";

const DEFAULT_CHAT_URL = "https://api.deepseek.com/chat/completions";

function resolveApiKey(): string {
  return (
    process.env.DEEPSEEK_API_KEY?.trim() ||
    process.env.VIBN_OPENAI_COMPATIBLE_API_KEY?.trim() ||
    ""
  );
}

function resolveChatUrl(): string {
  const raw = process.env.VIBN_OPENAI_COMPATIBLE_CHAT_URL?.trim();
  if (raw) return raw.replace(/\/$/, "");
  const base = process.env.VIBN_OPENAI_COMPATIBLE_BASE_URL?.trim().replace(
    /\/$/,
    "",
  );
  if (!base) return DEFAULT_CHAT_URL;
  if (base.endsWith("/chat/completions")) return base;
  return `${base}/chat/completions`;
}

function resolveModel(): string {
  return (
    process.env.VIBN_OPENAI_COMPATIBLE_MODEL?.trim() ||
    process.env.DEEPSEEK_MODEL?.trim() ||
    "deepseek-chat"
  );
}

/** Gemini API Catalog-style schema → OpenAI JSON Schema */
function geminiStyleToJsonSchema(node: unknown): unknown {
  if (node === null || typeof node !== "object" || Array.isArray(node))
    return node;
  const n = node as Record<string, unknown>;
  const out: Record<string, unknown> = {};

  for (const [key, val] of Object.entries(n)) {
    if (key === "type" && typeof val === "string") {
      const map: Record<string, string> = {
        OBJECT: "object",
        STRING: "string",
        NUMBER: "number",
        INTEGER: "integer",
        BOOLEAN: "boolean",
        ARRAY: "array",
      };
      const upper = val.toUpperCase();
      out.type = map[upper] ?? val.toLowerCase();
      continue;
    }
    if (
      key === "properties" &&
      val &&
      typeof val === "object" &&
      !Array.isArray(val)
    ) {
      out.properties = Object.fromEntries(
        Object.entries(val as object).map(([k, v]) => [
          k,
          geminiStyleToJsonSchema(v),
        ]),
      );
      continue;
    }
    if (key === "items") {
      out.items = geminiStyleToJsonSchema(val);
      continue;
    }
    out[key] =
      val && typeof val === "object" && !Array.isArray(val)
        ? geminiStyleToJsonSchema(val)
        : val;
  }
  return out;
}

function toOpenAiTools(
  tools: ToolDefinition[] | undefined,
): object[] | undefined {
  if (!tools?.length) return undefined;
  return tools.map((t) => ({
    type: "function",
    function: {
      name: t.name,
      description: t.description,
      parameters: geminiStyleToJsonSchema(t.parameters) as Record<
        string,
        unknown
      >,
    },
  }));
}

/**
 * OpenAI Chat Completions forbid `user`/`assistant` between an assistant
 * `tool_calls` block and the matching `tool` replies. Gemini-oriented code
 * may inject recovery `user` rows between individual tool results — move
 * those users to immediately after all tool rows for that assistant turn.
 */
function reorderMessagesForOpenAiToolPairs(
  messages: ChatMessage[],
): ChatMessage[] {
  const result: ChatMessage[] = [];
  let i = 0;
  while (i < messages.length) {
    const m = messages[i]!;
    if (m.role !== "assistant" || !m.toolCalls?.length) {
      result.push(m);
      i++;
      continue;
    }

    const expectedIds = m.toolCalls.map((tc) => tc.id);
    const pending = new Set(expectedIds);
    result.push(m);
    i++;

    const toolById = new Map<string, ChatMessage>();
    const bufferedUsers: ChatMessage[] = [];

    while (i < messages.length && pending.size > 0) {
      const n = messages[i]!;
      if (n.role === "tool" && n.toolCallId && pending.has(n.toolCallId)) {
        toolById.set(n.toolCallId, n);
        pending.delete(n.toolCallId);
        i++;
        continue;
      }
      if (n.role === "user") {
        bufferedUsers.push(n);
        i++;
        continue;
      }
      break;
    }

    for (const id of expectedIds) {
      const t = toolById.get(id);
      if (t) result.push(t);
    }
    result.push(...bufferedUsers);
  }
  return result;
}

function toOpenAiMessages(
  systemPrompt: string,
  messages: ChatMessage[],
): object[] {
  const normalized = reorderMessagesForOpenAiToolPairs(messages);
  const out: object[] = [{ role: "system", content: systemPrompt }];
  for (const m of normalized) {
    if (m.role === "user") {
      out.push({ role: "user", content: m.content });
    } else if (m.role === "assistant") {
      const hasTools = Boolean(m.toolCalls?.length);
      const text = typeof m.content === "string" ? m.content.trim() : "";
      const msg: Record<string, unknown> = {
        role: "assistant",
        content: text.length > 0 ? m.content : hasTools ? null : "",
      };
      if (hasTools && m.toolCalls) {
        msg.tool_calls = m.toolCalls.map((tc) => ({
          id: tc.id,
          type: "function",
          function: {
            name: tc.name,
            arguments: JSON.stringify(tc.args ?? {}),
          },
        }));
      }
      out.push(msg);
    } else if (m.role === "tool") {
      const body =
        typeof m.content === "string"
          ? m.content
          : JSON.stringify(m.content ?? "");
      out.push({
        role: "tool",
        tool_call_id: m.toolCallId ?? "",
        content: body.length > 0 ? body : "(empty)",
      });
    }
  }
  return out;
}

function parseAssistantMessage(message: Record<string, unknown> | undefined): {
  text: string;
  thoughts: string;
  toolCalls: ToolCall[];
} {
  const rawText = typeof message?.content === "string" ? message.content : "";
  const thoughts =
    typeof message?.reasoning_content === "string"
      ? message.reasoning_content
      : typeof (message as { reasoning?: string })?.reasoning === "string"
        ? (message as { reasoning: string }).reasoning
        : "";

  const stripTags = (s: string) =>
    s
      .replace(/<tool_calls>[\s\S]*?<\/tool_calls>/g, "")
      .replace(/<think>[\s\S]*?<\/think>/g, "")
      .trim();

  // DeepSeek separates thinking from speaking — during tool loops it
  // often puts everything in reasoning_content and leaves content empty.
  // When that happens, surface the reasoning as the user-visible text
  // so the user isn't staring at silent tool pills.
  const text = stripTags(rawText || thoughts);
  const toolCalls: ToolCall[] = [];
  const rawCalls = message?.tool_calls;
  if (Array.isArray(rawCalls)) {
    for (const c of rawCalls) {
      const call = c as Record<string, unknown>;
      if (call.type !== "function") continue;
      const fn = call.function as Record<string, unknown> | undefined;
      const name = typeof fn?.name === "string" ? fn.name : "";
      const id =
        typeof call.id === "string"
          ? call.id
          : `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`;
      let args: Record<string, unknown> = {};
      const argStr = typeof fn?.arguments === "string" ? fn.arguments : "{}";
      try {
        args = JSON.parse(argStr || "{}") as Record<string, unknown>;
      } catch {
        args = {};
      }
      if (name) toolCalls.push({ id, name, args });
    }
  }
  return { text, thoughts, toolCalls };
}

/**
 * Non-streaming chat + tool calls — mirrors {@link callGeminiChat} return shape.
 */
export async function callOpenAiCompatibleChat(opts: {
  systemPrompt: string;
  messages: ChatMessage[];
  tools?: ToolDefinition[];
  temperature?: number;
  /** Unused for OpenAI-compat; kept for call-site symmetry */
  includeThoughts?: boolean;
}): Promise<{
  text: string;
  thoughts: string;
  toolCalls: ToolCall[];
  finishReason?: string;
  error?: string;
}> {
  const apiKey = resolveApiKey();
  if (!apiKey) {
    return {
      text: "",
      thoughts: "",
      toolCalls: [],
      error:
        "No API key: set DEEPSEEK_API_KEY or VIBN_OPENAI_COMPATIBLE_API_KEY for OpenAI-compatible chat.",
    };
  }

  const url = resolveChatUrl();
  const model = resolveModel();
  const tools = toOpenAiTools(opts.tools);
  const oaiMessages = toOpenAiMessages(opts.systemPrompt, opts.messages);
  const body: Record<string, unknown> = {
    model,
    messages: oaiMessages,
    temperature: opts.temperature ?? 0.7,
    max_tokens: 8192,
    stream: false,
  };
  if (tools?.length) body.tools = tools;

  // ── Request logging (DeepSeek 400 debug) ──────────────────────────────
  const msgSummary = oaiMessages.map((m: any) => ({
    role: m.role,
    has_tool_calls:
      m.role === "assistant" ? Boolean(m.tool_calls?.length) : undefined,
    tool_calls_ids:
      m.role === "assistant" && m.tool_calls?.length
        ? m.tool_calls.map((tc: any) => tc.id)
        : undefined,
    tool_call_id: m.role === "tool" ? m.tool_call_id : undefined,
    content_len: typeof m.content === "string" ? m.content.length : 0,
  }));
  console.error(
    "[deepseek] request",
    JSON.stringify({
      url,
      model,
      msg_count: oaiMessages.length,
      has_tools: Boolean(tools?.length),
      tool_count: tools?.length ?? 0,
      msg_summary: msgSummary,
      last_5_roles: msgSummary.slice(-5).map((m: any) => m.role),
    }),
  );
  // ───────────────────────────────────────────────────────────────────────

  let res: Response;
  try {
    res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify(body),
    });
  } catch (e) {
    return {
      text: "",
      thoughts: "",
      toolCalls: [],
      error: `Network error: ${e instanceof Error ? e.message : String(e)}`,
    };
  }

  const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
  if (!res.ok) {
    // ── Error logging (DeepSeek 400 debug) ───────────────────────────────
    console.error(
      "[deepseek] error response",
      JSON.stringify(
        {
          status: res.status,
          status_text: res.statusText,
          headers: Object.fromEntries(res.headers.entries()),
          body: data,
          // include the last few messages sent so we can see the exact
          // pattern that triggered the error
          last_5_sent: msgSummary.slice(-5),
        },
        null,
        2,
      ),
    );
    // ─────────────────────────────────────────────────────────────────────
    const errObj = data?.error as Record<string, unknown> | undefined;
    const msg =
      (typeof errObj?.message === "string" && errObj.message) ||
      JSON.stringify(data).slice(0, 280);
    return {
      text: "",
      thoughts: "",
      toolCalls: [],
      error: `Chat API error ${res.status}: ${msg}`,
    };
  }

  const choice = (data.choices as Record<string, unknown>[] | undefined)?.[0];
  const message = choice?.message as Record<string, unknown> | undefined;
  const { text, thoughts, toolCalls } = parseAssistantMessage(message);
  const finishReason =
    typeof choice?.finish_reason === "string"
      ? choice.finish_reason
      : undefined;

  return { text, thoughts, toolCalls, finishReason };
}
