#!/usr/bin/env node
//'use strict'
import { BuildCapabilityString, PathObj, PackageLspVersion, PackageVersion, accumulateStartupOptions, getBuildTimeString, FishLspHelp, FishLspManPage, SourcesDict, smallFishLogo, isPkgBinary } from './utils/commander-cli-subcommands';
import { createConnection, InitializeParams, InitializeResult, StreamMessageReader, StreamMessageWriter } from 'vscode-languageserver/node';
import { Command, Option } from 'commander';
import FishServer from './server';
import { buildFishLspCompletions } from './utils/get-lsp-completions';
import { createServerLogger, log, logger, logToStdout, logToStdoutJoined } from './logger';
import { configHandlers, generateJsonSchemaShellScript, config, showJsonSchemaShellScript, updateHandlers, validHandlers, Config } from './config';

export function startServer() {
  // Create a connection for the server.
  // The connection uses stdin/stdout for communication.
  const connection = createConnection(
    new StreamMessageReader(process.stdin),
    new StreamMessageWriter(process.stdout),
  );
  connection.onInitialize(
    async (params: InitializeParams): Promise<InitializeResult> => {
      // connection.console.log(`Initialized server FISH-LSP with ${JSON.stringify(params, null, 2)}`);
      const server = await FishServer.create(connection, params);
      server.register(connection);
      return server.initialize(params);
    },
  );
  connection.listen();
  createServerLogger(config.fish_lsp_logfile, true, connection.console);
  logger.log('Starting FISH-LSP server');
  logger.log('Server started with the following handlers:', configHandlers);
  logger.log('Server started with the following config:', config);
}

async function timeOperation<T>(
  operation: () => Promise<T>,
  label: string,
): Promise<T> {
  const start = performance.now();
  try {
    const result = await operation();
    const end = performance.now();
    const duration = end - start;
    logToStdoutJoined(
      `${label}:`.padEnd(55),
      `${duration.toFixed(2)}ms`.padStart(10),
    );
    return result;
  } catch (error) {
    const end = performance.now();
    const duration = end - start;
    logToStdout(`${label} failed after ${duration.toFixed(2)}ms`);
    throw error;
  }
}

async function timeServerStartup() {
  // define a local server instance
  let server: FishServer | undefined;

  // 1. Time server creation and startup
  await timeOperation(async () => {
    const connection = createConnection(
      new StreamMessageReader(process.stdin),
      new StreamMessageWriter(process.stdout),
    );
    // connection.console.log('Starting FISH-LSP server');
    const startupParams: InitializeParams = {
      processId: process.pid,
      rootUri: process.cwd(),
      capabilities: {},
    };
    server = await FishServer.create(connection, startupParams);
    server.register(connection);
    server.initialize(startupParams);
    connection.listen();
    return server;
  }, 'Server Start Time');

  // 2. Time server initialization and background analysis
  await timeOperation(async () => {
    await server?.startBackgroundAnalysis();
  }, 'Background Analysis Time');

  // 3. Log the number of files indexed
  logToStdoutJoined(
    'Total Files Indexed: '.padEnd(55),
    `${server?.analyzer.amountIndexed} files`.padStart(10),
  );

  // 4. Log the directories indexed
  const all_indexed = config.fish_lsp_all_indexed_paths;
  logToStdoutJoined(
    "Indexed Files in '$fish_lsp_all_indexed_paths':".padEnd(55),
    `${all_indexed.length} paths`.padStart(10),
  );
  const maxItemLen = all_indexed.reduce((max, item) => Math.max(max, item.length), 0);
  const startStr = ' '.repeat(3);
  config.fish_lsp_all_indexed_paths.forEach((item, idx) => logToStdoutJoined(
    `${startStr}$fish_lsp_all_indexed_paths[${idx + 1}]  `.padEnd(64 - maxItemLen),
    `|${item}|`.padStart(maxItemLen + 2),
  ));
}

/**
 *  creates local 'commandBin' used for commander.js
 */
const createFishLspBin = (): Command => {
  const bin = new Command('fish-lsp')
    .description(`Description:\n${FishLspHelp.description || 'fish-lsp command output'}`)
    .helpOption('-h, --help', 'show the relevant help info. Use `--help-all` for comprehensive documentation of all commands and flags. Other `--help-*` flags are also available.')
    .version(PackageVersion, '-v, --version', 'output the version number')
    .enablePositionalOptions(true)
    .configureHelp({
      showGlobalOptions: false,
      commandUsage: (_) => FishLspHelp.usage,
    })
    .showSuggestionAfterError(true)
    .showHelpAfterError(true)
    .addHelpText('after', FishLspHelp.after);
  return bin;
};

// start adding options to the command
export const commandBin = createFishLspBin();

