import type { TypedFetchConfig } from '../types/config.js'

export interface BodyPreparationOptions {
  /**
   * Force JSON serialization (default: true). Set to false when passing binary/FormData bodies
   */
  json?: boolean
}

export interface PreparedBody {
  bodyInit: BodyInit | null
  sample?: unknown
}

export interface ResolvedBodyArgs<TBody> {
  body?: TBody
  init: RequestInit
}

const REQUEST_INIT_KEYS = new Set([
  'body',
  'headers',
  'method',
  'mode',
  'credentials',
  'cache',
  'redirect',
  'referrer',
  'referrerPolicy',
  'integrity',
  'keepalive',
  'signal',
  'priority',
  'window',
  'duplex'
])

const hasBlob = typeof Blob !== 'undefined'
const hasFormData = typeof FormData !== 'undefined'
const hasURLSearchParams = typeof URLSearchParams !== 'undefined'
const hasReadableStream = typeof ReadableStream !== 'undefined'

function isBinaryBody(value: unknown): value is BodyInit {
  if (value === null || value === undefined) return false
  if (typeof value === 'string') return false

  if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) return true
  if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(value)) return true
  if (hasBlob && value instanceof Blob) return true
  if (hasFormData && value instanceof FormData) return true
  if (hasURLSearchParams && value instanceof URLSearchParams) return true
  if (hasReadableStream && value instanceof ReadableStream) return true

  return false
}

function tryParseJSON(value: string): unknown {
  try {
    return JSON.parse(value)
  } catch {
    return undefined
  }
}

export function prepareBodyPayload(body: unknown, options: BodyPreparationOptions = {}): PreparedBody {
  const { json = true } = options

  if (body === undefined) {
    return { bodyInit: null }
  }

  if (!json) {
    return { bodyInit: body as BodyInit }
  }

  if (typeof body === 'string') {
    return { bodyInit: body, sample: tryParseJSON(body) }
  }

  if (body === null || typeof body === 'number' || typeof body === 'boolean') {
    return { bodyInit: JSON.stringify(body), sample: body }
  }

  if (isBinaryBody(body)) {
    return { bodyInit: body }
  }

  if (typeof body === 'object') {
    return { bodyInit: JSON.stringify(body), sample: body }
  }

  return { bodyInit: body as BodyInit }
}

export function mergePartialConfig(
  target: TypedFetchConfig = {},
  source: TypedFetchConfig = {}
): TypedFetchConfig {
  const result: TypedFetchConfig = { ...target }

  for (const key in source) {
    const value = source[key as keyof TypedFetchConfig]
    if (value === undefined) continue

    if (value && typeof value === 'object' && !Array.isArray(value)) {
      result[key as keyof TypedFetchConfig] = {
        ...(result[key as keyof TypedFetchConfig] as Record<string, unknown> | undefined),
        ...value
      } as any
    } else {
      result[key as keyof TypedFetchConfig] = value as any
    }
  }

  return result
}

export function isRequestInitLike(value: unknown): value is RequestInit {
  if (!value || typeof value !== 'object') return false
  for (const key of REQUEST_INIT_KEYS) {
    if (key in (value as Record<string, unknown>)) {
      return true
    }
  }
  return false
}

export function resolveBodyArgs<TBody>(
  bodyOrInit?: TBody | RequestInit,
  maybeInit?: RequestInit
): ResolvedBodyArgs<TBody> {
  if (bodyOrInit === undefined) {
    return { init: maybeInit ? { ...maybeInit } : {} }
  }

  if (isRequestInitLike(bodyOrInit)) {
    const { body, ...rest } = bodyOrInit as RequestInit & Record<string, unknown>
    const init = rest as RequestInit
    if (body === undefined) {
      return { init }
    }
    return {
      body: body as TBody,
      init
    }
  }

  return {
    body: bodyOrInit as TBody,
    init: maybeInit ? { ...maybeInit } : {}
  }
}
