import {
  BankAccountListView,
  BankAccountView as SdkBankAccountView,
  CardListView,
  CipherListView,
  CopyableCipherFields,
  LoginListView,
  LoginUriView as LoginListUriView,
} from "@bitwarden/sdk-internal";

import { UriMatchStrategy, UriMatchStrategySetting } from "../../models/domain/domain-service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { Utils } from "../../platform/misc/utils";
import { BankAccountType, BankAccountTypeI18nKeys, CipherType } from "../enums";
import { Cipher } from "../models/domain/cipher";
import { BankAccountView } from "../models/view/bank-account.view";
import { CardView } from "../models/view/card.view";
import { CipherView } from "../models/view/cipher.view";
import { LoginUriView } from "../models/view/login-uri.view";
import { LoginView } from "../models/view/login.view";

/**
 * Type union of {@link CipherView} and {@link CipherListView}.
 */
export type CipherViewLike = CipherView | CipherListView;

/**
 * Utility class for working with ciphers that can be either a {@link CipherView} or a {@link CipherListView}.
 */
export class CipherViewLikeUtils {
  /** @returns true when the given cipher is an instance of {@link CipherListView}. */
  static isCipherListView = (cipher: CipherViewLike | Cipher): cipher is CipherListView => {
    return typeof cipher.type === "object" || typeof cipher.type === "string";
  };

  /** @returns The login object from the input cipher. If the cipher is not of type Login, returns null. */
  static getLogin = (cipher: CipherViewLike): LoginListView | LoginView | null => {
    if (this.isCipherListView(cipher)) {
      if (typeof cipher.type !== "object") {
        return null;
      }

      return "login" in cipher.type ? cipher.type.login : null;
    }

    return cipher.type === CipherType.Login ? cipher.login : null;
  };

  /** @returns The first URI for a login cipher. If the cipher is not of type Login or has no associated URIs, returns null. */
  static uri = (cipher: CipherViewLike) => {
    const login = this.getLogin(cipher);
    if (!login) {
      return null;
    }

    if ("uri" in login) {
      return login.uri;
    }

    return login.uris?.length ? login.uris[0].uri : null;
  };

  /** @returns The login object from the input cipher. If the cipher is not of type Login, returns null. */
  static getCard = (cipher: CipherViewLike): CardListView | CardView | null => {
    if (this.isCipherListView(cipher)) {
      if (typeof cipher.type !== "object") {
        return null;
      }

      return "card" in cipher.type ? cipher.type.card : null;
    }

    return cipher.type === CipherType.Card ? cipher.card : null;
  };

  /**  @returns `true` when the cipher has been archived, `false` otherwise. */
  static isArchived = (cipher: CipherViewLike): boolean => {
    if (this.isCipherListView(cipher)) {
      return !!cipher.archivedDate;
    }

    return cipher.isArchived;
  };

  /**  @returns `true` when the cipher has been deleted, `false` otherwise. */
  static isDeleted = (cipher: CipherViewLike): boolean => {
    if (this.isCipherListView(cipher)) {
      return !!cipher.deletedDate;
    }

    return cipher.isDeleted;
  };

  /**  @returns `true` when the cipher is not assigned to a collection, `false` otherwise. */
  static isUnassigned = (cipher: CipherViewLike): boolean => {
    if (this.isCipherListView(cipher)) {
      return (
        cipher.organizationId != null &&
        (cipher.collectionIds == null || cipher.collectionIds.length === 0)
      );
    }

    return cipher.isUnassigned;
  };

  /** @returns `true` when the user can assign the cipher to a collection, `false` otherwise. */
  static canAssignToCollections = (cipher: CipherViewLike): boolean => {
    if (this.isCipherListView(cipher)) {
      if (!cipher.organizationId) {
        return true;
      }

      return cipher.edit && cipher.viewPassword;
    }

    return cipher.canAssignToCollections;
  };

  /**
   * Returns the type of the cipher.
   * For consistency, when the given cipher is a {@link CipherListView} the {@link CipherType} equivalent will be returned.
   */
  static getType = (cipher: CipherViewLike | Cipher): CipherType => {
    if (!this.isCipherListView(cipher)) {
      return cipher.type;
    }

    // CipherListViewType is a string, so we need to map it to CipherType.
    switch (true) {
      case cipher.type === "secureNote":
        return CipherType.SecureNote;
      case cipher.type === "sshKey":
        return CipherType.SshKey;
      case typeof cipher.type === "object" && "bankAccount" in cipher.type:
        return CipherType.BankAccount;
      case cipher.type === "passport":
        return CipherType.Passport;
      case cipher.type === "identity":
        return CipherType.Identity;
      case typeof cipher.type === "object" && "card" in cipher.type:
        return CipherType.Card;
      case typeof cipher.type === "object" && "login" in cipher.type:
        return CipherType.Login;
      case cipher.type === "driversLicense":
        return CipherType.DriversLicense;
      default:
        throw new Error(`Unknown cipher type: ${cipher.type}`);
    }
  };

