import type {EventSourceFetchInit, FetchLikeResponse} from 'eventsource'
import type {FetchFunction, FetchInit} from 'get-it'

import type {InitializedClientConfig} from '../types'

/** @internal */
export interface EventSourceFetchOptions {
  /**
   * Headers that must be merged into every EventSource request: the custom
   * `headers` from the client config, which the native EventSource API has
   * no equivalent for. The credential is not applied here; the stream that
   * owns the connection wraps this fetch and adds it per attempt (see
   * `connectAuthenticatedEventSource`).
   */
  headers?: Record<string, string>
}

/**
 * Build a `fetch` implementation suitable for the `eventsource` package's
 * `fetch` option. Routes the EventSource connection through the same
 * transport layer the rest of the client uses, so things like the
 * test-fetch override, the per-request `proxy` config, and `HTTPS_PROXY`
 * env-var support apply to SSE too.
 *
 * Resolution order on each request:
 *
 *   1. `config.resolveFetch(config.proxy)` if set — the client's fetch
 *      resolver, so SSE uses the same transport as regular requests:
 *      custom fetch variants (incl. the test suite's injected mock),
 *      undici configuration, an explicit `proxy` config, and env-proxy
 *      support all apply to SSE too. The Node entry supplies get-it's
 *      undici-backed fetch (threading the resolver through the env
 *      instead of importing `get-it/node` directly keeps `undici` out of
 *      the browser bundle); the browser entry leaves it unset.
 *   2. `globalThis.fetch`. Note that Node's global fetch does NOT read
 *      proxy env vars (that is opt-in via `NODE_USE_ENV_PROXY`), which
 *      is one of the reasons step 1 exists.
 *
 * The returned fetch always merges `options.headers` into the outgoing
 * request, regardless of which underlying fetch was picked.
 *
 * @internal
 */
export function resolveEventSourceFetch(
  config: InitializedClientConfig,
  options: EventSourceFetchOptions = {},
): EventSourceFetch {
  const extraHeaders = options.headers

  return function eventSourceFetch(url, init) {
    const baseFetch = pickBaseFetch(config)

    // Extra `EventSourceFetchInit` fields get-it's `FetchInit` doesn't
    // declare (`mode`, `cache`) survive the spread and reach whichever
    // fetch implementation is effective.
    const mergedInit: FetchInit = {...init}
    if (extraHeaders) {
      const headers = new Headers(init?.headers)
      for (const [key, value] of Object.entries(extraHeaders)) {
        headers.set(key, value)
      }
      mergedInit.headers = headers
    }
    // get-it's `FetchResponse` is a structural superset of the package's
    // `FetchLikeResponse`, so it can be handed over as-is.
    return baseFetch(typeof url === 'string' ? url : url.href, mergedInit)
  }
}

/**
 * The fetch shape handed to the `eventsource` package: accepts what its
 * `FetchLike` passes in, requires only what get-it's `FetchFunction`
 * guarantees back — the full `typeof fetch` contract is not needed anywhere
 * in this chain.
 *
 * @internal
 */
export type EventSourceFetch = (
  url: string | URL,
  init?: EventSourceFetchInit,
) => Promise<FetchLikeResponse>

/**
 * The fetch the client's own transport resolves for this config: the
 * configured `resolveFetch` (honouring an explicit `proxy`) if present,
 * otherwise the global fetch. Shared by the EventSource connection and the
 * `/check/cors` probe so both resolve identically.
 *
 * @internal
 */
export function pickBaseFetch(config: InitializedClientConfig): FetchFunction {
  if (config.resolveFetch) {
    return config.resolveFetch(typeof config.proxy === 'string' ? config.proxy : undefined)
  }
  return globalThis.fetch.bind(globalThis)
}
