// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Code, DubboError } from "@apachedubbo/dubbo";

/**
 * Similar to DubboError.from(), this function turns any value into
 * a DubboError, but special cases some Node.js specific error codes and
 * sets an appropriate Dubbo error code.
 */
export function dubboErrorFromNodeReason(reason: unknown): DubboError {
  let code = Code.Internal;
  const chain = unwrapNodeErrorChain(reason).map(getNodeErrorProps);
  if (chain.some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
    // We do not want intentional errors from the server to be shadowed
    // by client-side errors.
    // This can occur if the server has written a response with an error
    // and has ended the connection. This response may already sit in a
    // buffer on the client, while it is still writing to the request
    // body.
    // To avoid this problem, we wrap ERR_STREAM_WRITE_AFTER_END as a
    // DubboError with Code.Aborted. The special meaning of this code
    // in this situation is documented in StreamingConn.send() and in
    // createServerStreamingFn().
    code = Code.Aborted;
  } else if (
    chain.some(
      (p) =>
        p.code == "ERR_STREAM_DESTROYED" ||
        p.code == "ERR_HTTP2_INVALID_STREAM" ||
        p.code == "ECONNRESET"
    )
  ) {
    // A handler whose stream is suddenly destroyed usually means the client
    // hung up. This behavior is triggered by the crosstest "cancel_after_begin".
    code = Code.Aborted;
  } else if (
    chain.some(
      (p) =>
        p.code == "ENOTFOUND" ||
        p.code == "EAI_AGAIN" ||
        p.code == "ECONNREFUSED"
    )
  ) {
    // Calling an unresolvable host should raise a DubboError with
    // Code.Aborted.
    // This behavior is covered by the crosstest "unresolvable_host".
    code = Code.Unavailable;
  }
  const ce = DubboError.from(reason, code);
  if (ce !== reason) {
    ce.cause = reason;
  }
  return ce;
}

/**
 * Unwraps a chain of errors, by walking through all "cause" properties
 * recursively.
 * This function is useful to find the root cause of an error.
 */
export function unwrapNodeErrorChain(reason: unknown): Error[] {
  const chain: Error[] = [];
  for (;;) {
    if (!(reason instanceof Error)) {
      break;
    }
    if (chain.includes(reason)) {
      // safeguard against infinite loop when "cause" points to an ancestor
      break;
    }
    chain.push(reason);
    if (!("cause" in reason)) {
      break;
    }
    reason = reason.cause;
  }
  return chain;
}

/**
 * Returns standard Node.js error properties from the given reason, if present.
 *
 * For context: Every error raised by Node.js APIs should expose a `code`
 * property - a string that permanently identifies the error. Some errors may
 * have an additional `syscall` property - a string that identifies native
 * components, for example "getaddrinfo" of libuv.
 * For more information, see https://github.com/nodejs/node/blob/f6052c68c1f9a4400a723e9c0b79da14197ab754/lib/internal/errors.js
 */
export function getNodeErrorProps(reason: unknown): {
  code?: string;
  syscall?: string;
} {
  const props: { code?: string; syscall?: string } = {};
  if (reason instanceof Error) {
    if ("code" in reason && typeof reason.code == "string") {
      props.code = reason.code;
    }
    if ("syscall" in reason && typeof reason.syscall == "string") {
      props.syscall = reason.syscall;
    }
  }
  return props;
}

/**
 * Returns a DubboError for an HTTP/2 error code.
 */
export function dubboErrorFromH2ResetCode(
  rstCode: number
): DubboError | undefined {
  switch (rstCode) {
    case H2Code.PROTOCOL_ERROR:
    case H2Code.INTERNAL_ERROR:
    case H2Code.FLOW_CONTROL_ERROR:
    case H2Code.SETTINGS_TIMEOUT:
    case H2Code.FRAME_SIZE_ERROR:
    case H2Code.COMPRESSION_ERROR:
    case H2Code.CONNECT_ERROR:
      return new DubboError(
        `http/2 stream closed with error code ${
          H2Code[rstCode]
        } (0x${rstCode.toString(16)})`,
        Code.Internal
      );
    case H2Code.REFUSED_STREAM:
      return new DubboError(
        `http/2 stream closed with error code ${
          H2Code[rstCode]
        } (0x${rstCode.toString(16)})`,
        Code.Unavailable
      );
    case H2Code.CANCEL:
      return new DubboError(
        `http/2 stream closed with error code ${
          H2Code[rstCode]
        } (0x${rstCode.toString(16)})`,
        Code.Canceled
      );
    case H2Code.ENHANCE_YOUR_CALM:
      return new DubboError(
        `http/2 stream closed with error code ${
          H2Code[rstCode]
        } (0x${rstCode.toString(16)})`,
        Code.ResourceExhausted
      );
    case H2Code.INADEQUATE_SECURITY:
      return new DubboError(
        `http/2 stream closed with error code ${
          H2Code[rstCode]
        } (0x${rstCode.toString(16)})`,
        Code.PermissionDenied
      );
    case H2Code.HTTP_1_1_REQUIRED:
      return new DubboError(
        `http/2 stream closed with error code ${
          H2Code[rstCode]
        } (0x${rstCode.toString(16)})`,
        Code.PermissionDenied
      );
    case H2Code.STREAM_CLOSED:
    default:
      // Intentionally not mapping STREAM_CLOSED (0x5), see https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#errors
      break;
  }
  return undefined;
}

export enum H2Code {
  PROTOCOL_ERROR = 0x1,
  INTERNAL_ERROR = 0x2,
  FLOW_CONTROL_ERROR = 0x3,
  SETTINGS_TIMEOUT = 0x4,
  STREAM_CLOSED = 0x5,
  FRAME_SIZE_ERROR = 0x6,
  REFUSED_STREAM = 0x7,
  CANCEL = 0x8,
  COMPRESSION_ERROR = 0x9,
  CONNECT_ERROR = 0xa,
  ENHANCE_YOUR_CALM = 0xb,
  INADEQUATE_SECURITY = 0xc,
  HTTP_1_1_REQUIRED = 0xd,
}
