import { firstValueFrom, switchMap, catchError } from "rxjs";

import {
  CipherListView,
  CipherView as SdkCipherView,
  CreateAttachmentRequest,
  CreatedAttachment,
} from "@bitwarden/sdk-internal";

import { DECRYPT_ERROR } from "../../key-management/crypto/models/enc-string";
import { LogService } from "../../platform/abstractions/log.service";
import { SdkService, asUuid } from "../../platform/abstractions/sdk/sdk.service";
import {
  CipherId,
  CollectionId,
  EmergencyAccessId,
  OrganizationId,
  UserId,
} from "../../types/guid";
import { CipherSdkService, DecryptAllCiphersResult } from "../abstractions/cipher-sdk.service";
import { Cipher } from "../models/domain/cipher";
import { CipherView } from "../models/view/cipher.view";

export class DefaultCipherSdkService implements CipherSdkService {
  constructor(
    private sdkService: SdkService,
    private logService: LogService,
  ) {}

  async createWithServer(
    cipherView: CipherView,
    userId: UserId,
    orgAdmin?: boolean,
  ): Promise<CipherView | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();

          const sdkCreateRequest = cipherView.toSdkCreateCipherRequest(sdkCiphersClient);

          let result: SdkCipherView;
          if (orgAdmin) {
            result = await sdkCiphersClient.admin().create(sdkCreateRequest);
          } else {
            result = await sdkCiphersClient.create(sdkCreateRequest);
          }

          return CipherView.fromSdkCipherView(result, sdkCiphersClient);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to create cipher: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async updateWithServer(
    cipher: CipherView,
    userId: UserId,
    originalCipherView?: CipherView,
    orgAdmin?: boolean,
  ): Promise<CipherView | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();

          const sdkUpdateRequest = cipher.toSdkUpdateCipherRequest(sdkCiphersClient);

          let result: SdkCipherView;
          if (orgAdmin) {
            result = await sdkCiphersClient
              .admin()
              .edit(
                sdkUpdateRequest,
                originalCipherView?.toSdkCipherView(sdkCiphersClient) ||
                  new CipherView().toSdkCipherView(sdkCiphersClient),
              );
          } else if (cipher.edit) {
            result = await sdkCiphersClient.edit(sdkUpdateRequest);
          } else {
            const sdkPartialUpdateRequest = cipher.toSdkPartialUpdateCipherRequest();
            result = await sdkCiphersClient.edit_partial(sdkPartialUpdateRequest);
          }

          return CipherView.fromSdkCipherView(result, sdkCiphersClient);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to update cipher: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async deleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          if (asAdmin) {
            await ref.value.vault().ciphers().admin().delete(asUuid(id));
          } else {
            await ref.value.vault().ciphers().delete(asUuid(id));
          }
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to delete cipher: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async deleteManyWithServer(
    ids: string[],
    userId: UserId,
    asAdmin = false,
    orgId?: OrganizationId,
  ): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          if (asAdmin) {
            if (orgId == null) {
              throw new Error("Organization ID is required for admin delete.");
            }
            await ref.value
              .vault()
              .ciphers()
              .admin()
              .delete_many(
                ids.map((id) => asUuid(id)),
                asUuid(orgId),
              );
          } else {
            await ref.value
              .vault()
              .ciphers()
              .delete_many(ids.map((id) => asUuid(id)));
          }
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to delete multiple ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async softDeleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          if (asAdmin) {
            await ref.value.vault().ciphers().admin().soft_delete(asUuid(id));
          } else {
            await ref.value.vault().ciphers().soft_delete(asUuid(id));
          }
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to soft delete cipher: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async softDeleteManyWithServer(
    ids: string[],
    userId: UserId,
    asAdmin = false,
    orgId?: OrganizationId,
  ): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          if (asAdmin) {
            if (orgId == null) {
              throw new Error("Organization ID is required for admin soft delete.");
            }
            await ref.value
              .vault()
              .ciphers()
              .admin()
              .soft_delete_many(
                ids.map((id) => asUuid(id)),
                asUuid(orgId),
              );
          } else {
            await ref.value
              .vault()
              .ciphers()
              .soft_delete_many(ids.map((id) => asUuid(id)));
          }
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to soft delete multiple ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async restoreWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          if (asAdmin) {
            await ref.value.vault().ciphers().admin().restore(asUuid(id));
          } else {
            await ref.value.vault().ciphers().restore(asUuid(id));
          }
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to restore cipher: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async restoreManyWithServer(ids: string[], userId: UserId, orgId?: string): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();

