///|
/// Immutable description of the entity producing telemetry.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/data-model/#resource-data-model
pub struct Resource {
  attributes : Map[String, @common.Value]
  schema_url : String?
} derive(Eq, ToJson, Debug)

///|
/// Detector callback used to discover resource attributes from the runtime or
/// environment.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
pub struct ResourceDetector {
  detect_fn : () -> Resource
}

///|
/// Builder that combines explicit attributes with detector output.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
pub struct ResourceBuilder {
  mut resource : Resource
  detectors : Array[ResourceDetector]
}

///|
fn resource_from_attributes(
  attributes : ArrayView[@common.KeyValue],
  schema_url? : String? = None,
) -> Resource {
  let map : Map[String, @common.Value] = Map([])
  for attribute in attributes {
    map[attribute.key.as_string()] = attribute.value
  }
  { attributes: map, schema_url }
}

///|
/// Returns an empty resource.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#the-empty-resource
pub fn Resource::empty() -> Resource {
  { attributes: {}, schema_url: None }
}

///|
/// Starts building a resource with the SDK-provided, telemetry, and environment
/// detectors enabled by default.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#create
pub fn Resource::builder() -> ResourceBuilder {
  {
    resource: Resource::empty(),
    detectors: [
      sdk_provided_resource_detector(),
      telemetry_resource_detector(),
      env_resource_detector(),
    ],
  }
}

///|
/// Starts building a resource without any default detectors.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#create
pub fn Resource::builder_empty() -> ResourceBuilder {
  { resource: Resource::empty(), detectors: [] }
}

///|
/// Creates a resource from explicit attributes. Later duplicate keys win.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#create
pub fn Resource::new(attributes : ArrayView[@common.KeyValue]) -> Resource {
  resource_from_attributes(attributes)
}

///|
/// Returns the optional schema URL attached to this resource.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#retrieve-attributes
pub fn Resource::schema_url(self : Resource) -> String? {
  self.schema_url
}

///|
/// Returns the number of attributes stored in the resource.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#retrieve-attributes
pub fn Resource::len(self : Resource) -> Int {
  self.attributes.length()
}

///|
/// Returns whether the resource carries no attributes.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#retrieve-attributes
pub fn Resource::is_empty(self : Resource) -> Bool {
  self.attributes.is_empty()
}

///|
/// Looks up one resource attribute.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#retrieve-attributes
pub fn Resource::get(self : Resource, key : StringView) -> @common.Value? {
  self.attributes.get_from_string(key)
}

///|
/// Returns a copy of all resource attributes.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#retrieve-attributes
pub fn Resource::attributes(self : Resource) -> Map[String, @common.Value] {
  self.attributes.copy()
}

///|
/// Merges two resources, preferring attributes from `other`. Conflicting schema
/// URLs clear the merged schema URL.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#merge
pub fn Resource::merge(self : Resource, other : Resource) -> Resource {
  if self.is_empty() && self.schema_url is None {
    return other
  }
  if other.is_empty() && other.schema_url is None {
    return self
  }
  let attributes = self.attributes.copy()
  for key, value in other.attributes {
    attributes[key] = value
  }
  let schema_url = match (self.schema_url, other.schema_url) {
    (Some(left), Some(right)) if left == right => Some(left)
    (Some(_), Some(_)) => None
    (Some(left), None) => Some(left)
    (None, Some(right)) => Some(right)
    (None, None) => None
  }
  { attributes, schema_url }
}

///|
pub impl Default for Resource with fn default() -> Resource {
  Resource::empty()
}

///|
/// Creates a detector from a callback.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
pub fn ResourceDetector::new(detect_fn : () -> Resource) -> ResourceDetector {
  { detect_fn, }
}

///|
/// Runs the detector and returns the discovered resource.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
pub fn ResourceDetector::detect(self : ResourceDetector) -> Resource {
  (self.detect_fn)()
}

///|
fn default_service_name() -> String {
  let args = @env.args()
  let process_name = if args.length() > 0 {
    @utils.basename(args[0])
  } else {
    "moonbit"
  }
  "unknown_service:\{process_name}"
}

