// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as inquirer from "inquirer";
import { firstValueFrom, map, switchMap } from "rxjs";

import { UpdateCollectionRequest } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { FolderExport } from "@bitwarden/common/models/export/folder.export";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { KeyService } from "@bitwarden/key-management";

import { OrganizationCollectionRequest } from "../admin-console/models/request/organization-collection.request";
import { OrganizationCollectionResponse } from "../admin-console/models/response/organization-collection.response";
import { SelectionReadOnly } from "../admin-console/models/selection-read-only";
import { Response } from "../models/response";
import { CliUtils } from "../utils";
import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response";
import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service";

export class EditCommand {
  constructor(
    private cipherService: CipherService,
    private folderService: FolderService,
    private keyService: KeyService,
    private encryptService: EncryptService,
    private apiService: ApiService,
    private folderApiService: FolderApiServiceAbstraction,
    private accountService: AccountService,
    private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
    private policyService: PolicyService,
    private billingAccountProfileStateService: BillingAccountProfileStateService,
    private cipherAuthorizationService: CipherAuthorizationService,
  ) {}

  async run(
    object: string,
    id: string,
    requestJson: any,
    cmdOptions: Record<string, any>,
  ): Promise<Response> {
    if (process.env.BW_SERVE !== "true" && (requestJson == null || requestJson === "")) {
      requestJson = await CliUtils.readStdin();
    }

    if (requestJson == null || requestJson === "") {
      return Response.badRequest("`requestJson` was not provided.");
    }

    let req: any = null;
    if (typeof requestJson !== "string") {
      req = requestJson;
    } else {
      try {
        const reqJson = Buffer.from(requestJson, "base64").toString();
        req = JSON.parse(reqJson);
        // FIXME: Remove when updating file. Eslint update
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
      } catch (e) {
        return Response.badRequest("Error parsing the encoded request data.");
      }
    }

    if (id != null) {
      id = id.toLowerCase();
    }

    const normalizedOptions = new Options(cmdOptions);
    switch (object.toLowerCase()) {
      case "item":
        return await this.editCipher(id, req);
      case "item-collections":
        return await this.editCipherCollections(id, req);
      case "folder":
        return await this.editFolder(id, req);
      case "org-collection":
        return await this.editOrganizationCollection(id, req, normalizedOptions);
      default:
        return Response.badRequest("Unknown object.");
    }
  }

  private async editCipher(id: string, req: CipherExport) {
    const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
    const cipher = await this.cipherService.get(id, activeUserId);
    const hasPremium = await firstValueFrom(
      this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
    );

    if (cipher == null) {
      return Response.notFound();
    }

    const canEditCipher = await firstValueFrom(
      this.cipherAuthorizationService.canEditCipher$(cipher),
    );
    if (!canEditCipher) {
      return Response.noEditPermission();
    }

    let cipherView = await this.cipherService.decrypt(cipher, activeUserId);
    if (cipherView.isDeleted) {
      return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
    }
    cipherView = CipherExport.toView(req, cipherView);

    // When a user is editing an archived cipher and does not have premium, automatically unarchive it
    if (cipherView.isArchived && !hasPremium) {
      const acceptedPrompt = await this.promptForArchiveEdit();

      if (!acceptedPrompt) {
        return Response.error("Edit cancelled.");
      }

      cipherView.archivedDate = null;
    }

    const isCipherRestricted =
      await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView);
    if (isCipherRestricted) {
      return Response.error("Editing this item type is restricted by organizational policy.");
    }

    const isPersonalVaultItem = cipherView.organizationId == null;

