/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: BUSL-1.1
 */

import keys from 'core/utils/key-codes';
import AdapterError from '@ember-data/adapter/error';
import { parse } from 'shell-quote';

import argTokenizer from './arg-tokenizer';
import { StringMap } from 'vault/vault/app-types';

// Add new commands to `log-help` component for visibility
const supportedCommands = ['read', 'write', 'list', 'delete', 'kv-get'];
const uiCommands = ['api', 'clearall', 'clear', 'fullscreen', 'refresh'];

interface DataObj {
  [key: string]: string | string[];
}

export function extractDataFromStrings(dataArray: string[]): DataObj {
  if (!dataArray) return {};
  return dataArray.reduce((accumulator: DataObj, val: string) => {
    // will be "key=value" or "foo=bar=baz"
    // split on the first =
    // default to value of empty string
    const [item = '', value = ''] = val.split(/=(.+)?/);
    if (!item) return accumulator;

    // if it exists in data already, then we have multiple
    // foo=bar in the list and need to make it an array
    const existingValue = accumulator[item];
    if (existingValue) {
      accumulator[item] = Array.isArray(existingValue) ? [...existingValue, value] : [existingValue, value];
      return accumulator;
    }
    accumulator[item] = value;
    return accumulator;
  }, {});
}

interface Flags {
  field?: string;
  format?: string;
  force?: boolean;
  wrapTTL?: boolean;
  [key: string]: string | boolean | undefined;
}
export function extractFlagsFromStrings(flagArray: string[], method: string): Flags {
  if (!flagArray) return {};
  return flagArray.reduce((accumulator: Flags, val: string) => {
    // val will be "-flag=value" or "--force"
    // split on the first =
    // default to value or true
    const [item, value] = val.split(/=(.+)?/);
    if (!item) return accumulator;

    let flagName = item.replace(/^-/, '');
    if (flagName === 'wrap-ttl') {
      flagName = 'wrapTTL';
    } else if (method === 'write') {
      if (flagName === 'f' || flagName === '-force') {
        flagName = 'force';
      }
    }
    accumulator[flagName] = value || true;
    return accumulator;
  }, {});
}

interface CommandFns {
  [key: string]: CallableFunction;
}

export function executeUICommand(
  command: string,
  logAndOutput: CallableFunction,
  commandFns: CommandFns
): boolean {
  const cmd = command.startsWith('api') ? 'api' : command;
  const isUICommand = uiCommands.includes(cmd);
  if (isUICommand) {
    logAndOutput(command);
  }
  const execCommand = commandFns[cmd];
  if (execCommand && typeof execCommand === 'function') {
    execCommand();
  }
  return isUICommand;
}

interface ParsedCommand {
  method: string;
  path: string;
  flagArray: string[];
  dataArray: string[];
}
export function parseCommand(command: string): ParsedCommand {
  const args: string[] = argTokenizer(parse(command));
  if (args[0] === 'vault') {
    args.shift();
  }

  const [method = '', ...rest] = args;
  let path = '';
  const flags: string[] = [];
  const data: string[] = [];

  rest.forEach((arg) => {
    if (arg.startsWith('-')) {
      flags.push(arg);
    } else {
      if (path) {
        const strippedArg = arg
          // we'll have arg=something or arg="lol I need spaces", so need to split on the first =
          .split(/=(.+)/)
          // if there were quotes, there's an empty string as the last member in the array that we don't want,
          // so filter it out
          .filter((str) => str !== '')
          // glue the data back together
          .join('=');
        data.push(strippedArg);
      } else {
        path = arg;
      }
    }
  });

  if (!supportedCommands.includes(method)) {
    throw new Error('invalid command');
  }
  return { method, flagArray: flags, path, dataArray: data };
}

interface LogResponse {
  auth?: StringMap;
  data?: StringMap;
  wrap_info?: StringMap;
  [key: string]: unknown;
}

export function logFromResponse(response: LogResponse, path: string, method: string, flags: Flags) {
  const { format, field } = flags;
  const respData: StringMap | undefined = response && (response.auth || response.data || response.wrap_info);
  const secret: StringMap | LogResponse = respData || response;

  if (!respData) {
    if (method === 'write') {
      return { type: 'success', content: `Success! Data written to: ${path}` };
    } else if (method === 'delete') {
      return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` };
    }
  }

  if (field) {
    const fieldValue = secret[field];
    let response;
    if (fieldValue) {
      if (format && format === 'json') {
        return { type: 'json', content: fieldValue };
      }
      if (typeof fieldValue == 'string') {
        response = { type: 'text', content: fieldValue };
      } else if (typeof fieldValue == 'number') {
        response = { type: 'text', content: JSON.stringify(fieldValue) };
      } else if (typeof fieldValue == 'boolean') {
        response = { type: 'text', content: JSON.stringify(fieldValue) };
      } else if (Array.isArray(fieldValue)) {
        response = { type: 'text', content: JSON.stringify(fieldValue) };
      } else {
        response = { type: 'object', content: fieldValue };
      }
    } else {
      response = { type: 'error', content: `Field "${field}" not present in secret` };
    }
    return response;
  }

  if (format && format === 'json') {
    // just print whole response
    return { type: 'json', content: response };
  }

  if (method === 'list') {
    return { type: 'list', content: secret };
  }

  return { type: 'object', content: secret };
}

interface CustomError extends AdapterError {
  httpStatus: number;
  path: string;
  errors: string[];
}
export function logFromError(error: CustomError, vaultPath: string, method: string) {
  let content;
  const { httpStatus, path } = error;
  const verbClause = {
    read: 'reading from',
    'kv-get': 'reading secret',
    write: 'writing to',
    list: 'listing',
    delete: 'deleting at',
  }[method];

  content = `Error ${verbClause}: ${vaultPath}.\nURL: ${path}\nCode: ${httpStatus}`;

  if (typeof error.errors[0] === 'string') {
    content = `${content}\nErrors:\n  ${error.errors.join('\n  ')}`;
  }

  return { type: 'error', content };
}

interface CommandLog {
  type: string;
  content?: string;
}
export function shiftCommandIndex(keyCode: number, history: CommandLog[], index: number) {
  let newInputValue;
  const commandHistoryLength = history.length;

  if (!commandHistoryLength) {
    return [];
  }

  if (keyCode === keys.UP) {
    index -= 1;
    if (index < 0) {
      index = commandHistoryLength - 1;
    }
  } else {
    index += 1;
    if (index === commandHistoryLength) {
      newInputValue = '';
    }
    if (index > commandHistoryLength) {
      index -= 1;
    }
  }

  if (newInputValue !== '') {
    const objAt = history[index];
    newInputValue = objAt?.content;
  }

  return [index, newInputValue];
}

export function formattedErrorFromInput(path: string, method: string, flags: Flags, dataArray: string[]) {
  if (path === undefined) {
    return { type: 'error', content: 'A path is required to make a request.' };
  }
  if (method === 'write' && !flags.force && dataArray.length === 0) {
    return { type: 'error', content: 'Must supply data or use -force' };
  }
  return;
}
