import * as program from "commander";
import * as inquirer from "inquirer";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access";
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";

import { DownloadCommand } from "../../../commands/download.command";
import { Response } from "../../../models/response";
import { SendAccessResponse } from "../models/send-access.response";

export class SendReceiveCommand extends DownloadCommand {
  private canInteract: boolean;
  private decKey: SymmetricCryptoKey;
  private sendAccessRequest: SendAccessRequest;

  constructor(
    private apiService: ApiService,
    cryptoService: CryptoService,
    private cryptoFunctionService: CryptoFunctionService,
    private platformUtilsService: PlatformUtilsService,
    private environmentService: EnvironmentService,
    private sendApiService: SendApiService
  ) {
    super(cryptoService);
  }

  async run(url: string, options: program.OptionValues): Promise<Response> {
    this.canInteract = process.env.BW_NOINTERACTION !== "true";

    let urlObject: URL;
    try {
      urlObject = new URL(url);
    } catch (e) {
      return Response.badRequest("Failed to parse the provided Send url");
    }

    const apiUrl = this.getApiUrl(urlObject);
    const [id, key] = this.getIdAndKey(urlObject);

    if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
      return Response.badRequest("Failed to parse url, the url provided is not a valid Send url");
    }

    const keyArray = Utils.fromUrlB64ToArray(key);
    this.sendAccessRequest = new SendAccessRequest();

    let password = options.password;
    if (password == null || password === "") {
      if (options.passwordfile) {
        password = await NodeUtils.readFirstLine(options.passwordfile);
      } else if (options.passwordenv && process.env[options.passwordenv]) {
        password = process.env[options.passwordenv];
      }
    }

    if (password != null && password !== "") {
      this.sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray);
    }

    const response = await this.sendRequest(apiUrl, id, keyArray);

    if (response instanceof Response) {
      // Error scenario
      return response;
    }

    if (options.obj != null) {
      return Response.success(new SendAccessResponse(response));
    }

    switch (response.type) {
      case SendType.Text:
        // Write to stdout and response success so we get the text string only to stdout
        process.stdout.write(response?.text?.text);
        return Response.success();
      case SendType.File: {
        const downloadData = await this.sendApiService.getSendFileDownloadData(
          response,
          this.sendAccessRequest,
          apiUrl
        );
        return await this.saveAttachmentToFile(
          downloadData.url,
          this.decKey,
          response?.file?.fileName,
          options.output
        );
      }
      default:
        return Response.success(new SendAccessResponse(response));
    }
  }

  private getIdAndKey(url: URL): [string, string] {
    const result = url.hash.slice(1).split("/").slice(-2);
    return [result[0], result[1]];
  }

  private getApiUrl(url: URL) {
    const urls = this.environmentService.getUrls();
    if (url.origin === "https://send.bitwarden.com") {
      return "https://api.bitwarden.com";
    } else if (url.origin === urls.api) {
      return url.origin;
    } else if (this.platformUtilsService.isDev() && url.origin === urls.webVault) {
      return urls.api;
    } else {
      return url.origin + "/api";
    }
  }

  private async getUnlockedPassword(password: string, keyArray: ArrayBuffer) {
    const passwordHash = await this.cryptoFunctionService.pbkdf2(
      password,
      keyArray,
      "sha256",
      100000
    );
    return Utils.fromBufferToB64(passwordHash);
  }

  private async sendRequest(
    url: string,
    id: string,
    key: ArrayBuffer
  ): Promise<Response | SendAccessView> {
    try {
      const sendResponse = await this.sendApiService.postSendAccess(
        id,
        this.sendAccessRequest,
        url
      );

      const sendAccess = new SendAccess(sendResponse);
      this.decKey = await this.cryptoService.makeSendKey(key);
      return await sendAccess.decrypt(this.decKey);
    } catch (e) {
      if (e instanceof ErrorResponse) {
        if (e.statusCode === 401) {
          if (this.canInteract) {
            const answer: inquirer.Answers = await inquirer.createPromptModule({
              output: process.stderr,
            })({
              type: "password",
              name: "password",
              message: "Send password:",
            });

            // reattempt with new password
            this.sendAccessRequest.password = await this.getUnlockedPassword(answer.password, key);
            return await this.sendRequest(url, id, key);
          }

          return Response.badRequest("Incorrect or missing password");
        } else if (e.statusCode === 405) {
          return Response.badRequest("Bad Request");
        } else if (e.statusCode === 404) {
          return Response.notFound();
        }
      }
      return Response.error(e);
    }
  }
}
