import { ServiceError as GrpcServiceError, status as grpcStatus } from '@grpc/grpc-js';
import { decodePriority, LoadedDataConverter, NamespaceNotFoundError } from '@temporalio/common';
import {
  decodeSearchAttributes,
  decodeTypedSearchAttributes,
  searchAttributePayloadConverter,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { Replace } from '@temporalio/common/lib/type-helpers';
import { optionalTsToDate, requiredTsToDate } from '@temporalio/common/lib/time';
import { decodeMapFromPayloads } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
import { temporal, google } from '@temporalio/proto';
import {
  CountWorkflowExecution,
  RawWorkflowExecutionInfo,
  WorkflowExecutionInfo,
  WorkflowExecutionStatusName,
} from './types';

function workflowStatusCodeToName(code: temporal.api.enums.v1.WorkflowExecutionStatus): WorkflowExecutionStatusName {
  return workflowStatusCodeToNameInternal(code) ?? 'UNKNOWN';
}

/**
 * Intentionally leave out `default` branch to get compilation errors when new values are added
 */
function workflowStatusCodeToNameInternal(
  code: temporal.api.enums.v1.WorkflowExecutionStatus
): WorkflowExecutionStatusName {
  switch (code) {
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_UNSPECIFIED:
      return 'UNSPECIFIED';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING:
      return 'RUNNING';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED:
      return 'FAILED';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT:
      return 'TIMED_OUT';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED:
      return 'CANCELLED';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED:
      return 'TERMINATED';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED:
      return 'COMPLETED';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW:
      return 'CONTINUED_AS_NEW';
    case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_PAUSED:
      return 'PAUSED';
  }
}

export async function executionInfoFromRaw<T>(
  raw: RawWorkflowExecutionInfo,
  dataConverter: LoadedDataConverter,
  rawDataToEmbed: T
): Promise<Replace<WorkflowExecutionInfo, { raw: T }>> {
  return {
    type: raw.type!.name!,
    workflowId: raw.execution!.workflowId!,
    runId: raw.execution!.runId!,
    taskQueue: raw.taskQueue!,
    status: {
      code: raw.status!,
      name: workflowStatusCodeToName(raw.status!),
    },
    // Safe to convert to number, max history length is 50k, which is much less than Number.MAX_SAFE_INTEGER
    historyLength: raw.historyLength!.toNumber(),
    // Exact truncation for multi-petabyte histories
    // historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
    historySize: raw.historySizeBytes?.toNumber() || undefined,
    startTime: requiredTsToDate(raw.startTime, 'startTime'),
    executionTime: optionalTsToDate(raw.executionTime),
    closeTime: optionalTsToDate(raw.closeTime),
    memo: await decodeMapFromPayloads(dataConverter, raw.memo?.fields),
    searchAttributes: decodeSearchAttributes(raw.searchAttributes?.indexedFields),
    typedSearchAttributes: decodeTypedSearchAttributes(raw.searchAttributes?.indexedFields),
    parentExecution: raw.parentExecution
      ? {
          workflowId: raw.parentExecution.workflowId!,
          runId: raw.parentExecution.runId!,
        }
      : undefined,
    rootExecution: raw.rootExecution
      ? {
          workflowId: raw.rootExecution.workflowId!,
          runId: raw.rootExecution.runId!,
        }
      : undefined,
    raw: rawDataToEmbed,
    priority: decodePriority(raw.priority),
  };
}

export function decodeCountWorkflowExecutionsResponse(
  raw: temporal.api.workflowservice.v1.ICountWorkflowExecutionsResponse
): CountWorkflowExecution {
  return {
    // Note: lossy conversion of Long to number
    count: raw.count!.toNumber(),
    groups: raw.groups!.map((group) => {
      return {
        // Note: lossy conversion of Long to number
        count: group.count!.toNumber(),
        groupValues: group.groupValues!.map((value) => searchAttributePayloadConverter.fromPayload(value)),
      };
    }),
  };
}

type ErrorDetailsName = `temporal.api.errordetails.v1.${keyof typeof temporal.api.errordetails.v1}`;
type FailureName = `temporal.api.failure.v1.${keyof typeof temporal.api.failure.v1}`;

/**
 * If the error type can be determined based on embedded grpc error details,
 * then rethrow the appropriate TypeScript error. Otherwise do nothing.
 *
 * This function should be used before falling back to generic error handling
 * based on grpc error code. Very few error types are currently supported, but
 * this function will be expanded over time as more server error types are added.
 */
export function rethrowKnownErrorTypes(err: GrpcServiceError): void {
  // We really don't expect multiple error details, but this really is an array, so just in case...
  for (const entry of getGrpcStatusDetails(err) ?? []) {
    if (!entry.type_url || !entry.value) continue;
    const type = entry.type_url.replace(/^type.googleapis.com\//, '') as ErrorDetailsName;

    switch (type) {
      case 'temporal.api.errordetails.v1.NamespaceNotFoundFailure': {
        const { namespace } = temporal.api.errordetails.v1.NamespaceNotFoundFailure.decode(entry.value);
        throw new NamespaceNotFoundError(namespace);
      }
      case 'temporal.api.errordetails.v1.MultiOperationExecutionFailure': {
        // MultiOperationExecutionFailure contains error statuses for multiple
        // operations. A MultiOperationExecutionAborted error status means that
        // the corresponding operation was aborted due to an error in one of the
        // other operations. We rethrow the first operation error that is not
        // MultiOperationExecutionAborted.
        const { statuses } = temporal.api.errordetails.v1.MultiOperationExecutionFailure.decode(entry.value);
        for (const status of statuses) {
          const detail = status.details?.[0];
          const statusType = detail?.type_url?.replace(/^type.googleapis.com\//, '') as FailureName | undefined;
          if (
            statusType === 'temporal.api.failure.v1.MultiOperationExecutionAborted' ||
            status.code === grpcStatus.OK
          ) {
            continue;
          }
          err.message = status.message ?? err.message;
          err.code = status.code || err.code;
          err.details = detail?.value?.toString() || err.details;
          throw err;
        }
      }
    }
  }
}

function getGrpcStatusDetails(err: GrpcServiceError): google.rpc.Status['details'] | undefined {
  const statusBuffer = err.metadata.get('grpc-status-details-bin')?.[0];
  if (!statusBuffer || typeof statusBuffer === 'string') {
    return undefined;
  }
  return google.rpc.Status.decode(statusBuffer).details;
}
