#!/usr/bin/env tsx
/**
 * generate-completions.ts — auto-generates shell completions from the commander.js command tree.
 *
 * Usage:
 *   tsx scripts/generate-completions.ts           # print generated files to stdout
 *   tsx scripts/generate-completions.ts --write   # write completions/ files
 *   tsx scripts/generate-completions.ts --check   # exit 0 if files match, 1 if stale
 *
 * Requires `pnpm build` to have run first (workspace packages must be compiled).
 */

import { Command, type Option, type Argument } from 'commander';
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');

// ============================================================
// Configuration — hints for argument completions
// ============================================================

type CompletionHint =
  | 'project_names'
  | 'resource_types'
  | 'resource_names'
  | 'instance_names'
  | 'available_servers'
  | 'project_servers'
  | 'file'
  | { static: string[] };

/**
 * Maps `command.argumentName` → how to complete that argument.
 * Anything not listed here gets no special completion (user types freeform).
 */
const COMPLETION_HINTS: Record<string, CompletionHint> = {
  'console.project':              'project_names',
  'get.resource':                 'resource_types',
  'get.id':                       'resource_names',
  'describe.resource':            'resource_types',
  'describe.id':                  'resource_names',
  'delete.resource':              'resource_types',
  'delete.id':                    'resource_names',
  'edit.resource':                { static: ['servers', 'secrets', 'projects', 'groups', 'rbac', 'prompts', 'promptrequests', 'personalities'] },
  'edit.name-or-id':              'resource_names',
  'patch.resource':               'resource_types',
  'patch.name':                   'resource_names',
  'approve.resource':             { static: ['promptrequest'] },
  'approve.name':                 'resource_names',
  'logs.name':                    'instance_names',
  'attach-server.server-name':    'available_servers',
  'detach-server.server-name':    'project_servers',
  'apply.file':                   'file',
};

/** Options whose values are file paths (use -rF in fish, -f in bash). */
const FILE_OPTIONS = new Set([
  'apply.file',
  'backup.output',
  'restore.input',
  'create-prompt.content-file',
  'create-promptrequest.content-file',
]);

/** Commands shown ONLY when --project/-p is on the command line. */
const PROJECT_ONLY_COMMANDS = new Set(['attach-server', 'detach-server', 'favourites']);

/** Commands that appear in BOTH project and non-project context. */
const PROJECT_SCOPED_COMMANDS = new Set([
  'get', 'describe', 'delete', 'logs', 'create', 'edit', 'help',
]);

/** Completely hidden commands (never shown in completions). */
const NEVER_SHOW_COMMANDS = new Set(['mcp']);

/**
 * Commands that follow the resource-type → resource-name two-arg pattern.
 * Used to generate guard functions.
 */
const RESOURCE_COMMANDS = ['get', 'describe', 'delete', 'edit', 'patch', 'approve'];

// ============================================================
// Command tree extraction
// ============================================================

interface CmdInfo {
  name: string;
  description: string;
  hidden: boolean;
  options: OptInfo[];
  args: ArgInfo[];
  subcommands: CmdInfo[];
}

interface OptInfo {
  short?: string;       // e.g. '-o'
  long: string;         // e.g. '--output'
  description: string;
  takesValue: boolean;
  choices?: string[];
  negate: boolean;
}

interface ArgInfo {
  name: string;
  description: string;
  required: boolean;
  variadic: boolean;
  choices?: string[];
}

function extractOption(opt: Option): OptInfo {
  return {
    short: (opt as unknown as Record<string, string>).short || undefined,
    long: (opt as unknown as Record<string, string>).long,
    description: opt.description,
    takesValue: (opt as unknown as Record<string, boolean>).required || (opt as unknown as Record<string, boolean>).optional || false,
    choices: (opt as unknown as Record<string, string[] | undefined>).argChoices || undefined,
    negate: (opt as unknown as Record<string, boolean>).negate || false,
  };
}

function extractArgument(arg: Argument): ArgInfo {
  return {
    name: (arg as unknown as Record<string, string>)._name ?? arg.name(),
    description: arg.description,
    required: (arg as unknown as Record<string, boolean>).required,
    variadic: (arg as unknown as Record<string, boolean>).variadic,
    choices: (arg as unknown as Record<string, string[] | undefined>)._choices || undefined,
  };
}

function extractCommand(cmd: Command): CmdInfo {
  const options = (cmd.options as Option[])
    .filter((o) => {
      const long = (o as unknown as Record<string, string>).long;
      // Skip --help and --version (handled globally)
      return long !== '--help' && long !== '--version';
    })
    .map(extractOption);

  const args = ((cmd as unknown as Record<string, Argument[]>).registeredArguments ?? [])
    .map(extractArgument);

  const subcommands = (cmd.commands as Command[])
    .filter((sub) => sub.name() !== 'help')  // skip commander's auto-generated help
    .map(extractCommand)

  // Re-add 'help' as a minimal command for completion purposes
  if ((cmd.commands as Command[]).some((sub) => sub.name() === 'help')) {
    subcommands.push({
      name: 'help',
      description: 'display help for command',
      hidden: false,
      options: [],
      args: [],
      subcommands: [],
    });
  }

  return {
    name: cmd.name(),
    description: cmd.description(),
    hidden: (cmd as unknown as Record<string, boolean>)._hidden ?? false,
    options,
    args,
    subcommands,
  };
}

