import "zod-openapi/extend"
import { App } from "./app/app"
import { Server } from "./server/server"
import fs from "fs/promises"
import path from "path"
import { Share } from "./share/share"
import url from "node:url"
import { Global } from "./global"
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { RunCommand } from "./cli/cmd/run"
import { GenerateCommand } from "./cli/cmd/generate"
import { ScrapCommand } from "./cli/cmd/scrap"
import { Log } from "./util/log"
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { ModelsCommand } from "./cli/cmd/models"
import { Provider } from "./provider/provider"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
import { Bus } from "./bus"
import { Config } from "./config/config"
import { NamedError } from "./util/error"
import { FormatError } from "./cli/error"
import { ServeCommand } from "./cli/cmd/serve"

const cancel = new AbortController()

process.on("unhandledRejection", (e) => {
  Log.Default.error("rejection", {
    e: e instanceof Error ? e.message : e,
  })
})

process.on("uncaughtException", (e) => {
  Log.Default.error("exception", {
    e: e instanceof Error ? e.message : e,
  })
})

const cli = yargs(hideBin(process.argv))
  .scriptName("opencode")
  .help("help", "show help")
  .version("version", "show version number", Installation.VERSION)
  .alias("version", "v")
  .option("print-logs", {
    describe: "print logs to stderr",
    type: "boolean",
  })
  .middleware(async () => {
    await Log.init({ print: process.argv.includes("--print-logs") })
    Log.Default.info("opencode", {
      version: Installation.VERSION,
      args: process.argv.slice(2),
    })
  })
  .usage("\n" + UI.logo())
  .command({
    command: "$0 [project]",
    describe: "start opencode tui",
    builder: (yargs) =>
      yargs.positional("project", {
        type: "string",
        describe: "path to start opencode in",
      }),
    handler: async (args) => {
      while (true) {
        const cwd = args.project ? path.resolve(args.project) : process.cwd()
        try {
          process.chdir(cwd)
        } catch (e) {
          UI.error("Failed to change directory to " + cwd)
          return
        }
        const result = await App.provide({ cwd }, async (app) => {
          const providers = await Provider.list()
          if (Object.keys(providers).length === 0) {
            return "needs_provider"
          }

          await Share.init()
          const server = Server.listen({
            port: 0,
            hostname: "127.0.0.1",
          })

          let cmd = ["go", "run", "./main.go"]
          let cwd = url.fileURLToPath(
            new URL("../../tui/cmd/opencode", import.meta.url),
          )
          if (Bun.embeddedFiles.length > 0) {
            const blob = Bun.embeddedFiles[0] as File
            let binaryName = blob.name
            if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
              binaryName += ".exe"
            }
            const binary = path.join(Global.Path.cache, "tui", binaryName)
            const file = Bun.file(binary)
            if (!(await file.exists())) {
              await Bun.write(file, blob, { mode: 0o755 })
              await fs.chmod(binary, 0o755)
            }
            cwd = process.cwd()
            cmd = [binary]
          }
          const proc = Bun.spawn({
            cmd: [...cmd, ...process.argv.slice(2)],
            signal: cancel.signal,
            cwd,
            stdout: "inherit",
            stderr: "inherit",
            stdin: "inherit",
            env: {
              ...process.env,
              OPENCODE_SERVER: server.url.toString(),
              OPENCODE_APP_INFO: JSON.stringify(app),
            },
            onExit: () => {
              server.stop()
            },
          })

          ;(async () => {
            if (Installation.VERSION === "dev") return
            if (Installation.isSnapshot()) return
            const config = await Config.global()
            if (config.autoupdate === false) return
            const latest = await Installation.latest().catch(() => {})
            if (!latest) return
            if (Installation.VERSION === latest) return
            const method = await Installation.method()
            if (method === "unknown") return
            await Installation.upgrade(method, latest)
              .then(() => {
                Bus.publish(Installation.Event.Updated, { version: latest })
              })
              .catch(() => {})
          })()

          await proc.exited
          server.stop()

          return "done"
        })
        if (result === "done") break
        if (result === "needs_provider") {
          UI.empty()
          UI.println(UI.logo("   "))
          UI.empty()
          await AuthLoginCommand.handler(args)
        }
      }
    },
  })
  .command(RunCommand)
  .command(GenerateCommand)
  .command(ScrapCommand)
  .command(AuthCommand)
  .command(UpgradeCommand)
  .command(ServeCommand)
  .command(ModelsCommand)
  .fail((msg) => {
    if (
      msg.startsWith("Unknown argument") ||
      msg.startsWith("Not enough non-option arguments")
    ) {
      cli.showHelp("log")
    }
  })
  .strict()

try {
  await cli.parse()
} catch (e) {
  const data: Record<string, any> = {}
  if (e instanceof NamedError) {
    const obj = e.toObject()
    Object.assign(data, {
      ...obj.data,
    })
  }
  if (e instanceof Error) {
    Object.assign(data, {
      name: e.name,
      message: e.message,
      cause: e.cause?.toString(),
    })
  }
  Log.Default.error("fatal", data)
  const formatted = FormatError(e)
  if (formatted) UI.error(formatted)
  if (formatted === undefined)
    UI.error(
      "Unexpected error, check log file at " + Log.file() + " for more details",
    )
}

cancel.abort()
