/*
 * Copyright 2009-2024 Roland Huss
 *
 * 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.
 */

// --- Types related to configuration options - global and request-related

/**
 * Narrowed list of options that can be passed directly to `fetch()` `options` argument
 */
export type UsedFetchOptions = Pick<RequestInit, "cache" | "credentials" | "headers" | "redirect">

/**
 * Processing parameters that influence Jolokia operations.
 * See `org.jolokia.server.core.config.ConfigKey` enum values with `requestConfig=true`.
 * These values may be specified when creating Jolokia instance, but may be overridden for each request.
 * These are sent either as GET query parameters or within `config` key of JSON data for POST requests.
 * @see {https://jolokia.org/reference/html/manual/jolokia_protocol.html#processing-parameters Jolokia Processing Parameters}
 */
export type ProcessingParameters = {
  /**
   * Maximum traversal depth for serialization of complex return values
   */
  maxDepth?: number
  /**
   * Maximum size of collections returned during serialization. If larger, the collection is returned truncated.
   */
  maxCollectionSize?: number
  /**
   * Maximum number of objects contained in the response.
   */
  maxObjects?: number
  /**
   * How to serialize long values in the JSON response: `number` or `string`.
   * The default `number` simply serializes longs as numbers in JSON.
   * If set to `string`, longs are serialized as strings.
   * It can be useful when a JavaScript client consumes the JSON response,
   * because numbers greater than the max safe integer don't retain their precision
   * in JavaScript.
   * @since 2.0.3
   */
  serializeLong?: "number" | "string"
  /**
   * If set to true, errors during JMX operations and JSON serialization
   * are ignored.Otherwise if a single deserialization fails, the whole request
   * returns with an error. This works only for certain operations like pattern reads.
   */
  ignoreErrors?: boolean
  /**
   * If set to `true`, then in case of an error the stack trace is
   * included. With `false` no stack trace will be returned, and when
   * this parameter is set to `runtime` only for `RuntimeException`
   * a stack trace is put into the error response. Default is `false`
   * if not set otherwise in the global agent configuration.
   */
  includeStackTrace?: boolean | "true" | "false" | "runtime"
  /**
   * If this parameter is set to `true` then a serialized version of
   * the exception is included in an error response. This value is put under the
   * key `error_value` in the response value. By default this is set
   * to `false` except when the agent global configuration option is
   * configured otherwise.
   */
  serializeException?: boolean
  /**
   * Defaults to `true` to return the canonical format of property
   * lists. If set to `false` then the default unsorted property list
   * is returned.
   */
  canonicalNaming?: boolean
  /**
   * The MIME type to return for the response. By default, this is `text/plain`,
   * but it can be useful for some tools to change it to `application/json`.
   * Init parameters can be used to change the default mime type. Only `text/plain`
   * and `application/json` are allowed. For any other value Jolokia
   * will fallback to `text/plain`.
   */
  mimeType?: "text/plain" | "application/json"
  /**
   * Whether to include request in its response. There's a global variant of this option
   * but it may be overridden at request time.
   */
  includeRequest?: boolean
  /**
   * If this parameter is given, its value is interpreted as epoch time (seconds
   * since 1.1.1970) and if the requested value did not change since this time,
   * an empty response (with no `value`) is returned and the response
   * status code is set to 304 ("Not modified"). This option is currently only
   * supported for `LIST` requests. The time value can be extracted
   * from a previous response's `timestamp`.
   */
  ifModifiedSince?: number
  /**
   * Whether to provide optimized list response, where instead of domains and mbeans, we return a cache and domains.
   * domains' mbeans may refer to cached MBeanInfo instead of a copy (important when we have multiple
   * mbeans with the same MBeanInfo)
   */
  listCache?: boolean
}

/**
 * Base request and configuration options which include the ones handled at server-side by Jolokia agent, but also
 * the options used with Fetch API at client-side.
 * We're mixing-in only selected {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch#options | RequestInit}
 * options.
 */
export type BaseRequestOptions = ProcessingParameters & UsedFetchOptions & {
  /**
   * Agent URL, which is mandatory
   */
  url?: URL | string
  /**
   * Selects HTTP method to use (`post` or `get`).
   * Note that bulk requests can't be used with `get` method. Also, when using a `read` type request for multiple
   * attributes, this also can only be sent as `post` requests. If not given, a HTTP method is determined dynamically.
   * If a method is selected which doesn't fit to the request, an error is raised.
   */
  method?: "get" | "post"
  /**
   * The type of data specified that impact response processing. The default value is `json`,
   * and the response is parsed as JSON to an object. If the value is `text`,
   * the response is returned as plain text without parsing (and without splitting to individual responses).
   * The client is then responsible for parsing the response. This can be useful when a custom JSON
   * parsing is necessary.
   * Jolokia Simple API (jolokia-simple.js) doesn't support `text` as dataType.
   * @since 2.0.2
   */
  dataType?: "json" | "text"
  /**
   * A username used for HTTP Basic authentication. User may also pass value for `Authorization` header directly
   * with `headers` option ({@link UsedFetchOptions#headers}).
   */
  username?: string
  /**
   * A password used for HTTP Basic authentication. User may also pass value for `Authorization` header directly
   * with `headers` option ({@link UsedFetchOptions#headers}).
   */
  password?: string
  /**
   * Timeout for the HTTP request
   */
  timeout?: number
}

