import path from "path"
import { pathToFileURL } from "url"
import { Effect } from "effect"
import { NoSuchModelError, type Provider as SDK } from "ai"
import * as Log from "@saeeol/core/util/log"
import { Npm } from "@saeeol/core/npm"
import { Hash } from "@saeeol/core/util/hash"
import * as Bus from "@/bus"
import { BUNDLED_PROVIDERS } from "./bundled-providers"
import { ProviderInstallEvent } from "./provider-events"
import { buildTimeoutSignal } from "@/saeeol/provider/provider"
import { InitError } from "./provider-types"
import type { State, BundledSDK } from "./provider-types"
import type { Model } from "./provider-schema"

const log = Log.create({ service: "provider" })

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,
  })
}

export async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
  try {
    using _ = log.time("getSDK", {
      providerID: model.providerID,
    })
    const provider = s.providers[model.providerID]
    const options = { ...provider.options }

    if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
      delete options.fetch
    }

    if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
      options["includeUsage"] = true
    }

    const baseURL = iife(() => {
      let url =
        typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
      if (!url) return

      const loader = s.varsLoaders[model.providerID]
      if (loader) {
        const vars = loader(options)
        for (const [key, value] of Object.entries(vars)) {
          const field = "${" + key + "}"
          url = url.replaceAll(field, value)
        }
      }

      url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
        const val = envs[String(key)]
        return val ?? item
      })
      return url
    })

    if (baseURL !== undefined) options["baseURL"] = baseURL
    if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
    if (model.headers)
      options["headers"] = {
        ...options["headers"],
        ...model.headers,
      }

    const key = Hash.fast(
      JSON.stringify({
        providerID: model.providerID,
        npm: model.api.npm,
        options,
      }),
    )
    const existing = s.sdk.get(key)
    if (existing) return existing

    const customFetch = options["fetch"]
    const chunkTimeout = options["chunkTimeout"]
    delete options["chunkTimeout"]

    options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
      const fetchFn = customFetch ?? fetch
      const opts = init ?? {}
      const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
      const timeout = buildTimeoutSignal(options)
      const signals: AbortSignal[] = []

      if (opts.signal) signals.push(opts.signal)
      if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
      if (timeout.signal) signals.push(timeout.signal)

      const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
      if (combined) opts.signal = combined

      if (
        (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") &&
        opts.body &&
        opts.method === "POST"
      ) {
        const body = JSON.parse(opts.body as string)
        const keepIds = body.store === true
        if (!keepIds && Array.isArray(body.input)) {
          for (const item of body.input) {
            if ("id" in item) {
              delete item.id
            }
          }
          opts.body = JSON.stringify(body)
        }
      }
      try {
        const res = await fetchFn(input, {
          ...opts,
          // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
          timeout: false,
        })
        timeout.clear()
        if (!chunkAbortCtl) return res
        return wrapSSE(res, chunkTimeout, chunkAbortCtl)
      } catch (err) {
        timeout.clear()
        throw err
      }
    }

    const bundledLoader = BUNDLED_PROVIDERS[model.api.npm]
    if (bundledLoader) {
      log.info("using bundled provider", {
        providerID: model.providerID,
        pkg: model.api.npm,
      })
      const factory = await bundledLoader()
      const loaded = factory({
        name: model.providerID,
        ...options,
      })
      s.sdk.set(key, loaded)
      return loaded as SDK
    }

    let installedPath: string
    if (!model.api.npm.startsWith("file://")) {
      log.info("installing provider package", { providerID: model.providerID, pkg: model.api.npm })
      void Bus.publish(ProviderInstallEvent.Started, {
        providerID: model.providerID,
        pkg: model.api.npm,
      }).catch(() => {})
      try {
        const item = await Npm.add(model.api.npm)
        if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
        installedPath = item.entrypoint
        void Bus.publish(ProviderInstallEvent.Completed, {
          providerID: model.providerID,
          pkg: model.api.npm,
        }).catch(() => {})
      } catch (installErr) {
        void Bus.publish(ProviderInstallEvent.Failed, {
          providerID: model.providerID,
          pkg: model.api.npm,
          error: installErr instanceof Error ? installErr.message : String(installErr),
        }).catch(() => {})
        throw installErr
      }
    } else {
      log.info("loading local provider", { pkg: model.api.npm })
      installedPath = model.api.npm
    }

    const importSpec = installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
    const mod = await import(importSpec)

    const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
    const loaded = fn({
      name: model.providerID,
      ...options,
    })
    s.sdk.set(key, loaded)
    return loaded as SDK
  } catch (e) {
    throw new InitError({ providerID: model.providerID }, { cause: e })
  }
}

function iife<T>(fn: () => T): T {
  return fn()
}
