///|
/// A deterministic path through JSON object keys.
///
/// Array indexes, wildcards, and fuzzy key matching are intentionally excluded
/// so a profile cannot silently change meaning when an input schema changes.
pub(all) struct JsonFieldPath {
  segments : Array[String]
} derive(Debug, Eq)

///|
/// A string supplied by a profile or read from an input record.
pub(all) enum ProfileStringValue {
  Constant(String)
  JsonString(JsonFieldPath)
} derive(Debug, Eq)

///|
/// Exact source-to-canonical resource identity mapping.
pub(all) struct ResourceAlias {
  source_id : String
  canonical_id : String
} derive(Debug, Eq)

///|
/// Mapping rules for one typed resource identity.
pub(all) struct ResourceMappingProfile {
  kind : ResourceKind
  id : ProfileStringValue
  prefix : String
  aliases : Array[ResourceAlias]
  attributes_path : JsonFieldPath?
} derive(Debug, Eq)

///|
/// Mapping rules for the producer of a canonical event.
pub(all) struct SourceMappingProfile {
  system : ProfileStringValue
  component : ProfileStringValue
  host : ProfileStringValue?
  instrumentation_scope : ProfileStringValue?
} derive(Debug, Eq)

///|
/// A versioned, reusable mapping from one JSONL schema to CanonicalEvent.
///
/// Required values fail explicitly. Optional configured values may be absent,
/// but a present non-string value is rejected instead of being coerced.
pub(all) struct JsonlMappingProfile {
  id : String
  version : String
  source : SourceMappingProfile
  event_id : ProfileStringValue
  event_time : JsonFieldPath?
  observed_time : JsonFieldPath?
  severity : JsonFieldPath?
  resource : ResourceMappingProfile?
  category : EventCategory
  event_type : ProfileStringValue
  action : ProfileStringValue?
  outcome : EventOutcome
  body_path : JsonFieldPath?
} derive(Debug, Eq)

///|
/// Evidence identity shared by every event produced from one JSONL stream.
pub(all) struct ProfileEvidence {
  evidence_id : String
  path : String
  sha256 : String?
} derive(Debug, Eq)

///|
/// A profile definition or input record could not be mapped safely.
pub(all) suberror ProfileMapError {
  InvalidProfile(field~ : String, reason~ : String)
  MissingValue(line_number~ : Int, path~ : String)
  InvalidValue(line_number~ : Int, path~ : String, value~ : String)
} derive(Debug, Eq)

///|
fn path_label(path : JsonFieldPath) -> String {
  let mut label = ""
  for index, segment in path.segments {
    label = if index == 0 { segment } else { "\{label}.\{segment}" }
  }
  label
}

///|
fn validate_path(
  field : String,
  path : JsonFieldPath,
) -> Unit raise ProfileMapError {
  if path.segments.is_empty() {
    raise ProfileMapError::InvalidProfile(
      field~,
      reason="path must contain at least one object key",
    )
  }
  for segment in path.segments {
    if segment.trim() == "" {
      raise ProfileMapError::InvalidProfile(
        field~,
        reason="path keys must not be empty",
      )
    }
  }
}

///|
fn validate_string_value(
  field : String,
  value : ProfileStringValue,
) -> Unit raise ProfileMapError {
  match value {
    Constant(value) =>
      if value.trim() == "" {
        raise ProfileMapError::InvalidProfile(
          field~,
          reason="constant must not be empty",
        )
      }
    JsonString(path) => validate_path(field, path)
  }
}

///|
fn validate_optional_string_value(
  field : String,
  value : ProfileStringValue?,
) -> Unit raise ProfileMapError {
  match value {
    Some(value) => validate_string_value(field, value)
    None => ()
  }
}