/**
 * Configuration of remote JMX Agent accessed using
 * {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#protocol-proxy Jolokia proxy mode}
 */
export type RemoteAgentOptions = {
  /** Remote JMX Agent URL, for example `service:jmx:rmi:///jndi/rmi://targethost:9999/jmxrmi` */
  url: string
  /** Remote JMX username */
  user?: string
  /** Remote JMX password */
  password?: string
}

/**
 * Jolokia configuration object to specify default configuration values used when request-specific options are
 * not present.
 */
export type JolokiaConfiguration = BaseRequestOptions & {
  /**
   * Interval for poll-fetching (in ms) that can be changed during {@link IJolokia#start}
   */
  fetchInterval?: number
  /**
   * Fetch API error callback configured for all `request()` calls of this instance. Can be used both for promise
   * and callback modes
   */
  fetchError?: "ignore" | FetchErrorCallback
}

/**
 * Options that can be passed to a {@link IJolokia#request} method, which include success and error callback. When
 * not specified {@link IJolokia#request} method returns a Promise resolving to an array of responses. Previously,
 * lack of specified callbacks meant "synchronous AJAX request". Now it's "Fetch request returning a Promise".
 * A callback always receives a response and its index (when using bulk requests or multi-read attribute requests)
 */
export type RequestOptions = BaseRequestOptions & {
  /**
   * A hint about the value we want the returned promise to resolve to. `default` (or when not specified) means
   * a promise resolving to an array of Jolokia (good and error) responses is returned. Also, when `dataType=text`
   * this promise resolves to a string value for the response.
   * When specifying `response`, we're returning a Fetch API Response object directly, so user may do whatever is needed.
   */
  resolve?: "default" | "response"
  /**
   * A callback (which may be called with successful JSON response or a string) or an array of callbacks (JSON only)
   * to be used by {@link IJolokia#request} method
   */
  success?: "ignore" | ResponseCallback | TextResponseCallback | ResponseCallbacks
  /**
   * A callback (which may be called with error JSON response) or an array of callbacks
   * to be used by {@link IJolokia#request} method for error response
   */
  error?: "ignore" | ErrorCallback | ErrorCallbacks
  /**
   * Fetch API error callback configured for single `request()` call. Can be used both in callback and in promise modes.
   */
  fetchError?: "ignore" | FetchErrorCallback
}

// --- Types related to Jolokia requests

/**
 * An enumeration of supported Jolokia operation types
 */
export type RequestType = "read" | "write" | "exec" | "search" | "list" | "notification" | "version"

/**
 * A Jolokia base request object to be sent to remote Jolokia agent
 */
export type BaseRequest = {
  /**
   * A type of Jolokia request according to
   * {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html Jolokia protocol}
   */
  type: RequestType
  /**
   * Request configuration object
   */
  config?: ProcessingParameters
  /**
   * Remote/proxied JMX Agent configuration
   */
  target?: RemoteAgentOptions
}

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#read read request}
 */
export type ReadRequest = BaseRequest & {
  type: "read"
  /**
   * target MBean for attribute access
   */
  mbean: string
  /**
   * attribute(s) to read
   */
  attribute?: string | string[]
  /**
   * path within Jolokia traversal for MBean domains/names
   */
  path?: string | string[]
}

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#write write request}
 */
export type WriteRequest = BaseRequest & {
  type: "write"
  /**
   * target MBean for attribute access
   */
  mbean: string
  /**
   * attribute to write
   */
  attribute: string
  /**
   * Value to be set for the attribute
   */
  value: unknown
  /**
   * path within Jolokia traversal for MBean domains/names
   */
  path?: string | string[]
}

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#exec exec request}
 */
export type ExecRequest = BaseRequest & {
  type: "exec"
  /**
   * target MBean for invocation
   */
  mbean: string
  /**
   * Operation name for Jolokia `exec` request
   */
  operation: string
  /**
   * Arguments to use with Jolokia `exec` request
   */
  arguments?: unknown[]
}

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#search search request}
 */
export type SearchRequest = BaseRequest & {
  type: "search"
  /**
   * MBean pattern for searching
   */
  mbean: string
}

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#list list request}
 */
export type ListRequest = BaseRequest & {
  type: "list"
  /**
   * path within Jolokia traversal for MBean domains/names
   */
  path?: string
}

/**
 * Type of notification mode
 */
