/*
 * Copyright 2019 gRPC authors.
 *
 * 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 * as semver from 'semver';

import {
  ClientDuplexStream,
  ClientReadableStream,
  ClientUnaryCall,
  ClientWritableStream,
  ServiceError,
} from './call';
import { CallCredentials } from './call-credentials';
import { Deadline, StatusObject } from './call-stream';
import { Channel, ConnectivityState, ChannelImplementation } from './channel';
import { ChannelCredentials } from './channel-credentials';
import {
  CallOptions,
  Client,
  CallInvocationTransformer,
  CallProperties,
} from './client';
import { LogVerbosity, Status } from './constants';
import * as logging from './logging';
import {
  Deserialize,
  loadPackageDefinition,
  makeClientConstructor,
  MethodDefinition,
  Serialize,
  ServiceDefinition,
} from './make-client';
import { Metadata } from './metadata';
import {
  Server,
  UntypedHandleCall,
  UntypedServiceImplementation,
} from './server';
import { KeyCertPair, ServerCredentials } from './server-credentials';
import { StatusBuilder } from './status-builder';
import {
  handleBidiStreamingCall,
  handleServerStreamingCall,
  handleUnaryCall,
  ServerUnaryCall,
  ServerReadableStream,
  ServerWritableStream,
  ServerDuplexStream,
} from './server-call';

const supportedNodeVersions = require('../../package.json').engines.node;
if (!semver.satisfies(process.version, supportedNodeVersions)) {
  throw new Error(`@grpc/grpc-js only works on Node ${supportedNodeVersions}`);
}

interface IndexedObject {
  [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
  [key: number]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}

function mixin(...sources: IndexedObject[]) {
  const result: { [key: string]: Function } = {};
  for (const source of sources) {
    for (const propName of Object.getOwnPropertyNames(source)) {
      const property: any = source[propName]; // eslint-disable-line @typescript-eslint/no-explicit-any
      if (typeof property === 'function') {
        result[propName] = property;
      }
    }
  }
  return result;
}

export interface OAuth2Client {
  getRequestMetadata: (
    url: string,
    callback: (
      err: Error | null,
      headers?: {
        [index: string]: string;
      }
    ) => void
  ) => void;
  getRequestHeaders: (url?: string) => Promise<{ [index: string]: string }>;
}

/**** Client Credentials ****/

// Using assign only copies enumerable properties, which is what we want
export const credentials = mixin(
  {
    /**
     * Create a gRPC credential from a Google credential object.
     * @param googleCredentials The authentication client to use.
     * @return The resulting CallCredentials object.
     */
    createFromGoogleCredential: (
      googleCredentials: OAuth2Client
    ): CallCredentials => {
      return CallCredentials.createFromMetadataGenerator(
        (options, callback) => {
          // google-auth-library pre-v2.0.0 does not have getRequestHeaders
          // but has getRequestMetadata, which is deprecated in v2.0.0
          let getHeaders: Promise<{ [index: string]: string }>;
          if (typeof googleCredentials.getRequestHeaders === 'function') {
            getHeaders = googleCredentials.getRequestHeaders(
              options.service_url
            );
          } else {
            getHeaders = new Promise((resolve, reject) => {
              googleCredentials.getRequestMetadata(
                options.service_url,
                (err, headers) => {
                  if (err) {
                    reject(err);
                    return;
                  }
                  resolve(headers);
                }
              );
            });
          }
          getHeaders.then(
            (headers) => {
              const metadata = new Metadata();
              for (const key of Object.keys(headers)) {
                metadata.add(key, headers[key]);
              }
              callback(null, metadata);
            },
            (err) => {
              callback(err);
            }
          );
        }
      );
    },

    /**
     * Combine a ChannelCredentials with any number of CallCredentials into a
     * single ChannelCredentials object.
     * @param channelCredentials The ChannelCredentials object.
     * @param callCredentials Any number of CallCredentials objects.
     * @return The resulting ChannelCredentials object.
     */
    combineChannelCredentials: (
      channelCredentials: ChannelCredentials,
      ...callCredentials: CallCredentials[]
    ): ChannelCredentials => {
      return callCredentials.reduce(
        (acc, other) => acc.compose(other),
        channelCredentials
      );
    },

    /**
     * Combine any number of CallCredentials into a single CallCredentials
     * object.
     * @param first The first CallCredentials object.
     * @param additional Any number of additional CallCredentials objects.
     * @return The resulting CallCredentials object.
     */
    combineCallCredentials: (
      first: CallCredentials,
      ...additional: CallCredentials[]
    ): CallCredentials => {
      return additional.reduce((acc, other) => acc.compose(other), first);
    },
  },
  ChannelCredentials,
  CallCredentials
);

