/**
 * Codex adapter — SANDCODE SDK ↔ ChatGPT Codex Responses API.
 *
 * The ChatGPT subscription Codex backend lives at
 * `https://chatgpt.com/backend-api/codex/responses`. It speaks OpenAI's
 * Responses API (not Chat Completions), requires a bearer OAuth access
 * token from the Codex OAuth flow, and an `OpenAI-Chatgpt-Account-Id`
 * header pulled from the access-token JWT claims.
 *
 * This adapter only supports what SANDCODE actually emits: text, tool
 * calls/results, streaming. No images, no multi-part inputs for v1.
 */

import { APIError } from '@sandcode/sdk'

import { logForDebugging } from '../../utils/debug.js'
import {
  type CodexTokens,
  needsRefresh,
  refreshTokens,
} from '../oauth/codexOAuth.js'
import {
  recordCodexCapHit,
  recordCodexUsageHeaders,
} from '../oauth/codexRateLimits.js'
import { loadCodexTokens, saveCodexTokens } from '../oauth/codexTokenStore.js'

export const CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex'
export const CODEX_RESPONSES_PATH = '/responses'

// ─────────────────────────────────────────────────────────────
// Minimal SDK-shaped types (kept in lockstep with openaiCompatAdapter.ts)
// ─────────────────────────────────────────────────────────────

type TextBlockParam = { type: 'text'; text: string }
type ToolUseBlockParam = { type: 'tool_use'; id: string; name: string; input: unknown }
type ToolResultBlockParam = {
  type: 'tool_result'
  tool_use_id: string
  content: string | Array<{ type: 'text'; text: string }>
}
type MessageParam = {
  role: 'user' | 'assistant'
  content: string | Array<TextBlockParam | ToolUseBlockParam | ToolResultBlockParam>
}
type Tool = { name: string; description?: string; input_schema?: Record<string, unknown> }
type ContentBlock =
  | { type: 'text'; text: string }
  | { type: 'thinking'; thinking: string }
  | { type: 'tool_use'; id: string; name: string; input: unknown }