async function extractTree(): Promise<CmdInfo> {
  // createProgram() is safe to call — it reads config from disk (defaults if missing)
  // and creates an ApiClient (no network calls).
  const { createProgram } = await import('../src/cli/src/index.js') as { createProgram: () => Command };
  const program = createProgram();
  return extractCommand(program);
}

// ============================================================
// Resource aliases (mirrors RESOURCE_ALIASES from shared.ts)
// ============================================================

const CANONICAL_RESOURCES = [
  'servers', 'instances', 'secrets', 'secretbackends', 'llms', 'agents', 'personalities', 'templates', 'projects',
  'users', 'groups', 'rbac', 'prompts', 'promptrequests',
  'serverattachments', 'proxymodels', 'inference-tasks', 'all',
];

const ALIAS_ENTRIES: [string, string][] = [
  ['server', 'servers'], ['srv', 'servers'],
  ['instance', 'instances'], ['inst', 'instances'],
  ['secret', 'secrets'], ['sec', 'secrets'],
  ['secretbackend', 'secretbackends'], ['sb', 'secretbackends'],
  ['llm', 'llms'], ['llms', 'llms'],
  ['agent', 'agents'], ['agents', 'agents'],
  ['personality', 'personalities'], ['personalities', 'personalities'],
  ['template', 'templates'], ['tpl', 'templates'],
  ['project', 'projects'], ['proj', 'projects'],
  ['user', 'users'],
  ['group', 'groups'],
  ['rbac', 'rbac'], ['rbac-definition', 'rbac'], ['rbac-binding', 'rbac'],
  ['prompt', 'prompts'], ['prompts', 'prompts'],
  ['promptrequest', 'promptrequests'], ['promptrequests', 'promptrequests'], ['pr', 'promptrequests'],
  ['serverattachment', 'serverattachments'], ['serverattachments', 'serverattachments'], ['sa', 'serverattachments'],
  ['proxymodel', 'proxymodels'], ['proxymodels', 'proxymodels'], ['pm', 'proxymodels'],
  // v5: inference-task queue. Short forms (`task`, `tasks`) are what
  // operators actually type — `mcpctl get tasks`.
  ['task', 'inference-tasks'], ['tasks', 'inference-tasks'],
  ['inference-task', 'inference-tasks'], ['inference-tasks', 'inference-tasks'],
  ['all', 'all'],
];

const ALL_ALIASES = [...CANONICAL_RESOURCES, ...ALIAS_ENTRIES.map(([a]) => a)];
// Deduplicate (some canonicals are also aliases)
const ALL_ALIASES_UNIQUE = [...new Set(ALL_ALIASES)];

// ============================================================
// Fish completion generator
// ============================================================

