export * as AISDK from "./aisdk"

import { makeLocationNode } from "./effect/app-node"
import type {
  JSONSchema7,
  JSONValue,
  LanguageModelV3,
  LanguageModelV3CallOptions,
  LanguageModelV3FinishReason,
  LanguageModelV3FunctionTool,
  LanguageModelV3Message,
  LanguageModelV3Prompt,
  LanguageModelV3StreamPart,
  LanguageModelV3ToolChoice,
  SharedV3ProviderOptions,
} from "@ai-sdk/provider"
import {
  FinishReason,
  InvalidProviderOutputReason,
  LLMEvent,
  LLMError,
  Model,
  ProviderID,
  ProviderMetadata,
  ToolResultValue,
  UnknownProviderReason,
  type ContentPart,
  type LLMRequest,
  type ToolDefinition,
  type UsageInput,
} from "@opencode-ai/ai"
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
import { ProviderShared } from "@opencode-ai/ai/protocols/shared"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { State } from "./state"

type SDK = any
type UserContent = Extract<LanguageModelV3Message, { role: "user" }>["content"]
type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["content"]
type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>

export interface SDKEvent {
  readonly model: ModelV2.Info
  readonly package: string
  readonly options: Record<string, any>
  sdk?: SDK
}

export interface LanguageEvent {
  readonly model: ModelV2.Info
  readonly sdk: SDK
  readonly options: Record<string, any>
  language?: LanguageModelV3
}

function wrapSSE(res: Response, ms: number, ctl: AbortController) {
  if (typeof ms !== "number" || ms <= 0) return res
  if (!res.body) return res
  if (!res.headers.get("content-type")?.includes("text/event-stream")) return res

  const reader = res.body.getReader()
  const body = new ReadableStream<Uint8Array>({
    async pull(ctrl) {
      const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
        const id = setTimeout(() => {
          const err = new Error("SSE read timed out")
          ctl.abort(err)
          void reader.cancel(err)
          reject(err)
        }, ms)

        reader.read().then(
          (part) => {
            clearTimeout(id)
            resolve(part)
          },
          (err) => {
            clearTimeout(id)
            reject(err)
          },
        )
      })

      if (part.done) {
        ctrl.close()
        return
      }

      ctrl.enqueue(part.value)
    },
    async cancel(reason) {
      ctl.abort(reason)
      await reader.cancel(reason)
    },
  })

  return new Response(body, {
    headers: new Headers(res.headers),
    status: res.status,
    statusText: res.statusText,
  })
}

function prepareOptions(model: ModelV2.Info, pkg: string) {
  const projected = mapBodyToProviderOptions(model, pkg)
  const options: Record<string, any> = {
    name: model.providerID,
    ...(model.settings ?? {}),
    headers: model.headers,
    body: projected.body,
  }

  const customFetch = options.fetch
  const chunkTimeout = options.chunkTimeout
  delete options.chunkTimeout
  options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
    const opts = { ...(init ?? {}) }
    const signals = [
      opts.signal,
      typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined,
      options.timeout !== undefined && options.timeout !== null && options.timeout !== false
        ? AbortSignal.timeout(options.timeout)
        : undefined,
    ].filter((item): item is AbortSignal | AbortController => Boolean(item))
    const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController)
    const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item))
    if (abortSignals.length === 1) opts.signal = abortSignals[0]
    if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)

    if (
      (pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") &&
      opts.body &&
      opts.method === "POST"
    ) {
      const body = JSON.parse(opts.body as string)
      if (body.store !== true && Array.isArray(body.input)) {
        for (const item of body.input) {
          if ("id" in item) delete item.id
        }
        opts.body = JSON.stringify(body)
      }
    }

    if (typeof opts.body === "string" && model.body !== undefined) {
      const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
      if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
        opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body))
      }
    }

    const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
      ...opts,
      timeout: false,
    })
    if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
    return wrapSSE(res, chunkTimeout, chunkAbortCtl)
  }

  return options
}

export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
  providerID: ProviderV2.ID,
  cause: Schema.Defect(),
}) {}