export type NotificationMode = "sse" | "pull"

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#notification notification request}
 */
export type NotificationRequest = BaseRequest & {
  type: "notification"
  /**
   * operation command to use
   */
  command: "register" | "unregister" | "add" | "remove" | "ping" | "open" | "list"
  /**
   * client ID to use
   */
  client?: string
  /**
   * Notification mode to use
   */
  mode?: NotificationMode
  /**
   * MBean to use for registering notification handler
   */
  mbean?: string
  /**
   * `filter` parameter
   */
  filter?: string
  /**
   * `handback` parameter
   */
  handback?: unknown
  /**
   * `handle` parameter
   */
  handle?: string
}

/**
 * Jolokia {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#version version request}
 */
export type VersionRequest = BaseRequest & {
  type: "version"
}

/**
 * Generic Jolokia request object which combines all possible requests and can be used with tricky TypeScript
 * function matching when using functions as values in typed object/map.
 */
export type GenericRequest =
  Partial<ReadRequest & WriteRequest & ExecRequest & SearchRequest & ListRequest & NotificationRequest & VersionRequest>
  & BaseRequest

/**
 * Jolokia request - one of supported request types
 */
export type JolokiaRequest =
  | ReadRequest
  | WriteRequest
  | ExecRequest
  | SearchRequest
  | ListRequest
  | NotificationRequest
  | VersionRequest

// --- Types related to Jolokia responses

/**
 * A Jolokia base response received from Jolokia agent
 */
export type JolokiaResponse = {
  /**
   * Jolokia response status based on HTTP statuses, but not necessarily matching the HTTP status of HTTP response
   */
  status: number
  /** Timestamp of the response added by History interceptor (if available) */
  timestamp?: number
}

/**
 * Jolokia successful response. This is kind of an _envelope_ of all possible successful responses where the details
 * (specific to given request type) are contained in `value` field.
 */
export type JolokiaSuccessResponse = JolokiaResponse & {
  /** Original request for this response. May be missing if `includeRequest` is `false`. */
  request?: JolokiaRequest
  /** Value returned for the request. Can be null for WriteRequest, but still the value should be available */
  value: string | number | boolean | JolokiaResponseValue | null
  /** History of previous responses for a given request (if History interceptor is available) */
  history?: JolokiaResponse
}

/**
 * Error information shared between various messages
 */
export type BasicErrorResponseInformation = {
  /** An error _flag_ for quick check if a response is an error */
  ".error"?: boolean
  /** Short error message */
  error: string
  /** Exception class name that caused this error response */
  error_type: string
  /** Exception (or cause) if it's a JMX Exception */
  error_type_jmx?: string
}

/**
 * Jolokia error response
 */
export type JolokiaErrorResponse = JolokiaResponse & BasicErrorResponseInformation & {
  /** Original request for this response if it could be parsed */
  request?: JolokiaRequest
  /** Stack trace as string value when `allowErrorDetails` is `true` and `includeStackTrace` is `true` or `runtime` */
  stacktrace?: string
  /** JSON value of an Exception created by bean extractor (using `java.lang.Exception` getters) */
  error_value?: Record<string, unknown>
}

/**
 * Jolokia HTTP/`fetch()` error response type to handle lower level errors (including "connection refused" for example).
 * {@link FetchErrorCallback} has separate parameters, but this type combines all possible error values
 */
export type JolokiaFetchErrorResponse = Response | DOMException | TypeError | string | null

// --- Types related to Jolokia response values - specific to Jolokia request types, but can differ
//     also within single type. For example "notification" requests have different "command" field values with
//     distinct responses (or no responses at all). "read" type just returns attribute values (or all attributes)
//     for given MBean, so it's hard to craft single response value type

export type JolokiaResponseValue =
  | VersionResponseValue
  | ReadResponseValue
  | ExecResponseValue
  | SearchResponseValue
  | ListResponseValue
  | NotificationResponseValue
  | NotificationPullValue

// ------ Read response

/**
 * Generic response value for `read` request
 */
export type ReadResponseValue = string | number | boolean | null | Record<string, unknown>

// ------ Write response

/**
 * Generic response value for `write` request
 */
export type WriteResponseValue = string | number | boolean | null | Record<string, unknown>

// ------ Exec response

/**
 * Generic response value for `write` request
 */
export type ExecResponseValue = string | number | boolean | null | Record<string, unknown>

// ------ Search response

/**
 * Straightforward response for `search` request
 */
export type SearchResponseValue = string[]

// ------ Version response

/**
 * Response matching the result of `org.jolokia.server.core.service.impl.VersionRequestHandler.handleRequest()`
 */