// hidden global options
commandBin
  .addOption(new Option('--help-man', 'show special manpage output').hideHelp(true))
  .addOption(new Option('--help-all', 'show all help info').hideHelp(true))
  .addOption(new Option('--help-short', 'show mini help info').hideHelp(true))
  .action(opt => {
    if (opt.helpMan) {
      const { path: _path, content } = FishLspManPage();
      logToStdout(content.join('\n').trim());
    } else if (opt.helpAll) {
      const globalOpts = commandBin.options.concat(new Option('-h, --help', 'show help'));
      const subCommands = commandBin.commands.map((cmd) => {
        return [
          `  ${cmd.name()} ${cmd.usage()}\t${cmd.summary()}`,
          cmd.options.map(o => `    ${o.flags}\t\t${o.description}`).join('\n'),
          ''].join('\n');
      });
      logToStdout(['NAME:',
        'fish-lsp - an lsp for the fish shell language',
        '',
        'USAGE: ',
        FishLspHelp.beforeAll,
        '',
        'DESCRIPTION:',
        '  ' + commandBin.description().split('\n').slice(1).join('\n').trim(),
        '',
        'OPTIONS:',
        '  ' + globalOpts.map(o => '  ' + o.flags + '\t' + o.description).join('\n').trim(),
        '',
        'SUBCOMMANDS:',
        subCommands.join('\n'),
        '',
        'EXAMPLES:',
        FishLspHelp.after.split('\n').slice(2).join('\n'),
      ].join('\n').trim());
    } else if (opt.helpShort) {
      logToStdout([
        'Usage: fish-lsp ', commandBin.usage().split('\n').slice(0, 1),
        '',
        commandBin.description(),
      ].join('\n'));
    }
    return;
  });

// START
commandBin.command('start [TOGGLE]')
  .summary('subcmd to start the lsp using stdin/stdout')
  .description('start the language server for a connection to a client')
  .option('--dump', 'stop lsp & show the startup options being read')
  .option('--enable <string...>', 'enable the startup option')
  .option('--disable <string...>', 'disable the startup option')
  .addHelpText('afterAll', [
    '',
    'STRINGS FOR \'--enable/--disable\':',
    `(${validHandlers.map((opt, index) => {
      return index < validHandlers.length - 1 && index > 0 && index % 5 === 0 ? `${opt},\n` :
        index < validHandlers.length - 1 ? `${opt},` : opt;
    }).join(' ')})`,
    '',
    'Examples:',
    '\tfish-lsp start --disable hover  # only disable the hover feature',
    '\tfish-lsp start --disable complete logging index hover --show',
    '\tfish-lsp start --enable --disable logging complete codeAction',
  ].join('\n'))
  .action(() => {
    // NOTE: `config` is a global object, already initialized. Here, we are updating its
    // values passed from the shell environment, and then possibly overriding them with
    // the command line args.

    // use the `config` object's shell environment values to update the handlers
    updateHandlers(config.fish_lsp_enabled_handlers, true);
    updateHandlers(config.fish_lsp_disabled_handlers, false);

    // override `configHandlers` with command line args
    const { enabled, disabled, dumpCmd } = accumulateStartupOptions(commandBin.args);
    updateHandlers(enabled, true);
    updateHandlers(disabled, false);
    Config.fixPopups(enabled, disabled);

    // Dump the configHandlers, if requested from the command line. This stops the server.
    if (dumpCmd) {
      log(JSON.stringify({ handlers: configHandlers }, null, 2));
      log(JSON.stringify({ config: config }, null, 2));
      process.exit(0);
    }

    /* config needs to be used in `startServer()` below */
    startServer();
  });

// LOGGER
commandBin.command('logger')
  .summary('test the logger by displaying it')
  .option('-s, --show', 'show the logger and don\'t edit it')
  .option('-c, --clear', 'clear the logger')
  .option('-d, --date', 'write the date')
  .option('-q, --quiet', 'silence logging')
  .option('--config', 'show the logger config')
  .action(args => {
    const logger = createServerLogger(config.fish_lsp_logfile, false);
    const objArgs = Object.getOwnPropertyNames(args);
    const argsQueue = objArgs;
    let currentArg: string = '';
    while (argsQueue.length !== 0) {
      currentArg = argsQueue.shift() || '';
      if (currentArg === 'clear') logger.clearLogFile();
      if (currentArg === 'quiet') logger.toggleSilence();
      if (currentArg === 'date') logger.log(getBuildTimeString());
      if (currentArg === 'config') logToStdout(JSON.stringify(logger.getLoggingOpts()));
      if (currentArg === 'show') break;
    }

    if (!args.show) return;
    logger.showLogfileText();
    return;
  });