function initError(providerID: ProviderV2.ID) {
  return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
}

export interface Interface {
  readonly hook: {
    readonly sdk: (
      callback: (event: SDKEvent) => Effect.Effect<void> | void,
    ) => Effect.Effect<State.Registration, never, Scope.Scope>
    readonly language: (
      callback: (event: LanguageEvent) => Effect.Effect<void> | void,
    ) => Effect.Effect<State.Registration, never, Scope.Scope>
  }
  readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
  readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
  readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
  readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}

export const locationLayer = Layer.effect(
  Service,
  Effect.gen(function* () {
    let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
    let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
    const languages = new Map<string, LanguageModelV3>()
    const sdks = new Map<string, SDK>()
    const functionIDs = new WeakMap<object, number>()
    let nextFunctionID = 0
    const cacheKey = (input: unknown) =>
      JSON.stringify(input, (_key, value: unknown) => {
        if (typeof value !== "function") return value
        const existing = functionIDs.get(value)
        if (existing !== undefined) return `function:${existing}`
        const id = nextFunctionID++
        functionIDs.set(value, id)
        return `function:${id}`
      }) ?? ""

    const register = <Event>(
      hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
      update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
    ) =>
      Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
        const scope = yield* Scope.Scope
        let active = true
        update([...hooks(), callback])
        const dispose = Effect.sync(() => {
          if (!active) return
          active = false
          update(hooks().filter((item) => item !== callback))
        })
        yield* Scope.addFinalizer(scope, dispose)
        return { dispose }
      })

    const run = Effect.fnUntraced(function* <Event>(
      hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
      event: Event,
    ) {
      for (const hook of hooks) {
        const result = hook(event)
        if (Effect.isEffect(result)) yield* result
      }
      return event
    })

    const service = Service.of({
      hook: {
        sdk: register(
          () => sdkHooks,
          (next) => (sdkHooks = next),
        ),
        language: register(
          () => languageHooks,
          (next) => (languageHooks = next),
        ),
      },
      runSDK: (event) => run(sdkHooks, event),
      runLanguage: (event) => run(languageHooks, event),
      language: Effect.fn("AISDK.language")(function* (model) {
        const key = cacheKey({
          providerID: model.providerID,
          id: model.id,
          modelID: model.modelID,
          package: model.package,
          settings: model.settings,
          headers: model.headers,
          body: model.body,
          limit: model.limit,
        })
        const existing = languages.get(key)
        if (existing) return existing
        if (!ProviderV2.isAISDK(model.package))
          return yield* new InitError({
            providerID: model.providerID,
            cause: new Error(`Unsupported package ${model.package}`),
          })

        const packageName = ProviderV2.packageName(model.package)
        const options = prepareOptions(model, packageName)
        const sdkKey = cacheKey({
          providerID: model.providerID,
          package: packageName,
          settings: model.settings,
          headers: model.headers,
          body: model.body,
        })
        const sdk =
          sdks.get(sdkKey) ??
          (yield* service.runSDK({ model, package: packageName, options }).pipe(initError(model.providerID))).sdk
        if (!sdk)
          return yield* new InitError({
            providerID: model.providerID,
            cause: new Error("No AISDK provider plugin returned an SDK"),
          })
        sdks.set(sdkKey, sdk)
        const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
        const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.modelID ?? model.id)).pipe(
          initError(model.providerID),
        )
        languages.set(key, language)
        return language
      }),
      model: Effect.fn("AISDK.model")(function* (model) {
        return modelFromLanguage(model, yield* service.language(model))
      }),
    })
    return service
  }),
)

export const defaultLayer = locationLayer