  /**
   * @returns The subtitle of the cipher.
   *
   * When `i18nService` is supplied, cipher types that depend on localized values
   * (e.g. the bank-account `accountType` dropdown) produce a translated subtitle.
   * Without `i18nService`, the cipher's raw `subTitle` / `subtitle` is returned —
   * suitable for non-Angular callers (search indexing, CLI) where translation is
   * unnecessary or unavailable.
   */
  static subtitle = (cipher: CipherViewLike, i18nService?: I18nService): string | undefined => {
    const baseSubtitle = this.isCipherListView(cipher) ? cipher.subtitle : cipher.subTitle;

    if (!i18nService) {
      return baseSubtitle;
    }

    switch (this.getType(cipher)) {
      case CipherType.BankAccount: {
        const bankAccountSubtitle = this.bankAccountSubtitle(cipher, i18nService);
        return bankAccountSubtitle ?? baseSubtitle;
      }
      default:
        return baseSubtitle;
    }
  };

  /** @returns The bank account object from the input cipher. If the cipher is not of type BankAccount, returns null. */
  static getBankAccount = (
    cipher: CipherViewLike,
  ): BankAccountView | SdkBankAccountView | BankAccountListView | null => {
    if (this.isCipherListView(cipher)) {
      if (typeof cipher.type !== "object") {
        return null;
      }

      return "bankAccount" in cipher.type ? cipher.type.bankAccount : null;
    }

    return cipher.type === CipherType.BankAccount ? (cipher.bankAccount ?? null) : null;
  };

  private static bankAccountSubtitle = (
    cipher: CipherViewLike,
    i18nService: I18nService,
  ): string | undefined => {
    const bankAccount = this.getBankAccount(cipher);
    if (!bankAccount) {
      return undefined;
    }

    const accountTypeKey = bankAccount.accountType
      ? BankAccountTypeI18nKeys[bankAccount.accountType as BankAccountType]
      : undefined;
    const translatedAccountType = accountTypeKey ? i18nService.t(accountTypeKey) : undefined;

    let shownDigits: string | undefined = undefined;
    if (bankAccount.accountNumber && bankAccount.accountNumber?.length > 4) {
      shownDigits = bankAccount.accountNumber?.slice(-4);
    } else if (bankAccount.accountNumber && bankAccount.accountNumber?.length > 1) {
      const length = bankAccount.accountNumber?.length;
      const negativeSlice = (length - 1) * -1;
      shownDigits = bankAccount.accountNumber?.slice(negativeSlice);
    }

    if (translatedAccountType && shownDigits) {
      return `${translatedAccountType}, *${shownDigits}`;
    }
    if (translatedAccountType) {
      return translatedAccountType;
    }
    if (shownDigits) {
      return `*${shownDigits}`;
    }

    return undefined;
  };

  /** @returns `true` when the cipher has attachments, false otherwise. */
  static hasAttachments = (cipher: CipherViewLike): boolean => {
    if (this.isCipherListView(cipher)) {
      return typeof cipher.attachments === "number" && cipher.attachments > 0;
    }

    return cipher.hasAttachments;
  };

  /**
   * @returns `true` when one of the URIs for the cipher can be launched.
   * When a non-login cipher is passed, it will return false.
   */
  static canLaunch = (cipher: CipherViewLike): boolean => {
    const login = this.getLogin(cipher);

    if (!login) {
      return false;
    }

    return !!login.uris?.map((u) => toLoginUriView(u)).some((uri) => uri.canLaunch);
  };

  /**
   * @returns The first launch-able URI for the cipher.
   * When a non-login cipher is passed or none of the URLs, it will return undefined.
   */
  static getLaunchUri = (cipher: CipherViewLike): string | undefined => {
    const login = this.getLogin(cipher);

    if (!login) {
      return undefined;
    }

    return login.uris?.map((u) => toLoginUriView(u)).find((uri) => uri.canLaunch)?.launchUri;
  };