export type VersionResponseValue = {
  /** Agent version used */
  agent: string
  /** Protocol version used */
  protocol: string
  /** agentId (configurable at server side) */
  id?: string
  /** Details about the agent */
  details?: AgentDetails
  /**
   * Additional information about the agent - for each
   * `org.jolokia.server.core.service.request.RequestHandler.getProvider()`
   */
  info: Record<string, AgentInfo>
  /** Configuration options for the agent */
  config: Record<string, string>
}

/**
 * Details about the agent. See `org.jolokia.server.core.service.api.AgentDetails.toJSONObject()`
 */
export type AgentDetails = {
  // Agent URL as the agent sees itself
  url: string
  // Whether the agent is secured and an authentication is required (0,1). If not given, this info is not known
  secured: boolean
  // Vendor of the detected container
  server_vendor: string
  // The product in which the agent is running
  server_product: string
  // Version of the server
  server_version: string
  // Version of the agent
  agent_version: string
  // The agent id
  agent_id: string
  // Description of the agent (if any)
  agent_description: string
}

/**
 * Additional information about single Jolokia `RequestHandler`
 */
export type AgentInfo = {
  product?: string
  vendor?: string
  version?: string
  extraInfo?: Record<string, unknown>
}

// ------ List response
//        these responses depend on the `path` attribute, so may be any subtree of full tree of all JMX domains
//        also with maxDepth processing parameter you can trim the leaves of the returned tree. when a map is trimmed
//        by Jolokia, it is replaced by string value "1" (kind of marker), so we can (with maxDepth=1)
//        get something like this:
//          "value": {
//            "JMImplementation": "1",
//            "java.util.logging": "1",
//            "jdk.management.jfr": "1",
//            "java.lang": "1",
//            "com.sun.management": "1",
//            "java.nio": "1",
//            "jolokia": "1"
//          }

/**
 * All possible (and worth mentioning) `list` response values depending on the `path` parameters.
 */
export type ListResponseValue =
  | OptimizedJmxDomains
  | JmxDomains
  | JmxDomain
  | MBeanInfo
  | MBeanInfoError
  | null

/**
 * Full JMX tree of domains in new optimized structure. Without optimizations we have top-level {@link JmxDomains}
 * type which in optimized mode is moved under `domains` key and single key of {@link JmxDomain} may be a string key
 * to select cached {@link MBeanInfo} under `cache` key.
 */
export type OptimizedJmxDomains = {
  /** a record of cache-key -> cached-MBeanInfo object which may be referred to from some jmx domain objects */
  cache: Record<string, MBeanInfo>
  /** a map of domain -> MBean -> MBeanInfo, where MBeanInfo may be just a key to cached value from `cache` */
  domains: JmxDomains
}

/**
 * Full JMX tree of domains, mbeans, attributes/operations/notifications with details. The key is domain name.
 */
export type JmxDomains = Record<string, JmxDomain>

/**
 * Single JMX domain with details about. The key is a list of key-value properties of MBean ObjectName, so
 * together with parent domain it give full ObjectName.
 *
 * This string value is dual-purpose:
 * * With small enough `maxDepth`, we may end up just with "1" as the info (the way Jolokia trims the depth
 *   of responses) - that's why it may be a string.
 * * With `listCache=true`, some domains instead of full MBeanInfo value may point to the cached value using
 *   string _key_
 */
export type JmxDomain = Record<string, MBeanInfo | MBeanInfoError | string>

/**
 * Information about single MBean. Thanks to `org.jolokia.server.core.service.api.DataUpdater` services, the
 * list of fields for MBeanInfo is extensible (for example RBAC information can be added).
 */
export type MBeanInfo = {
  /** MBean description */
  desc?: string
  /** Fully qualified Java class name of the MBean */
  class?: string
  /** Map of attribute definitions */
  attr?: Record<string, MBeanAttribute>
  /** Map of operation (possible overridden) definitions */
  op?: Record<string, MBeanOperation | MBeanOperation[]>
  /** Map of constructors */
  ctor?: Record<string, MBeanConstructor | MBeanConstructor[]>
  /** Map of notification definitions */
  notif?: Record<string, MBeanNotification>
  /** Map of ObjectName key/values (if `listKeys` parameter is set) */
  keys?: Record<string, string>
  /** Possible fields from custom data updaters */
  [key: string]: unknown
}

/**
 * Information about single MBean in case there's a problem getting such information
 */
export type MBeanInfoError = BasicErrorResponseInformation

// --------- List response types related to JMX attribute, operation and notification descriptors
//           These also may be strings like (for {"type":"list","path":"java.lang/type=Memory"} and maxDepth=1):
//             "value": {
//               "op": "1",
//               "notif": "1",
//               "attr": "1",
//               "class": "sun.management.MemoryImpl",
//               "desc": "Information on the management interface of the MBean"
//             },
//           but we won't declare such exotic type alternatives

/**
 * Description of a single MBean attribute
 */