// INFO
commandBin.command('info')
  .summary('show the build info of fish-lsp')
  .option('--bin', 'show the path of the fish-lsp executable')
  .option('--repo', 'show the path of the entire fish-lsp repo')
  .option('--time', 'show the path of the entire fish-lsp repo')
  .option('--lsp-version', 'show the lsp version')
  .option('--capabilities', 'show the lsp capabilities')
  .option('--man-file', 'show the man file path')
  .option('--logs-file', 'show the logs.txt file path')
  .option('--more', 'show the build time of the fish-lsp executable')
  .option('--time-startup', 'time the startup of the fish-lsp executable')
  .action(async args => {
    const capabilities = BuildCapabilityString()
      .split('\n')
      .map(line => `  ${line}`).join('\n');
    if (args.timeStartup) {
      await timeServerStartup();
      process.exit(0);
    }
    if (args.bin || args.repo) {
      const logPath = args.bin ? PathObj.bin : PathObj.repo;
      const wpath = args.bin ? 'BINARY' : 'REPOSITORY';
      logToStdout(wpath + ' ' + smallFishLogo());
      logToStdout(logPath);
      process.exit(0);
    }
    if (args.time) {
      logToStdout(`Build Time: ${getBuildTimeString()}`);
      process.exit(0);
    }
    if (args.capabilities) {
      logToStdout(`Capabilities:\n${capabilities}`);
      process.exit(0);
    }
    if (args.lspVersion) {
      logToStdout(`LSP Version: ${PackageLspVersion}`);
      process.exit(0);
    }
    if (args.manFile) {
      logToStdout(PathObj.manFile);
      process.exit(0);
    }
    if (args.logsFile) {
      logToStdout(config.fish_lsp_logfile);
      process.exit(0);
    }
    logToStdout(`Repository: ${PathObj.repo}`);
    logToStdout(`Version: ${PackageVersion}`);
    logToStdout(`Build Time: ${getBuildTimeString()}`);
    logToStdout(`Install Type: ${isPkgBinary() ? 'standalone executable' : 'local build'}`);
    logToStdout(`Node Version: ${process.version}`);
    logToStdout(`LSP Version: ${PackageLspVersion}`);
    logToStdout(`Binary File: ${PathObj.bin}`);
    logToStdout(`Man File: ${PathObj.manFile}`);
    logToStdout(`Log File: ${config.fish_lsp_logfile}`);
    logToStdout('CAPABILITIES:');
    logToStdout(capabilities);
    process.exit(0);
  });

// URL
commandBin.command('url')
  .summary('show a helpful url related to the fish-lsp')
  .description('show the related url to the fish-lsp')
  .option('--repo, --git', 'show the github repo')
  .option('--npm', 'show the npm package url')
  .option('--homepage', 'show the homepage')
  .option('--contributions', 'show the contributions url')
  .option('--wiki', 'show the github wiki')
  .option('--issues, --report', 'show the issues page')
  .option('--discussions', 'show the discussions page')
  .option('--clients-repo', 'show the clients configuration repo')
  .option('--sources', 'show a list of helpful sources')
  .action(args => {
    const amount = Object.keys(args).length;
    if (amount === 0) logToStdout('https://fish-lsp.dev');
    Object.keys(args).forEach(key => logToStdout(SourcesDict[key]?.toString() || ''));
    process.exit(0);
  });

// COMPLETE
commandBin.command('complete')
  .summary('generate completions file for ~/.config/fish/completions')
  .option('--names', 'show the feature names of the completions')
  .option('--toggles', 'show the feature names of the completions')
  .option('--fish', 'show fish script')
  .option('--features', 'show features')
  .description('copy completions output to fish-lsp completions file')
  .action(args => {
    if (args.names) {
      commandBin.commands.forEach(cmd => logToStdout(cmd.name() + '\t' + cmd.summary()));
      process.exit(0);
    } else if (args.toggles) {
      commandBin.commands.forEach(cmd => {
        logToStdout(cmd.name() + '\t' + cmd.summary());
        Object.entries(cmd.opts()).forEach(opt => logToStdout('--' + opt[0]));
      });
      process.exit(0);
    } else if (args.fish) {
      logToStdout(buildFishLspCompletions(commandBin));
      process.exit(0);
    } else if (args.features) {
      Object.entries(configHandlers).forEach((name) => logToStdout(name.toString()));
      process.exit(0);
    }
    logToStdout(buildFishLspCompletions(commandBin));
    process.exit(0);
  });

// ENV
commandBin.command('env')
  .summary('generate fish shell env variables to be used by lsp')
  .description('generate fish-lsp env variables')
  .option('-c, --create', 'build initial fish-lsp env variables')
  .option('-s, --show', 'show the current fish-lsp env variables')
  .option('--no-comments', 'skip comments in output')
  .option('--no-global', 'use local env variables')
  .option('--no-local', 'do not use local scope for variables')
  .option('--no-export', 'don\'t export the variables')
  .action(args => {
    if (args.show) {
      showJsonSchemaShellScript(args.comments, args.global, args.local, args.export);
      process.exit(0);
    }
    generateJsonSchemaShellScript(args.comments, args.global, args.local, args.export);
  });

/**
 * ADD HELP MESSAGE WHEN NO SUBCOMMAND IS GIVEN
 */
// if (process.argv.length <= 2 && process.env['NODE_TEST'] !== 'test') {
//   process.argv.push('--help')
// }

/**
 * PARSE THE SUBCOMMAND/OPTION
 */
commandBin.parse();