type SdkMessage = {
  id: string
  type: 'message'
  role: 'assistant'
  content: ContentBlock[]
  model: string
  stop_reason: string | null
  stop_sequence: string | null
  usage: {
    input_tokens: number
    output_tokens: number
    cache_creation_input_tokens?: number
    cache_read_input_tokens?: number
  }
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RawMessageStreamEvent = any // matches openaiCompatAdapter

// ─────────────────────────────────────────────────────────────
// Responses API shapes (only what we send/receive)
// ─────────────────────────────────────────────────────────────

interface ResponsesInputMessage {
  type: 'message'
  role: 'user' | 'assistant' | 'system'
  content: Array<{ type: 'input_text' | 'output_text'; text: string }>
}
interface ResponsesFunctionCall {
  type: 'function_call'
  call_id: string
  name: string
  arguments: string
}
interface ResponsesFunctionCallOutput {
  type: 'function_call_output'
  call_id: string
  output: string
}
type ResponsesInputItem =
  | ResponsesInputMessage
  | ResponsesFunctionCall
  | ResponsesFunctionCallOutput

interface ResponsesTool {
  type: 'function'
  name: string
  description: string
  parameters: Record<string, unknown>
  strict?: boolean
}

interface ResponsesRequest {
  model: string
  instructions?: string
  input: ResponsesInputItem[]
  tools?: ResponsesTool[]
  tool_choice?: 'auto' | 'none' | 'required'
  stream?: boolean
  /** Codex backend requires this to be explicitly `false`. */
  store: false
  reasoning?: { effort?: 'minimal' | 'low' | 'medium' | 'high'; summary?: 'auto' | 'detailed' }
  // Codex CLI sends this for session correlation on the backend.
  prompt_cache_key?: string
}

// ─────────────────────────────────────────────────────────────
// Stream wrapper — must match the shape openaiCompatAdapter's does
// ─────────────────────────────────────────────────────────────

class SandcodeCompatStream<T> implements AsyncIterable<T> {
  controller: AbortController
  private iterator: AsyncIterator<T>
  constructor(gen: AsyncGenerator<T>, controller: AbortController) {
    this.controller = controller
    this.iterator = gen[Symbol.asyncIterator]()
  }
  [Symbol.asyncIterator](): AsyncIterator<T> {
    return this.iterator
  }
}

// ─────────────────────────────────────────────────────────────
// SDK → Responses conversion
// ─────────────────────────────────────────────────────────────

function systemToInstructions(
  system: string | TextBlockParam[] | undefined,
): string | undefined {
  if (!system) return undefined
  return typeof system === 'string' ? system : system.map((b) => b.text).join('\n')
}

function convertMessagesToResponses(messages: MessageParam[]): ResponsesInputItem[] {
  const items: ResponsesInputItem[] = []

  for (const msg of messages) {
    const blocks = Array.isArray(msg.content)
      ? msg.content
      : [{ type: 'text' as const, text: String(msg.content) }]

    if (msg.role === 'user') {
      const textParts: string[] = []
      for (const block of blocks) {
        if (block.type === 'text') {
          textParts.push(block.text)
        } else if (block.type === 'tool_result') {
          // Tool results become their own input items (sibling, not child).
          const output =
            typeof block.content === 'string'
              ? block.content
              : block.content.map((c) => ('text' in c ? c.text : '')).join('\n')
          items.push({
            type: 'function_call_output',
            call_id: block.tool_use_id,
            output,
          })
        }
        // images: skipped in v1
      }
      if (textParts.length > 0) {
        items.push({
          type: 'message',
          role: 'user',
          content: [{ type: 'input_text', text: textParts.join('\n') }],
        })
      }
    } else if (msg.role === 'assistant') {
      const textParts: string[] = []
      const toolCalls: ResponsesFunctionCall[] = []
      for (const block of blocks) {
        if (block.type === 'text') {
          textParts.push(block.text)
        } else if (block.type === 'tool_use') {
          toolCalls.push({
            type: 'function_call',
            call_id: block.id,
            name: block.name,
            arguments: JSON.stringify(block.input ?? {}),
          })
        }
      }
      if (textParts.length > 0) {
        items.push({
          type: 'message',
          role: 'assistant',
          content: [{ type: 'output_text', text: textParts.join('\n') }],
        })
      }
      for (const tc of toolCalls) items.push(tc)
    }
  }
  return items
}

function convertToolsToResponses(tools?: Tool[]): ResponsesTool[] | undefined {
  if (!tools?.length) return undefined
  return tools.map((t) => ({
    type: 'function',
    name: t.name,
    description: t.description ?? '',
    parameters: (t.input_schema ?? {}) as Record<string, unknown>,
  }))
}

// ─────────────────────────────────────────────────────────────
// Token resolution (refresh-ahead)
// ─────────────────────────────────────────────────────────────

let inflightRefresh: Promise<CodexTokens> | null = null

async function getValidTokens(): Promise<CodexTokens> {
  let tokens = await loadCodexTokens()
  if (!tokens) {
    throw new APIError(
      401,
      undefined,
      'Not signed in to ChatGPT. Run /login-codex to authorize.',
      undefined,
    )
  }
  if (!needsRefresh(tokens)) return tokens
  if (!inflightRefresh) {
    inflightRefresh = (async () => {
      const refreshed = await refreshTokens(tokens!)
      await saveCodexTokens(refreshed)
      return refreshed
    })().finally(() => {
      inflightRefresh = null
    })
  }
  tokens = await inflightRefresh
  return tokens
}

// ─────────────────────────────────────────────────────────────
// Non-streaming response → SdkMessage
// ─────────────────────────────────────────────────────────────

interface ResponsesOutputMessage {
  type: 'message'
  id: string
  role: 'assistant'
  content: Array<
    | { type: 'output_text'; text: string }
    | { type: 'reasoning'; summary?: string; text?: string }
  >
}
interface ResponsesOutputFunctionCall {
  type: 'function_call'
  id: string
  call_id: string
  name: string
  arguments: string
}
interface ResponsesOutputReasoning {
  type: 'reasoning'
  id: string
  summary?: Array<{ text: string }>
}
type ResponsesOutputItem =
  | ResponsesOutputMessage
  | ResponsesOutputFunctionCall
  | ResponsesOutputReasoning

interface ResponsesObject {
  id: string
  model: string
  status: 'completed' | 'in_progress' | 'failed' | 'cancelled' | 'incomplete'
  output: ResponsesOutputItem[]
  usage?: {
    input_tokens?: number
    output_tokens?: number
    input_tokens_details?: { cached_tokens?: number }
    reasoning_tokens?: number
  }
  incomplete_details?: { reason?: string }
}

function convertResponseToSdk(resp: ResponsesObject, fallbackModel: string): SdkMessage {
  const blocks: ContentBlock[] = []
  let sawToolCall = false

  for (const item of resp.output) {
    if (item.type === 'reasoning') {
      const text = item.summary?.map((s) => s.text).join('\n') ?? ''
      if (text) blocks.push({ type: 'thinking', thinking: text })
    } else if (item.type === 'message') {
      for (const c of item.content) {
        if (c.type === 'output_text' && c.text) blocks.push({ type: 'text', text: c.text })
      }
    } else if (item.type === 'function_call') {
      sawToolCall = true
      let input: unknown = {}
      try {
        input = JSON.parse(item.arguments || '{}')
      } catch {
        // leave input as {} — the tool will see empty args
      }
      blocks.push({ type: 'tool_use', id: item.call_id, name: item.name, input })
    }
  }

  const stopReason =
    resp.status === 'incomplete' && resp.incomplete_details?.reason === 'max_output_tokens'
      ? 'max_tokens'
      : sawToolCall
        ? 'tool_use'
        : 'end_turn'

  const inputTokens = resp.usage?.input_tokens ?? 0
  const cached = resp.usage?.input_tokens_details?.cached_tokens ?? 0

  return {
    id: resp.id,
    type: 'message',
    role: 'assistant',
    content: blocks.length > 0 ? blocks : [{ type: 'text', text: '' }],
    model: resp.model || fallbackModel,
    stop_reason: stopReason,
    stop_sequence: null,
    usage: {
      input_tokens: inputTokens - cached,
      output_tokens: resp.usage?.output_tokens ?? 0,
      cache_creation_input_tokens: 0,
      cache_read_input_tokens: cached,
    },
  }
}

// ─────────────────────────────────────────────────────────────
// SSE parsing helpers
// ─────────────────────────────────────────────────────────────

async function* parseSSE(
  body: ReadableStream<Uint8Array>,
): AsyncGenerator<{ event?: string; data: string }> {
  const reader = body.getReader()
  const decoder = new TextDecoder()
  let buffer = ''
  while (true) {
    const { value, done } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })
    let idx: number
    while ((idx = buffer.indexOf('\n\n')) !== -1) {
      const raw = buffer.slice(0, idx)
      buffer = buffer.slice(idx + 2)
      let event: string | undefined
      const dataLines: string[] = []
      for (const line of raw.split('\n')) {
        if (line.startsWith('event:')) event = line.slice(6).trim()
        else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim())
      }
      if (dataLines.length === 0) continue
      yield { event, data: dataLines.join('\n') }
    }
  }
}