export type MBeanAttribute = {
  /** Java type */
  type: string
  /** Open type */
  openType?: OpenType
  /** Description */
  desc: string
  /** Read-Write flag */
  rw: boolean
  /** Read flag */
  r: boolean
  /** Write flag */
  w: boolean
  /** "Is" flag for a getter (`isXX` / `getXX`) */
  is: boolean
}

/**
 * Description of a single MBean constructor
 */
export type MBeanConstructor = {
  /** Constructor arguments */
  args: MBeanOperationArgument[]
  /** Description */
  desc: string
}

/**
 * Description of a single MBean operation
 */
export type MBeanOperation = {
  /** Operation arguments */
  args: MBeanOperationArgument[]
  /** Java return type */
  ret: string
  /** OpenType return type */
  openRet?: OpenType
  /** Description */
  desc: string
}

/**
 * Description of a single MBean operation's argument
 */
export type MBeanOperationArgument = {
  /** Argument description */
  desc: string
  /** Argument name */
  name: string
  /** Argument type */
  type: string
  /** Argument Open type */
  openType?: OpenType
}

/**
 * Description of a single MBean attribute
 */
export type MBeanNotification = {
  /** Notification name */
  name: string
  /** Notification description */
  desc: string
  /** Notification Java types */
  types: string[]
}

/**
 * JSON representation of `javax.management.openmbean.OpenType`. Can be recursive and should allow for great
 * flexibility in handling complex types.
 */
export type OpenType = SimpleOpenType | ArrayOpenType | CompositeOpenType | TabularOpenType

export type BasicOpenType = {
  /** There are only four kinds of an open type */
  kind: 'simple' | 'array' | 'composite' | 'tabular'
  /**
   * Full Java class name for a type - primitive types use wrapped class names instead.
   * Composite types are _always_ `javax.management.openmbean.CompositeData`
   * Tabular types are _always_ `javax.management.openmbean.TabularData`
   */
  class: string
  /**
   * Actual type name - possibly with type information, like `java.util.Map<java.lang.String, java.lang.Integer>`
   * This is generated using `com.sun.jmx.mbeanserver.MXBeanIntrospector.typeName()`
   */
  type?: string
  /** Optional `javax.management.openmbean.OpenType.description` */
  desc?: string
}

/** `javax.management.openmbean.SimpleType` - no need to use `kind: simple` and repeat the same data for class/type/desc */
export type SimpleOpenType = string

/** `javax.management.openmbean.ArrayType` */
export type ArrayOpenType = BasicOpenType & {
  /** Is the array using primitive elements? */
  primitive: boolean
  /** Dimension of the array */
  dimension: number
  /** OpenType element of the array */
  elemType: OpenType
}

/** `javax.management.openmbean.CompositeType` */
export type CompositeOpenType = BasicOpenType & {
  /** CompositeType items as a record with String item names as keys */
  items: Record<string, OpenType>
}

/** `javax.management.openmbean.TabularType` */
export type TabularOpenType = BasicOpenType & {
  /** Ordered list of items from a `rowType` that build the index of TabularType */
  index: string[]
  /** Rows are always composite types */
  rowType: CompositeOpenType
}

// ------ Notification response
//        See: org.jolokia.service.jmx.handler.notification.NotificationDispatcher.dispatch()

/**
 * Notification response values depend on the command of Notification request ({@link NotificationRequest#command})
 * Commands `unregister`, `remove`, `ping` and `open` do not return any value.
 */
export type NotificationResponseValue =
  | NotificationRegisterResponseValue
  | NotificationAddResponseValue
  | NotificationListResponseValue
  | null

/**
 * Result of notification `register` command for registration of a new client
 */
export type NotificationRegisterResponseValue = {
  /** Client id for the registered notification client (global for single Jolokia instance) */
  id: string
  /** Map of all `org.jolokia.server.core.service.notification.NotificationBackend` configurations */
  backend: Record<string, NotificationBackendConfig>
}

/**
 * Configuration if a single `org.jolokia.server.core.service.notification.NotificationBackend`
 */
export type NotificationBackendConfig = PullNotificationClientConfig | SseNotificationClientConfig

/**
 * A handle identifying the registered listener for given client
 */
export type NotificationAddResponseValue = string

/**
 * Result of notification `list` command that lists handles for given client
 */
export type NotificationListResponseValue = Record<string, NotificationOptions>

/**
 * Type describing a result of `pull` operation on "pull" notification MBean.
 * See `org.jolokia.server.core.service.notification.NotificationResult`.
 * While it's de-facto a response to generic Jolokia `exec` operation on particular bean, we provide a type
 * definition for it.
 */
export type NotificationPullValue = {
  /** Array of `javax.management.Notification` objects */
  notifications: JMXNotification[]
  handback: unknown
  dropped: number
  handle?: string
}

/**
 * Type representing a `javax.management.Notification` object
 */
export type JMXNotification = {
  message: string
  sequenceNumber: number
  timeStamp: number
  type: string
  userData: unknown
}

