import {
  booleanAttribute,
  Directive,
  HostBinding,
  HostListener,
  Input,
  OnChanges,
  Optional,
} from "@angular/core";
import { firstValueFrom } from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
  CipherViewLike,
  CipherViewLikeUtils,
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { MenuItemComponent, BitIconButtonComponent } from "@bitwarden/components";

import { CopyAction, CopyCipherFieldService } from "..";

/**
 * Directive to copy a specific field from a cipher on click. Uses the `CopyCipherFieldService` to
 * handle the copying of the field and any necessary password re-prompting or totp generation.
 *
 * Automatically disables the host element if the field to copy is not available or null.
 *
 * If the host element is a menu item, it will be hidden when disabled.
 *
 * @example
 * ```html
 * <button appCopyField="username" [cipher]="cipher">Copy Username</button>
 * ```
 */
@Directive({
  selector: "[appCopyField]",
})
export class CopyCipherFieldDirective implements OnChanges {
  // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
  // eslint-disable-next-line @angular-eslint/prefer-signals
  @Input({
    alias: "appCopyField",
    required: true,
  })
  action!: CopyAction;

  // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
  // eslint-disable-next-line @angular-eslint/prefer-signals
  @Input({ required: true })
  cipher!: CipherViewLike;

  /**
   * Externally-forced disabled state, e.g. while the containing list is refreshing. This is
   * combined with the field-availability check so the directive remains the sole writer of the
   * host and icon-button disabled state. Binding `[disabled]` directly on the same element would
   * otherwise clobber the disabled state this directive owns — an external re-enable could make a
   * button with no value to copy clickable again.
   */
  // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
  // eslint-disable-next-line @angular-eslint/prefer-signals
  @Input({ transform: booleanAttribute })
  disabled = false;

  constructor(
    private copyCipherFieldService: CopyCipherFieldService,
    private accountService: AccountService,
    private cipherService: CipherService,
    @Optional() private menuItemComponent?: MenuItemComponent,
    @Optional() private iconButtonComponent?: BitIconButtonComponent,
  ) {}

  @HostBinding("attr.disabled")
  protected hostDisabled: boolean | null = null;

  /**
   * Hide the element if it is disabled and is a menu item.
   * @private
   */
  @HostBinding("class.tw-hidden")
  private get hidden() {
    return this.hostDisabled && this.menuItemComponent;
  }

  @HostListener("click")
  async copy() {
    // Guard against copying when disabled. The click-capture guard normally prevents this, but
    // this ensures a disabled field (no value, or externally disabled) never copies an empty value.
    if (this.hostDisabled) {
      return;
    }
    const value = await this.getValueToCopy();
    await this.copyCipherFieldService.copy(value ?? "", this.action, this.cipher);
  }

  async ngOnChanges() {
    await this.updateDisabledState();
  }

  private async updateDisabledState() {
    const unavailable =
      !this.cipher ||
      !this.hasValueToCopy() ||
      (this.action === "totp" && !(await this.copyCipherFieldService.totpAllowed(this.cipher)));

    this.hostDisabled = this.disabled || unavailable ? true : null;

    // When used on an icon button, update the disabled state of the button component
    if (this.iconButtonComponent) {
      this.iconButtonComponent.disabled.set(this.hostDisabled ?? false);
    }

    // If the directive is used on a menu item, update the menu item to prevent keyboard navigation
    if (this.menuItemComponent) {
      this.menuItemComponent.disabled = this.hostDisabled ?? false;
    }
  }

  /** Returns `true` when the cipher has the associated value as populated. */
  private hasValueToCopy() {
    return CipherViewLikeUtils.hasCopyableValue(this.cipher, this.action);
  }

  /** Returns the value of the cipher to be copied. */
  private async getValueToCopy() {
    let _cipher: CipherView;

    if (CipherViewLikeUtils.isCipherListView(this.cipher)) {
      // When the cipher is of type `CipherListView`, the full cipher needs to be decrypted
      const activeAccountId = await firstValueFrom(
        this.accountService.activeAccount$.pipe(getUserId),
      );
      const encryptedCipher = await this.cipherService.get(
        uuidAsString(this.cipher.id!),
        activeAccountId,
      );
      _cipher = await this.cipherService.decrypt(encryptedCipher, activeAccountId);
    } else {
      _cipher = this.cipher;
    }

    switch (this.action) {
      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 null;
    }
  }
}