  /**
   * @returns `true` when the `targetUri` matches for any URI on the cipher.
   * Uses the existing logic from `LoginView.matchesUri` for both `CipherView` and `CipherListView`
   */
  static matchesUri = (
    cipher: CipherViewLike,
    targetUri: string,
    equivalentDomains: Set<string>,
    defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain,
    overrideNeverMatchStrategy?: true,
  ): boolean => {
    if (CipherViewLikeUtils.getType(cipher) !== CipherType.Login) {
      return false;
    }

    if (!this.isCipherListView(cipher)) {
      return cipher.login.matchesUri(
        targetUri,
        equivalentDomains,
        defaultUriMatch,
        overrideNeverMatchStrategy,
      );
    }

    const login = this.getLogin(cipher);
    if (!login?.uris?.length) {
      return false;
    }

    const loginUriViews = login.uris
      .filter((u) => !!u.uri)
      .map((u) => {
        const view = new LoginUriView();
        view.match = u.match ?? defaultUriMatch;
        view.uri = u.uri!; // above `filter` ensures `u.uri` is not null or undefined
        return view;
      });

    return loginUriViews.some((uriView) =>
      uriView.matchesUri(targetUri, equivalentDomains, defaultUriMatch, overrideNeverMatchStrategy),
    );
  };

  /** @returns true when the `copyField` is populated on the given cipher. */
  static hasCopyableValue = (cipher: CipherViewLike, copyField: string): boolean => {
    // `CipherListView` instances do not contain the values to be copied, but rather a list of copyable fields.
    // When the copy action is performed on a `CipherListView`, the full cipher will need to be decrypted.
    if (this.isCipherListView(cipher)) {
      // For login ciphers, cross-check against the decrypted data available in LoginListView
      // when possible. The SDK's copyableFields can report fields as copyable even when
      // the decrypted value is empty.
      if (this.getType(cipher) === CipherType.Login) {
        const login = this.getLogin(cipher);
        if (copyField === "username") {
          return !!login?.username;
        }
      }

      let _copyField = copyField;

      if (_copyField === "username" && this.getType(cipher) === CipherType.Identity) {
        _copyField = "usernameIdentity";
      }

      return cipher.copyableFields?.includes(copyActionToCopyableFieldMap[_copyField]) ?? false;
    }

    // When the full cipher is available, check the specific field
    switch (copyField) {
      case "username":
        return !!cipher.login?.username || !!cipher.identity?.username;
      case "password":
        return !!cipher.login?.password;
      case "totp":
        return !!cipher.login?.totp;
      case "cardNumber":
        return !!cipher.card?.number;
      case "securityCode":
        return !!cipher.card?.code;
      case "email":
        return !!cipher.identity?.email;
      case "phone":
        return !!cipher.identity?.phone;
      case "address":
        return !!cipher.identity?.fullAddressForCopy;
      case "secureNote":
        return !!cipher.notes;
      case "privateKey":
        return !!cipher.sshKey?.privateKey;
      case "publicKey":
        return !!cipher.sshKey?.publicKey;
      case "keyFingerprint":
        return !!cipher.sshKey?.keyFingerprint;
      case "nameOnAccount":
        return !!cipher.bankAccount?.nameOnAccount;
      case "accountNumber":
        return !!cipher.bankAccount?.accountNumber;
      case "routingNumber":
        return !!cipher.bankAccount?.routingNumber;
      case "branchNumber":
        return !!cipher.bankAccount?.branchNumber;
      case "pin":
        return !!cipher.bankAccount?.pin;
      case "iban":
        return !!cipher.bankAccount?.iban;
      case "swiftCode":
        return !!cipher.bankAccount?.swiftCode;
      case "firstNameLicense":
        return !!cipher.driversLicense?.firstName;
      case "middleNameLicense":
        return !!cipher.driversLicense?.middleName;
      case "lastNameLicense":
        return !!cipher.driversLicense?.lastName;
      case "licenseNumber":
        return !!cipher.driversLicense?.licenseNumber;
      case "passportNumber":
        return !!cipher.passport?.passportNumber;
      case "nationalIdentificationNumber":
        return !!cipher.passport?.nationalIdentificationNumber;
      case "givenName":
        return !!cipher.passport?.givenName;
      case "surname":
        return !!cipher.passport?.surname;
      default:
        return false;
    }
  };

  /** @returns true when the cipher has fido2 credentials */
  static hasFido2Credentials = (cipher: CipherViewLike): boolean => {
    const login = this.getLogin(cipher);

    return !!login?.fido2Credentials?.length;
  };