          // No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
          // The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
          if (orgId) {
            await ref.value
              .vault()
              .ciphers()
              .admin()
              .restore_many(
                ids.map((id) => asUuid(id)),
                asUuid(orgId),
              );
          } else {
            await ref.value
              .vault()
              .ciphers()
              .restore_many(ids.map((id) => asUuid(id)));
          }
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to restore multiple ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async shareWithServer(
    cipherView: CipherView,
    organizationId: OrganizationId,
    collectionIds: CollectionId[],
    userId: UserId,
    originalCipherView?: CipherView,
  ): Promise<CipherView | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();

          const sdkCipherView = cipherView.toSdkCipherView(sdkCiphersClient);

          const result = await sdkCiphersClient.share_cipher(
            sdkCipherView,
            asUuid(organizationId),
            collectionIds.map((id) => asUuid(id)),
            originalCipherView?.toSdkCipherView(sdkCiphersClient),
          );

          return CipherView.fromSdkCipherView(result, sdkCiphersClient);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to share cipher: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async shareManyWithServer(
    cipherViews: CipherView[],
    organizationId: OrganizationId,
    collectionIds: CollectionId[],
    userId: UserId,
  ): Promise<CipherView[]> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();

          const sdkCipherViews = cipherViews.map((cv) => cv.toSdkCipherView(sdkCiphersClient));

          const results = await sdkCiphersClient.share_ciphers_bulk(
            sdkCipherViews,
            asUuid(organizationId),
            collectionIds.map((id) => asUuid(id)),
          );

          return results
            .map((c) => CipherView.fromSdkCipherView(c, sdkCiphersClient))
            .filter((c): c is CipherView => c !== undefined);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to share multiple ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async deleteAttachmentWithServer(
    cipherId: CipherId,
    attachmentId: string,
    userId: UserId,
    asAdmin = false,
  ): Promise<Cipher | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();

          const result = asAdmin
            ? await ref.value
                .vault()
                .attachments()
                .admin()
                .delete_attachment(asUuid(cipherId), attachmentId)
            : await ref.value
                .vault()
                .attachments()
                .delete_attachment(asUuid(cipherId), attachmentId);

