/**
 * Reginald's model client — a thin OpenAI-wire chat-completions client over an
 * injected fetch (the same seam the gitea client uses). `@commitea/core` stays
 * pure: no network, no globals. The desktop main process passes the real
 * `fetch`; tests pass a stub. Points at any OpenAI-compatible endpoint (LM
 * Studio, Ollama, the OpenAI API) — the model router picks which.
 */

import type { FetchLike } from '../gitea/types.js'

/** Where a model lives + which model to ask for. */
export interface ModelConfig {
  /** OpenAI-compatible base, including the version segment, e.g. `http://localhost:1234/v1`. */
  baseUrl: string
  model: string
  /** Bearer key; omitted for local servers that don't check it. */
  apiKey?: string
}

/** A model's request to run a tool. `arguments` is a raw JSON string (OpenAI shape). */
export interface ToolCall {
  id: string
  name: string
  arguments: string
}

/** One turn in the conversation, in our normalized shape. */
export interface ChatMessage {
  role: 'system' | 'user' | 'assistant' | 'tool'
  content: string
  /** assistant turns that requested tools. */
  toolCalls?: ToolCall[]
  /** tool turns: which call they answer. */
  toolCallId?: string
  /** tool turns: the function name. */
  name?: string
}

/** A tool the model may call — name, description, and a JSON-Schema parameter spec. */
export interface ToolDecl {
  name: string
  description: string
  parameters: object
}

export interface CompletionResult {
  content: string
  toolCalls: ToolCall[]
}

export interface ChatClient {
  complete(messages: ChatMessage[], tools?: ToolDecl[]): Promise<CompletionResult>
}

/** Map our message shape to the OpenAI wire shape. */
function toWireMessage(m: ChatMessage): Record<string, unknown> {
  if (m.role === 'assistant' && m.toolCalls?.length) {
    return {
      role: 'assistant',
      content: m.content || null,
      tool_calls: m.toolCalls.map((tc) => ({
        id: tc.id,
        type: 'function',
        function: { name: tc.name, arguments: tc.arguments },
      })),
    }
  }
  if (m.role === 'tool') {
    return { role: 'tool', tool_call_id: m.toolCallId, content: m.content }
  }
  return { role: m.role, content: m.content }
}

function toWireTool(t: ToolDecl): Record<string, unknown> {
  return { type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters } }
}

/** Shape of the one choice we read back. */
interface RawChoiceMessage {
  content: string | null
  tool_calls?: { id: string; function: { name: string; arguments: string } }[]
}

export function createChatClient(config: ModelConfig, fetchImpl: FetchLike): ChatClient {
  const url = `${config.baseUrl.replace(/\/+$/, '')}/chat/completions`

  return {
    async complete(messages, tools) {
      const body: Record<string, unknown> = {
        model: config.model,
        messages: messages.map(toWireMessage),
        temperature: 0,
      }
      if (tools?.length) {
        body.tools = tools.map(toWireTool)
        body.tool_choice = 'auto'
      }
      const res = await fetchImpl(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
        },
        body: JSON.stringify(body),
      })
      if (!res.ok) {
        const text = await res.text().catch(() => '')
        throw new Error(`model completion failed (${res.status}): ${text.slice(0, 200)}`)
      }
      const json = (await res.json()) as { choices?: { message: RawChoiceMessage }[] }
      const msg = json.choices?.[0]?.message
      return {
        content: msg?.content ?? '',
        toolCalls: (msg?.tool_calls ?? []).map((tc) => ({
          id: tc.id,
          name: tc.function.name,
          arguments: tc.function.arguments,
        })),
      }
    },
  }
}