/**** Metadata ****/

export { Metadata };

/**** Constants ****/

export {
  LogVerbosity as logVerbosity,
  Status as status,
  ConnectivityState as connectivityState,
  // TODO: Other constants as well
};

/**** Client ****/

export {
  Client,
  loadPackageDefinition,
  makeClientConstructor,
  makeClientConstructor as makeGenericClientConstructor,
  CallProperties,
  CallInvocationTransformer,
  ChannelImplementation as Channel,
  Channel as ChannelInterface,
};

/**
 * Close a Client object.
 * @param client The client to close.
 */
export const closeClient = (client: Client) => client.close();

export const waitForClientReady = (
  client: Client,
  deadline: Date | number,
  callback: (error?: Error) => void
) => client.waitForReady(deadline, callback);

/* Interfaces */

export {
  ChannelCredentials,
  CallCredentials,
  Deadline,
  Serialize as serialize,
  Deserialize as deserialize,
  ClientUnaryCall,
  ClientReadableStream,
  ClientWritableStream,
  ClientDuplexStream,
  CallOptions,
  MethodDefinition,
  StatusObject,
  ServiceError,
  ServerUnaryCall,
  ServerReadableStream,
  ServerWritableStream,
  ServerDuplexStream,
  ServiceDefinition,
  UntypedHandleCall,
  UntypedServiceImplementation,
};

/**** Server ****/

export { handleBidiStreamingCall, handleServerStreamingCall, handleUnaryCall };

/* eslint-disable @typescript-eslint/no-explicit-any */
export type Call =
  | ClientUnaryCall
  | ClientReadableStream<any>
  | ClientWritableStream<any>
  | ClientDuplexStream<any, any>;
/* eslint-enable @typescript-eslint/no-explicit-any */

/**** Unimplemented function stubs ****/

/* eslint-disable @typescript-eslint/no-explicit-any */

export const loadObject = (value: any, options: any) => {
  throw new Error(
    'Not available in this library. Use @grpc/proto-loader and loadPackageDefinition instead'
  );
};

export const load = (filename: any, format: any, options: any) => {
  throw new Error(
    'Not available in this library. Use @grpc/proto-loader and loadPackageDefinition instead'
  );
};

export const setLogger = (logger: Partial<Console>): void => {
  logging.setLogger(logger);
};

export const setLogVerbosity = (verbosity: LogVerbosity): void => {
  logging.setLoggerVerbosity(verbosity);
};

export { Server };
export { ServerCredentials };
export { KeyCertPair };

export const getClientChannel = (client: Client) => {
  return Client.prototype.getChannel.call(client);
};

export { StatusBuilder };

export { Listener } from './call-stream';

export {
  Requester,
  ListenerBuilder,
  RequesterBuilder,
  Interceptor,
  InterceptorProvider,
  InterceptingCall,
  InterceptorConfigurationError,
} from './client-interceptors';

export { GrpcObject } from './make-client';

import * as resolver from './resolver';
import * as load_balancer from './load-balancer';

(() => {
  resolver.registerAll();
  load_balancer.registerAll();
})();