///|
fn validate_resource_profile(
  resource : ResourceMappingProfile,
) -> Unit raise ProfileMapError {
  validate_string_value("resource.id", resource.id)
  match resource.attributes_path {
    Some(path) => validate_path("resource.attributes_path", path)
    None => ()
  }
  for index, resource_alias in resource.aliases {
    if resource_alias.source_id.trim() == "" ||
      resource_alias.canonical_id.trim() == "" {
      raise ProfileMapError::InvalidProfile(
        field="resource.aliases",
        reason="alias identities must not be empty",
      )
    }
    for previous in 0.. Unit raise ProfileMapError {
  if profile.id.trim() == "" {
    raise ProfileMapError::InvalidProfile(
      field="id",
      reason="profile id must not be empty",
    )
  }
  if profile.version.trim() == "" {
    raise ProfileMapError::InvalidProfile(
      field="version",
      reason="profile version must not be empty",
    )
  }
  validate_string_value("source.system", profile.source.system)
  validate_string_value("source.component", profile.source.component)
  validate_optional_string_value("source.host", profile.source.host)
  validate_optional_string_value(
    "source.instrumentation_scope",
    profile.source.instrumentation_scope,
  )
  validate_string_value("event_id", profile.event_id)
  validate_string_value("event_type", profile.event_type)
  validate_optional_string_value("action", profile.action)
  match profile.event_time {
    Some(path) => validate_path("event_time", path)
    None => ()
  }
  match profile.observed_time {
    Some(path) => validate_path("observed_time", path)
    None => ()
  }
  match profile.severity {
    Some(path) => validate_path("severity", path)
    None => ()
  }
  match profile.resource {
    Some(resource) => validate_resource_profile(resource)
    None => ()
  }
  match profile.body_path {
    Some(path) => validate_path("body_path", path)
    None => ()
  }
}

///|
fn json_at_path(value : Json, path : JsonFieldPath) -> Json? {
  let mut current = value
  for segment in path.segments {
    match current {
      Object(object) =>
        match object.get(segment) {
          Some(next) => current = next
          None => return None
        }
      _ => return None
    }
  }
  Some(current)
}

///|
fn mapped_required_string(
  record : JsonlRecord,
  mapping : ProfileStringValue,
) -> String raise ProfileMapError {
  match mapping {
    Constant(value) => value.trim().to_owned()
    JsonString(path) =>
      match json_at_path(record.value, path) {
        Some(String(value)) =>
          if value.trim() == "" {
            raise ProfileMapError::InvalidValue(
              line_number=record.line_number,
              path=path_label(path),
              value~,
            )
          } else {
            value.trim().to_owned()
          }
        Some(value) =>
          raise ProfileMapError::InvalidValue(
            line_number=record.line_number,
            path=path_label(path),
            value=value.stringify(),
          )
        None =>
          raise ProfileMapError::MissingValue(
            line_number=record.line_number,
            path=path_label(path),
          )
      }
  }
}

///|
fn mapped_optional_string(
  record : JsonlRecord,
  mapping : ProfileStringValue?,
) -> String? raise ProfileMapError {
  match mapping {
    None => None
    Some(Constant(value)) => Some(value.trim().to_owned())
    Some(JsonString(path)) =>
      match json_at_path(record.value, path) {
        Some(String(value)) =>
          if value.trim() == "" {
            None
          } else {
            Some(value.trim().to_owned())
          }
        Some(value) =>
          raise ProfileMapError::InvalidValue(
            line_number=record.line_number,
            path=path_label(path),
            value=value.stringify(),
          )
        None => None
      }
  }
}

///|
fn mapped_timestamp(
  record : JsonlRecord,
  path : JsonFieldPath?,
) -> NormalizedTimestamp? raise ProfileMapError {
  match path {
    None => None
    Some(path) =>
      match json_at_path(record.value, path) {
        None => None
        Some(String(value)) =>
          Some(normalize_timestamp(value)) catch {
            _ =>
              raise ProfileMapError::InvalidValue(
                line_number=record.line_number,
                path=path_label(path),
                value~,
              )
          }
        Some(value) =>
          raise ProfileMapError::InvalidValue(
            line_number=record.line_number,
            path=path_label(path),
            value=value.stringify(),
          )
      }
  }
}

///|
fn mapped_severity(
  record : JsonlRecord,
  path : JsonFieldPath?,
) -> Severity? raise ProfileMapError {
  match path {
    None => None
    Some(path) =>
      match json_at_path(record.value, path) {
        None => None
        Some(String(value)) => Some(normalize_severity(value))
        Some(value) =>
          raise ProfileMapError::InvalidValue(
            line_number=record.line_number,
            path=path_label(path),
            value=value.stringify(),
          )
      }
  }
}

///|
fn canonical_resource_id(
  source_id : String,
  resource : ResourceMappingProfile,
) -> String {
  for resource_alias in resource.aliases {
    if resource_alias.source_id == source_id {
      return resource_alias.canonical_id
    }
  }
  "\{resource.prefix}\{source_id}"
}

///|
fn mapped_resource_ref(
  record : JsonlRecord,
  resource : ResourceMappingProfile?,
) -> ResourceRef? raise ProfileMapError {
  match resource {
    None => None
    Some(resource) => {
      let source_id = mapped_required_string(record, resource.id)
      let attributes : Json = match resource.attributes_path {
        None => {}
        Some(path) =>
          match json_at_path(record.value, path) {
            Some(value) => value
            None =>
              raise ProfileMapError::MissingValue(
                line_number=record.line_number,
                path=path_label(path),
              )
          }
      }
      Some({
        kind: resource.kind,
        id: canonical_resource_id(source_id, resource),
        attributes,
      })
    }
  }
}

///|
fn mapped_body(
  record : JsonlRecord,
  path : JsonFieldPath?,
) -> Json raise ProfileMapError {
  match path {
    None => record.value
    Some(path) =>
      match json_at_path(record.value, path) {
        Some(value) => value
        None =>
          raise ProfileMapError::MissingValue(
            line_number=record.line_number,
            path=path_label(path),
          )
      }
  }
}

///|
/// Applies one validated profile to ordered JSONL records.
///
/// The original object is retained in attributes and raw_content. Every result
/// records the profile id/version and physical evidence line in provenance.
pub fn apply_jsonl_profile(
  records : Array[JsonlRecord],
  profile : JsonlMappingProfile,
  evidence : ProfileEvidence,
) -> Array[CanonicalEvent] raise ProfileMapError {
  validate_jsonl_profile(profile)
  if evidence.evidence_id.trim() == "" {
    raise ProfileMapError::InvalidProfile(
      field="evidence.evidence_id",
      reason="evidence id must not be empty",
    )
  }
  if evidence.path.trim() == "" {
    raise ProfileMapError::InvalidProfile(
      field="evidence.path",
      reason="evidence path must not be empty",
    )
  }
  let events : Array[CanonicalEvent] = []
  for record in records {
    events.push({
      event_id: mapped_required_string(record, profile.event_id),
      event_time: mapped_timestamp(record, profile.event_time),
      observed_time: mapped_timestamp(record, profile.observed_time),
      source: {
        system: mapped_required_string(record, profile.source.system),
        component: mapped_required_string(record, profile.source.component),
        host: mapped_optional_string(record, profile.source.host),
        instrumentation_scope: mapped_optional_string(
          record,
          profile.source.instrumentation_scope,
        ),
      },
      resource: mapped_resource_ref(record, profile.resource),
      category: profile.category,
      event_type: mapped_required_string(record, profile.event_type),
      action: mapped_optional_string(record, profile.action),
      outcome: profile.outcome,
      severity: mapped_severity(record, profile.severity),
      body: mapped_body(record, profile.body_path),
      raw_content: record.value.stringify(),
      attributes: record.value,
      provenance: {
        evidence_id: evidence.evidence_id,
        path: evidence.path,
        line_number: Some(record.line_number),
        byte_offset: None,
        byte_length: None,
        sha256: evidence.sha256,
        profile_id: profile.id,
        profile_version: profile.version,
      },
    })
  }
  events
}