function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
  const packageName = ProviderV2.packageName(info.package!)
  const projected = mapBodyToProviderOptions(info, packageName)
  const optionKey = providerOptionKey(packageName, info.providerID)
  const providerOptions = (() => {
    if (projected.settings === undefined) return
    if (packageName === "@ai-sdk/gateway") return gatewayProviderOptions(info.modelID ?? info.id, projected.settings)
    if (packageName === "@ai-sdk/azure") return { openai: projected.settings, azure: projected.settings }
    return { [optionKey]: projected.settings }
  })()
  const route: AnyRoute = {
    id: `ai-sdk:${packageName}`,
    provider: ProviderID.make(info.providerID),
    providerMetadataKey: optionKey,
    protocol: "ai-sdk",
    endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
    auth: Auth.none,
    transport: {
      id: "ai-sdk",
      prepare: (input) => Effect.succeed(input.body),
      frames: () => Stream.empty,
    },
    defaults: {
      headers: info.headers,
      http:
        projected.body === undefined && info.headers === undefined
          ? undefined
          : {
              body: projected.body === undefined ? undefined : { ...projected.body },
              headers: info.headers,
            },
      limits: { context: info.limit.context, output: info.limit.output },
      providerOptions,
    },
    body: {
      schema: Schema.Unknown,
      from: (request) => Effect.succeed(callOptions(request)),
    },
    with: () => route,
    model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
    prepareTransport: (body) => Effect.succeed(body),
    streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
  }
  return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route })
}

function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) {
  const gateway =
    typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
      ? Object.fromEntries(Object.entries(settings.gateway))
      : undefined
  const model = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== "gateway"))
  if (Object.keys(model).length === 0) return gateway === undefined ? undefined : { gateway }

  const separator = modelID.indexOf("/")
  const prefix = separator > 0 ? modelID.slice(0, separator) : undefined
  if (prefix)
    return { ...(gateway === undefined ? {} : { gateway }), [prefix === "amazon" ? "bedrock" : prefix]: model }
  if (typeof gateway === "object" && gateway !== null && !Array.isArray(gateway))
    return { gateway: { ...gateway, ...model } }
  return { gateway: model }
}

function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
  if (packageName === "@ai-sdk/google") return "google"
  if (packageName === "@ai-sdk/google-vertex") return "vertex"
  if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
  if (packageName === "@ai-sdk/amazon-bedrock") return "bedrock"
  if (packageName === "@ai-sdk/amazon-bedrock/mantle") return "openai"
  if (packageName === "@ai-sdk/azure") return "azure"
  if (packageName === "@ai-sdk/github-copilot") return "copilot"
  if (packageName === "@jerome-benoit/sap-ai-provider-v2") return "sap-ai"
  if (packageName === "@ai-sdk/openai-compatible") return providerID.split(".")[0]
  if (packageName === "@openrouter/ai-sdk-provider") return "openrouter"
  if (packageName === "ai-gateway-provider") return "openaiCompatible"
  if (packageName?.startsWith("@ai-sdk/")) return packageName.slice("@ai-sdk/".length)
  return providerID
}

function requestSettings(settings: Readonly<Record<string, unknown>> | undefined) {
  if (settings === undefined) return undefined
  const result = Object.fromEntries(
    Object.entries(settings).filter(
      ([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key),
    ),
  )
  return Object.keys(result).length === 0 ? undefined : result
}

function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) {
  const settings = requestSettings(model.settings)
  const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning)
  const forceReasoning =
    ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) &&
    (pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined)
  const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings
  if (!pro) return { settings: normalized, body: model.body }
  const body = { ...model.body }
  delete body.reasoning
  return {
    settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }),
    body: Object.keys(body).length === 0 ? undefined : body,
  }
}

function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
  return {
    prompt: prompt(request),
    maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
    temperature: request.generation?.temperature,
    stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
    topP: request.generation?.topP,
    topK: request.generation?.topK,
    presencePenalty: request.generation?.presencePenalty,
    frequencyPenalty: request.generation?.frequencyPenalty,
    seed: request.generation?.seed,
    responseFormat: responseFormat(request),
    tools: request.tools.map(tool),
    toolChoice: toolChoice(request.toolChoice),
    headers: request.http?.headers,
    providerOptions: providerOptions(request.providerOptions),
  }
}

function prompt(request: LLMRequest): LanguageModelV3Prompt {
  const system = request.system
    .map((part) => part.text)
    .filter(Boolean)
    .join("\n\n")
  const messages = request.messages.flatMap(message)
  if (!system.length) return messages
  return [{ role: "system", content: system }, ...messages]
}

