import * as logger from "loglevel";
import { Logger, LogLevelDesc } from "loglevel";
import { MissingConfigurationError } from "./Errors";
import type { Agent as HTTPAgent } from "http";
import type { Agent as HTTPSAgent } from "https";
import type { AxiosRequestConfig } from "axios";
import { DocumentSchema, SearchResponse } from "./Documents";

export interface NodeConfiguration {
  host: string;
  port: number;
  protocol: string;
  path?: string;
  url?: string;
}

export interface NodeConfigurationWithHostname {
  host: string;
  port: number;
  protocol: string;
  path?: string;
}

export interface NodeConfigurationWithUrl {
  url: string;
}

export interface ConfigurationOptions {
  apiKey: string;
  nodes:
    | NodeConfiguration[]
    | NodeConfigurationWithHostname[]
    | NodeConfigurationWithUrl[];
  randomizeNodes?: boolean;
  /**
   * @deprecated
   * masterNode is now consolidated to nodes, starting with Typesense Server v0.12'
   */
  masterNode?:
    | NodeConfiguration
    | NodeConfigurationWithHostname
    | NodeConfigurationWithUrl;
  /**
   * @deprecated
   * readReplicaNodes is now consolidated to nodes, starting with Typesense Server v0.12'
   */
  readReplicaNodes?:
    | NodeConfiguration[]
    | NodeConfigurationWithHostname[]
    | NodeConfigurationWithUrl[];
  nearestNode?:
    | NodeConfiguration
    | NodeConfigurationWithHostname
    | NodeConfigurationWithUrl;
  connectionTimeoutSeconds?: number;
  timeoutSeconds?: number;
  healthcheckIntervalSeconds?: number;
  numRetries?: number;
  retryIntervalSeconds?: number;
  sendApiKeyAsQueryParam?: boolean | undefined;
  useServerSideSearchCache?: boolean;
  cacheSearchResultsForSeconds?: number;
  additionalHeaders?: Record<string, string>;

  logLevel?: LogLevelDesc;
  logger?: Logger;

  /**
   * Set a custom HTTP Agent
   *
   * This is helpful for eg, to enable keepAlive which helps prevents ECONNRESET socket hang up errors
   *    Usage:
   *      const { Agent: HTTPAgent } = require("http");
   *      ...
   *      httpAgent: new HTTPAgent({ keepAlive: true }),
   * @type {HTTPAgent}
   */
  httpAgent?: HTTPAgent;

  /**
   * Set a custom HTTPS Agent
   *
   * This is helpful for eg, to enable keepAlive which helps prevents ECONNRESET socket hang up errors
   *    Usage:
   *      const { Agent: HTTPSAgent } = require("https");
   *      ...
   *      httpsAgent: new HTTPSAgent({ keepAlive: true }),
   * @type {HTTPSAgent}
   */
  httpsAgent?: HTTPSAgent;

  /**
   * Set a custom paramsSerializer
   *
   * See axios documentation for more information on how to use this parameter: https://axios-http.com/docs/req_config
   *  This is helpful for handling React Native issues like this: https://github.com/axios/axios/issues/6102#issuecomment-2085301397
   * @type {any}
   */
  paramsSerializer?: any;

  /**
   * Set a custom axios adapter
   *
   * Useful for customizing the underlying HTTP client library used by Typesense.
   *
   * For example, you can use this to use a custom HTTP client library like `fetch`, in order for the library to work on the edge.
   * Related GiHub issue: https://github.com/typesense/typesense-js/issues/161
   *
   * See axios documentation for more information on how to use this parameter: https://axios-http.com/docs/req_config
   */
  axiosAdapter?: AxiosRequestConfig["adapter"];
}

/**
 * Configuration options for streaming responses
 */
export interface BaseStreamConfig {
  /**
   * Callback function that will be called for each chunk of data received
   * during streaming
   */
  onChunk?: (data: { conversation_id: string; message: string }) => void;
  /**
   * Callback function that will be called if there is an error during streaming
   */
  onError?: (error: Error) => void;
}

/**
 * Stream configuration for standard search responses
 * For specialized responses like MultiSearch, extend BaseStreamConfig with the appropriate onComplete signature
 */
export interface StreamConfig<T extends DocumentSchema>
  extends BaseStreamConfig {
  /**
   * Callback function that will be called when the streaming is complete
   */
  onComplete?: (data: SearchResponse<T>) => void;
}

export default class Configuration {
  readonly nodes:
    | NodeConfiguration[]
    | NodeConfigurationWithHostname[]
    | NodeConfigurationWithUrl[];
  readonly nearestNode?:
    | NodeConfiguration
    | NodeConfigurationWithHostname
    | NodeConfigurationWithUrl;
  readonly connectionTimeoutSeconds: number;
  readonly healthcheckIntervalSeconds: number;
  readonly numRetries: number;
  readonly retryIntervalSeconds: number;
  readonly apiKey: string;
  readonly sendApiKeyAsQueryParam?: boolean;
  readonly cacheSearchResultsForSeconds: number;
  readonly useServerSideSearchCache: boolean;
  readonly logger: Logger;
  readonly logLevel: LogLevelDesc;
  readonly additionalHeaders?: Record<string, string>;
  readonly httpAgent?: HTTPAgent;
  readonly httpsAgent?: HTTPSAgent;
  readonly paramsSerializer?: any;
  readonly axiosAdapter?: AxiosRequestConfig["adapter"];

