import * as grpc from '@grpc/grpc-js'
import { Status } from '@grpc/grpc-js/build/src/constants'
import { ISyncingServer, SyncRequest, SyncResponse } from '@standardnotes/grpc'
import { Logger } from 'winston'
import { MapperInterface } from '@standardnotes/domain-core'

import { ItemHash } from '../../Domain/Item/ItemHash'
import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { ApiVersion } from '../../Domain/Api/ApiVersion'
import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
import { CheckForTrafficAbuse } from '../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
import { Metric } from '../../Domain/Metrics/Metric'

export class SyncingServer implements ISyncingServer {
  constructor(
    private syncItemsUseCase: SyncItems,
    private syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
    private mapper: MapperInterface<SyncResponse20200115, SyncResponse>,
    protected checkForTrafficAbuse: CheckForTrafficAbuse,
    private strictAbuseProtection: boolean,
    private itemOperationsAbuseTimeframeLengthInMinutes: number,
    private itemOperationsAbuseThreshold: number,
    private freeUsersItemOperationsAbuseThreshold: number,
    private payloadSizeAbuseThreshold: number,
    private freeUsersPayloadSizeAbuseThreshold: number,
    private payloadSizeAbuseTimeframeLengthInMinutes: number,
    private logger: Logger,
  ) {}