function generateFish(root: CmdInfo): string {
  const lines: string[] = [];
  const emit = (s: string) => lines.push(s);

  emit('# mcpctl fish completions — auto-generated by scripts/generate-completions.ts');
  emit('# DO NOT EDIT MANUALLY — run: pnpm completions:generate');
  emit('');

  // --- Erase stale completions ---
  emit('# Erase any stale completions from previous versions');
  emit('complete -c mcpctl -e');
  emit('');

  // --- Categorize commands ---
  const visibleCmds: CmdInfo[] = [];   // shown without --project
  const projectCmds: CmdInfo[] = [];   // shown with --project
  const projectOnlyCmds: CmdInfo[] = [];  // ONLY shown with --project

  for (const cmd of root.subcommands) {
    if (NEVER_SHOW_COMMANDS.has(cmd.name)) continue;
    if (PROJECT_ONLY_COMMANDS.has(cmd.name)) {
      projectOnlyCmds.push(cmd);
      projectCmds.push(cmd);
    } else if (PROJECT_SCOPED_COMMANDS.has(cmd.name)) {
      visibleCmds.push(cmd);
      projectCmds.push(cmd);
    } else if (!cmd.hidden) {
      visibleCmds.push(cmd);
    }
  }

  const visibleNames = visibleCmds.map((c) => c.name).join(' ');
  const projectNames = projectCmds.map((c) => c.name).join(' ');

  emit(`set -l commands ${visibleNames}`);
  emit(`set -l project_commands ${projectNames}`);
  emit('');

  // --- Disable file completions by default ---
  emit('# Disable file completions by default');
  emit('complete -c mcpctl -f');
  emit('');

  // --- Global options ---
  emit('# Global options');
  emit("complete -c mcpctl -s v -l version -d 'Show version'");
  for (const opt of root.options) {
    const parts = ['complete -c mcpctl'];
    if (opt.short) parts.push(`-s ${opt.short.replace('-', '')}`);
    parts.push(`-l ${opt.long.replace(/^--/, '')}`);
    parts.push(`-d '${esc(opt.description)}'`);
    if (opt.long === '--project') {
      parts.push(`-xa '(__mcpctl_project_names)'`);
    } else if (opt.takesValue) {
      if (opt.choices) {
        parts.push(`-xa '${opt.choices.join(' ')}'`);
      } else {
        parts.push('-x');
      }
    }
    emit(parts.join(' '));
  }
  emit("complete -c mcpctl -s h -l help -d 'Show help'");
  emit('');

  // --- Runtime helper functions ---
  emit('# ---- Runtime helpers ----');
  emit('');
  emitFishHelpers(emit);

  // --- Top-level command completions ---
  emit('# Top-level commands (without --project)');
  for (const cmd of visibleCmds) {
    emit(`complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a ${cmd.name} -d '${esc(cmd.description)}'`);
  }
  emit('');

  emit('# Project-scoped commands (with --project)');
  for (const cmd of projectCmds) {
    emit(`complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a ${cmd.name} -d '${esc(cmd.description)}'`);
  }
  emit('');

  // --- Resource type completions ---
  emit('# Resource types — only when resource type not yet selected');
  // Group commands by their resource type hint
  const resourceTypeCmds: string[] = [];
  const staticTypeCmds: { cmds: string[]; types: string[] }[] = [];

  for (const cmdName of RESOURCE_COMMANDS) {
    const hint = COMPLETION_HINTS[`${cmdName}.resource`] ?? COMPLETION_HINTS[`${cmdName}.${getFirstArgName(root, cmdName)}`];
    if (hint === 'resource_types') {
      resourceTypeCmds.push(cmdName);
    } else if (hint && typeof hint === 'object' && 'static' in hint) {
      // Check if we already have a group with the same types
      const key = hint.static.join(' ');
      const existing = staticTypeCmds.find((g) => g.types.join(' ') === key);
      if (existing) {
        existing.cmds.push(cmdName);
      } else {
        staticTypeCmds.push({ cmds: [cmdName], types: hint.static });
      }
    }
  }

  if (resourceTypeCmds.length > 0) {
    emit(`complete -c mcpctl -n "__fish_seen_subcommand_from ${resourceTypeCmds.join(' ')}; and __mcpctl_needs_resource_type" -a "$resources" -d 'Resource type'`);
  }
  for (const group of staticTypeCmds) {
    emit(`complete -c mcpctl -n "__fish_seen_subcommand_from ${group.cmds.join(' ')}; and __mcpctl_needs_resource_type" -a '${group.types.join(' ')}' -d 'Resource type'`);
  }
  emit('');

  // --- Resource name completions ---
  const resourceNameCmds = RESOURCE_COMMANDS.filter((cmdName) => {
    const cmd = root.subcommands.find((c) => c.name === cmdName);
    if (!cmd) return false;
    return cmd.args.some((a) => {
      const hint = COMPLETION_HINTS[`${cmdName}.${a.name}`];
      return hint === 'resource_names';
    });
  });

  if (resourceNameCmds.length > 0) {
    emit('# Resource names — after resource type is selected');
    emit(`complete -c mcpctl -n "__fish_seen_subcommand_from ${resourceNameCmds.join(' ')}; and not __mcpctl_needs_resource_type" -a '(__mcpctl_resource_names)' -d 'Resource name'`);
    emit('');
  }

  // --- Subcommand completions (config, create) ---
  for (const cmd of root.subcommands) {
    if (cmd.subcommands.length === 0) continue;

    const subNames = cmd.subcommands.map((s) => s.name);
    emit(`# ${cmd.name} subcommands`);
    emit(`set -l ${cmd.name}_cmds ${subNames.join(' ')}`);

    for (const sub of cmd.subcommands) {
      emit(`complete -c mcpctl -n "__fish_seen_subcommand_from ${cmd.name}; and not __fish_seen_subcommand_from $${cmd.name}_cmds" -a ${sub.name} -d '${esc(sub.description)}'`);
    }
    emit('');

    // Subcommand options
    for (const sub of cmd.subcommands) {
      if (sub.options.length === 0) continue;
      emit(`# ${cmd.name} ${sub.name} options`);
      for (const opt of sub.options) {
        const parts = [`complete -c mcpctl -n "__mcpctl_subcmd_active ${cmd.name} ${sub.name}"`];
        if (opt.short) parts.push(`-s ${opt.short.replace('-', '')}`);
        parts.push(`-l ${opt.long.replace(/^--/, '')}`);
        parts.push(`-d '${esc(opt.description)}'`);
        if (opt.negate) {
          // --no-X flags are boolean, no value
        } else if (opt.takesValue) {
          const fileKey = `${cmd.name}-${sub.name}.${opt.long.replace(/^--/, '')}`;
          if (FILE_OPTIONS.has(fileKey)) {
            parts.push('-rF');
          } else if (opt.choices) {
            parts.push(`-xa '${opt.choices.join(' ')}'`);
          } else if (opt.long === '--project') {
            parts.push(`-xa '(__mcpctl_project_names)'`);
          } else {
            parts.push('-x');
          }
        }
        emit(parts.join(' '));
      }
      emit('');
    }
  }

  // --- Per-command option completions (top-level commands without subcommands) ---
  for (const cmd of root.subcommands) {
    if (cmd.subcommands.length > 0) continue;  // Handled above
    if (cmd.options.length === 0) continue;
    if (NEVER_SHOW_COMMANDS.has(cmd.name)) continue;

    emit(`# ${cmd.name} options`);
    for (const opt of cmd.options) {
      const parts = [`complete -c mcpctl -n "__fish_seen_subcommand_from ${cmd.name}"`];
      if (opt.short) parts.push(`-s ${opt.short.replace('-', '')}`);
      parts.push(`-l ${opt.long.replace(/^--/, '')}`);
      parts.push(`-d '${esc(opt.description)}'`);
      if (opt.negate) {
        // boolean flag
      } else if (opt.takesValue) {
        const fileKey = `${cmd.name}.${opt.long.replace(/^--/, '')}`;
        if (FILE_OPTIONS.has(fileKey)) {
          parts.push('-rF');
        } else if (opt.choices) {
          parts.push(`-xa '${opt.choices.join(' ')}'`);
        } else if (opt.long === '--project') {
          parts.push(`-xa '(__mcpctl_project_names)'`);
        } else {
          parts.push('-x');
        }
      }
      emit(parts.join(' '));
    }
    emit('');
  }

  // --- Special argument completions ---
  // logs: instance names
  emit("# logs: takes a server/instance name");
  emit("complete -c mcpctl -n \"__fish_seen_subcommand_from logs; and __mcpctl_needs_arg_for logs\" -a '(__mcpctl_instance_names)' -d 'Server name'");
  emit('');

  // console: project name
  emit("# console: takes a project name");
  emit("complete -c mcpctl -n \"__fish_seen_subcommand_from console; and __mcpctl_needs_arg_for console\" -a '(__mcpctl_project_names)' -d 'Project name'");
  emit('');

  // attach-server / detach-server
  emit("# attach-server: show servers NOT in the project (only if no server arg yet)");
  emit("complete -c mcpctl -n \"__fish_seen_subcommand_from attach-server; and __mcpctl_needs_server_arg\" -a '(__mcpctl_available_servers)' -d 'Server'");
  emit('');
  emit("# detach-server: show servers IN the project (only if no server arg yet)");
  emit("complete -c mcpctl -n \"__fish_seen_subcommand_from detach-server; and __mcpctl_needs_server_arg\" -a '(__mcpctl_project_servers)' -d 'Server'");
  emit('');

  // apply: allow file completions for positional argument
  emit("# apply: allow file completions for positional argument");
  emit("complete -c mcpctl -n \"__fish_seen_subcommand_from apply\" -F");
  emit('');

  // help completions
  emit('# help completions');
  emit('complete -c mcpctl -n "__fish_seen_subcommand_from help" -a "$commands"');

  return lines.join('\n') + '\n';
}