// ─────────────────────────────────────────────────────────────
// Responses SSE → SDK event stream
//
// Event types emitted by the Responses API (partial list):
//   response.created, response.in_progress
//   response.output_item.added       — new message / function_call / reasoning
//   response.output_text.delta       — streamed text into current message
//   response.function_call_arguments.delta — streamed json for a function call
//   response.output_item.done
//   response.completed               — final response object with usage
//   response.failed, response.incomplete
//   error                            — out-of-band error
// ─────────────────────────────────────────────────────────────

async function* streamResponsesToSdk(
  body: ReadableStream<Uint8Array>,
  model: string,
  abortController: AbortController,
): AsyncGenerator<RawMessageStreamEvent> {
  let blockIndex = 0
  let textBlockOpen = false
  // Map from Responses output_index -> SDK block index
  const toolBlocks = new Map<number, { sdkIdx: number; callId: string; name: string }>()
  const reasoningBlocks = new Map<number, { sdkIdx: number }>()

  let inputTokens = 0
  let outputTokens = 0
  let cachedTokens = 0
  let stopReason: string = 'end_turn'
  let sawToolCall = false

  yield {
    type: 'message_start',
    message: {
      id: `msg_codex_${Date.now()}`,
      type: 'message',
      role: 'assistant',
      content: [],
      model,
      stop_reason: null,
      stop_sequence: null,
      usage: { input_tokens: 0, output_tokens: 0 },
    },
  } as unknown as RawMessageStreamEvent

  for await (const frame of parseSSE(body)) {
    if (abortController.signal.aborted) break
    let payload: Record<string, unknown>
    try {
      payload = JSON.parse(frame.data) as Record<string, unknown>
    } catch {
      continue
    }
    const eventType =
      frame.event ||
      (payload.type as string | undefined) ||
      ''

    if (eventType === 'response.output_item.added') {
      const item = payload.item as { type: string; call_id?: string; name?: string; id: string }
      const outputIndex = (payload.output_index as number) ?? 0
      if (item.type === 'function_call' && item.call_id && item.name) {
        if (textBlockOpen) {
          yield { type: 'content_block_stop', index: blockIndex } as RawMessageStreamEvent
          blockIndex++
          textBlockOpen = false
        }
        const sdkIdx = blockIndex++
        toolBlocks.set(outputIndex, { sdkIdx, callId: item.call_id, name: item.name })
        sawToolCall = true
        yield {
          type: 'content_block_start',
          index: sdkIdx,
          content_block: { type: 'tool_use', id: item.call_id, name: item.name, input: {} },
        } as RawMessageStreamEvent
      } else if (item.type === 'reasoning') {
        const sdkIdx = blockIndex++
        reasoningBlocks.set(outputIndex, { sdkIdx })
        yield {
          type: 'content_block_start',
          index: sdkIdx,
          content_block: { type: 'thinking', thinking: '' },
        } as RawMessageStreamEvent
      }
      // message items open lazily on first output_text.delta
    } else if (eventType === 'response.output_text.delta') {
      const delta = (payload.delta as string) ?? ''
      if (!textBlockOpen) {
        yield {
          type: 'content_block_start',
          index: blockIndex,
          content_block: { type: 'text', text: '' },
        } as RawMessageStreamEvent
        textBlockOpen = true
      }
      if (delta) {
        yield {
          type: 'content_block_delta',
          index: blockIndex,
          delta: { type: 'text_delta', text: delta },
        } as RawMessageStreamEvent
      }
    } else if (eventType === 'response.function_call_arguments.delta') {
      const delta = (payload.delta as string) ?? ''
      const outputIndex = (payload.output_index as number) ?? 0
      const t = toolBlocks.get(outputIndex)
      if (t && delta) {
        yield {
          type: 'content_block_delta',
          index: t.sdkIdx,
          delta: { type: 'input_json_delta', partial_json: delta },
        } as RawMessageStreamEvent
      }
    } else if (
      eventType === 'response.reasoning_summary_text.delta' ||
      eventType === 'response.reasoning.delta'
    ) {
      const delta = (payload.delta as string) ?? ''
      const outputIndex = (payload.output_index as number) ?? 0
      const r = reasoningBlocks.get(outputIndex)
      if (r && delta) {
        yield {
          type: 'content_block_delta',
          index: r.sdkIdx,
          delta: { type: 'thinking_delta', thinking: delta },
        } as RawMessageStreamEvent
      }
    } else if (eventType === 'response.output_item.done') {
      const outputIndex = (payload.output_index as number) ?? 0
      const t = toolBlocks.get(outputIndex)
      const r = reasoningBlocks.get(outputIndex)
      if (t) {
        yield { type: 'content_block_stop', index: t.sdkIdx } as RawMessageStreamEvent
        toolBlocks.delete(outputIndex)
      } else if (r) {
        yield { type: 'content_block_stop', index: r.sdkIdx } as RawMessageStreamEvent
        reasoningBlocks.delete(outputIndex)
      } else if (textBlockOpen) {
        yield { type: 'content_block_stop', index: blockIndex } as RawMessageStreamEvent
        blockIndex++
        textBlockOpen = false
      }
    } else if (eventType === 'response.completed') {
      const resp = payload.response as ResponsesObject | undefined
      if (resp?.usage) {
        inputTokens = resp.usage.input_tokens ?? 0
        outputTokens = resp.usage.output_tokens ?? 0
        cachedTokens = resp.usage.input_tokens_details?.cached_tokens ?? 0
      }
      if (resp?.status === 'incomplete' && resp.incomplete_details?.reason === 'max_output_tokens') {
        stopReason = 'max_tokens'
      } else if (sawToolCall) {
        stopReason = 'tool_use'
      } else {
        stopReason = 'end_turn'
      }
    } else if (eventType === 'response.failed' || eventType === 'error') {
      const err = (payload.error as { message?: string })?.message || 'Codex stream failed'
      throw new APIError(undefined, undefined, `Codex: ${err}`, undefined)
    }
  }

  // Close any dangling blocks.
  if (textBlockOpen) {
    yield { type: 'content_block_stop', index: blockIndex } as RawMessageStreamEvent
  }
  for (const [, t] of toolBlocks) {
    yield { type: 'content_block_stop', index: t.sdkIdx } as RawMessageStreamEvent
  }
  for (const [, r] of reasoningBlocks) {
    yield { type: 'content_block_stop', index: r.sdkIdx } as RawMessageStreamEvent
  }

  yield {
    type: 'message_delta',
    delta: { stop_reason: stopReason },
    usage: {
      input_tokens: inputTokens - cachedTokens,
      output_tokens: outputTokens,
      cache_read_input_tokens: cachedTokens,
    },
  } as RawMessageStreamEvent
  yield { type: 'message_stop' } as RawMessageStreamEvent
}