    const organizationOwnershipPolicyApplies = await firstValueFrom(
      this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, activeUserId),
    );

    if (isPersonalVaultItem && organizationOwnershipPolicyApplies) {
      return Response.error(
        "An organization policy restricts editing this cipher. Please use the share command first before modifying it.",
      );
    }

    try {
      const decCipher = await this.cipherService.updateWithServer(cipherView, activeUserId);
      const res = new CipherResponse(decCipher);
      return Response.success(res);
    } catch (e) {
      return Response.error(e);
    }
  }

  private async editCipherCollections(id: string, req: string[]) {
    const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));

    const cipher = await this.cipherService.get(id, activeUserId);
    if (cipher == null) {
      return Response.notFound();
    }
    if (cipher.organizationId == null) {
      return Response.badRequest(
        "Item does not belong to an organization. Consider moving it first.",
      );
    }
    if (!cipher.viewPassword) {
      return Response.noEditPermission();
    }

    cipher.collectionIds = req;
    try {
      const updatedCipher = await this.cipherService.saveCollectionsWithServer(
        cipher,
        activeUserId,
      );
      const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
      const res = new CipherResponse(decCipher);
      return Response.success(res);
    } catch (e) {
      return Response.error(e);
    }
  }

  private async editFolder(id: string, req: FolderExport) {
    const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
    const folder = await this.folderService.getFromState(id, activeUserId);
    if (folder == null) {
      return Response.notFound();
    }

    const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
    let folderView = await folder.decrypt(userKey);
    folderView = FolderExport.toView(req, folderView);

    const encFolder = await this.folderService.encrypt(folderView, userKey);
    try {
      const folder = await this.folderApiService.save(encFolder, activeUserId);
      const updatedFolder = new Folder(folder);
      const decFolder = await updatedFolder.decrypt(userKey);
      const res = new FolderResponse(decFolder);
      return Response.success(res);
    } catch (e) {
      return Response.error(e);
    }
  }

  private async editOrganizationCollection(
    id: string,
    req: OrganizationCollectionRequest,
    options: Options,
  ) {
    if (options.organizationId == null || options.organizationId === "") {
      return Response.badRequest("`organizationid` option is required.");
    }
    if (!Utils.isGuid(id)) {
      return Response.badRequest("`" + id + "` is not a GUID.");
    }
    if (!Utils.isGuid(options.organizationId)) {
      return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
    }
    if (options.organizationId !== req.organizationId) {
      return Response.badRequest("`organizationid` option does not match request object.");
    }
    if (req.name == null || req.name.trim() === "") {
      return Response.badRequest("Collection name is required.");
    }
    try {
      const orgKey = await firstValueFrom(
        this.accountService.activeAccount$.pipe(
          getUserId,
          switchMap((userId) => this.keyService.orgKeys$(userId)),
          map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null),
        ),
      );
      if (orgKey == null) {
        throw new Error("No encryption key for this organization.");
      }

      const groups =
        req.groups == null
          ? null
          : req.groups.map(
              (g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage),
            );
      const users =
        req.users == null
          ? null
          : req.users.map(
              (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
            );
      const request = new UpdateCollectionRequest({
        name: await this.encryptService.encryptString(req.name, orgKey),
        externalId: req.externalId,
        users,
        groups,
      });

      const response = await this.apiService.putCollection(req.organizationId, id, request);
      const view = CollectionExport.toView(req, response.id);
      const serverGroups = response.groups.map(
        (g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords, g.manage),
      );
      const serverUsers = response.users.map(
        (u) => new SelectionReadOnly(u.id, u.readOnly, u.hidePasswords, u.manage),
      );
      const res = new OrganizationCollectionResponse(view, serverGroups, serverUsers);
      return Response.success(res);
    } catch (e) {
      return Response.error(e);
    }
  }

  /** Prompt the user to accept movement of their cipher back to the their vault. */
  private async promptForArchiveEdit(): Promise<boolean> {
    // When user has disabled interactivity or does not have the ability to prompt,
    // automatically move the item back to the vault and inform them.
    if (
      process.env.BW_SERVE === "true" ||
      process.env.BW_NOINTERACTION === "true" ||
      !process.stdin.isTTY
    ) {
      CliUtils.writeLn(
        "Archive is only available with a Premium subscription, which has ended. Your edit was saved and the item was moved back to your vault.",
      );
      return true;
    }

    const answer: inquirer.Answers = await inquirer.createPromptModule({
      output: process.stderr,
    })({
      type: "list",
      name: "confirm",
      message:
        "When you edit and save details for an archived item without a Premium subscription, it'll be moved from your archive back to your vault.",
      choices: [
        {
          name: "Move now",
          value: "confirmed",
        },
        {
          name: "Cancel",
          value: "cancel",
        },
      ],
    });

    return answer.confirm === "confirmed";
  }
}

class Options {
  organizationId: string;

  constructor(passedOptions: Record<string, any>) {
    this.organizationId = passedOptions?.organizationid || passedOptions?.organizationId;
  }
}