function emitFishHelpers(emit: (s: string) => void): void {
  emit(`# Helper: check if --project or -p was given
function __mcpctl_has_project
    set -l tokens (commandline -opc)
    for i in (seq (count $tokens))
        if test "$tokens[$i]" = "--project" -o "$tokens[$i]" = "-p"
            return 0
        end
    end
    return 1
end
`);

  const aliasListStr = ALL_ALIASES_UNIQUE.join(' ');
  const resCmdStr = RESOURCE_COMMANDS.join(' ');

  emit(`# Resource type detection
set -l resources ${CANONICAL_RESOURCES.join(' ')}

function __mcpctl_needs_resource_type
    set -l resource_aliases ${aliasListStr}
    set -l tokens (commandline -opc)
    set -l found_cmd false
    for tok in $tokens
        if $found_cmd
            if contains -- $tok $resource_aliases
                return 1 # resource type already present
            end
        end
        if contains -- $tok ${resCmdStr}
            set found_cmd true
        end
    end
    if $found_cmd
        return 0 # command found but no resource type yet
    end
    return 1
end
`);

  emit(`# Map any resource alias to the canonical plural form for API calls
function __mcpctl_resolve_resource
    switch $argv[1]`);
  // Group aliases by canonical name
  const groups = new Map<string, string[]>();
  for (const canonical of CANONICAL_RESOURCES) {
    groups.set(canonical, []);
  }
  for (const [alias, canonical] of ALIAS_ENTRIES) {
    const g = groups.get(canonical) ?? [];
    if (!g.includes(alias)) g.push(alias);
    groups.set(canonical, g);
  }
  for (const [canonical, aliases] of groups) {
    const allForms = [...new Set([...aliases, canonical])].join(' ');
    emit(`        case ${allForms};${' '.repeat(Math.max(1, 24 - allForms.length))}echo ${canonical}`);
  }
  emit(`        case '*';                      echo $argv[1]`);
  emit(`    end`);
  emit(`end
`);

  emit(`function __mcpctl_get_resource_type
    set -l resource_aliases ${aliasListStr}
    set -l tokens (commandline -opc)
    set -l found_cmd false
    for tok in $tokens
        if $found_cmd
            if contains -- $tok $resource_aliases
                __mcpctl_resolve_resource $tok
                return
            end
        end
        if contains -- $tok ${resCmdStr}
            set found_cmd true
        end
    end
end
`);

  emit(`# Fetch resource names dynamically from the API
function __mcpctl_resource_names
    set -l resource (__mcpctl_get_resource_type)
    if test -z "$resource"
        return
    end
    if test "$resource" = "instances"
        mcpctl get instances -o json 2>/dev/null | jq -r '.[][].server.name' 2>/dev/null
    else if test "$resource" = "prompts" -o "$resource" = "promptrequests"
        mcpctl get $resource -A -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
    else
        mcpctl get $resource -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
    end
end
`);

  emit(`# Fetch project names for --project value
function __mcpctl_project_names
    mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
end
`);

  emit(`# Helper: get the --project/-p value from the command line
function __mcpctl_get_project_value
    set -l tokens (commandline -opc)
    for i in (seq (count $tokens))
        if test "$tokens[$i]" = "--project" -o "$tokens[$i]" = "-p"; and test $i -lt (count $tokens)
            echo $tokens[(math $i + 1)]
            return
        end
    end
end
`);

  emit(`# Servers currently attached to the project (for detach-server)
function __mcpctl_project_servers
    set -l proj (__mcpctl_get_project_value)
    if test -z "$proj"
        return
    end
    mcpctl --project $proj get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
end
`);

  emit(`# Servers NOT attached to the project (for attach-server)
function __mcpctl_available_servers
    set -l proj (__mcpctl_get_project_value)
    if test -z "$proj"
        mcpctl get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
        return
    end
    set -l all (mcpctl get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)
    set -l attached (mcpctl --project $proj get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)
    for s in $all
        if not contains -- $s $attached
            echo $s
        end
    end
end
`);

  emit(`# Instance names for logs
function __mcpctl_instance_names
    mcpctl get instances -o json 2>/dev/null | jq -r '.[][].server.name' 2>/dev/null
end
`);

  emit(`# Helper: check if a positional arg has been given for a specific command
function __mcpctl_needs_arg_for
    set -l cmd $argv[1]
    set -l tokens (commandline -opc)
    set -l found false
    for tok in $tokens
        if $found
            if not string match -q -- '-*' $tok
                return 1 # arg already present
            end
        end
        if test "$tok" = "$cmd"
            set found true
        end
    end
    if $found
        return 0 # command found but no arg yet
    end
    return 1
end
`);

  emit(`# Helper: check if attach-server/detach-server already has a server argument
function __mcpctl_needs_server_arg
    set -l tokens (commandline -opc)
    set -l found_cmd false
    for tok in $tokens
        if $found_cmd
            if not string match -q -- '-*' $tok
                return 1 # server arg already present
            end
        end
        if contains -- $tok attach-server detach-server
            set found_cmd true
        end
    end
    if $found_cmd
        return 0
    end
    return 1
end
`);

  emit(`# Helper: check if a specific parent-child subcommand pair is active
function __mcpctl_subcmd_active
    set -l parent $argv[1]
    set -l child $argv[2]
    set -l tokens (commandline -opc)
    set -l found_parent false
    for tok in $tokens
        if $found_parent
            if test "$tok" = "$child"
                return 0
            end
            if not string match -q -- '-*' $tok
                return 1 # different subcommand
            end
        end
        if test "$tok" = "$parent"
            set found_parent true
        end
    end
    return 1
end
`);
}

