/**
 * Static Fish completion script generator.
 *
 * Produces a self-contained completion script from a `CommandDescriptor` —
 * no re-invocation of the CLI at runtime.
 *
 * @internal
 */
import type * as Completions from "../../Completions.ts"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const escapeFishString = (s: string): string => s.replace(/'/g, "\\'")

/**
 * Build a Fish condition that checks the current subcommand context.
 *
 * For root-level completions with subcommands: `__fish_use_subcommand`
 * For nested commands: verify the parent subcommand is active AND none of its
 * child subcommands have been entered yet.
 */
const subcommandCondition = (
  parentPath: ReadonlyArray<string>,
  childSubcommandNames: ReadonlyArray<string>
): string => {
  if (parentPath.length === 0) {
    if (childSubcommandNames.length > 0) {
      return `__fish_use_subcommand`
    }
    return ``
  }
  const parent = parentPath[parentPath.length - 1]
  if (childSubcommandNames.length > 0) {
    // Show only when parent is active but no child subcommand has been entered
    return `__fish_seen_subcommand_from ${parent}; and not __fish_seen_subcommand_from ${
      childSubcommandNames.join(" ")
    }`
  }
  return `__fish_seen_subcommand_from ${parent}`
}

/**
 * Build a __fish_contains_opt condition that checks whether any form of this
 * flag has already been typed. Returns the condition string without wrapping
 * quotes (the caller adds those).
 */
const flagContainsOptCondition = (flag: Completions.FlagDescriptor): string => {
  const optArgs: Array<string> = []
  for (const alias of flag.aliases) {
    if (alias.length === 1) {
      optArgs.push(`-s ${alias}`)
    }
  }
  // Long names (fish __fish_contains_opt uses bare words for long opts)
  optArgs.push(flag.name)
  for (const alias of flag.aliases) {
    if (alias.length > 1) {
      optArgs.push(alias)
    }
  }
  if (flag.type._tag === "Boolean") {
    optArgs.push(`no-${flag.name}`)
  }
  return `not __fish_contains_opt ${optArgs.join(" ")}`
}

const flagCompletionArgs = (flag: Completions.FlagDescriptor): Array<string> => {
  const args: Array<string> = [`-l ${flag.name}`]
  for (const alias of flag.aliases) {
    if (alias.length === 1) {
      args.push(`-s ${alias}`)
    } else {
      args.push(`-l ${alias}`)
    }
  }
  if (flag.description) {
    args.push(`-d '${escapeFishString(flag.description)}'`)
  }
  const valueArgs = flagValueArgs(flag.type)
  if (valueArgs) {
    args.push(valueArgs)
  }
  return args
}

const flagValueArgs = (type: Completions.FlagType): string | undefined => {
  switch (type._tag) {
    case "Boolean":
      return undefined
    case "Choice":
      return `-r -f -a '${type.values.join(" ")}'`
    case "Path":
      if (type.pathType === "directory") return `-r -F`
      return `-r -F`
    default:
      // -r: requires a value, -f: don't fall back to file completion
      return `-r -f`
  }
}

const argValueArgs = (type: Completions.ArgumentType): string | undefined => {
  switch (type._tag) {
    case "Choice":
      return `-r -f -a '${type.values.join(" ")}'`
    case "Path":
      return `-r -F`
    default:
      return undefined
  }
}

// ---------------------------------------------------------------------------
// Generator
// ---------------------------------------------------------------------------

const generateCompletions = (
  executableName: string,
  descriptor: Completions.CommandDescriptor,
  parentPath: ReadonlyArray<string>,
  lines: Array<string>
): void => {
  const allSubNames = descriptor.subcommands.map((s) => s.name)
  const condition = subcommandCondition(parentPath, allSubNames)
  const conditionArg = condition ? `-n '${condition}'` : ``

  // Suppress default file completion unless the command has path-type
  // positional arguments. Without this, fish falls back to listing files
  // even when only flags are valid.
  const hasPathArgs = descriptor.arguments.some((a) => a.type._tag === "Path")
  if (!hasPathArgs) {
    const parts = [`complete -c ${executableName}`]
    if (conditionArg) parts.push(conditionArg)
    parts.push(`-f`)
    lines.push(parts.join(" "))
  }

  // Subcommand completions
  for (const sub of descriptor.subcommands) {
    const parts = [`complete -c ${executableName}`]
    if (conditionArg) parts.push(conditionArg)
    parts.push(`-f -a '${escapeFishString(sub.name)}'`)
    if (sub.description) {
      parts.push(`-d '${escapeFishString(sub.description)}'`)
    }
    lines.push(parts.join(" "))
  }

  // Flag completions
  for (const flag of descriptor.flags) {
    // Only apply __fish_contains_opt dedup for boolean flags. For value-taking
    // flags, the dedup condition would suppress the entry while fish is waiting
    // for a value (e.g. typing `--env <TAB>` wouldn't show choices).
    const isBoolean = flag.type._tag === "Boolean"
    const flagCondition = isBoolean
      ? (condition ? `${condition}; and ${flagContainsOptCondition(flag)}` : flagContainsOptCondition(flag))
      : condition
    const flagCondArg = flagCondition ? `-n '${flagCondition}'` : ``

    const parts = [`complete -c ${executableName}`]
    if (flagCondArg) parts.push(flagCondArg)
    parts.push(...flagCompletionArgs(flag))
    lines.push(parts.join(" "))

    // Boolean negation
    if (isBoolean) {
      const negParts = [`complete -c ${executableName}`]
      if (flagCondArg) negParts.push(flagCondArg)
      negParts.push(`-l no-${flag.name}`)
      if (flag.description) {
        negParts.push(`-d '${escapeFishString(`Disable ${flag.name}`)}'`)
      }
      lines.push(negParts.join(" "))
    }
  }

  // Flags as -a entries for bare <TAB> visibility.
  // Fish only shows -l/-s entries when the current word starts with -, so
  // without this, flags are invisible on bare <TAB>. Guarded to only show
  // when NOT already typing an option (avoids duplication with -l/-s above)
  // and hidden once a flag has been used (__fish_contains_opt).
  if (descriptor.flags.length > 0) {
    const notDash = `not string match -q -- "-*" (commandline -ct)`
    const bareBase = condition ? `${condition}; and ${notDash}` : notDash

    for (const flag of descriptor.flags) {
      const bareCondition = `${bareBase}; and ${flagContainsOptCondition(flag)}`
      const isBoolean = flag.type._tag === "Boolean"

      const parts = [`complete -c ${executableName}`]
      parts.push(`-n '${bareCondition}'`)
      parts.push(`-f -a '--${flag.name}'`)
      if (flag.description) {
        parts.push(`-d '${escapeFishString(flag.description)}'`)
      }
      lines.push(parts.join(" "))

      if (isBoolean) {
        const negParts = [`complete -c ${executableName}`]
        negParts.push(`-n '${bareCondition}'`)
        negParts.push(`-f -a '--no-${flag.name}'`)
        if (flag.description) {
          negParts.push(`-d '${escapeFishString(`Disable ${flag.name}`)}'`)
        }
        lines.push(negParts.join(" "))
      }
    }
  }

  // Argument completions (type hints only)
  for (const arg of descriptor.arguments) {
    const valueArg = argValueArgs(arg.type)
    if (valueArg) {
      const parts = [`complete -c ${executableName}`]
      if (conditionArg) parts.push(conditionArg)
      parts.push(valueArg)
      if (arg.description) {
        parts.push(`-d '${escapeFishString(arg.description)}'`)
      }
      lines.push(parts.join(" "))
    }
  }

  // Recurse into subcommands
  for (const sub of descriptor.subcommands) {
    generateCompletions(executableName, sub, [...parentPath, sub.name], lines)
  }
}

/** @internal */
export const generate = (
  executableName: string,
  descriptor: Completions.CommandDescriptor
): string => {
  const lines: Array<string> = []

  lines.push(`###-begin-${executableName}-completions-###`)
  lines.push(`#`)
  lines.push(`# Static completion script for Fish`)
  lines.push(`#`)
  lines.push(`# Installation:`)
  lines.push(`#   ${executableName} --completions fish > ~/.config/fish/completions/${executableName}.fish`)
  lines.push(`#`)
  lines.push(``)

  generateCompletions(executableName, descriptor, [], lines)

  lines.push(``)
  lines.push(`###-end-${executableName}-completions-###`)

  return lines.join("\n")
}