// --- Types related to callbacks
//     Even if we switched from xhr to Fetch API, we didn't go full-Promises. Callbacks are needed for registered
//     (background) tasks/jobs, so we allow then for jolokia.request() as well (promise is not returned in this case)
//     when using jolokia.request(), we can pass either a request or an array of requests (bulk request).
//
//     In both cases we can specify a callback or an array of callbacks and for bulk requests callbacks will be called.
//     In round-robin fashion (for bulk-request of 3 and array of 2 callbacks, first callback will be called with
//     response 0 and 2, while callback 1 will be called with response 1).
//
//     For registered jobs, we can specify a generic callback which will be called with all the responses. This
//     is not possible with normal jolokia.request() call

export type GenericCallback = (response: JolokiaResponse, index: number) => void

/**
 * Single response callback receiving JSON response and its index (when calling Jolokia with an array
 * of requests).
 */
export type ResponseCallback = (response: JolokiaSuccessResponse, index: number) => void

/**
 * Single response callback receiving entire text response.
 */
export type TextResponseCallback = (response: string) => void

/**
 * An array of callbacks, each accepting JSON response (successful) with response index. Only JSON type is handled.
 */
export type ResponseCallbacks = ((response: JolokiaSuccessResponse, index: number) => void)[]

/**
 * A callback interface to be notified about error response from Jolokia agent.
 */
export type ErrorCallback = (response: JolokiaErrorResponse, index: number) => void

/**
 * An array of callbacks, each accepting JSON response (error) with response index.
 */
export type ErrorCallbacks = ((response: JolokiaErrorResponse, index: number) => void)[]

/**
 * A callback used in `fetch().catch()` when `success` and `error` callbacks are passed (callback-mode). In promise
 * mode, user may attach `.catch()` to the returned promise explicitly (or face an exception).
 * See <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#exceptions">Fetch Exceptions</a>.
 *
 * This callback _should_ be used in _callback mode_, where `success` (and possibly `error`) callbacks are passed
 * and user is not interested in returned Promise. There are two kinds of _errors_ when using `fetch()`:
 * * HTTP != 200 errors (jquery uses `status >= 200 && status < 300 || status === 304`)
 * * networking/configuration errors (for example when bad header is set or `get` is used with non-empty body)
 *
 * This callback _may_ be used in _promise mode_ as well, so the returned promise has a `.catch()` block attached
 * invoking the passed callback.
 *
 * networking errors are thrown by `fetch()`, but HTTP errors are passed in returned async `Response` object.
 *
 * From Jolokia perspective this callback is used in `Jolokia.request()`, not in lower-level `fetch()`, so we handle
 * both errors in the same way.
 */
export type FetchErrorCallback = (response: Response | null, error: DOMException | TypeError | string | null) => void

/**
 * A response callback used for registered jobs called periodically. In addition to standard {@link ResponseCallback},
 * this method receives registered job ID as 2nd argument and index of the response is passed as 3rd argument.
 * Only JSON responses are handled.
 */
export type JobResponseCallback = (response: JolokiaSuccessResponse, jobId: number, index: number) => void

/**
 * A Jolokia error response callback used for registered jobs called periodically. In addition to standard
 * {@link ErrorCallback}, this method receives registered job ID as 2nd argument and index of the response is passed
 * as 3rd argument.
 */
export type JobErrorCallback = (response: JolokiaErrorResponse, jobId: number, index: number) => void

/**
 * Special job-related callback, which simply receives all the responses/errors in single array parameter or
 * (when HTTP/`fetch()` error occurred), a single {@link JolokiaFetchErrorResponse} is passed.
 *
 * The spread syntax used may be tricky, because jobs may be registered with multiple requests, so a callback should
 * accept a spread parameter too. But TypeScript uses function equivalence as long as the called function may
 * accept _any_ parameters passed. So we can pass a JobCallback as a function which accepts a single parameter
 * of one of the 3 types of responses too! This is fine when registering a job with one Jolokia request. But may
 * start to be confusing with more requests in a job.
 */
export type JobCallback = (...responses: (JolokiaSuccessResponse | JolokiaErrorResponse | JolokiaFetchErrorResponse)[]) => void

// --- Types related to job management

/**
 * Object used with {@link IJolokia#register} to setup periodically invoked Jolokia job
 */
export type JobRegistrationConfig = {
  /** Generic callback accepting intermixed successful and error responses in single array */
  callback?: JobCallback
  /** Success callback called for each successful response */
  success?: JobResponseCallback
  /** Error callback called for each Jolokia error response */
  error?: JobErrorCallback
  /** Error callback called for single HTTP/`fetch()` error - not specific to any request of the job or even the job itself */
  fetchError?: FetchErrorCallback
  /** Parameters to be send under `config` key of POST data */
  config?: ProcessingParameters
  /** Flag to handle HTTP 304 (not modified) headers */
  onlyIfModified?: boolean
}