  async syncItems(
    call: grpc.ServerUnaryCall<SyncRequest, SyncResponse>,
    callback: grpc.sendUnaryData<SyncResponse>,
  ): Promise<void> {
    try {
      const userUuid = call.metadata.get('x-user-uuid').pop() as string
      const isFreeUser = call.metadata.get('x-is-free-user').pop() === 'true'
      const hasContentLimit = call.metadata.get('x-has-content-limit').pop() === 'true'

      const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
        metricToCheck: Metric.NAMES.ItemOperation,
        userUuid,
        threshold: isFreeUser ? this.freeUsersItemOperationsAbuseThreshold : this.itemOperationsAbuseThreshold,
        timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
      })
      if (checkForItemOperationsAbuseResult.isFailed()) {
        this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
          userId: userUuid,
        })
        if (this.strictAbuseProtection) {
          const metadata = new grpc.Metadata()
          metadata.set(
            'x-sync-error-message',
            'You have exceeded the maximum bandwidth allotted to your account in a 5-minute period. Please wait to try again, or upgrade your account for increased limits.',
          )
          metadata.set('x-sync-error-response-code', '429')

          return callback(
            {
              code: Status.INVALID_ARGUMENT,
              message: checkForItemOperationsAbuseResult.getError(),
              name: 'INVALID_ARGUMENT',
              metadata,
            },
            null,
          )
        }
      }

      const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
        metricToCheck: Metric.NAMES.ContentSizeUtilized,
        userUuid,
        threshold: isFreeUser ? this.freeUsersPayloadSizeAbuseThreshold : this.payloadSizeAbuseThreshold,
        timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
      })
      if (checkForPayloadSizeAbuseResult.isFailed()) {
        this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
          userId: userUuid,
        })

        if (this.strictAbuseProtection) {
          const metadata = new grpc.Metadata()
          metadata.set(
            'x-sync-error-message',
            'You have exceeded the maximum bandwidth allotted to your account in a 5-minute period. Please wait to try again, or upgrade your account for increased limits.',
          )
          metadata.set('x-sync-error-response-code', '429')

          return callback(
            {
              code: Status.INVALID_ARGUMENT,
              message: checkForPayloadSizeAbuseResult.getError(),
              name: 'INVALID_ARGUMENT',
              metadata,
            },
            null,
          )
        }
      }

      const itemHashesRPC = call.request.getItemsList()
      const itemHashes: ItemHash[] = []
      for (const itemHash of itemHashesRPC) {
        const itemHashOrError = ItemHash.create({
          uuid: itemHash.getUuid(),
          content: itemHash.hasContent() ? itemHash.getContent() : undefined,
          content_type: itemHash.hasContentType() ? (itemHash.getContentType() as string) : null,
          deleted: itemHash.hasDeleted() ? itemHash.getDeleted() : undefined,
          duplicate_of: itemHash.hasDuplicateOf() ? itemHash.getDuplicateOf() : undefined,
          auth_hash: itemHash.hasAuthHash() ? itemHash.getAuthHash() : undefined,
          enc_item_key: itemHash.hasEncItemKey() ? itemHash.getEncItemKey() : undefined,
          items_key_id: itemHash.hasItemsKeyId() ? itemHash.getItemsKeyId() : undefined,
          created_at: itemHash.hasCreatedAt() ? itemHash.getCreatedAt() : undefined,
          created_at_timestamp: itemHash.hasCreatedAtTimestamp() ? itemHash.getCreatedAtTimestamp() : undefined,
          updated_at: itemHash.hasUpdatedAt() ? itemHash.getUpdatedAt() : undefined,
          updated_at_timestamp: itemHash.hasUpdatedAtTimestamp() ? itemHash.getUpdatedAtTimestamp() : undefined,
          user_uuid: userUuid,
          key_system_identifier: itemHash.hasKeySystemIdentifier()
            ? (itemHash.getKeySystemIdentifier() as string)
            : null,
          shared_vault_uuid: itemHash.hasSharedVaultUuid() ? (itemHash.getSharedVaultUuid() as string) : null,
        })

        if (itemHashOrError.isFailed()) {
          const metadata = new grpc.Metadata()
          metadata.set('x-sync-error-message', itemHashOrError.getError())
          metadata.set('x-sync-error-response-code', '400')

          return callback(
            {
              code: Status.INVALID_ARGUMENT,
              message: itemHashOrError.getError(),
              name: 'INVALID_ARGUMENT',
              metadata,
            },
            null,
          )
        }

        itemHashes.push(itemHashOrError.getValue())
      }

      let sharedVaultUuids: string[] | undefined = undefined
      const sharedVaultUuidsList = call.request.getSharedVaultUuidsList()
      if (sharedVaultUuidsList.length > 0) {
        sharedVaultUuids = sharedVaultUuidsList
      }

      const apiVersion = call.request.hasApiVersion() ? (call.request.getApiVersion() as string) : ApiVersion.v20161215
      const readOnlyAccess = call.metadata.get('x-read-only-access').pop() === 'true'
      if (readOnlyAccess) {
        this.logger.debug('Syncing with read-only access', {
          codeTag: 'SyncingServer',
          userId: userUuid,
        })
      }

      const syncResult = await this.syncItemsUseCase.execute({
        userUuid,
        itemHashes,
        computeIntegrityHash: call.request.hasComputeIntegrity() ? call.request.getComputeIntegrity() === true : false,
        syncToken: call.request.hasSyncToken() ? call.request.getSyncToken() : undefined,
        cursorToken: call.request.getCursorToken() ? call.request.getCursorToken() : undefined,
        limit: call.request.hasLimit() ? call.request.getLimit() : undefined,
        contentType: call.request.hasContentType() ? call.request.getContentType() : undefined,
        apiVersion,
        snjsVersion: call.metadata.get('x-snjs-version').pop() as string,
        readOnlyAccess,
        sessionUuid: call.metadata.get('x-session-uuid').pop() as string,
        sharedVaultUuids,
        isFreeUser,
        hasContentLimit,
      })
      if (syncResult.isFailed()) {
        const metadata = new grpc.Metadata()
        metadata.set('x-sync-error-message', syncResult.getError())
        metadata.set('x-sync-error-response-code', '400')

        return callback(
          {
            code: Status.INVALID_ARGUMENT,
            message: syncResult.getError(),
            name: 'INVALID_ARGUMENT',
            metadata,
          },
          null,
        )
      }

      const syncResponse = await this.syncResponseFactoryResolver
        .resolveSyncResponseFactoryVersion(apiVersion)
        .createResponse(syncResult.getValue())

      const projection = this.mapper.toProjection(syncResponse as SyncResponse20200115)

      callback(null, projection)
    } catch (error) {
      this.logger.error(`[SyncingServer] Error syncing items via gRPC: ${(error as Error).message}`)

      return callback(
        {
          code: Status.UNKNOWN,
          message: 'An error occurred while syncing items',
          name: 'UNKNOWN',
        },
        null,
      )
    }
  }
}
