export * as ShellSelect from "./select"

import path from "path"
import { readFile } from "fs/promises"
import { statSync } from "fs"
import { Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { which } from "../util/which"

const META: Record<string, { deny?: boolean; login?: boolean; ps?: boolean }> = {
  bash: { login: true },
  dash: { login: true },
  fish: { deny: true, login: true },
  ksh: { login: true },
  nu: { deny: true },
  powershell: { ps: true },
  pwsh: { ps: true },
  sh: { login: true },
  zsh: { login: true },
}

export type Item = {
  path: string
  name: string
  acceptable: boolean
}

export const Options = Schema.Struct({
  gitbash: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type

function stat(file: string) {
  return statSync(file, { throwIfNoEntry: false }) ?? undefined
}

function full(file: string, options?: Options) {
  if (process.platform !== "win32") return file
  const shell = FSUtil.windowsPath(file)
  if (path.win32.dirname(shell) !== ".") {
    if (shell.startsWith("/") && name(shell) === "bash") return gitbash(options) || shell
    return shell
  }
  if (name(shell) === "bash") return gitbash(options) || which(shell) || shell
  return which(shell) || shell
}

function meta(file: string) {
  return META[name(file)]
}

function ok(file: string) {
  return meta(file)?.deny !== true
}

function rooted(file: string) {
  return path.isAbsolute(FSUtil.windowsPath(file))
}

function resolve(file: string, options?: Options) {
  const shell = full(file, options)
  if (rooted(shell)) {
    if (stat(shell)?.isFile()) return shell
    return
  }
  return which(shell) ?? undefined
}

function win(options?: Options) {
  return Array.from(
    new Set(
      [which("pwsh"), which("powershell"), gitbash(options), process.env.COMSPEC || "cmd.exe"]
        .filter((item): item is string => Boolean(item))
        .map((file) => full(file, options)),
    ),
  )
}

async function unix() {
  const text = await readFile("/etc/shells", "utf8").catch(() => "")
  if (text) return Array.from(new Set(text.split("\n").filter((line) => line.trim() && !line.startsWith("#"))))
  return ["/bin/bash", "/bin/zsh", "/bin/sh"]
}

function select(file: string | undefined, options?: Options, opts?: { acceptable?: boolean }) {
  if (file && (!opts?.acceptable || ok(file))) {
    const shell = resolve(file, options)
    if (shell) return shell
  }
  if (process.platform === "win32") return win(options)[0]
  return fallback()
}

export function gitbash(options?: Options) {
  if (process.platform !== "win32") return
  if (options?.gitbash) return options.gitbash
  const git = which("git")
  if (!git) return
  const file = path.join(git, "..", "..", "bin", "bash.exe")
  if (stat(file)?.size) return file
}

function fallback() {
  if (process.platform === "darwin") return "/bin/zsh"
  const bash = which("bash")
  if (bash) return bash
  return "/bin/sh"
}

export function name(file: string) {
  if (process.platform === "win32") return path.win32.parse(FSUtil.windowsPath(file)).name.toLowerCase()
  return path.basename(file).toLowerCase()
}

export function login(file: string) {
  return meta(file)?.login === true
}

export function ps(file: string) {
  return meta(file)?.ps === true
}

function info(file: string, options?: Options): Item {
  const item = full(file, options)
  const n = name(item)
  return {
    path: item,
    name: resolve(n, options) ? n : item,
    acceptable: ok(item),
  }
}

export function args(file: string, command: string) {
  const n = name(file)
  if (n === "nu" || n === "fish") return ["-c", command]
  if (n === "zsh" || n === "bash") return ["-c", command]
  if (n === "cmd") return ["/c", command]
  if (ps(file)) return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command]
  return ["-c", command]
}

let defaultPreferred: string | undefined
let defaultAcceptable: string | undefined

export function preferred(configShell?: string, options?: Options) {
  if (configShell) return select(configShell, options)
  if (options?.gitbash) return select(process.env.SHELL, options)
  defaultPreferred ??= select(process.env.SHELL)
  return defaultPreferred
}
preferred.reset = () => {
  defaultPreferred = undefined
}

export function acceptable(configShell?: string, options?: Options) {
  if (configShell) return select(configShell, options, { acceptable: true })
  if (options?.gitbash) return select(process.env.SHELL, options, { acceptable: true })
  defaultAcceptable ??= select(process.env.SHELL, undefined, { acceptable: true })
  return defaultAcceptable
}
acceptable.reset = () => {
  defaultAcceptable = undefined
}

export async function list(options?: Options): Promise<Item[]> {
  const shells = process.platform === "win32" ? win(options) : await unix()
  return shells.filter((shell) => resolve(shell, options)).map((shell) => info(shell, options))
}
