import type { Argv } from "yargs"
import path from "path"
import { pathToFileURL } from "url"
import { text as streamText } from "node:stream/consumers"
import { Filesystem } from "@/util/filesystem"
import { createSaeeolClient } from "@saeeol/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "@/provider/provider"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { validateCloudFork } from "@/saeeol/cloud-session"
import { SaeeolRunAuto } from "@/saeeol/cli/run-auto"
import { resolveSessionID, shareSession, resolveAgent } from "./run-session"
import { runEventLoop } from "./run-events"

export const RunCommand = cmd({
  command: "run [message..]",
  describe: "run saeeol with a message",
  builder: (yargs: Argv) => {
    return (
      yargs
        .positional("message", { describe: "message to send", type: "string", array: true, default: [] })
        .option("command", { describe: "the command to run, use message for args", type: "string" })
        .option("continue", { alias: ["c"], describe: "continue the last session", type: "boolean" })
        .option("session", { alias: ["s"], describe: "session id to continue", type: "string" })
        .option("fork", { describe: "fork before continuing (requires --continue or --session)", type: "boolean" })
        .option("share", { type: "boolean", describe: "share the session" })
        .option("model", { type: "string", alias: ["m"], describe: "model (provider/model)" })
        .option("agent", { type: "string", describe: "agent to use" })
        .option("format", { type: "string", choices: ["default", "json"], default: "default", describe: "output format" })
        .option("file", { alias: ["f"], type: "string", array: true, describe: "file(s) to attach" })
        .option("title", { type: "string", describe: "title for the session" })
        .option("attach", { type: "string", describe: "attach to a running saeeol server" })
        .option("password", { alias: ["p"], type: "string", describe: "basic auth password" })
        .option("dir", { type: "string", describe: "directory to run in" })
        .option("port", { type: "number", describe: "port for the local server" })
        .option("variant", { type: "string", describe: "model variant (reasoning effort)" })
        .option("thinking", { type: "boolean", describe: "show thinking blocks", default: false })
        .option("auto", { type: "boolean", describe: "auto-approve all permissions", default: false })
    )
  },
  handler: async (args) => {
    let message = [...args.message, ...(args["--"] || [])]
      .map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
      .join(" ")

    const directory = (() => {
      if (!args.dir) return undefined
      if (args.attach) return args.dir
      try { process.chdir(args.dir); return process.cwd() }
      catch { UI.error("Failed to change directory to " + args.dir); process.exit(1) }
    })()

    const files = await resolveFiles(args.file)
    if (!process.stdin.isTTY) message += "\n" + (await streamText(process.stdin))
    if (message.trim().length === 0 && !args.command) {
      UI.error("You must provide a message or a command"); process.exit(1)
    }
    if (args.fork && !args.continue && !args.session) {
      UI.error("--fork requires --continue or --session"); process.exit(1)
    }
    const cloudForkError = validateCloudFork(args)
    if (cloudForkError) { UI.error(cloudForkError); process.exit(1) }

    async function execute(sdk: ReturnType<typeof createSaeeolClient>) {
      const sessionID = await resolveSessionID(sdk, {
        continue: args.continue,
        session: args.session,
        cloudFork: args.cloudFork as boolean | undefined,
        fork: args.fork,
        title: args.title,
        message,
      })
      if (!sessionID) { UI.error("Session not found"); process.exit(1) }

      const auto = SaeeolRunAuto.create(sessionID)
      await shareSession(sdk, sessionID, args.share)
      const agent = await resolveAgent(args.attach ? sdk : undefined, args.agent, args.attach)

      runEventLoop({ sdk, sessionID, format: args.format, thinking: args.thinking, auto }).catch((e) => {
        console.error(e); process.exit(1)
      })

      if (args.command) {
        await sdk.session.command({
          sessionID, agent, model: args.model, command: args.command,
          arguments: message, variant: args.variant,
        })
      } else {
        const model = args.model ? Provider.parseModel(args.model) : undefined
        await sdk.session.prompt({
          sessionID, agent, model, variant: args.variant,
          parts: [...files, { type: "text", text: message }],
        })
      }
    }

    if (args.attach) {
      const headers = buildAuthHeaders(args.password)
      const sdk = createSaeeolClient({ baseUrl: args.attach, directory, headers })
      return await execute(sdk)
    }

    await bootstrap(process.cwd(), async () => {
      const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
        const request = new Request(input, init)
        return Server.Default().app.fetch(request)
      }) as typeof globalThis.fetch
      const sdk = createSaeeolClient({ baseUrl: "http://saeeol.internal", fetch: fetchFn })
      await execute(sdk)
    })
  },
})

function buildAuthHeaders(password?: string) {
  const pw = password ?? process.env.SAEEOL_SERVER_PASSWORD
  if (!pw) return undefined
  const username = process.env.SAEEOL_SERVER_USERNAME ?? "saeeol"
  const auth = `Basic ${Buffer.from(`${username}:${pw}`).toString("base64")}`
  return { Authorization: auth }
}

async function resolveFiles(fileArgs: string | string[] | undefined) {
  if (!fileArgs) return []
  const list = Array.isArray(fileArgs) ? fileArgs : [fileArgs]
  const files: { type: "file"; url: string; filename: string; mime: string }[] = []
  for (const filePath of list) {
    const resolved = path.resolve(process.cwd(), filePath)
    if (!(await Filesystem.exists(resolved))) { UI.error(`File not found: ${filePath}`); process.exit(1) }
    const mime = (await Filesystem.isDir(resolved)) ? "application/x-directory" : "text/plain"
    files.push({ type: "file", url: pathToFileURL(resolved).href, filename: path.basename(resolved), mime })
  }
  return files
}