          return Cipher.fromSdkCipher(result);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to delete cipher attachment: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async getAttachmentDownloadUrl(
    cipherId: CipherId,
    attachmentId: string,
    userId: UserId,
    options?: { asAdmin?: boolean; emergencyAccessId?: EmergencyAccessId },
  ): Promise<string> {
    if (options?.asAdmin && options?.emergencyAccessId) {
      throw new Error("asAdmin and emergencyAccessId are mutually exclusive");
    }

    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const attachments = ref.value.vault().attachments();

          if (options?.asAdmin) {
            return await attachments
              .admin()
              .get_attachment_download_url(asUuid(cipherId), attachmentId);
          }
          return await attachments.get_attachment_download_url(
            asUuid(cipherId),
            attachmentId,
            options?.emergencyAccessId,
          );
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to get attachment download URL: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async createAttachment(
    cipherId: CipherId,
    request: CreateAttachmentRequest,
    userId: UserId,
  ): Promise<CreatedAttachment> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          return await ref.value.vault().attachments().create_attachment(asUuid(cipherId), request);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to create attachment: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async renewAttachmentUploadUrl(
    cipherId: CipherId,
    attachmentId: string,
    userId: UserId,
  ): Promise<string> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          return await ref.value
            .vault()
            .attachments()
            .renew_file_upload_url(asUuid(cipherId), attachmentId);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to renew attachment upload URL: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async upgradeAttachment(
    cipherId: CipherId,
    attachmentId: string,
    userId: UserId,
  ): Promise<CipherView | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();
          const result = await ref.value
            .vault()
            .attachments()
            .upgrade_attachment(asUuid(cipherId), attachmentId);
          return CipherView.fromSdkCipherView(result, sdkCiphersClient);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to upgrade attachment: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async getAllDecrypted(userId: UserId): Promise<DecryptAllCiphersResult> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();

          const decryptResult = await sdkCiphersClient.get_all();

          const successes = [...(decryptResult.successes ?? [])]
            .map((sdkCipherView: any) =>
              CipherView.fromSdkCipherView(sdkCipherView, sdkCiphersClient),
            )
            .filter((v): v is CipherView => v !== undefined);

          const failures: CipherView[] = [...(decryptResult.failures ?? [])].map((failure: any) => {
            const cipherView = new CipherView(Cipher.fromSdkCipher(failure));
            cipherView.name = DECRYPT_ERROR;
            cipherView.decryptionFailure = true;
            return cipherView;
          });

          return { successes, failures };
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to list and decrypt ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async getAllFromApiForOrganization(
    organizationId: string,
    userId: UserId,
    includeMemberItems: boolean,
  ): Promise<[Cipher[], CipherListView[]]> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();

          const result = await ref.value
            .vault()
            .ciphers()
            .admin()
            .list_org_ciphers(asUuid(organizationId), includeMemberItems);

          const ciphers = result.ciphers
            .map((c) => Cipher.fromSdkCipher(c))
            .filter((c): c is Cipher => c !== undefined);

          return [ciphers, result.listViews] as [Cipher[], CipherListView[]];
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to list organization ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async getManyFromApiForOrganization(
    organizationId: string,
    userId: UserId,
  ): Promise<[Cipher[], CipherListView[]]> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();

          const result = await ref.value
            .vault()
            .ciphers()
            .admin()
            .list_assigned_org_ciphers(asUuid(organizationId));

          const ciphers = result.ciphers
            .map((c) => Cipher.fromSdkCipher(c))
            .filter((c): c is Cipher => c !== undefined);

          return [ciphers, result.listViews] as [Cipher[], CipherListView[]];
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to list assigned organization ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async bulkUpdateCollectionsWithServer(
    orgId: OrganizationId,
    userId: UserId,
    cipherIds: CipherId[],
    collectionIds: CollectionId[],
    removeCollections: boolean,
  ): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          await ref.value
            .vault()
            .ciphers()
            .bulk_update_collections(
              asUuid(orgId),
              cipherIds.map((id) => asUuid(id)),
              collectionIds.map((id) => asUuid(id)),
              removeCollections,
            );
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to bulk update cipher collections: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async moveManyWithServer(ids: string[], folderId: string | null, userId: UserId): Promise<void> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          await ref.value
            .vault()
            .ciphers()
            .move_many(
              ids.map((id) => asUuid(id)),
              folderId == null ? undefined : asUuid(folderId),
            );
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to move multiple ciphers: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async saveCollectionsWithServerAdmin(
    cipherId: string,
    collectionIds: string[],
    userId: UserId,
  ): Promise<CipherView | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();
          const result = await sdkCiphersClient.admin().update_collection(
            asUuid(cipherId),
            collectionIds.map((id) => asUuid(id)),
          );
          return CipherView.fromSdkCipherView(result, sdkCiphersClient);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to update cipher collections as admin: ${error}`);
          throw error;
        }),
      ),
    );
  }

  async saveCollectionsWithServer(
    cipherId: string,
    collectionIds: string[],
    userId: UserId,
  ): Promise<CipherView | undefined> {
    return await firstValueFrom(
      this.sdkService.userClient$(userId).pipe(
        switchMap(async (sdk) => {
          using ref = sdk.take();
          const sdkCiphersClient = ref.value.vault().ciphers();
          const result = await sdkCiphersClient.update_collection(
            asUuid(cipherId),
            collectionIds.map((id) => asUuid(id)),
            false,
          );
          return CipherView.fromSdkCipherView(result, sdkCiphersClient);
        }),
        catchError((error: unknown) => {
          this.logService.error(`Failed to update cipher collections: ${error}`);
          throw error;
        }),
      ),
    );
  }
}