  /**
   * Returns the `decryptionFailure` property from the cipher when available.
   * TODO: https://bitwarden.atlassian.net/browse/PM-22515 - alter for `CipherListView` if needed
   */
  static decryptionFailure = (cipher: CipherViewLike): boolean => {
    return "decryptionFailure" in cipher ? cipher.decryptionFailure : false;
  };

  /**
   * Returns the notes from the cipher.
   *
   * @param cipher - The cipher to extract notes from (either `CipherView` or `CipherListView`)
   * @returns The notes string if present, or `undefined` if not set
   */
  static getNotes = (cipher: CipherViewLike): string | undefined => {
    return cipher.notes;
  };

  /**
   * Returns the fields from the cipher.
   *
   * @param cipher - The cipher to extract fields from (either `CipherView` or `CipherListView`)
   * @returns Array of field objects with `name` and `value` properties, `undefined` if not set
   */
  static getFields = (
    cipher: CipherViewLike,
  ): { name?: string | null; value?: string | undefined }[] | undefined => {
    if (this.isCipherListView(cipher)) {
      return cipher.fields;
    }
    return cipher.fields;
  };

  /**
   * Returns attachment filenames from the cipher.
   *
   * @param cipher - The cipher to extract attachment names from (either `CipherView` or `CipherListView`)
   * @returns Array of attachment filenames, `undefined` if attachments are not present
   */
  static getAttachmentNames = (cipher: CipherViewLike): string[] | undefined => {
    if (this.isCipherListView(cipher)) {
      return cipher.attachmentNames;
    }

    return cipher.attachments
      ?.map((a) => a.fileName)
      .filter((name): name is string => name != null);
  };

  /**
   * Extracts hostname from a login URI.
   *
   * @param uri - The URI object (either `LoginUriView` class or `LoginListUriView`)
   * @returns The hostname if available, `undefined` otherwise
   *
   * @remarks
   * - For `LoginUriView` (CipherView): Uses the built-in `hostname` getter
   * - For `LoginListUriView` (CipherListView): Computes hostname using `Utils.getHostname()`
   * - Returns `undefined` for RegularExpression match types or when hostname cannot be extracted
   */
  static getUriHostname = (uri: LoginListUriView | LoginUriView): string | undefined => {
    if ("hostname" in uri && typeof uri.hostname !== "undefined") {
      return uri.hostname ?? undefined;
    }

    if (uri.match !== UriMatchStrategy.RegularExpression && uri.uri) {
      const hostname = Utils.getHostname(uri.uri);
      return hostname === "" ? undefined : hostname;
    }

    return undefined;
  };
}

/**
 * Mapping between the generic copy actions and the specific fields in a `CipherViewLike`.
 */
const copyActionToCopyableFieldMap: Record<string, CopyableCipherFields> = {
  usernameLogin: "LoginUsername",
  password: "LoginPassword",
  totp: "LoginTotp",
  cardNumber: "CardNumber",
  securityCode: "CardSecurityCode",
  usernameIdentity: "IdentityUsername",
  email: "IdentityEmail",
  phone: "IdentityPhone",
  address: "IdentityAddress",
  secureNote: "SecureNotes",
  privateKey: "SshKey",
  publicKey: "SshKey",
  keyFingerprint: "SshKey",
  nameOnAccount: "BankAccountNameOnAccount",
  accountNumber: "BankAccountAccountNumber",
  routingNumber: "BankAccountRoutingNumber",
  branchNumber: "BankAccountBranchNumber",
  pin: "BankAccountPin",
  iban: "BankAccountIban",
  swiftCode: "BankAccountSwift",
  firstNameLicense: "DriversLicenseFirstName",
  middleNameLicense: "DriversLicenseMiddleName",
  lastNameLicense: "DriversLicenseLastName",
  licenseNumber: "DriversLicenseLicenseNumber",
  givenName: "PassportGivenName",
  surname: "PassportSurname",
  passportNumber: "PassportPassportNumber",
  nationalIdentificationNumber: "PassportNationalIdentificationNumber",
};

/** Converts a `LoginListUriView` to a `LoginUriView`. */
const toLoginUriView = (uri: LoginListUriView | LoginUriView): LoginUriView => {
  if (uri instanceof LoginUriView) {
    return uri;
  }

  const loginUriView = new LoginUriView();
  if (uri.match) {
    loginUriView.match = uri.match;
  }
  if (uri.uri) {
    loginUriView.uri = uri.uri;
  }
  return loginUriView;
};
