import {
  CipherCreateRequest,
  CipherEditRequest,
  CipherPartialEditRequest,
  CiphersClient,
  CipherViewType,
  CipherView as SdkCipherView,
} from "@bitwarden/sdk-internal";

import { EncString } from "../../../key-management/crypto/models/enc-string";
import { View } from "../../../models/view/view";
import { asUuid, uuidAsString } from "../../../platform/abstractions/sdk/sdk.service";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { DeepJsonify } from "../../../types/deep-jsonify";
import { CipherType, LinkedIdType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
import { LocalData, toSdkLocalData, fromSdkLocalData } from "../data/local.data";
import { Cipher } from "../domain/cipher";

import { AttachmentView } from "./attachment.view";
import { BankAccountView } from "./bank-account.view";
import { CardView } from "./card.view";
import { DriversLicenseView } from "./drivers-license.view";
import { Fido2CredentialView } from "./fido2-credential.view";
import { FieldView } from "./field.view";
import { IdentityView } from "./identity.view";
import { ItemView } from "./item.view";
import { LoginView } from "./login.view";
import { PassportView } from "./passport.view";
import { PasswordHistoryView } from "./password-history.view";
import { SecureNoteView } from "./secure-note.view";
import { SshKeyView } from "./ssh-key.view";

export class CipherView implements View, InitializerMetadata {
  readonly initializerKey = InitializerKey.CipherView;

  id: string = "";
  organizationId?: string;
  folderId?: string;
  name: string = "";
  notes?: string;
  type: CipherType = CipherType.Login;
  favorite = false;
  organizationUseTotp = false;
  permissions?: CipherPermissionsApi = new CipherPermissionsApi();
  edit = false;
  viewPassword = true;
  localData?: LocalData;
  login = new LoginView();
  identity = new IdentityView();
  card = new CardView();
  secureNote = new SecureNoteView();
  sshKey = new SshKeyView();
  bankAccount = new BankAccountView();
  driversLicense = new DriversLicenseView();
  passport = new PassportView();
  attachments: AttachmentView[] = [];
  fields: FieldView[] = [];
  passwordHistory: PasswordHistoryView[] = [];
  collectionIds: string[] = [];
  revisionDate: Date;
  creationDate: Date;
  deletedDate?: Date;
  archivedDate?: Date;
  reprompt: CipherRepromptType = CipherRepromptType.None;
  // We need a copy of the encrypted key so we can pass it to
  // the SdkCipherView during encryption
  key?: EncString;

  /**
   * Flag to indicate if the cipher decryption failed.
   */
  decryptionFailure = false;

  constructor(c?: Cipher) {
    if (!c) {
      this.creationDate = this.revisionDate = new Date();
      return;
    }

    this.id = c.id;
    this.organizationId = c.organizationId;
    this.folderId = c.folderId;
    this.favorite = c.favorite;
    this.organizationUseTotp = c.organizationUseTotp;
    this.edit = c.edit;
    this.viewPassword = c.viewPassword;
    this.permissions = c.permissions;
    this.type = c.type;
    this.localData = c.localData;
    this.collectionIds = c.collectionIds;
    this.revisionDate = c.revisionDate;
    this.creationDate = c.creationDate;
    this.deletedDate = c.deletedDate;
    this.archivedDate = c.archivedDate;
    // Old locally stored ciphers might have reprompt == null. If so set it to None.
    this.reprompt = c.reprompt ?? CipherRepromptType.None;
    this.key = c.key;
  }

  private get item(): ItemView | undefined {
    switch (this.type) {
      case CipherType.Login:
        return this.login;
      case CipherType.SecureNote:
        return this.secureNote;
      case CipherType.Card:
        return this.card;
      case CipherType.Identity:
        return this.identity;
      case CipherType.SshKey:
        return this.sshKey;
      case CipherType.BankAccount:
        return this.bankAccount;
      case CipherType.DriversLicense:
        return this.driversLicense;
      case CipherType.Passport:
        return this.passport;
      default:
        break;
    }

    return undefined;
  }

  get subTitle(): string | undefined {
    return this.item?.subTitle;
  }

  get canBeArchived(): boolean {
    return !this.isDeleted && !this.isArchived;
  }

  get hasPasswordHistory(): boolean {
    return this.passwordHistory && this.passwordHistory.length > 0;
  }

  get hasLoginPassword(): boolean {
    return (
      this.type === CipherType.Login && this.login?.password != null && this.login.password !== ""
    );
  }

  get hasAttachments(): boolean {
    return !!this.attachments && this.attachments.length > 0;
  }

  get hasOldAttachments(): boolean {
    if (this.hasAttachments) {
      return this.attachments.some((a) => a.isLegacyAttachment());
    }
    return false;
  }

  get isUserOwnedCipher(): boolean {
    return this.organizationId == null;
  }

  get hasFields(): boolean {
    return this.fields && this.fields.length > 0;
  }

  get passwordRevisionDisplayDate(): Date | undefined {
    if (this.type !== CipherType.Login || this.login == null) {
      return undefined;
    } else if (this.login.password == null || this.login.password === "") {
      return undefined;
    }
    return this.login.passwordRevisionDate;
  }

  get isDeleted(): boolean {
    return this.deletedDate != null;
  }

  get isArchived(): boolean {
    return this.archivedDate != null;
  }

  get linkedFieldOptions() {
    return this.item?.linkedFieldOptions;
  }

  get isUnassigned(): boolean {
    return (
      this.organizationId != null && (this.collectionIds == null || this.collectionIds.length === 0)
    );
  }

  get canAssignToCollections(): boolean {
    if (this.organizationId == null) {
      return true;
    }

    return this.edit && this.viewPassword;
  }
  /**
   * Determines if the cipher can be launched in a new browser tab.
   */
  get canLaunch(): boolean {
    return this.type === CipherType.Login && this.login!.canLaunch;
  }

  linkedFieldValue(id: LinkedIdType) {
    const linkedFieldOption = this.linkedFieldOptions?.get(id);
    const item = this.item;
    if (linkedFieldOption == null || item == null) {
      return undefined;
    }

    return item[linkedFieldOption.propertyKey as keyof typeof item];
  }

  // This is used as a marker to indicate that the cipher view object still has its prototype
  toJSON() {
    return this;
  }

  static fromJSON(obj: Partial<DeepJsonify<CipherView>>): CipherView | null {
    if (obj == null) {
      return null;
    }

    const view = new CipherView();
    view.type = obj.type ?? CipherType.Login;
    view.id = obj.id ?? "";
    view.organizationId = obj.organizationId ?? undefined;
    view.folderId = obj.folderId ?? undefined;
    view.collectionIds = obj.collectionIds ?? [];
    view.name = obj.name ?? "";
    view.notes = obj.notes;
    view.edit = obj.edit ?? false;
    view.viewPassword = obj.viewPassword ?? true;
    view.favorite = obj.favorite ?? false;
    view.organizationUseTotp = obj.organizationUseTotp ?? false;
    view.localData = obj.localData ? obj.localData : undefined;
    view.permissions = obj.permissions ? CipherPermissionsApi.fromJSON(obj.permissions) : undefined;
    view.reprompt = obj.reprompt ?? CipherRepromptType.None;
    view.decryptionFailure = obj.decryptionFailure ?? false;
    if (obj.creationDate) {
      view.creationDate = new Date(obj.creationDate);
    }
    if (obj.revisionDate) {
      view.revisionDate = new Date(obj.revisionDate);
    }
    view.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
    view.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
    view.attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)) ?? [];
    view.fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)) ?? [];
    view.passwordHistory =
      obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? [];

    if (obj.key != null) {
      let key: EncString | undefined;
      if (typeof obj.key === "string") {
        // If the key is a string, we need to parse it as EncString
        key = EncString.fromJSON(obj.key);
      } else if ((obj.key as any) instanceof EncString) {
        // If the key is already an EncString instance, we can use it directly
        key = obj.key;
      }
      view.key = key;
    }

    switch (obj.type) {
      case CipherType.Card:
        view.card = CardView.fromJSON(obj.card);
        break;
      case CipherType.Identity:
        view.identity = IdentityView.fromJSON(obj.identity);
        break;
      case CipherType.Login:
        view.login = LoginView.fromJSON(obj.login);
        break;
      case CipherType.SecureNote:
        view.secureNote = SecureNoteView.fromJSON(obj.secureNote);
        break;
      case CipherType.SshKey:
        view.sshKey = SshKeyView.fromJSON(obj.sshKey);
        break;
      case CipherType.BankAccount:
        view.bankAccount = BankAccountView.fromJSON(obj.bankAccount);
        break;
      case CipherType.DriversLicense:
        view.driversLicense = DriversLicenseView.fromJSON(obj.driversLicense);
        break;
      case CipherType.Passport:
        view.passport = PassportView.fromJSON(obj.passport);
        break;
      default:
        break;
    }

    return view;
  }

  /**
   * Creates a CipherView from the SDK CipherView.
   */
  static fromSdkCipherView(obj: SdkCipherView, sdk?: CiphersClient): CipherView | undefined {
    if (obj == null) {
      return undefined;
    }

    const attachments = obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)!) ?? [];

    if (obj.attachmentDecryptionFailures?.length) {
      obj.attachmentDecryptionFailures.forEach((attachment) => {
        const attachmentView = AttachmentView.fromSdkAttachmentView(attachment, true);
        if (attachmentView) {
          attachments.push(attachmentView);
        }
      });
    }

    const cipherView = new CipherView();
    cipherView.id = uuidAsString(obj.id);
    cipherView.organizationId = uuidAsString(obj.organizationId);
    cipherView.folderId = uuidAsString(obj.folderId);
    cipherView.name = obj.name;
    cipherView.notes = obj.notes;
    cipherView.type = obj.type;
    cipherView.favorite = obj.favorite;
    cipherView.organizationUseTotp = obj.organizationUseTotp;
    cipherView.permissions = obj.permissions
      ? CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions)
      : undefined;
    cipherView.edit = obj.edit;
    cipherView.viewPassword = obj.viewPassword;
    cipherView.localData = fromSdkLocalData(obj.localData);
    cipherView.attachments = attachments;
    cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)!) ?? [];
    cipherView.passwordHistory =
      obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)!) ?? [];
    cipherView.collectionIds = obj.collectionIds?.map((i) => uuidAsString(i)) ?? [];
    cipherView.revisionDate = new Date(obj.revisionDate);
    cipherView.creationDate = new Date(obj.creationDate);
    cipherView.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
    cipherView.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
    cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
    cipherView.key = obj.key ? EncString.fromJSON(obj.key) : undefined;

    switch (obj.type) {
      case CipherType.Card:
        cipherView.card = obj.card ? CardView.fromSdkCardView(obj.card) : new CardView();
        break;
      case CipherType.Identity:
        cipherView.identity = obj.identity
          ? IdentityView.fromSdkIdentityView(obj.identity)
          : new IdentityView();
        break;
      case CipherType.Login:
        cipherView.login = obj.login ? LoginView.fromSdkLoginView(obj.login) : new LoginView();
        if (sdk && obj.login?.fido2Credentials?.length) {
          const fido2CredentialViews = sdk.decrypt_fido2_credentials(obj);
          const decryptedKeyValue = sdk.decrypt_fido2_private_key(obj);
          cipherView.login.fido2Credentials = fido2CredentialViews
            .map((cred) => {
              const view = Fido2CredentialView.fromSdkFido2CredentialView(cred);
              if (view) {
                view.keyValue = decryptedKeyValue;
              }
              return view;
            })
            .filter((cred): cred is Fido2CredentialView => !!cred);
        }
        break;
      case CipherType.SecureNote:
        cipherView.secureNote = obj.secureNote
          ? SecureNoteView.fromSdkSecureNoteView(obj.secureNote)
          : new SecureNoteView();
        break;
      case CipherType.SshKey:
        cipherView.sshKey = obj.sshKey
          ? SshKeyView.fromSdkSshKeyView(obj.sshKey)
          : new SshKeyView();
        break;
      case CipherType.BankAccount:
        cipherView.bankAccount = obj.bankAccount
          ? BankAccountView.fromSdkBankAccountView(obj.bankAccount)
          : new BankAccountView();
        break;
      case CipherType.DriversLicense:
        cipherView.driversLicense = obj.driversLicense
          ? DriversLicenseView.fromSdkDriversLicenseView(obj.driversLicense)
          : new DriversLicenseView();
        break;
      case CipherType.Passport:
        cipherView.passport = obj.passport
          ? PassportView.fromSdkPassportView(obj.passport)
          : new PassportView();
        break;
      default:
        break;
    }

    return cipherView;
  }

  /**
   * Maps CipherView to an SDK CipherCreateRequest
   *
   * @returns {CipherCreateRequest} The SDK cipher create request object
   */
  toSdkCreateCipherRequest(sdk: CiphersClient): CipherCreateRequest {
    const sdkCipherCreateRequest: CipherCreateRequest = {
      organizationId: this.organizationId ? asUuid(this.organizationId) : undefined,
      collectionIds: this.collectionIds ? this.collectionIds.map((i) => asUuid(i)) : [],
      folderId: this.folderId ? asUuid(this.folderId) : undefined,
      name: this.name ?? "",
      notes: this.notes,
      favorite: this.favorite ?? false,
      reprompt: this.reprompt ?? CipherRepromptType.None,
      fields: this.fields?.map((f) => f.toSdkFieldView()),
      type: this.getSdkCipherViewType(),
      archivedDate: this.archivedDate?.toISOString(),
    };

    // If the cipher has FIDO2 credentials, we need to set them on the SDK create request
    // separately due to restrictions in how the SDK handles them.
    if (this.type === CipherType.Login && this.login?.hasFido2Credentials) {
      const sdkCipherView: SdkCipherView = this.toSdkCipherView(sdk);
      sdkCipherCreateRequest.type = { login: sdkCipherView.login! };
    }

    return sdkCipherCreateRequest;
  }

  /**
   * Maps CipherView to an SDK CipherEditRequest
   *
   * @returns {CipherEditRequest} The SDK cipher edit request object
   */
  toSdkUpdateCipherRequest(sdk: CiphersClient): CipherEditRequest {
    const sdkCipherEditRequest: CipherEditRequest = {
      id: asUuid(this.id),
      organizationId: this.organizationId ? asUuid(this.organizationId) : undefined,
      folderId: this.folderId ? asUuid(this.folderId) : undefined,
      name: this.name ?? "",
      notes: this.notes,
      favorite: this.favorite ?? false,
      reprompt: this.reprompt ?? CipherRepromptType.None,
      fields: this.fields?.map((f) => f.toSdkFieldView()),
      type: this.getSdkCipherViewType(),
      revisionDate: this.revisionDate?.toISOString(),
      archivedDate: this.archivedDate?.toISOString(),
      attachments: this.attachments?.map((a) => a.toSdkAttachmentView()),
      key: this.key?.toSdk(),
    };

    // If the cipher has FIDO2 credentials, we need to set them on the SDK edit request
    // separately due to restrictions in how the SDK handles them.
    if (this.type === CipherType.Login && this.login?.hasFido2Credentials) {
      const sdkCipherView: SdkCipherView = this.toSdkCipherView(sdk);
      sdkCipherEditRequest.type = { login: sdkCipherView.login! };
    }

    return sdkCipherEditRequest;
  }

  /**
   * Maps CipherView to an SDK CipherPartialEditRequest
   *
   * @returns {CipherPartialEditRequest} The SDK cipher edit request object
   */
  toSdkPartialUpdateCipherRequest(): CipherPartialEditRequest {
    const sdkCipherPartialEditRequest: CipherPartialEditRequest = {
      id: asUuid(this.id),
      folderId: this.folderId ? asUuid(this.folderId) : undefined,
      favorite: this.favorite ?? false,
    };

    return sdkCipherPartialEditRequest;
  }

  /**
   * Returns the SDK CipherViewType object for the cipher.
   *
   * @returns {CipherViewType} The SDK CipherViewType for the cipher.t
   */
  getSdkCipherViewType(): CipherViewType {
    let viewType: CipherViewType;
    switch (this.type) {
      case CipherType.Card:
        viewType = { card: this.card?.toSdkCardView() };
        break;
      case CipherType.Identity:
        viewType = { identity: this.identity?.toSdkIdentityView() };
        break;
      case CipherType.Login:
        viewType = { login: this.login?.toSdkLoginView() };
        break;
      case CipherType.SecureNote:
        viewType = { secureNote: this.secureNote?.toSdkSecureNoteView() };
        break;
      case CipherType.SshKey:
        viewType = { sshKey: this.sshKey?.toSdkSshKeyView() };
        break;
      case CipherType.BankAccount:
        viewType = { bankAccount: this.bankAccount?.toSdkBankAccountView() };
        break;
      case CipherType.DriversLicense:
        viewType = { driversLicense: this.driversLicense?.toSdkDriversLicenseView() };
        break;
      case CipherType.Passport:
        viewType = { passport: this.passport?.toSdkPassportView() };
        break;
      default:
        viewType = {
          // Default to empty login - should not be valid code path.
          login: new LoginView().toSdkLoginView(),
        };
        break;
    }
    return viewType;
  }

  /**
   * Maps CipherView to SdkCipherView
   *
   * If `sdk` parameter is provided, it will set the FIDO2 credentials on the SDK view,
   * since they remain encrypted and are not included in the standard mapping.
   *
   * If sdk is not provided, the caller is responsible for handling the FIDO2 credentials
   * on the SDK view separately.
   *
   * @returns {SdkCipherView} The SDK cipher view object
   */
  toSdkCipherView(sdk?: CiphersClient): SdkCipherView {
    let sdkCipherView: SdkCipherView = {
      id: this.id ? asUuid(this.id) : undefined,
      organizationId: this.organizationId ? asUuid(this.organizationId) : undefined,
      folderId: this.folderId ? asUuid(this.folderId) : undefined,
      name: this.name ?? "",
      notes: this.notes,
      type: this.type ?? CipherType.Login,
      favorite: this.favorite ?? false,
      organizationUseTotp: this.organizationUseTotp ?? false,
      permissions: this.permissions?.toSdkCipherPermissions(),
      edit: this.edit ?? true,
      viewPassword: this.viewPassword ?? true,
      localData: toSdkLocalData(this.localData),
      attachments: this.attachments?.map((a) => a.toSdkAttachmentView()),
      fields: this.fields?.map((f) => f.toSdkFieldView()),
      passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistoryView()),
      collectionIds: this.collectionIds?.map((i) => asUuid(i)) ?? [],
      // Revision and creation dates are non-nullable in SDKCipherView
      revisionDate: (this.revisionDate ?? new Date()).toISOString(),
      creationDate: (this.creationDate ?? new Date()).toISOString(),
      deletedDate: this.deletedDate?.toISOString(),
      archivedDate: this.archivedDate?.toISOString(),
      reprompt: this.reprompt ?? CipherRepromptType.None,
      key: this.key?.toSdk(),
      // Cipher type specific properties are set in the switch statement below
      // CipherView initializes each with default constructors (undefined values)
      // The SDK does not expect those undefined values and will throw exceptions
      login: undefined,
      card: undefined,
      identity: undefined,
      secureNote: undefined,
      sshKey: undefined,
      bankAccount: undefined,
      driversLicense: undefined,
      passport: undefined,
    };

    switch (this.type) {
      case CipherType.Card:
        sdkCipherView.card = this.card?.toSdkCardView();
        break;
      case CipherType.Identity:
        sdkCipherView.identity = this.identity?.toSdkIdentityView();
        break;
      case CipherType.Login:
        sdkCipherView.login = this.login?.toSdkLoginView();
        break;
      case CipherType.SecureNote:
        sdkCipherView.secureNote = this.secureNote?.toSdkSecureNoteView();
        break;
      case CipherType.SshKey:
        sdkCipherView.sshKey = this.sshKey?.toSdkSshKeyView();
        break;
      case CipherType.BankAccount:
        sdkCipherView.bankAccount = this.bankAccount?.toSdkBankAccountView();
        break;
      case CipherType.DriversLicense:
        sdkCipherView.driversLicense = this.driversLicense?.toSdkDriversLicenseView();
        break;
      case CipherType.Passport:
        sdkCipherView.passport = this.passport?.toSdkPassportView();
        break;
      default:
        break;
    }

    // If the cipher has FIDO2 credentials, we need to set them on the SDK view separately since they remain encrypted.
    if (sdk && this.type === CipherType.Login && this.login?.hasFido2Credentials) {
      const fido2Credentials = this.login.fido2Credentials.map((cred) =>
        cred.toSdkFido2CredentialFullView(),
      );
      sdkCipherView = sdk.set_fido2_credentials(sdkCipherView, fido2Credentials);
    }

    return sdkCipherView;
  }
}