// ============================================================
// Bash completion generator
// ============================================================

function generateBash(root: CmdInfo): string {
  const lines: string[] = [];
  const emit = (s: string) => lines.push(s);

  emit('# mcpctl bash completions — auto-generated by scripts/generate-completions.ts');
  emit('# DO NOT EDIT MANUALLY — run: pnpm completions:generate');
  emit('');

  // --- Categorize commands ---
  const visibleCmds: CmdInfo[] = [];
  const projectCmds: CmdInfo[] = [];

  for (const cmd of root.subcommands) {
    if (NEVER_SHOW_COMMANDS.has(cmd.name)) {
      // Include mcp in case handler but not in top-level list
    }
    if (PROJECT_ONLY_COMMANDS.has(cmd.name)) {
      projectCmds.push(cmd);
    } else if (PROJECT_SCOPED_COMMANDS.has(cmd.name)) {
      visibleCmds.push(cmd);
      projectCmds.push(cmd);
    } else if (!cmd.hidden) {
      visibleCmds.push(cmd);
    }
  }

  const visibleNames = visibleCmds.map((c) => c.name).join(' ');
  const projectNames = projectCmds.map((c) => c.name).join(' ');

  // Build global opts string from the commander tree
  const globalOptParts: string[] = ['-v', '--version'];
  for (const opt of root.options) {
    if (opt.short) globalOptParts.push(opt.short);
    globalOptParts.push(opt.long);
  }
  // Always include -h/--help
  globalOptParts.push('-h', '--help');

  emit('_mcpctl() {');
  emit('  local cur prev words cword');
  emit('  _init_completion || return');
  emit('');
  emit(`  local commands="${visibleNames}"`);
  emit(`  local project_commands="${projectNames}"`);
  emit(`  local global_opts="${globalOptParts.join(' ')}"`);
  emit(`  local resources="${CANONICAL_RESOURCES.join(' ')}"`);
  emit(`  local resource_aliases="${ALL_ALIASES_UNIQUE.join(' ')}"`);
  emit('');

  // --- has_project check ---
  emit('  # Check if --project/-p was given');
  emit('  local has_project=false');
  emit('  local i');
  emit('  for ((i=1; i < cword; i++)); do');
  emit('    if [[ "${words[i]}" == "--project" || "${words[i]}" == "-p" ]]; then');
  emit('      has_project=true');
  emit('      break');
  emit('    fi');
  emit('  done');
  emit('');

  // --- subcmd detection ---
  emit('  # Find the first subcommand');
  emit('  local subcmd=""');
  emit('  local subcmd_pos=0');
  emit('  for ((i=1; i < cword; i++)); do');
  emit('    if [[ "${words[i]}" == "--project" || "${words[i]}" == "--daemon-url" || "${words[i]}" == "-p" ]]; then');
  emit('      ((i++))');
  emit('      continue');
  emit('    fi');
  emit('    if [[ "${words[i]}" != -* ]]; then');
  emit('      subcmd="${words[i]}"');
  emit('      subcmd_pos=$i');
  emit('      break');
  emit('    fi');
  emit('  done');
  emit('');

  // --- resource_type detection ---
  emit('  # Find the resource type after resource commands');
  emit('  local resource_type=""');
  emit('  if [[ -n "$subcmd_pos" ]] && [[ $subcmd_pos -gt 0 ]]; then');
  emit('    for ((i=subcmd_pos+1; i < cword; i++)); do');
  emit('      if [[ "${words[i]}" != -* ]] && [[ " $resource_aliases " == *" ${words[i]} "* ]]; then');
  emit('        resource_type="${words[i]}"');
  emit('        break');
  emit('      fi');
  emit('    done');
  emit('  fi');
  emit('');

  // --- Helper functions ---
  emit('  # Helper: get --project/-p value');
  emit('  _mcpctl_get_project_value() {');
  emit('    local i');
  emit('    for ((i=1; i < cword; i++)); do');
  emit('      if [[ "${words[i]}" == "--project" || "${words[i]}" == "-p" ]] && (( i+1 < cword )); then');
  emit('        echo "${words[i+1]}"');
  emit('        return');
  emit('      fi');
  emit('    done');
  emit('  }');
  emit('');

  emit('  # Helper: fetch resource names');
  emit('  _mcpctl_resource_names() {');
  emit('    local rt="$1"');
  emit('    if [[ -n "$rt" ]]; then');
  emit('      if [[ "$rt" == "instances" ]]; then');
  emit("        mcpctl get instances -o json 2>/dev/null | jq -r '.[][].server.name' 2>/dev/null");
  emit('      else');
  emit("        mcpctl get \"$rt\" -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null");
  emit('      fi');
  emit('    fi');
  emit('  }');
  emit('');

  emit('  # Helper: find sub-subcommand (for config/create)');
  emit('  _mcpctl_get_subcmd() {');
  emit('    local parent_pos="$1"');
  emit('    local i');
  emit('    for ((i=parent_pos+1; i < cword; i++)); do');
  emit('      if [[ "${words[i]}" != -* ]]; then');
  emit('        echo "${words[i]}"');
  emit('        return');
  emit('      fi');
  emit('    done');
  emit('  }');
  emit('');

  // --- Option value completion ---
  emit('  # If completing option values');
  emit('  if [[ "$prev" == "--project" || "$prev" == "-p" ]]; then');
  emit('    local names');
  emit("    names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)");
  emit('    COMPREPLY=($(compgen -W "$names" -- "$cur"))');
  emit('    return');
  emit('  fi');
  emit('');

  // --- Case statement ---
  emit('  case "$subcmd" in');

  for (const cmd of root.subcommands) {
    emitBashCase(emit, cmd, root);
  }

  // help command
  emit('    help)');
  emit('      COMPREPLY=($(compgen -W "$commands" -- "$cur"))');
  emit('      return ;;');

  emit('  esac');
  emit('');

  // --- Default (no subcommand) ---
  emit('  # No subcommand yet — offer commands based on context');
  emit('  if [[ -z "$subcmd" ]]; then');
  emit('    if $has_project; then');
  emit('      COMPREPLY=($(compgen -W "$project_commands $global_opts" -- "$cur"))');
  emit('    else');
  emit('      COMPREPLY=($(compgen -W "$commands $global_opts" -- "$cur"))');
  emit('    fi');
  emit('  fi');

  emit('}');
  emit('');
  emit('complete -F _mcpctl mcpctl');

  return lines.join('\n') + '\n';
}