/**
 * Definition of registered Jolokia job - combines requests to send periodically and callbacks to notify when
 * the response is received
 */
export type Job = {
  requests: JolokiaRequest[]
  callback?: JobCallback
  success?: JobResponseCallback
  error?: JobErrorCallback
  fetchError?: FetchErrorCallback
  config?: ProcessingParameters
  onlyIfModified?: boolean
  lastModified?: number
}

// --- Types related to notifications

/**
 * Pull notification configuration options
 */
export type PullNotificationClientConfig = {
  /** MBean name which will collect the notifications - need to be fetched explicitly */
  store: string
  /** Size of backend storage for the notifications */
  maxEntries: number
}

/**
 * SSE notification configuration options
 */
export type SseNotificationClientConfig = {
  /** Content type for SSE notification mechanism */
  "backChannel.contentType": string
  /** Encoding used for SSE notification mechanism */
  "backChannel.encoding": string
}

/**
 * Configuration options for notification handling
 */
export type NotificationOptions = {
  /** Backend mode - one of the supported modes */
  mode?: NotificationMode
  /** MBean on which to register a notification listener */
  mbean?: string
  /** List of filter on notification types which are OR-ed together */
  filter?: string
  /** Additional configuration used for notifications */
  config?: Record<string, unknown>
  /** Any value that's returned with the notification for correlation purposes */
  handback?: string
  /** A callback for notification handling */
  callback?: (result: NotificationPullValue) => void
}

/**
 * Representation of registered notification listener
 */
export type NotificationHandle = {
  /** Handle ID within a given client */
  id: string
  /** A notification mode for this handle */
  mode: NotificationMode
}

// --- Jolokia interfaces - public API

/**
 * IJolokia creation interface - either using `new` or using function call syntax.
 */
interface JolokiaStatic {
  /** Creating {@link IJolokia} using function call syntax */
  (config: JolokiaConfiguration | string): IJolokia

  /** Creating {@link IJolokia} using object creation syntax with `new` */
  new(config: JolokiaConfiguration | string): IJolokia

  // --- Utility functions available statically (static methods) and in Jolokia.prototype (instance methods)
  //     (that's why these need to be duplicated in JolokiaStatic and in IJolokia interfaces)

  /**
   * Escape URL part (segment) according to
   * {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#_escaping_rules_in_get_requests Jolokia escaping rules}
   * @param part An URL segment for Jolokia GET URL
   * @returns URL segment with Jolokia escaping rules applied
   */
  escape(part: string): string

  /**
   * Escape URL part (segment) according to
   * {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#_escaping_rules_in_get_requests Jolokia escaping rules}
   * @param part a path used with POST requests
   * @returns URL segment with Jolokia escaping rules applied
   */
  escapePost(part: string): string

  /**
   * Utility method which checks whether a response is an error or a success (from Jolokia, not HTTP perspective)
   * @param resp response to check
   * @return true if response is an error, false otherwise
   */
  isError(resp: JolokiaResponse): resp is JolokiaErrorResponse

  /**
   * Utility method which checks whether a response is success response (HTTP perspective)
   * @param resp response to check
   * @return true if response is a success
   */
  isResponseSuccess(resp: unknown): resp is JolokiaSuccessResponse

  /**
   * Utility method which checks whether a response is a Jolokia error response (HTTP perspective). Jolokia error
   * is still delivered with HTTP response code 200, but the JSON response is a {@link JolokiaErrorResponse} type.
   * @param resp response to check
   * @return true if response is a success
   */
  isResponseError(resp: unknown): resp is JolokiaErrorResponse

  /**
   * Utility method which checks if a response from Jolokia call is the low level error related to HTTP/`fetch()`
   * calls.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#exceptions
   * @param resp
   */
  isResponseFetchError(resp: unknown): resp is JolokiaFetchErrorResponse
}

/**
 * Main Jolokia client interface for communication with remote Jolokia agent.
 */
interface IJolokia {

  /** Version of Jolokia JavaScript client library */
  readonly CLIENT_VERSION: string

  /**
   * The request method using one or more JSON requests and sending it to the agent. Beside the
   * request a bunch of options can be given, which are merged with the options provided
   * at the constructor (where the options given here take precedence).
   * @param request JSON request object (or array of objects) compliant with
   *        {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html|Jolokia protocol} representing
   *        the request(s) to send to remote Jolokia agent.
   * @param params parameters used for sending the request which may override default configuration.
   *        These are not options passed directly (unchanged) to `fetch()` call
   * @returns the response promise object resolving to desired value:<ul>
   *          <li>a string response (when `dataType=text`)</li>
   *          <li>an array of Jolokia responses or single JSON response (when `dataType=json`, default)</li>
   *          <li>a Fetch API Response object, when `resolve=response` is specified in {@link RequestOptions}</li>
   *          <li>When callbacks are specified in {@link RequestOptions}, the returned promise
   *          resolves to undefined, but user can attach `.catch()` for Fetch API error handling. The response is
   *          delivered via the callbacks</li></ul>
   */
  request(request: JolokiaRequest | JolokiaRequest[], params?: RequestOptions):
    Promise<string | JolokiaSuccessResponse | JolokiaErrorResponse | (JolokiaSuccessResponse | JolokiaErrorResponse)[] | Response | undefined>

