import { Auth } from "../../auth"
import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { ModelsDev } from "@/provider/models"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
import { Config } from "@/config/config"
import { Global } from "@saeeol/core/global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
import { Process } from "@/util/process"
import { text } from "node:stream/consumers"
import { Effect } from "effect"
import { Npm } from "@saeeol/core/npm"
import { put, handlePluginAuth, resolvePluginProviders } from "./providers-auth"

const getModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get()))
const refreshModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.refresh(true)))

export const ProvidersCommand = cmd({
  command: "auth",
  aliases: ["providers"],
  describe: "manage AI providers and credentials",
  builder: (yargs) =>
    yargs
      .command(ProvidersListCommand)
      .command(ProvidersLoginCommand)
      .command(ProvidersLogoutCommand)
      .command(ProvidersInstallCommand)
      .demandCommand(),
  async handler() {},
})

export const ProvidersListCommand = cmd({
  command: "list",
  aliases: ["ls"],
  describe: "list providers",
  async handler() {
    await Instance.provide({
      directory: process.cwd(),
      async fn() {
        UI.empty()
        const authPath = path.join(Global.Path.data, "auth.json")
        const homedir = os.homedir()
        const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
        prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
        const results = await AppRuntime.runPromise(
          Effect.gen(function* () {
            const auth = yield* Auth.Service
            return Object.entries(yield* auth.all())
          }),
        )
        const database = await getModels()
        for (const [providerID, result] of results) {
          const name = database[providerID]?.name || providerID
          prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
        }
        prompts.outro(`${results.length} credentials`)

        const activeEnvVars: Array<{ provider: string; envVar: string }> = []
        for (const [providerID, provider] of Object.entries(database)) {
          for (const envVar of provider.env) {
            if (process.env[envVar]) activeEnvVars.push({ provider: provider.name || providerID, envVar })
          }
        }
        if (activeEnvVars.length > 0) {
          UI.empty()
          prompts.intro("Environment")
          for (const { provider, envVar } of activeEnvVars) prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
          prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
        }
      },
    })
  },
})

export const ProvidersLoginCommand = cmd({
  command: "login [url]",
  describe: "log in to a provider",
  builder: (yargs) =>
    yargs
      .positional("url", { describe: "saeeol auth provider", type: "string" })
      .option("provider", { alias: ["p"], describe: "provider id or name", type: "string" })
      .option("method", { alias: ["m"], describe: "login method label", type: "string" }),
  async handler(args) {
    await Instance.provide({
      directory: process.cwd(),
      async fn() {
        UI.empty()
        prompts.intro("Add credential")
        if (args.url) return await loginViaUrl(args.url)

        await refreshModels().catch(() => {})
        const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
        const disabled = new Set(config.disabled_providers ?? [])
        const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined

        const providers = await getModels().then((x) => {
          const filtered: Record<string, (typeof x)[string]> = {}
          for (const [key, value] of Object.entries(x)) {
            if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) filtered[key] = value
          }
          return filtered
        })
        const hooks = await AppRuntime.runPromise(
          Effect.gen(function* () { const plugin = yield* Plugin.Service; return yield* plugin.list() }),
        )
        const priority: Record<string, number> = {
          saeeol: 0, anthropic: 2, "github-copilot": 3, openai: 4, google: 5, openrouter: 6, vercel: 7,
        }
        const pluginProviders = resolvePluginProviders({
          hooks, existingProviders: providers, disabled, enabled,
          providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).flatMap(([id, p]) => (p ? [[id, p.name]] : []))),
        })
        const options = [
          ...pipe(
            providers, values(),
            sortBy((x) => priority[x.id] ?? 99, (x) => x.name ?? x.id),
            map((x) => ({ label: x.name, value: x.id, hint: { saeeol: "recommended", openai: "ChatGPT login or API key" }[x.id] })),
          ),
          ...pluginProviders.map((x) => ({ label: x.name, value: x.id, hint: "plugin" })),
        ]

        let provider = await resolveProvider(args.provider, options)
        const plugin = hooks.findLast((x) => x.auth?.provider === provider)
        if (plugin?.auth) {
          const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
          if (handled) return
        }
        if (provider === "other") {
          provider = await promptCustomProvider(hooks, args.method)
          prompts.log.warn(`This only stores a credential for ${provider} - configure it in saeeol.json.`)
        }
        logProviderHints(provider)
        const key = await prompts.password({ message: "Enter your API key", validate: (x) => (x && x.length > 0 ? undefined : "Required") })
        if (prompts.isCancel(key)) throw new UI.CancelledError()
        await put(provider, { type: "api", key })
        prompts.outro("Done")
      },
    })
  },
})