function emitBashCase(emit: (s: string) => void, cmd: CmdInfo, root: CmdInfo): void {
  const name = cmd.name;

  // Collect all option flags for this command
  const optFlags = bashOptFlags(cmd);

  if (cmd.subcommands.length > 0) {
    // Commands with subcommands (config, create)
    const subNames = cmd.subcommands.map((s) => s.name);
    emit(`    ${name})`);
    emit(`      local ${name}_sub=$(_mcpctl_get_subcmd $subcmd_pos)`);
    emit(`      if [[ -z "$${name}_sub" ]]; then`);
    emit(`        COMPREPLY=($(compgen -W "${subNames.join(' ')} help" -- "$cur"))`);
    emit('      else');
    emit(`        case "$${name}_sub" in`);
    for (const sub of cmd.subcommands) {
      const subOpts = bashOptFlags(sub);
      emit(`          ${sub.name})`);
      emit(`            COMPREPLY=($(compgen -W "${subOpts}" -- "$cur"))`);
      emit('            ;;');
    }
    emit('          *)');
    emit('            COMPREPLY=($(compgen -W "-h --help" -- "$cur"))');
    emit('            ;;');
    emit('        esac');
    emit('      fi');
    emit('      return ;;');
    return;
  }

  // Resource-type commands (get, describe, delete, edit, patch, approve)
  if (RESOURCE_COMMANDS.includes(name)) {
    const hint = COMPLETION_HINTS[`${name}.resource`] ?? COMPLETION_HINTS[`${name}.${getFirstArgName(root, name)}`];
    let resourceList = '$resources';
    if (hint && typeof hint === 'object' && 'static' in hint) {
      resourceList = hint.static.join(' ');
    } else if (hint === 'resource_types') {
      resourceList = '$resources';
    }

    emit(`    ${name})`);
    emit('      if [[ -z "$resource_type" ]]; then');
    emit(`        COMPREPLY=($(compgen -W "${resourceList} ${optFlags}" -- "$cur"))`);
    emit('      else');
    emit('        local names');
    emit('        names=$(_mcpctl_resource_names "$resource_type")');
    emit(`        COMPREPLY=($(compgen -W "$names ${optFlags}" -- "$cur"))`);
    emit('      fi');
    emit('      return ;;');
    return;
  }

  // logs: first arg is instance name
  if (name === 'logs') {
    emit(`    ${name})`);
    emit('      if [[ $((cword - subcmd_pos)) -eq 1 ]]; then');
    emit('        local names');
    emit("        names=$(mcpctl get instances -o json 2>/dev/null | jq -r '.[][].server.name' 2>/dev/null)");
    emit(`        COMPREPLY=($(compgen -W "$names ${optFlags}" -- "$cur"))`);
    emit('      else');
    emit(`        COMPREPLY=($(compgen -W "${optFlags}" -- "$cur"))`);
    emit('      fi');
    emit('      return ;;');
    return;
  }

  // chat: first arg is agent name
  if (name === 'chat') {
    emit(`    ${name})`);
    emit('      if [[ $((cword - subcmd_pos)) -eq 1 ]]; then');
    emit('        local names');
    emit('        names=$(_mcpctl_resource_names "agents")');
    emit(`        COMPREPLY=($(compgen -W "$names ${optFlags}" -- "$cur"))`);
    emit('      else');
    emit(`        COMPREPLY=($(compgen -W "${optFlags}" -- "$cur"))`);
    emit('      fi');
    emit('      return ;;');
    return;
  }

  // chat-llm: first arg is LLM name
  if (name === 'chat-llm') {
    emit(`    ${name})`);
    emit('      if [[ $((cword - subcmd_pos)) -eq 1 ]]; then');
    emit('        local names');
    emit('        names=$(_mcpctl_resource_names "llms")');
    emit(`        COMPREPLY=($(compgen -W "$names ${optFlags}" -- "$cur"))`);
    emit('      else');
    emit(`        COMPREPLY=($(compgen -W "${optFlags}" -- "$cur"))`);
    emit('      fi');
    emit('      return ;;');
    return;
  }

  // console: first arg is project name
  if (name === 'console') {
    emit(`    ${name})`);
    emit('      if [[ $((cword - subcmd_pos)) -eq 1 ]]; then');
    emit('        local names');
    emit("        names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)");
    emit(`        COMPREPLY=($(compgen -W "$names ${optFlags}" -- "$cur"))`);
    emit('      else');
    emit(`        COMPREPLY=($(compgen -W "${optFlags}" -- "$cur"))`);
    emit('      fi');
    emit('      return ;;');
    return;
  }

  // attach-server
  if (name === 'attach-server') {
    emit('    attach-server)');
    emit('      if [[ $((cword - subcmd_pos)) -ne 1 ]]; then return; fi');
    emit('      local proj names all_servers proj_servers');
    emit('      proj=$(_mcpctl_get_project_value)');
    emit('      if [[ -n "$proj" ]]; then');
    emit("        all_servers=$(mcpctl get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)");
    emit("        proj_servers=$(mcpctl --project \"$proj\" get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)");
    emit('        names=$(comm -23 <(echo "$all_servers" | sort) <(echo "$proj_servers" | sort))');
    emit('      else');
    emit('        names=$(_mcpctl_resource_names "servers")');
    emit('      fi');
    emit('      COMPREPLY=($(compgen -W "$names" -- "$cur"))');
    emit('      return ;;');
    return;
  }

  // detach-server
  if (name === 'detach-server') {
    emit('    detach-server)');
    emit('      if [[ $((cword - subcmd_pos)) -ne 1 ]]; then return; fi');
    emit('      local proj names');
    emit('      proj=$(_mcpctl_get_project_value)');
    emit('      if [[ -n "$proj" ]]; then');
    emit("        names=$(mcpctl --project \"$proj\" get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)");
    emit('      fi');
    emit('      COMPREPLY=($(compgen -W "$names" -- "$cur"))');
    emit('      return ;;');
    return;
  }

  // apply: file completions
  if (name === 'apply') {
    emit('    apply)');
    emit(`      COMPREPLY=($(compgen -f -W "${optFlags}" -- "$cur"))`);
    emit('      return ;;');
    return;
  }

  // mcp: hidden but still handle
  if (name === 'mcp') {
    emit('    mcp)');
    emit(`      COMPREPLY=($(compgen -W "${optFlags}" -- "$cur"))`);
    emit('      return ;;');
    return;
  }

  // Generic command with options only
  emit(`    ${name})`);
  emit(`      COMPREPLY=($(compgen -W "${optFlags}" -- "$cur"))`);
  emit('      return ;;');
}

