/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions

package com.facebook.react.modules.network

import android.os.Bundle
import android.util.Base64
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.buildReadableArray
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import java.io.IOException
import java.net.SocketTimeoutException
import okhttp3.Headers
import okhttp3.RequestBody
import okio.Buffer

/**
 * Utility class for reporting network lifecycle events to JavaScript and InspectorNetworkReporter.
 */
internal object NetworkEventUtil {
  private const val MAX_BODY_PREVIEW_SIZE = 512 * 1024 // 512KB

  @JvmStatic
  fun onCreateRequest(
      devToolsRequestId: String,
      requestUrl: String,
      requestMethod: String,
      requestHeaders: Map<String, String>,
      /** Request body for DevTools preview. Only populate in debug builds. */
      requestBodyForDevTools: String?,
      encodedDataLength: Long,
  ) {
    if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
      InspectorNetworkReporter.reportRequestStart(
          devToolsRequestId,
          requestUrl,
          requestMethod,
          requestHeaders,
          requestBodyForDevTools.orEmpty(),
          encodedDataLength,
      )
      InspectorNetworkReporter.reportConnectionTiming(devToolsRequestId, requestHeaders)
    }
  }

  @JvmStatic
  fun onDataSend(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      progress: Long,
      total: Long,
  ) {
    reactContext?.emitDeviceEvent(
        "didSendNetworkData",
        buildReadableArray {
          add(requestId)
          add(progress.toInt())
          add(total.toInt())
        },
    )
  }

  @JvmStatic
  fun onIncrementalDataReceived(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      devToolsRequestId: String,
      data: String?,
      progress: Long,
      total: Long,
  ) {
    if (ReactNativeFeatureFlags.enableNetworkEventReporting() && data != null) {
      InspectorNetworkReporter.reportDataReceived(devToolsRequestId, data)
      InspectorNetworkReporter.maybeStoreResponseBodyIncremental(devToolsRequestId, data)
    }
    reactContext?.emitDeviceEvent(
        "didReceiveNetworkIncrementalData",
        buildReadableArray {
          add(requestId)
          add(data)
          add(progress.toInt())
          add(total.toInt())
        },
    )
  }

  @JvmStatic
  fun onDataReceivedProgress(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      progress: Long,
      total: Long,
  ) {
    reactContext?.emitDeviceEvent(
        "didReceiveNetworkDataProgress",
        buildReadableArray {
          add(requestId)
          add(progress.toInt())
          add(total.toInt())
        },
    )
  }

  @JvmStatic
  fun onDataReceived(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      devToolsRequestId: String,
      data: String?,
      responseType: String,
  ) {
    if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
      InspectorNetworkReporter.maybeStoreResponseBody(
          devToolsRequestId,
          data.orEmpty(),
          responseType == "base64",
      )
    }
    reactContext?.emitDeviceEvent(
        "didReceiveNetworkData",
        buildReadableArray {
          add(requestId)
          add(data)
        },
    )
  }

  @JvmStatic
  fun onDataReceived(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      devToolsRequestId: String,
      data: WritableMap,
      rawData: ByteArray,
  ) {
    if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
      InspectorNetworkReporter.maybeStoreResponseBody(
          devToolsRequestId,
          Base64.encodeToString(rawData, Base64.NO_WRAP),
          true,
      )
    }
    reactContext?.emitDeviceEvent(
        "didReceiveNetworkData",
        Arguments.createArray().apply {
          pushInt(requestId)
          pushMap(data)
        },
    )
  }

  @JvmStatic
  fun onRequestError(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      devToolsRequestId: String,
      error: String?,
      e: Throwable?,
  ) {
    if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
      InspectorNetworkReporter.reportRequestFailed(devToolsRequestId, false)
    }
    reactContext?.emitDeviceEvent(
        "didCompleteNetworkResponse",
        buildReadableArray {
          add(requestId)
          add(error)
          if (e?.javaClass == SocketTimeoutException::class.java) {
            add(true) // last argument is a time out boolean
          }
        },
    )
  }

  @JvmStatic
  fun onRequestSuccess(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      devToolsRequestId: String,
      encodedDataLength: Long,
  ) {
    if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
      InspectorNetworkReporter.reportResponseEnd(devToolsRequestId, encodedDataLength)
    }
    reactContext?.emitDeviceEvent(
        "didCompleteNetworkResponse",
        buildReadableArray {
          add(requestId)
          addNull()
        },
    )
  }

  @JvmStatic
  fun onResponseReceived(
      reactContext: ReactApplicationContext?,
      requestId: Int,
      devToolsRequestId: String,
      requestUrl: String?,
      statusCode: Int,
      headers: Map<String, String>,
      contentLength: Long,
  ) {
    val headersBundle = Bundle()
    for ((headerName, headerValue) in headers) {
      headersBundle.putString(headerName, headerValue)
    }

    if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
      InspectorNetworkReporter.reportResponseStart(
          devToolsRequestId,
          requestUrl.orEmpty(),
          statusCode,
          headers,
          contentLength,
      )
    }
    reactContext?.emitDeviceEvent(
        "didReceiveNetworkResponse",
        Arguments.createArray().apply {
          pushInt(requestId)
          pushInt(statusCode)
          pushMap(Arguments.fromBundle(headersBundle))
          pushString(requestUrl)
        },
    )
  }

  @JvmStatic
  fun okHttpHeadersToMap(headers: Headers): Map<String, String> {
    val responseHeaders = mutableMapOf<String, String>()
    for (i in 0 until headers.size()) {
      val headerName = headers.name(i)
      // multiple values for the same header
      if (responseHeaders.containsKey(headerName)) {
        responseHeaders[headerName] = "${responseHeaders[headerName]}, ${headers.value(i)}"
      } else {
        responseHeaders[headerName] = headers.value(i)
      }
    }
    return responseHeaders
  }

  @JvmStatic
  fun getRequestBodyPreview(requestBody: RequestBody?): String? {
    if (requestBody == null) {
      return null
    }

    // Unwrap ProgressRequestBody
    val body = (requestBody as? ProgressRequestBody)?.innerBody() ?: requestBody

    if (body.isOneShot()) {
      // Fallback - body cannot be read twice
      return "[Preview unavailable]"
    }

    return try {
      val buffer = Buffer()
      body.writeTo(buffer)

      val size = buffer.size()
      if (size <= MAX_BODY_PREVIEW_SIZE) {
        buffer.readUtf8()
      } else {
        buffer.readUtf8(MAX_BODY_PREVIEW_SIZE.toLong()) + "... (truncated, ${size} bytes total)"
      }
    } catch (e: IOException) {
      "[Preview unavailable]"
    }
  }
}
