/* eslint-disable @typescript-eslint/no-explicit-any */
import { Argument, Command, Option } from "commander";
import prettier from "prettier";

export interface Options {
  figSpecCommandName?: string;
}

const DEFAULT_FIG_SUBCOMMAND_NAME = "generate-fig-spec";

function convertDefaultValue(v: unknown) {
  if (typeof v === "string") return v;
  if (Array.isArray(v)) return v.join(",");
  if (typeof v === "object") return undefined;
  return String(v);
}

async function getTemplate(spec: Fig.Spec): Promise<string> {
  return prettier.format(
    `
    // Autogenerated by @fig/complete-commander

    const completionSpec: Fig.Spec = ${JSON.stringify(spec)}

    export default completionSpec;
  `,
    { parser: "typescript" }
  );
}

function generateArg(_arg: Argument & Record<string, any>): Fig.Arg {
  const { _name: name, description, required, variadic, defaultValue } = _arg;

  const arg: Fig.Arg = { name };

  if (description) arg.description = description;
  if (!required) arg.isOptional = true;
  if (variadic) arg.isVariadic = true;
  if (defaultValue) arg.default = convertDefaultValue(defaultValue);
  return arg;
}

function generateOption(_option: Option & Record<string, any>): Fig.Option {
  const {
    short,
    long,
    flags,
    description,
    mandatory,
    required,
    optional,
    variadic,
    argChoices,
    defaultValue,
  } = _option;

  const name = new Set<string>();
  if (short) name.add(short);
  if (long) name.add(long);
  const option: Fig.Option = { name: Array.from(name) };

  if (description) option.description = description;
  if (mandatory) option.isRequired = true;
  // Option argument e.g. "-f, --flag <string>"
  // If required and optional are both false it does not have an argument
  if (required || optional) {
    // eslint-disable-next-line no-useless-escape
    const matches = flags.match(/.*[\[<](.*)[\]>]/); // !!! This is all but an useless escape. It is required to make the regex working

    const arg: Fig.Arg = {
      name: matches ? matches[1].replace(/\./g, "") : "",
    };
    if (optional) arg.isOptional = true;
    if (variadic) arg.isVariadic = true;
    if (argChoices) arg.suggestions = argChoices;
    if (defaultValue) arg.default = convertDefaultValue(defaultValue);
    option.args = arg;
  }
  return option;
}

interface ExtendedCommand extends Command {
  _name: string;
  _description: string;
  _aliases: string[];
  _args: Argument[];
  options: Option[];
  _helpDescription: string;
  _helpShortFlag: string;
  _helpLongFlag: string;
  _addImplicitHelpCommandL?: boolean; // Deliberately undefined, not decided whether true or false
  _helpCommandName: string;
  _helpCommandnameAndArgs: string;
  _helpCommandDescription: string;
  _hasHelpOption: boolean;
  _hidden: boolean;
}

function helpSubcommand({
  _helpCommandName, // 'help'
  _helpCommandDescription,
  _helpCommandnameAndArgs, // 'help [cmd]'
}: ExtendedCommand): Fig.Subcommand {
  const [, arg] = _helpCommandnameAndArgs.split(" ");
  return {
    name: _helpCommandName,
    description: _helpCommandDescription,
    priority: 49,
    ...(arg && {
      args: {
        name: arg.slice(1, -1),
        isOptional: true,
        template: "help",
      },
    }),
  };
}

function helpOption({
  _helpDescription,
  _helpShortFlag,
  _helpLongFlag,
}: ExtendedCommand): Fig.Option {
  return {
    name: [_helpShortFlag, _helpLongFlag],
    description: _helpDescription,
    priority: 49,
  };
}

function generateCommand(
  _command: Command & Record<string, any>,
  figSpecCommandName: string
): Fig.Subcommand | undefined {
  const {
    _name,
    _description,
    _aliases,
    commands,
    _args,
    options,
    _addImplicitHelpCommandL,
    _hasHelpOption,
    _hidden,
  } = _command as ExtendedCommand;

  if (_name === figSpecCommandName) return undefined;
  const name = _aliases.length > 0 ? Array.from(new Set([_name, ..._aliases])) : _name;
  const command: Fig.Subcommand = { name };

  if (_description) command.description = _description;
  if (_hidden) command.hidden = true;
  // Subcommands
  if (commands.length) {
    command.subcommands = [];
    for (const cmd of commands) {
      const subcommand = generateCommand(cmd, figSpecCommandName);
      if (subcommand) command.subcommands.push(subcommand);
    }
    if (_addImplicitHelpCommandL !== false) {
      command.subcommands.push(helpSubcommand(_command as ExtendedCommand));
    }
  }
  // Options
  command.options = [];
  if (options.length) {
    command.options = options.map(generateOption);
  }
  if (_hasHelpOption) {
    command.options.push(helpOption(_command as ExtendedCommand));
  }
  // Args
  if (_args.length) {
    command.args = _args.map(generateArg);
  }
  return command;
}

export async function generateCompletionSpec(
  command: Command,
  options?: Options
): Promise<string | undefined> {
  const figSpecCommandName = options?.figSpecCommandName || DEFAULT_FIG_SUBCOMMAND_NAME;
  // The first subcommand will never have the name of the `figSpecCommandName`
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const spec = getTemplate(generateCommand(command, figSpecCommandName)!);
  return spec;
}

export function addCompletionSpecCommand(command: Command) {
  command
    .command(DEFAULT_FIG_SUBCOMMAND_NAME)
    .description("Print the Fig completion spec")
    .action(async () => {
      const spec = await generateCompletionSpec(command);
      console.log(spec);
    });
}
