import { status } from '@grpc/grpc-js';
import * as nexus from 'nexus-rpc';
import { isGrpcServiceError, ServiceError } from '@temporalio/client';
import { ApplicationFailure, CancelledFailure, LoadedDataConverter, Payload, ProtoFailure } from '@temporalio/common';
import { encodeErrorToFailure, decodeOptionalSingle } from '@temporalio/common/lib/internal-non-workflow';
import type { temporal } from '@temporalio/proto';

////////////////////////////////////////////////////////////////////////////////////////////////////
// Payloads
////////////////////////////////////////////////////////////////////////////////////////////////////

export async function decodePayload(
  dataConverter: LoadedDataConverter,
  payload: temporal.api.common.v1.IPayload | undefined
): Promise<unknown> {
  let decoded: Payload | undefined | null;
  try {
    decoded = await decodeOptionalSingle(dataConverter.payloadCodecs, payload);
  } catch (err) {
    if (err instanceof ApplicationFailure) {
      throw err;
    }
    throw new nexus.HandlerError('INTERNAL', `Payload codec failed to decode Nexus operation input`, { cause: err });
  }

  if (decoded == null) {
    return undefined;
  }

  try {
    return dataConverter.payloadConverter.fromPayload(decoded);
  } catch (err) {
    if (err instanceof ApplicationFailure) {
      throw err;
    }
    throw new nexus.HandlerError('BAD_REQUEST', `Payload converter failed to decode Nexus operation input`, {
      cause: err,
    });
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Failures
////////////////////////////////////////////////////////////////////////////////////////////////////

export async function operationErrorToProto(
  dataConverter: LoadedDataConverter,
  err: nexus.OperationError
): Promise<ProtoFailure> {
  let newError: Error;
  if (err.state === 'canceled') {
    newError = new CancelledFailure(err.message, undefined, err.cause);
  } else {
    newError = ApplicationFailure.create({
      message: err.message,
      type: 'OperationError',
      nonRetryable: true,
      cause: err.cause,
    });
  }
  newError.stack = err.stack;
  return await encodeErrorToFailure(dataConverter, newError);
}

export async function handlerErrorToProto(
  dataConverter: LoadedDataConverter,
  err: nexus.HandlerError
): Promise<ProtoFailure> {
  return await encodeErrorToFailure(dataConverter, err);
}

export function coerceToHandlerError(err: unknown): nexus.HandlerError {
  if (err instanceof nexus.HandlerError) {
    return err;
  }

  // REVIEW: This check could be moved down and fold into the next one but will keep for now to help readability.
  if (err instanceof ApplicationFailure && err.nonRetryable) {
    return new nexus.HandlerError('INTERNAL', 'Handler failed with non-retryable application error', {
      cause: err,
      retryableOverride: false,
    });
  }

  if (err instanceof ServiceError) {
    if (isGrpcServiceError(err.cause)) {
      switch (err.cause.code) {
        case status.INVALID_ARGUMENT:
          return new nexus.HandlerError('BAD_REQUEST', undefined, { cause: err });
        case status.ALREADY_EXISTS:
        case status.FAILED_PRECONDITION:
        case status.OUT_OF_RANGE:
          return new nexus.HandlerError('INTERNAL', undefined, { cause: err, retryableOverride: false });
        case status.ABORTED:
        case status.UNAVAILABLE:
          return new nexus.HandlerError('UNAVAILABLE', undefined, { cause: err });
        case status.CANCELLED:
        case status.DATA_LOSS:
        case status.INTERNAL:
        case status.UNKNOWN:
        case status.UNAUTHENTICATED:
        case status.PERMISSION_DENIED:
          // Note that UNAUTHENTICATED and PERMISSION_DENIED have Nexus error types but we convert to internal because
          // this is not a client auth error and happens when the handler fails to auth with Temporal and should be
          // considered retryable.
          return new nexus.HandlerError('INTERNAL', undefined, { cause: err });
        case status.NOT_FOUND:
          return new nexus.HandlerError('NOT_FOUND', undefined, { cause: err });
        case status.RESOURCE_EXHAUSTED:
          return new nexus.HandlerError('RESOURCE_EXHAUSTED', undefined, { cause: err });
        case status.UNIMPLEMENTED:
          return new nexus.HandlerError('NOT_IMPLEMENTED', undefined, { cause: err });
        case status.DEADLINE_EXCEEDED:
          return new nexus.HandlerError('UPSTREAM_TIMEOUT', undefined, { cause: err });
      }
    }
  }

  return new nexus.HandlerError('INTERNAL', undefined, { cause: err });
}