function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[] {
  switch (input.role) {
    case "system":
      return [{ role: "system", content: input.content.flatMap(text).join("\n\n") }]
    case "user":
      return [{ role: "user", content: input.content.flatMap(userPart) }]
    case "assistant":
      return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
    case "tool": {
      const content = input.content.flatMap(toolResultPart)
      return content.length ? [{ role: "tool", content }] : []
    }
  }
}

function text(part: ContentPart) {
  return part.type === "text" ? [part.text] : []
}

function userPart(part: ContentPart): UserContent {
  if (part.type === "text") return [{ type: "text", text: part.text }]
  if (part.type === "media")
    return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
  return []
}

function assistantPart(part: ContentPart): AssistantContent {
  switch (part.type) {
    case "text":
      return [{ type: "text", text: part.text }]
    case "media":
      return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
    case "reasoning":
      return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }]
    case "tool-call":
      return [
        {
          type: "tool-call",
          toolCallId: part.id,
          toolName: part.name,
          input: part.input,
          providerExecuted: part.providerExecuted,
          providerOptions: providerOptions(part.providerMetadata),
        },
      ]
    case "tool-result":
      return toolResultPart(part)
  }
}

function toolResultPart(part: ContentPart): ToolResultContent[] {
  if (part.type !== "tool-result") return []
  return [
    {
      type: "tool-result",
      toolCallId: part.id,
      toolName: part.name,
      output: toolOutput(part.result),
      providerOptions: providerOptions(part.providerMetadata),
    },
  ]
}

function toolOutput(result: ToolResultValue) {
  switch (result.type) {
    case "text":
    case "error":
      return { type: "text" as const, value: messageValue(result.value) }
  }
  return { type: "json" as const, value: jsonValue(result.value) }
}

function tool(input: ToolDefinition): LanguageModelV3FunctionTool {
  return {
    type: "function",
    name: input.name,
    description: input.description,
    inputSchema: input.inputSchema as JSONSchema7,
  }
}

function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice | undefined {
  if (!input) return undefined
  if (input.type === "tool") return input.name === undefined ? undefined : { type: "tool", toolName: input.name }
  return { type: input.type }
}

function responseFormat(request: LLMRequest): LanguageModelV3CallOptions["responseFormat"] {
  if (request.responseFormat?.type === "json")
    return { type: "json", schema: request.responseFormat.schema as JSONSchema7 }
  if (request.responseFormat) return { type: "text" }
}

function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined {
  if (!input) return undefined
  return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
}

function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
  const state = { step: 0, toolNames: {} as Record<string, string> }
  return Stream.concat(
    Stream.make(LLMEvent.stepStart({ index: state.step })),
    Stream.unwrap(
      Effect.tryPromise({
        try: () => language.doStream(options),
        catch: (error) => llmError("doStream", error),
      }).pipe(
        Effect.map((result) =>
          Stream.fromReadableStream({
            evaluate: () => result.stream,
            onError: (error) => llmError("readStream", error),
          }).pipe(
            Stream.mapEffect((event) => streamPartEvents(state, event)),
            Stream.flatMap((events) => Stream.fromIterable(events)),
          ),
        ),
      ),
    ),
  )
}