/** Build the option flags string for a command (for bash COMPREPLY). */
function bashOptFlags(cmd: CmdInfo): string {
  const parts: string[] = [];
  for (const opt of cmd.options) {
    if (opt.short) parts.push(opt.short);
    parts.push(opt.long);
  }
  parts.push('-h', '--help');
  return parts.join(' ');
}

// ============================================================
// Utilities
// ============================================================

/** Escape single quotes in fish strings. */
function esc(s: string): string {
  return s.replace(/'/g, "\\'");
}

/** Get the first argument name for a command. */
function getFirstArgName(root: CmdInfo, cmdName: string): string {
  const cmd = root.subcommands.find((c) => c.name === cmdName);
  return cmd?.args[0]?.name ?? '';
}

// ============================================================
// Main
// ============================================================

async function main(): Promise<void> {
  const mode = process.argv[2] ?? '';

  let tree: CmdInfo;
  try {
    tree = await extractTree();
  } catch (err) {
    console.error('Failed to extract command tree from createProgram().');
    console.error('Make sure workspace packages are built: pnpm build');
    console.error(err);
    process.exit(1);
  }

  const fishContent = generateFish(tree);
  const bashContent = generateBash(tree);

  const fishPath = join(ROOT, 'completions', 'mcpctl.fish');
  const bashPath = join(ROOT, 'completions', 'mcpctl.bash');

  if (mode === '--check') {
    let stale = false;
    try {
      const currentFish = readFileSync(fishPath, 'utf-8');
      if (currentFish !== fishContent) {
        console.error('completions/mcpctl.fish is stale');
        stale = true;
      }
    } catch {
      console.error('completions/mcpctl.fish does not exist');
      stale = true;
    }
    try {
      const currentBash = readFileSync(bashPath, 'utf-8');
      if (currentBash !== bashContent) {
        console.error('completions/mcpctl.bash is stale');
        stale = true;
      }
    } catch {
      console.error('completions/mcpctl.bash does not exist');
      stale = true;
    }
    if (stale) {
      console.error('Run: pnpm completions:generate');
      process.exit(1);
    }
    console.log('Completions are up to date.');
    process.exit(0);
  }

  if (mode === '--write') {
    writeFileSync(fishPath, fishContent);
    writeFileSync(bashPath, bashContent);
    console.log(`Wrote ${fishPath}`);
    console.log(`Wrote ${bashPath}`);
    process.exit(0);
  }

  // Default: print to stdout
  console.log('=== completions/mcpctl.fish ===');
  console.log(fishContent);
  console.log('=== completions/mcpctl.bash ===');
  console.log(bashContent);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