async function loginViaUrl(url: string) {
  const normalized = url.replace(/\/+$/, "")
  const wellknown = (await fetch(`${normalized}/.well-known/saeeol`).then((x) => x.json())) as {
    auth: { command: string[]; env: string }
  }
  prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
  const proc = Process.spawn(wellknown.auth.command, { stdout: "pipe" })
  if (!proc.stdout) { prompts.log.error("Failed"); prompts.outro("Done"); return }
  const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)])
  if (exit !== 0) { prompts.log.error("Failed"); prompts.outro("Done"); return }
  await put(normalized, { type: "wellknown", key: wellknown.auth.env, token: token.trim() })
  prompts.log.success("Logged into " + normalized)
  prompts.outro("Done")
}

async function resolveProvider(input: string | undefined, options: Array<{ label: string; value: string; hint?: string }>) {
  if (input) {
    const byID = options.find((x) => x.value === input)
    const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
    const alias = input.toLowerCase() === "codex" ? options.find((x) => x.value === "openai") : undefined
    const match = byID ?? byName ?? alias
    if (!match) { prompts.log.error(`Unknown provider "${input}"`); process.exit(1) }
    return match.value
  }
  const selected = await prompts.autocomplete({
    message: "Select provider", maxItems: 8,
    options: [...options, { value: "other", label: "Other" }],
  })
  if (prompts.isCancel(selected)) throw new UI.CancelledError()
  return selected as string
}

async function promptCustomProvider(hooks: import("@saeeol/plugin").Hooks[], method?: string) {
  const custom = await prompts.text({
    message: "Enter provider id",
    validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
  })
  if (prompts.isCancel(custom)) throw new UI.CancelledError()
  const provider = custom.replace(/^@ai-sdk\//, "")
  const customPlugin = hooks.findLast((x) => x.auth?.provider === provider)
  if (customPlugin?.auth) {
    const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, method)
    if (handled) return provider
  }
  return provider
}

function logProviderHints(provider: string) {
  if (provider === "amazon-bedrock") {
    prompts.log.info(
      "Amazon Bedrock auth priority:\n  1. Bearer token (AWS_BEARER_TOKEN_BEDROCK)\n  2. AWS credential chain\n" +
        "Configure via saeeol.json or AWS env vars.",
    )
  }
  if (provider === "saeeol") prompts.log.info("Create an api key at https://saeeol.ai/auth")
  if (provider === "vercel") prompts.log.info("Create an api key at https://vercel.link/ai-gateway-token")
  if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
    prompts.log.info("Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN env vars.")
  }
}

export const ProvidersLogoutCommand = cmd({
  command: "logout",
  describe: "log out from a configured provider",
  async handler() {
    await Instance.provide({
      directory: process.cwd(),
      async fn() {
        UI.empty()
        const credentials = await Auth.all().then((x) => Object.entries(x))
        prompts.intro("Remove credential")
        if (credentials.length === 0) { prompts.log.error("No credentials found"); return }
        const database = await getModels()
        const selected = await prompts.select({
          message: "Select provider",
          options: credentials.map(([key, value]) => ({
            label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")", value: key,
          })),
        })
        if (prompts.isCancel(selected)) throw new UI.CancelledError()
        await AppRuntime.runPromise(
          Effect.gen(function* () { const auth = yield* Auth.Service; yield* auth.remove(selected as string) }),
        )
        prompts.outro("Logout successful")
      },
    })
  },
})

export const ProvidersInstallCommand = cmd({
  command: "install [providers..]",
  describe: "pre-install provider SDK packages (skip runtime download delay)",
  builder: (yargs) =>
    yargs.positional("providers", {
      describe: "provider id(s) to install (e.g. anthropic openai google)",
      type: "string",
      array: true,
    }),
  async handler(args) {
    await Instance.provide({
      directory: process.cwd(),
      async fn() {
        UI.empty()
        prompts.intro("Install provider SDKs")

        await refreshModels().catch(() => {})
        const database = await getModels()

        // Build npm package → provider names mapping from models.dev data
        const npmToNames: Record<string, Set<string>> = {}
        for (const [id, info] of Object.entries(database)) {
          const pkg = info.npm
          if (!pkg) continue
          if (!npmToNames[pkg]) npmToNames[pkg] = new Set()
          npmToNames[pkg].add(info.name || id)
        }

        // Resolve target packages
        const targets = args.providers?.length
          ? args.providers
          : await (async () => {
              const options = Object.entries(npmToNames).map(([pkg, names]) => ({
                label: [...names].join(", "),
                value: pkg,
                hint: pkg,
              }))
              const selected = await prompts.multiselect({
                message: "Select providers to install",
                options,
                required: true,
              })
              if (prompts.isCancel(selected)) throw new UI.CancelledError()
              return selected as string[]
            })()

        for (const target of targets) {
          // If user typed a provider id (like "anthropic"), resolve to npm package
          const pkg = npmToNames[target] ? target
            : Object.entries(database).find(([id]) => id === target)?.[1]?.npm
            ?? target

          const spinner = prompts.spinner()
          spinner.start(`Installing ${pkg}`)
          try {
            await Npm.add(pkg)
            spinner.stop(`${pkg} installed`)
          } catch (err) {
            spinner.stop(`${pkg} failed: ${err instanceof Error ? err.message : String(err)}`)
          }
        }

        prompts.outro("Done")
      },
    })
  },
})