  /**
   * Register one or more requests for periodically polling the agent along with a callback to call on receipt
   * of the response.
   *
   * The first argument can be either an object or a function. The remaining arguments are interpreted
   * as Jolokia request objects.
   *
   * If a function is given or an object with an attribute `callback` holding a function, then
   * this function is called with all responses received as argument, regardless whether the individual response
   * indicates a success or error state.
   *
   * If the first argument is an object with two callback attributes `success` and `error`,
   * these functions are called for _each_ response separately, depending whether the response
   * indicates success or an error state. If multiple requests have been registered along with this callback object,
   * the callback is called multiple times, one for each response in the same order as the requests are given.
   * As second argument, the handle (job ID) which is returned by this method is given and as third argument the index
   * within the list of requests.
   *
   * If the first argument is an object, an additional 'config' attribute with processing parameters can
   * be given which is used as default for the registered requests.
   * Request with a 'config' section take precedence.
   *
   * @param callback a callback or job configuration (params + callbacks) for the registered job
   * @param requests an array of requests to be called periodically
   * @returns handle which can be used for unregistering the request again or for correlation purposes in the callbacks
   */
  register(callback: JobCallback | JobRegistrationConfig, ...requests: JolokiaRequest[]): number

  /**
   * Unregisters one or more request which has been registered with {@link #register}. As parameter
   * the handle returned during the registration process must be given
   * @param handle the job handle to unregister
   */
  unregister(handle: number): void

  /**
   * Return an array of jobIds for currently registered jobs.
   * @return Array of job jobIds or an empty array
   */
  jobs(): number[]

  /**
   * Start the poller. The interval between two polling attempts can be optionally given (in milliseconds) or
   * is taken from the parameter `fetchInterval` given at construction time. If no interval is given at all,
   * 30 seconds is the default.
   *
   * If the poller is already running (i.e. {@link #isRunning} is `true` then the scheduler
   * is restarted, but only if the new interval differs from the currently active one.
   *
   * Jolokia uses single function passed to `setInterval()` which calls remote agent with single bulk request
   * containing all registered jobs and each job's requests.
   *
   * @param interval Interval (in ms) for poll-fetching from remote Jolokia agent.
   */
  start(interval?: number): void

  /**
   * Stop the poller. If the poller is not running, no operation is performed.
   */
  stop(): void

  /**
   * Check whether the poller is running.
   * @returns true if the poller is running, false otherwise.
   */
  isRunning(): boolean

  /**
   * Registers a listener for single MBean returning a handle which can be used to cancel the listener
   * registration. Passed options are used to configure the handler for notifications.
   * @param opts
   * @returns a promise of {@link NotificationHandle} representing an MBean notification listener for single
   *          client (created during registration of first listener) for entire Jolokia instance
   */
  addNotificationListener(opts: NotificationOptions): Promise<NotificationHandle>

  /**
   * Unregisters previously registered listener
   * @param handle a listener registration handler created by {@link #addNotificationListener}.
   * @returns a promise resolving to true if listener was successfully unregistered at server side
   */
  removeNotificationListener(handle: NotificationHandle): Promise<boolean>

  /**
   * Removes the notification client, which is created for all the registrations.
   * @returns a promise resolving to true if client was successfully removed at server side
   */
  unregisterNotificationClient(): Promise<boolean>

  // --- Utility functions available statically (static methods) and in Jolokia.prototype (instance methods)
  //     (that's why these need to be duplicated in JolokiaStatic and in IJolokia interfaces)

  /**
   * Escape URL part (segment) according to
   * {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#_escaping_rules_in_get_requests Jolokia escaping rules}
   * @param part An URL segment for Jolokia GET URL
   * @returns URL segment with Jolokia escaping rules applied
   */
  escape(part: string): string

  /**
   * Escape URL part (segment) according to
   * {@link https://jolokia.org/reference/html/manual/jolokia_protocol.html#_escaping_rules_in_get_requests Jolokia escaping rules}
   * @param part a path used with POST requests
   * @returns URL segment with Jolokia escaping rules applied
   */
  escapePost(part: string): string

  /**
   * Utility method which checks whether a response is an error or a success (from Jolokia, not HTTP perspective)
   * @param resp response to check
   * @return true if response is an error, false otherwise
   */
  isError(resp: JolokiaResponse): resp is JolokiaErrorResponse
}

export type { IJolokia, JolokiaStatic }