  constructor(options: ConfigurationOptions) {
    this.nodes = options.nodes || [];
    this.nodes = this.nodes
      .map((node) => this.setDefaultPathInNode(node))
      .map((node) => this.setDefaultPortInNode(node))
      .map((node) => ({ ...node })) as NodeConfiguration[]; // Make a deep copy

    if (options.randomizeNodes == null) {
      options.randomizeNodes = true;
    }

    if (options.randomizeNodes === true) {
      this.shuffleArray(this.nodes);
    }

    this.nearestNode = options.nearestNode;
    this.nearestNode = this.setDefaultPathInNode(this.nearestNode);
    this.nearestNode = this.setDefaultPortInNode(this.nearestNode);

    this.connectionTimeoutSeconds =
      options.connectionTimeoutSeconds || options.timeoutSeconds || 5;
    this.healthcheckIntervalSeconds = options.healthcheckIntervalSeconds || 60;
    this.numRetries =
      (options.numRetries !== undefined && options.numRetries >= 0
        ? options.numRetries
        : this.nodes.length + (this.nearestNode == null ? 0 : 1)) || 3;
    this.retryIntervalSeconds = options.retryIntervalSeconds || 0.1;

    this.apiKey = options.apiKey;
    this.sendApiKeyAsQueryParam = options.sendApiKeyAsQueryParam; // We will set a default for this in Client and SearchClient

    this.cacheSearchResultsForSeconds =
      options.cacheSearchResultsForSeconds || 0; // Disable client-side cache by default
    this.useServerSideSearchCache = options.useServerSideSearchCache || false;

    this.axiosAdapter = options.axiosAdapter;
    this.logger = options.logger || logger;
    this.logLevel = options.logLevel || "warn";
    this.logger.setLevel(this.logLevel);

    this.additionalHeaders = options.additionalHeaders;

    this.httpAgent = options.httpAgent;
    this.httpsAgent = options.httpsAgent;

    this.paramsSerializer = options.paramsSerializer;

    this.showDeprecationWarnings(options);
    this.validate();
  }

  validate(): boolean {
    if (this.nodes == null || this.nodes.length === 0 || this.validateNodes()) {
      throw new MissingConfigurationError(
        "Ensure that nodes[].protocol, nodes[].host and nodes[].port are set",
      );
    }

    if (
      this.nearestNode != null &&
      this.isNodeMissingAnyParameters(this.nearestNode)
    ) {
      throw new MissingConfigurationError(
        "Ensure that nearestNodes.protocol, nearestNodes.host and nearestNodes.port are set",
      );
    }

    if (this.apiKey == null) {
      throw new MissingConfigurationError("Ensure that apiKey is set");
    }

    return true;
  }

  private validateNodes(): boolean {
    return this.nodes.some((node) => {
      return this.isNodeMissingAnyParameters(node);
    });
  }

  private isNodeMissingAnyParameters(
    node:
      | NodeConfiguration
      | NodeConfigurationWithHostname
      | NodeConfigurationWithUrl,
  ): boolean {
    return (
      !["protocol", "host", "port", "path"].every((key) => {
        return node.hasOwnProperty(key);
      }) && node["url"] == null
    );
  }

  private setDefaultPathInNode(
    node:
      | NodeConfiguration
      | NodeConfigurationWithHostname
      | NodeConfigurationWithUrl
      | undefined,
  ):
    | NodeConfiguration
    | NodeConfigurationWithHostname
    | NodeConfigurationWithUrl
    | undefined {
    if (node != null && !node.hasOwnProperty("path")) {
      node["path"] = "";
    }
    return node;
  }

  private setDefaultPortInNode(
    node:
      | NodeConfiguration
      | NodeConfigurationWithHostname
      | NodeConfigurationWithUrl
      | undefined,
  ):
    | NodeConfiguration
    | NodeConfigurationWithHostname
    | NodeConfigurationWithUrl
    | undefined {
    if (
      node != null &&
      !node.hasOwnProperty("port") &&
      node.hasOwnProperty("protocol")
    ) {
      switch (node["protocol"]) {
        case "https":
          node["port"] = 443;
          break;
        case "http":
          node["port"] = 80;
          break;
      }
    }
    return node;
  }

  private showDeprecationWarnings(options: ConfigurationOptions): void {
    if (options.timeoutSeconds) {
      this.logger.warn(
        "Deprecation warning: timeoutSeconds is now renamed to connectionTimeoutSeconds",
      );
    }
    if (options.masterNode) {
      this.logger.warn(
        "Deprecation warning: masterNode is now consolidated to nodes, starting with Typesense Server v0.12",
      );
    }
    if (options.readReplicaNodes) {
      this.logger.warn(
        "Deprecation warning: readReplicaNodes is now consolidated to nodes, starting with Typesense Server v0.12",
      );
    }
  }

  private shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
  }
}
