import path from "path"
import fuzzysort from "fuzzysort"
import { Effect, Layer } from "effect"
import { NoSuchModelError } from "ai"
import { sortBy } from "remeda"
import { Config } from "@/config/config"
import { Auth } from "../auth"
import { Plugin } from "../plugin"
import { Env } from "../env"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { InstanceState } from "@/effect/instance-state"
import * as ModelsDev from "./models"
import { makeRuntime } from "@/effect/run-service"
import { Global } from "@saeeol/core/global"
import { isRecord } from "@/util/record"
import { ProviderID, ModelID } from "./schema"
import { Model, Info } from "./provider-schema"
import { Service, ModelNotFoundError } from "./provider-types"
import type { Interface, State } from "./provider-types"
import { resolveSDK } from "./provider-resolve"
import { mapValues } from "remeda"
import { initState } from "./provider-state"
import { saeeolSmallModelPriority } from "@/saeeol/provider/provider"

const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
export function sort<T extends { id: string }>(models: T[]) {
  return sortBy(
    models,
    [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
    [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
    [(model) => model.id, "desc"],
  )
}

export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
  return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
}

export function parseModel(model: string) {
  const [providerID, ...rest] = model.split("/")
  return {
    providerID: ProviderID.make(providerID),
    modelID: ModelID.make(rest.join("/")),
  }
}

const layer: Layer.Layer<
  Service,
  never,
  Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service | ModelsDev.Service
> = Layer.effect(
  Service,
  Effect.gen(function* () {
    const fs = yield* AppFileSystem.Service
    const config = yield* Config.Service
    const auth = yield* Auth.Service
    const env = yield* Env.Service
    const plugin = yield* Plugin.Service
    const modelsDevSvc = yield* ModelsDev.Service

    const state = yield* initState({
      fs,
      config: { get: () => config.get() },
      auth: { get: (id) => auth.get(id).pipe(Effect.orDie), all: () => auth.all().pipe(Effect.orDie) },
      env: { all: () => env.all(), get: (key) => env.get(key) },
      plugin: { list: () => plugin.list() },
      modelsDevSvc: { get: () => modelsDevSvc.get() },
    })

    const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))

    const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
      InstanceState.use(state, (s) => s.providers[providerID]),
    )

    const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
      const s = yield* InstanceState.get(state)
      const provider = s.providers[providerID]
      if (!provider) {
        const available = Object.keys(s.providers)
        const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
        throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
      }

      const info = provider.models[modelID]
      if (!info) {
        const available = Object.keys(provider.models)
        const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
        throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
      }
      return info
    })

    const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
      const s = yield* InstanceState.get(state)
      const envs = yield* env.all()
      const key = `${model.providerID}/${model.id}`
      if (s.models.has(key)) return s.models.get(key)!

      return yield* Effect.promise(async () => {
        const provider = s.providers[model.providerID]
        const sdk = await resolveSDK(model, s, envs)

        try {
          const language = s.modelLoaders[model.providerID]
            ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
                ...provider.options,
                ...model.options,
              })
            : sdk.languageModel(model.api.id)
          s.models.set(key, language)
          return language
        } catch (e) {
          if (e instanceof NoSuchModelError)
            throw new ModelNotFoundError(
              {
                modelID: model.id,
                providerID: model.providerID,
              },
              { cause: e },
            )
          throw e
        }
      })
    })

    const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
      const s = yield* InstanceState.get(state)
      const provider = s.providers[providerID]
      if (!provider) return undefined
      for (const item of query) {
        for (const modelID of Object.keys(provider.models)) {
          if (modelID.includes(item)) return { providerID, modelID }
        }
      }
      return undefined
    })

    const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
      const cfg = yield* config.get()

      if (cfg.small_model) {
        const parsed = parseModel(cfg.small_model)
        return yield* getModel(parsed.providerID, parsed.modelID)
      }

      const s = yield* InstanceState.get(state)
      const provider = s.providers[providerID]
      if (!provider) return undefined

      let prio = [
        "claude-haiku-4-5",
        "claude-haiku-4.5",
        "3-5-haiku",
        "3.5-haiku",
        "gemini-3-flash",
        "gemini-2.5-flash",
        "gpt-5-nano",
      ]
      if (providerID.startsWith("saeeol")) {
        prio = ["gpt-5-nano"]
      }
      if (providerID.startsWith("github-copilot")) {
        prio = ["gpt-5-mini", "claude-haiku-4.5", ...prio]
      }
      const saeeolPriority = saeeolSmallModelPriority(providerID)
      if (saeeolPriority) prio = saeeolPriority
      for (const item of prio) {
        if (providerID === ProviderID.amazonBedrock) {
          const crossRegionPrefixes = ["global.", "us.", "eu."]
          const candidates = Object.keys(provider.models).filter((m) => m.includes(item))

          const globalMatch = candidates.find((m) => m.startsWith("global."))
          if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))

          const region = provider.options?.region
          if (region) {
            const regionPrefix = region.split("-")[0]
            if (regionPrefix === "us" || regionPrefix === "eu") {
              const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
              if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
            }
          }

          const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
          if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
        } else {
          for (const model of Object.keys(provider.models)) {
            if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
          }
        }
      }
      const saeeolFallback = s.providers[ProviderID.make("saeeol")]
      if (saeeolFallback?.models["saeeol-auto/small"]) {
        return yield* getModel(ProviderID.make("saeeol"), ModelID.make("saeeol-auto/small"))
      }

      return undefined
    })

    const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
      const cfg = yield* config.get()
      if (cfg.model) return parseModel(cfg.model)

      const s = yield* InstanceState.get(state)
      const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
        Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
          if (!isRecord(x) || !Array.isArray(x.recent)) return []
          return x.recent.flatMap((item) => {
            if (!isRecord(item)) return []
            if (typeof item.providerID !== "string") return []
            if (typeof item.modelID !== "string") return []
            return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
          })
        }),
        Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
      )
      for (const entry of recent) {
        const provider = s.providers[entry.providerID]
        if (!provider) continue
        if (!provider.models[entry.modelID]) continue
        return { providerID: entry.providerID, modelID: entry.modelID }
      }

      const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id))
      if (!provider) throw new Error("no providers found")
      const [model] = sort(Object.values(provider.models))
      if (!model) throw new Error("no models found")
      return {
        providerID: provider.id,
        modelID: model.id,
      }
    })

    return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
  }),
)

export const defaultLayer = Layer.suspend(() =>
  layer.pipe(
    Layer.provide(AppFileSystem.defaultLayer),
    Layer.provide(Env.defaultLayer),
    Layer.provide(Config.defaultLayer),
    Layer.provide(Auth.defaultLayer),
    Layer.provide(Plugin.defaultLayer),
    Layer.provide(ModelsDev.defaultLayer),
  ),
)

const { runPromise: runProviderPromise } = makeRuntime(Service, defaultLayer)
export const list = () => runProviderPromise((svc) => svc.list())
export const getModelExport = (providerID: ProviderID, modelID: ModelID) =>
  runProviderPromise((svc) => svc.getModel(providerID, modelID))
export const getProviderExport = (providerID: ProviderID) => runProviderPromise((svc) => svc.getProvider(providerID))
export const getLanguage = (model: Model) => runProviderPromise((svc) => svc.getLanguage(model))
export const getSmallModel = (providerID: ProviderID) => runProviderPromise((svc) => svc.getSmallModel(providerID))
export const defaultModel = () => runProviderPromise((svc) => svc.defaultModel())
