import { EMPTY, catchError, concatMap, firstValueFrom, map, throwError } from "rxjs";

import { CipherListView, DecryptCipherListResult } from "@bitwarden/sdk-internal";

import { LogService } from "../../platform/abstractions/log.service";
import { SdkService, asUuid, uuidAsString } from "../../platform/abstractions/sdk/sdk.service";
import { UserId, OrganizationId } from "../../types/guid";
import { UserKey } from "../../types/key";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { EncryptionContext } from "../abstractions/cipher.service";
import { CipherType } from "../enums";
import { Cipher } from "../models/domain/cipher";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { Fido2CredentialView } from "../models/view/fido2-credential.view";

export class DefaultCipherEncryptionService implements CipherEncryptionService {
  constructor(
    private sdkService: SdkService,
    private logService: LogService,
  ) {}

  async encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCipherView = model.toSdkCipherView(ref.value.vault().ciphers());

          const encryptionContext = await ref.value.vault().ciphers().encrypt(sdkCipherView);

          return {
            cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
            encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
          };
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to encrypt cipher: ${error}`);
          return EMPTY;
        }),
      ),
    );
  }

  async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
    if (!models || models.length === 0) {
      return [];
    }

    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();

          const results = await ref.value
            .vault()
            .ciphers()
            .encrypt_list(
              models.map((model) => model.toSdkCipherView(ref.value.vault().ciphers())),
            );
          return results.map((encryptionContext) => ({
            cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
            encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
          }));
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to encrypt ciphers in batch: ${error}`);
          // Propagate the underlying failure. Returning EMPTY here would make firstValueFrom
          // reject with an opaque "no elements in sequence" EmptyError.
          return throwError(() => (error instanceof Error ? error : new Error(String(error))));
        }),
      ),
    );
  }

  async moveToOrganization(
    model: CipherView,
    organizationId: OrganizationId,
    userId: UserId,
  ): Promise<EncryptionContext | undefined> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCipherView = model.toSdkCipherView(ref.value.vault().ciphers());

          const movedCipherView = ref.value
            .vault()
            .ciphers()
            .move_to_organization(sdkCipherView, asUuid(organizationId));

          const encryptionContext = await ref.value.vault().ciphers().encrypt(movedCipherView);

          return {
            cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
            encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
          };
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to move cipher to organization: ${error}`);
          return EMPTY;
        }),
      ),
    );
  }

  async encryptCipherForRotation(
    model: CipherView,
    userId: UserId,
    newKey: UserKey,
  ): Promise<EncryptionContext | undefined> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCipherView = model.toSdkCipherView(ref.value.vault().ciphers());

          const encryptionContext = await ref.value
            .vault()
            .ciphers()
            .encrypt_cipher_for_rotation(sdkCipherView, newKey.toBase64());

          return {
            cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
            encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
          };
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to rotate cipher data: ${error}`);
          return EMPTY;
        }),
      ),
    );
  }

  async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCipherView = await ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());

          const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;

          // Decrypt Fido2 credentials if available
          if (
            clientCipherView.type === CipherType.Login &&
            sdkCipherView.login?.fido2Credentials?.length
          ) {
            const fido2CredentialViews = ref.value
              .vault()
              .ciphers()
              .decrypt_fido2_credentials(sdkCipherView);

            // TEMPORARY: Manually decrypt the keyValue for Fido2 credentials
            // since we don't currently use the SDK for Fido2 Authentication.
            const decryptedKeyValue = ref.value
              .vault()
              .ciphers()
              .decrypt_fido2_private_key(sdkCipherView);

            clientCipherView.login.fido2Credentials = fido2CredentialViews
              .map((f) => {
                const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
                view.keyValue = decryptedKeyValue;
                return view;
              })
              .filter((view): view is Fido2CredentialView => view !== undefined);
          }

          return clientCipherView;
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to decrypt cipher ${error}`);
          return EMPTY;
        }),
      ),
    );
  }

  decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<[CipherView[], CipherView[]]> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();

          const successful: CipherView[] = [];
          const failed: CipherView[] = [];

          for (const cipher of ciphers) {
            try {
              const sdkCipherView = await ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());
              const clientCipherView = CipherView.fromSdkCipherView(sdkCipherView)!;

              // Handle FIDO2 credentials if present
              if (
                clientCipherView.type === CipherType.Login &&
                sdkCipherView.login?.fido2Credentials?.length
              ) {
                const fido2CredentialViews = ref.value
                  .vault()
                  .ciphers()
                  .decrypt_fido2_credentials(sdkCipherView);

                const decryptedKeyValue = ref.value
                  .vault()
                  .ciphers()
                  .decrypt_fido2_private_key(sdkCipherView);

                clientCipherView.login.fido2Credentials = fido2CredentialViews
                  .map((f) => {
                    const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
                    view.keyValue = decryptedKeyValue;
                    return view;
                  })
                  .filter((view): view is Fido2CredentialView => view !== undefined);
              }

              successful.push(clientCipherView);
            } catch (error) {
              this.logService.error(`Failed to decrypt cipher ${cipher.id}: ${error}`);
              const failedView = new CipherView(cipher);
              failedView.name = "[error: cannot decrypt]";
              failedView.decryptionFailure = true;
              failed.push(failedView);
            }
          }

          return [successful, failed] as [CipherView[], CipherView[]];
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to decrypt ciphers: ${error}`);
          return EMPTY;
        }),
      ),
    );
  }

  async decryptManyWithFailures(
    ciphers: Cipher[],
    userId: UserId,
  ): Promise<[CipherListView[], Cipher[]]> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        concatMap(async (sdk) => {
          using ref = sdk.take();

          const result: DecryptCipherListResult = await ref.value
            .vault()
            .ciphers()
            .decrypt_list_with_failures(ciphers.map((cipher) => cipher.toSdkCipher()));

          const decryptedCiphers = result.successes;
          const failedCiphers: Cipher[] = result.failures
            .map((cipher) => Cipher.fromSdkCipher(cipher))
            .filter((cipher): cipher is Cipher => cipher !== undefined);

          return [decryptedCiphers, failedCiphers] as [CipherListView[], Cipher[]];
        }),
      ),
    );
  }

  /**
   * Decrypts an attachment's content from a response object.
   *
   * @param cipher The encrypted cipher object that owns the attachment
   * @param attachment The encrypted attachment object
   * @param encryptedContent The encrypted content as a Uint8Array
   * @param userId The user ID whose key will be used for decryption
   *
   * @returns A promise that resolves to the decrypted content
   */
  async decryptAttachmentContent(
    cipher: Cipher,
    attachment: AttachmentView,
    encryptedContent: Uint8Array,
    userId: UserId,
  ): Promise<Uint8Array> {
    return firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        map((sdk) => {
          using ref = sdk.take();

          return ref.value
            .vault()
            .attachments()
            .decrypt_buffer(
              cipher.toSdkCipher(),
              attachment.toSdkAttachmentView(),
              encryptedContent,
            );
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to decrypt cipher buffer: ${error}`);
          return EMPTY;
        }),
      ),
    );
  }
}
