import { SyntaxNode } from 'web-tree-sitter';
import { findParentFunction, isCommandName, isCommandWithName, isEndStdinCharacter, isMatchingOption, isOption, isString } from '../utils/node-types';
import { getChildNodes, getRange } from '../utils/tree-sitter';
import { LspDocument } from '../document';
import { ChangeAnnotation, CodeAction, CodeActionKind, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver';
import { extractFunctionWithArgparseToCompletionsFile } from './refactors';

export type CompleteFlag = {
  shortOption?: string;
  longOption: string;
};

function parseArgparseFlag(text: string): CompleteFlag {
  // Remove any equals and following text
  const beforeEquals = text.split('=')[0] as string;

  // Check if it has a short/long split with '/'
  if (beforeEquals.includes('/')) {
    const [short, long] = beforeEquals.split('/') as [string, string];
    return {
      shortOption: short,
      longOption: long === '' ? '' : long,
    };
  }

  // No short option, just return as long option
  return {
    longOption: beforeEquals,
  };
}

function isSkipablePreviousOption(node: SyntaxNode): boolean {
  // don't skip previous nodes when the previous node is of the form:
  // ```fish
  // argparse -N=1 --max-args=2
  // ```
  if (node.text.includes('=')) return false;
  return isMatchingOption(node, { shortOption: '-N', longOption: '--min-args' }) ||
    isMatchingOption(node, { shortOption: '-n', longOption: '--name' }) ||
    isMatchingOption(node, { shortOption: '-x', longOption: '--exclusive' }) ||
    isMatchingOption(node, { shortOption: '-X', longOption: '--max-args' });
}

export function findFlagsToComplete(node: SyntaxNode): CompleteFlag[] {
  if (!isCommandWithName(node, 'argparse')) return [];

  const flags: CompleteFlag[] = [];

  for (const child of getChildNodes(node)) {
    // Stop at -- argument separator
    if (isEndStdinCharacter(child)) break;

    // skip `argparse` command name
    if (isCommandName(child)) continue;

    // Skip command name and actual options (like --ignore-unknown)
    if (isOption(child)) continue;

    // skip previous options that are not flags
    const prev = child.previousSibling;
    if (prev && isOption(prev) && isSkipablePreviousOption(prev)) continue;

    // Handle quoted strings
    if (isString(child)) {
      // Remove surrounding quotes
      const text = child.text.slice(1, -1);
      flags.push(parseArgparseFlag(text));
      continue;
    }

    // Handle unquoted option strings
    if (child.type === 'word' && !child.text.startsWith('-')) {
      flags.push(parseArgparseFlag(child.text));
    }
  }

  return flags;
}

export function buildCompleteString(commandName: string, flags: CompleteFlag[]): string {
  return flags.map(flag => {
    let text = `complete -c ${commandName}`;
    if (flag.shortOption) {
      text += ` -s ${flag.shortOption}`;
    }
    if (flag.longOption) {
      text += ` -l ${flag.longOption}`;
    }
    return text;
  }).join('\n');
}

/**
 * Helper function to build `argparse` completions for the current function in a
 * `conf.d/file.fish` file.
 * ___
 * Some example input can be seen below:
 * ___
 * ```fish
 * # ~/.config/fish/conf.d/file.fish
 * function some_function
 *     argparse h/help o/option= v/verbose -- $argv
 *     or return
 *
 *     echo 'do some stuff'
 * end
 * ```
 * ___
 * @param argparseNode The `argparse` node
 * @param functionNode The `function_definition` node
 * @param functionNameNode The `functionNode.firstNamedChild` node containing the name of the function
 * @returns A `CodeAction` object to create the completions file
 */
function buildConfdCompletions(
  argparseNode: SyntaxNode,
  functionNode: SyntaxNode,
  functionNameNode: SyntaxNode,
  doc: LspDocument,
): CodeAction | undefined {
  // get the path to the completions file. Should be in the conf.d directory
  const completionPath = doc.getRelativeFilenameToWorkspace();
  // get the flags and the function name
  const flags = findFlagsToComplete(argparseNode);
  const functionName = functionNameNode.text;
  // build the `complete -c command -s -l` string
  const completionText = buildCompleteString(functionName, flags);
  // Get the text to insert
  const selectedText = `\n# auto generated by fish-lsp\n${completionText}\n`;
  // Create a change annotation
  const changeAnnotation: ChangeAnnotation = {
    label: `Create completions for '${functionName}' in file: ${completionPath}`,
    description: `Create completions for '${functionName}' to file: ${completionPath}`,
  };
  // build the workspace edit
  const workspaceEdit: WorkspaceEdit = {
    documentChanges: [
      TextDocumentEdit.create(
        VersionedTextDocumentIdentifier.create(doc.uri, doc.version + 1),
        [TextEdit.insert(getRange(functionNode).end, selectedText)]),
    ],
    changeAnnotations: { [changeAnnotation.label]: changeAnnotation },
  };
  return {
    title: 'Create completions file',
    kind: CodeActionKind.QuickFix,
    edit: workspaceEdit,
  };
}

function getNodesForArgparse(selectedNode: SyntaxNode) {
  const node = selectedNode;
  if (isCommandWithName(node, 'argparse')) {
    const functionNode = findParentFunction(node);
    return {
      argparseNode: node,
      functionNode: functionNode,
      functionNameNode: functionNode?.firstChild,
    };
  }
  if (node.type === 'word' && node.parent && isCommandWithName(node.parent, 'argparse')) {
    const functionNode = findParentFunction(node.parent);
    return {
      argparseNode: node.parent,
      functionNode: functionNode,
      functionNameNode: functionNode?.firstChild,
    };
  }
  if (node.type === 'function_definition') {
    return {
      argparseNode: getChildNodes(node).find(n => isCommandWithName(n, 'argparse')),
      functionNode: node,
      functionNameNode: node.firstNamedChild,
    };
  }
  return {
    argparseNode: undefined,
    functionNode: undefined,
    functionNameNode: undefined,
  };
}

export function createArgparseCompletionsCodeAction(
  node: SyntaxNode,
  doc: LspDocument,
): CodeAction | undefined {
  const autoloadType = doc.getAutoloadType();
  const { argparseNode, functionNode, functionNameNode } = getNodesForArgparse(node);

  if (!argparseNode || !functionNode || !functionNameNode) return undefined;

  if (autoloadType === 'functions') {
    return extractFunctionWithArgparseToCompletionsFile(doc, getRange(functionNode), functionNode);
  }
  if (autoloadType === 'conf.d') {
    return buildConfdCompletions(node, functionNode, functionNameNode, doc);
  }
  return undefined;
}
