// Copyright 2025-present 650 Industries. All rights reserved.

import ExpoModulesCore

/// An unhandled JavaScript error forwarded from the JS-side `global.ErrorUtils` handler. Recorded as
/// an `exception` log event following OpenTelemetry's exception conventions.
@Record
struct ErrorReport {
  var source: Source = .global
  var type: String?
  var message: String = ""
  var stacktrace: String?
  var componentStack: String?
  var isFatal: Bool = false

  /// How the error was captured. A closed set so the `expo.error.source` attribute stays consistent.
  enum Source: String, Enumerable {
    case global
    case errorBoundary
    case reportedByUser
  }

  /// Builds the `exception` log event for the live path.
  func toLogRecord() -> LogRecord {
    return makeExceptionLogRecord(
      source: source.rawValue,
      type: type,
      message: message,
      stacktrace: stacktrace,
      componentStack: componentStack,
      isFatal: isFatal
    )
  }

  /// Snapshots this report for durable on-disk storage, capturing the session it belongs to and the
  /// time it happened (both resolved now, since by drain time the main session has rotated).
  ///
  /// Only fatal errors are persisted, and those come from the global handler, which has no React
  /// component stack, so `componentStack` isn't carried through this path.
  func toPendingError(sessionId: String) -> PendingErrorStore.PendingError {
    return PendingErrorStore.PendingError(
      source: source.rawValue,
      type: type,
      message: message,
      stacktrace: stacktrace,
      sessionId: sessionId,
      timestamp: Date.now.ISO8601Format()
    )
  }
}

extension PendingErrorStore.PendingError {
  /// Builds the `exception` log event for a fatal error ingested from disk on the next launch, using
  /// the session and timestamp captured at fatal time.
  func toLogRecord() -> LogRecord {
    return makeExceptionLogRecord(
      source: source,
      type: type,
      message: message,
      stacktrace: stacktrace,
      componentStack: nil,
      isFatal: true,
      timestamp: timestamp
    )
  }
}

/// Builds an `exception` log event following OpenTelemetry's exception-in-logs convention: the error
/// rides as `exception.*` attributes (the event name is `exception` because this captures errors from
/// a handler, not a specific operation), and `expo.error.*` carries the bits OTel has no field for (the
/// capture source and whether the error was fatal). Fatal errors log at `fatal` severity, the rest at
/// `error`. Shared by the live and ingested-fatal paths so both events keep the same shape.
///
/// Absent `type`/`stacktrace` are kept as explicit `null` rather than omitted (the boxed optionals
/// encode as JSON `null`), so every `exception` event carries the same attribute keys. The React
/// component stack only exists for error-boundary captures, so `expo.error.component_stack` is
/// omitted entirely when absent rather than logged as `null` on every event.
private func makeExceptionLogRecord(
  source: String,
  type: String?,
  message: String,
  stacktrace: String?,
  componentStack: String?,
  isFatal: Bool,
  timestamp: String = Date.now.ISO8601Format()
) -> LogRecord {
  var attributes: [String: Any] = [
    "expo.error.source": source,
    "expo.error.is_fatal": isFatal,
    "exception.type": type as Any,
    "exception.message": message,
    "exception.stacktrace": stacktrace as Any,
  ]
  if let componentStack {
    attributes["expo.error.component_stack"] = componentStack
  }
  return LogRecord(
    name: "exception",
    attributes: attributes,
    severity: isFatal ? .fatal : .error,
    timestamp: timestamp
  )
}