///|
/// Detects the fallback `service.name` derived from the current executable or
/// script name.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#sdk-provided-resource-attributes
pub fn sdk_provided_resource_detector() -> ResourceDetector {
  ResourceDetector::new(() => {
    Resource::new([
      @common.KeyValue::new("service.name", String(default_service_name())),
    ])
  })
}

///|
/// Detects the standard `telemetry.sdk.*` attributes for this MoonBit SDK.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#sdk-provided-resource-attributes
pub fn telemetry_resource_detector() -> ResourceDetector {
  ResourceDetector::new(() => {
    Resource::new([
      @common.KeyValue::new("telemetry.sdk.name", String("opentelemetry")),
      @common.KeyValue::new("telemetry.sdk.language", String("moonbit")),
      @common.KeyValue::new("telemetry.sdk.version", String("0.1.0")),
    ])
  })
}

///|
fn parse_resource_attributes(
  attributes : StringView,
) -> Array[@common.KeyValue] {
  let parsed = []
  for item in attributes.split(",") {
    let item = item.trim()
    if item.is_empty() {
      continue
    }
    guard item.find("=") is Some(index) else { return [] }
    let key = item[:index].trim()
    let value = item[index + 1:].trim()
    if key != "" {
      parsed.push(@common.KeyValue::new(key, String(value.to_owned())))
    }
  }
  parsed
}

///|
/// Detects resources from `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME`.
/// `OTEL_SERVICE_NAME` is merged last so it wins over `service.name` from the
/// generic attribute list.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#specifying-resource-information-via-an-environment-variable
pub fn env_resource_detector() -> ResourceDetector {
  ResourceDetector::new(() => {
    let mut resource = Resource::empty()
    if @utils.getenv("OTEL_RESOURCE_ATTRIBUTES") is Some(attributes) {
      let detected = Resource::new(parse_resource_attributes(attributes))
      resource = resource.merge(detected)
    }
    if @utils.getenv("OTEL_SERVICE_NAME") is Some(service_name) {
      let service_resource = Resource::new([
        @common.KeyValue::new("service.name", String(service_name)),
      ])
      resource = resource.merge(service_resource)
    }
    resource
  })
}

///|
/// Adds or replaces one attribute.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
pub fn ResourceBuilder::with_attribute(
  self : ResourceBuilder,
  key : StringView,
  value : @common.Value,
) -> ResourceBuilder {
  self.resource = self.resource.merge(
    Resource::new([@common.KeyValue::new(key, value)]),
  )
  self
}

///|
/// Adds or replaces multiple attributes.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
pub fn ResourceBuilder::with_attributes(
  self : ResourceBuilder,
  attributes : ArrayView[@common.KeyValue],
) -> ResourceBuilder {
  self.resource = self.resource.merge(Resource::new(attributes))
  self
}

///|
/// Sets `service.name` on the resource being built.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
pub fn ResourceBuilder::with_service_name(
  self : ResourceBuilder,
  service_name : StringView,
) -> ResourceBuilder {
  self.with_attribute("service.name", String(service_name.to_owned()))
}

///|
/// Sets the schema URL on the resource being built.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
pub fn ResourceBuilder::with_schema_url(
  self : ResourceBuilder,
  schema_url : StringView,
) -> ResourceBuilder {
  self.resource = {
    attributes: self.resource.attributes,
    schema_url: Some(schema_url.to_owned()),
  }
  self
}

///|
/// Appends an additional detector that will run before explicit builder
/// attributes are merged in.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#detecting-resource-information-from-the-environment
pub fn ResourceBuilder::with_detector(
  self : ResourceBuilder,
  detector : ResourceDetector,
) -> ResourceBuilder {
  self.detectors.push(detector)
  self
}

///|
/// Runs detectors in order and then merges in explicit builder attributes so
/// direct configuration takes precedence.
/// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#resource-creation
pub fn ResourceBuilder::build(self : ResourceBuilder) -> Resource {
  let mut resource = Resource::empty()
  for detector in self.detectors {
    resource = resource.merge(detector.detect())
  }
  resource.merge(self.resource)
}