function streamPartEvents(
  state: { step: number; toolNames: Record<string, string> },
  event: LanguageModelV3StreamPart,
): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> {
  switch (event.type) {
    case "stream-start":
    case "response-metadata":
    case "raw":
    case "file":
    case "source":
    case "tool-approval-request":
      return Effect.succeed([])
    case "text-start":
      return Effect.succeed([
        LLMEvent.textStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
      ])
    case "text-delta":
      return Effect.succeed([
        LLMEvent.textDelta({
          id: event.id,
          text: event.delta,
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
      ])
    case "text-end":
      return Effect.succeed([
        LLMEvent.textEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
      ])
    case "reasoning-start":
      return Effect.succeed([
        LLMEvent.reasoningStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
      ])
    case "reasoning-delta":
      return Effect.succeed([
        LLMEvent.reasoningDelta({
          id: event.id,
          text: event.delta,
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
      ])
    case "reasoning-end":
      return Effect.succeed([
        LLMEvent.reasoningEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
      ])
    case "tool-input-start":
      state.toolNames[event.id] = event.toolName
      return Effect.succeed([
        LLMEvent.toolInputStart({
          id: event.id,
          name: event.toolName,
          providerExecuted: event.providerExecuted,
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
      ])
    case "tool-input-delta":
      return Effect.succeed([
        LLMEvent.toolInputDelta({ id: event.id, name: state.toolNames[event.id] ?? "unknown", text: event.delta }),
      ])
    case "tool-input-end":
      return Effect.succeed([
        LLMEvent.toolInputEnd({
          id: event.id,
          name: state.toolNames[event.id] ?? "unknown",
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
      ])
    case "tool-call":
      state.toolNames[event.toolCallId] = event.toolName
      return ProviderShared.parseToolInput("aisdk", event.toolName, event.input).pipe(
        Effect.map((input) => [
          LLMEvent.toolCall({
            id: event.toolCallId,
            name: event.toolName,
            input,
            providerExecuted: event.providerExecuted,
            providerMetadata: providerMetadata(event.providerMetadata),
          }),
        ]),
        Effect.catch((error) =>
          event.providerExecuted
            ? Effect.fail(error)
            : Effect.succeed([
                LLMEvent.toolInputError({
                  id: event.toolCallId,
                  name: event.toolName,
                  raw: event.input,
                }),
              ]),
        ),
      )
    case "tool-result":
      delete state.toolNames[event.toolCallId]
      return Effect.succeed([
        LLMEvent.toolResult({
          id: event.toolCallId,
          name: event.toolName,
          result: ToolResultValue.make(event.result, event.isError ? "error" : "json"),
          providerExecuted: true,
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
      ])
    case "finish":
      return Effect.succeed([
        LLMEvent.stepFinish({
          index: state.step++,
          reason: finishReason(event.finishReason),
          usage: usage(event.usage),
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
        LLMEvent.finish({
          reason: finishReason(event.finishReason),
          usage: usage(event.usage),
          providerMetadata: providerMetadata(event.providerMetadata),
        }),
      ])
    case "error":
      return Effect.fail(llmError("stream", event.error))
  }
}

function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["usage"]): UsageInput | undefined {
  const output = {
    inputTokens: input.inputTokens.total,
    nonCachedInputTokens: input.inputTokens.noCache,
    cacheReadInputTokens: input.inputTokens.cacheRead,
    cacheWriteInputTokens: input.inputTokens.cacheWrite,
    outputTokens: input.outputTokens.total,
    reasoningTokens: input.outputTokens.reasoning,
    totalTokens:
      input.inputTokens.total === undefined || input.outputTokens.total === undefined
        ? undefined
        : input.inputTokens.total + input.outputTokens.total,
  }
  return Object.values(output).some((value) => value !== undefined) ? output : undefined
}

function finishReason(value: LanguageModelV3FinishReason): FinishReason {
  return value.unified === "other" ? "unknown" : value.unified
}

function providerMetadata(value: unknown) {
  return Schema.is(ProviderMetadata)(value) ? value : undefined
}

function jsonObject(input: Record<string, unknown>) {
  return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonValue(value)]))
}

function jsonValue(input: unknown): JSONValue {
  try {
    const encoded = JSON.stringify(input)
    return encoded === undefined ? null : (JSON.parse(encoded) as JSONValue)
  } catch {
    return messageValue(input)
  }
}

function messageValue(input: unknown) {
  if (typeof input === "string") return input
  try {
    return JSON.stringify(input) ?? String(input)
  } catch {
    return String(input)
  }
}

function llmError(method: string, error: unknown) {
  const reason =
    error instanceof LLMError
      ? new InvalidProviderOutputReason({ message: error.message })
      : new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
  return new LLMError({
    module: "AISDK",
    method,
    reason,
  })
}

export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })
