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
}

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
}