// ─────────────────────────────────────────────────────────────
// Headers
// ─────────────────────────────────────────────────────────────

function buildCodexHeaders(tokens: CodexTokens, opts: { session?: string }): Headers {
  const h = new Headers({
    'content-type': 'application/json',
    accept: 'text/event-stream',
    authorization: `Bearer ${tokens.access_token}`,
    // Required for ChatGPT subscription routing.
    'openai-beta': 'responses=experimental',
    originator: 'codex_cli_rs',
    'user-agent': 'sandcode-codex/1',
  })
  if (tokens.account_id) h.set('chatgpt-account-id', tokens.account_id)
  if (opts.session) {
    h.set('session_id', opts.session)
    h.set('openai-conversation-id', opts.session)
  }
  return h
}

// ─────────────────────────────────────────────────────────────
// Public factory — matches the return shape of createOpenAICompatClient
// ─────────────────────────────────────────────────────────────

export interface CodexClientConfig {
  defaultModel: string
  /** Optional session id for correlation. */
  sessionId?: string
  /** Reasoning defaults; caller can override via params.thinking. */
  reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high'
}

// Return type mirrors the @sandcode/sdk client shape — see the same pattern
// in openaiCompatAdapter.ts::createOpenAICompatClient.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createCodexClient(config: CodexClientConfig): any {
  logForDebugging(`[codex] Creating ChatGPT Codex client (base=${CODEX_BASE_URL})`)

  async function doCreate(params: Record<string, unknown>, _options?: Record<string, unknown>) {
    const abortController = new AbortController()
    const modelId = (params.model as string) || config.defaultModel
    const isStreaming = params.stream === true
    const fetchSignal = (_options as { signal?: AbortSignal } | undefined)?.signal
    const signal = fetchSignal ?? abortController.signal

    const tokens = await getValidTokens()

    const instructions = systemToInstructions(
      params.system as string | TextBlockParam[] | undefined,
    )
    const input = convertMessagesToResponses(params.messages as MessageParam[])
    const tools = convertToolsToResponses(params.tools as Tool[] | undefined)

    // Map thinking.type=enabled -> reasoning effort. Default to 'medium' for
    // GPT-5 models (Codex CLI's default). Disabled -> no `reasoning` param.
    const thinkingParam = params.thinking as
      | { type?: 'enabled' | 'disabled' }
      | undefined
    const reasoning =
      thinkingParam?.type === 'enabled'
        ? { effort: config.reasoningEffort ?? 'medium', summary: 'auto' as const }
        : config.reasoningEffort
          ? { effort: config.reasoningEffort, summary: 'auto' as const }
          : undefined

    // The ChatGPT-subscription Codex backend is a *restricted* slice of the
    // Responses API. It rejects `max_output_tokens`, `temperature`, `store`,
    // and several other params that the paid `api.openai.com/v1/responses`
    // endpoint accepts. Keep the body minimal — what the real Codex CLI
    // sends: model + instructions + input + tools + stream + reasoning.
    const body: ResponsesRequest = {
      model: modelId,
      ...(instructions ? { instructions } : {}),
      input,
      ...(tools ? { tools, tool_choice: 'auto' } : {}),
      stream: isStreaming,
      // Codex enforces stateless calls on the ChatGPT subscription backend —
      // it rejects omission of this field with "Store must be set to false".
      store: false,
      ...(reasoning ? { reasoning } : {}),
      ...(config.sessionId ? { prompt_cache_key: config.sessionId } : {}),
    }

    const headers = buildCodexHeaders(tokens, { session: config.sessionId })
    const url = `${CODEX_BASE_URL}${CODEX_RESPONSES_PATH}`

    logForDebugging(
      `[codex] POST ${url} model=${modelId} stream=${isStreaming} items=${input.length}`,
    )

    const res = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(body),
      signal,
    })

    recordCodexUsageHeaders(res.headers)

    if (!res.ok) {
      const text = await res.text().catch(() => '')
      let parsed: { error?: { type?: string; message?: string; plan_type?: string; resets_at?: number } }
      try {
        parsed = JSON.parse(text)
      } catch {
        parsed = {}
      }
      if (parsed.error?.type === 'usage_limit_reached') {
        recordCodexCapHit({
          planType: parsed.error.plan_type,
          resetsAt: parsed.error.resets_at,
        })
      }
      const errHeaders: Record<string, string> = {}
      res.headers.forEach((v, k) => {
        errHeaders[k] = v
      })
      throw new APIError(
        res.status,
        parsed.error as object | undefined,
        `Codex API error (${res.status}): ${parsed.error?.message ?? text.slice(0, 200)}`,
        errHeaders,
      )
    }

    if (isStreaming) {
      if (!res.body) {
        throw new APIError(undefined, undefined, 'Codex stream missing body', undefined)
      }
      const gen = streamResponsesToSdk(res.body, modelId, abortController)
      return new SandcodeCompatStream(gen, abortController)
    }

    const json = (await res.json()) as ResponsesObject
    return convertResponseToSdk(json, modelId)
  }

  // Proxy shaped like @sandcode/sdk. The runtime calls
  // `client.beta.messages.create({...}, {...}).withResponse()` to pull the
  // underlying HTTP response for headers/usage — the returned Promise must
  // carry a `.withResponse()` method, mirroring what openaiCompatAdapter
  // does. Without it you get: "... .withResponse is not a function".
  const wrapCreate = (
    params: Record<string, unknown>,
    options?: Record<string, unknown>,
  ) => {
    const promise = doCreate(params, options)
    return Object.assign(promise, {
      withResponse: async () => ({
        data: await promise,
        // We don't keep the raw Response around (streams are already
        // consumed by the time doCreate resolves). An empty Response is
        // enough for downstream code that only checks headers that
        // recordCodexUsageHeaders already captured.
        response: new Response(),
        request_id: `codex_${Date.now()}`,
      }),
    })
  }

  const client = {
    beta: { messages: { create: wrapCreate } },
    messages: {
      create: (params: Record<string, unknown>, options?: Record<string, unknown>) =>
        client.beta.messages.create(params, options),
    },
  }
  return client
}
