///|
pub(all) struct RobotEpisodeDirectoryImportRequest {
  root : String
  source_dir : String
  dataset_id : @data_core.DatasetId
  episode_id : @robot_data.RobotEpisodeId
  robot_id : @robot_data.RobotId
  model_id : @robot_data.RobotModelId
  imported_at_ms : Int64
  source_label : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ImportedRobotEpisodeSignal {
  source_path : String
  relative_path : String
  payload_path : String
  signal : @robot_data.RobotSignalRef
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ImportedRobotEpisodeReplay {
  source_path : String
  relative_path : String
  payload_path : String
  replay : @robot_data.RobotReplayArtifact
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ImportedRobotEpisodeQuality {
  source_path : String
  relative_path : String
  payload_path : String
  payload : @robot_data.RobotPayloadRef
  report : @robot_data.RobotQualityReport
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct RobotEpisodeDirectoryImportResult {
  root : String
  source_dir : String
  dataset_id : @data_core.DatasetId
  episode_id : @robot_data.RobotEpisodeId
  robot_id : @robot_data.RobotId
  model_id : @robot_data.RobotModelId
  signal_count : Int
  replay_count : Int
  quality_report_count : Int
  episode : @robot_data.RobotEpisodeManifest
  signals : Array[ImportedRobotEpisodeSignal]
  replays : Array[ImportedRobotEpisodeReplay]
  quality_reports : Array[ImportedRobotEpisodeQuality]
  materialization : RobotCatalogMaterialization
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn robot_episode_directory_import_request(
  root : String,
  source_dir : String,
  dataset_id : @data_core.DatasetId,
  episode_id : @robot_data.RobotEpisodeId,
  robot_id : @robot_data.RobotId,
  model_id : @robot_data.RobotModelId,
  imported_at_ms : Int64,
  source_label? : String = "Robot episode directory import",
) -> RobotEpisodeDirectoryImportRequest {
  {
    root,
    source_dir,
    dataset_id,
    episode_id,
    robot_id,
    model_id,
    imported_at_ms,
    source_label,
  }
}

///|
fn validate_episode_import_id(
  value : String,
  kind : String,
) -> Result[Unit, @data_store.DataStoreIssue] {
  if value == "" ||
    value.contains("/") ||
    value.contains("\\") ||
    value == "." ||
    value == ".." {
    Err(
      import_issue(
        "invalid-\{kind}",
        value,
        "\{kind} must be non-empty and path-safe",
      ),
    )
  } else {
    Ok(())
  }
}

///|
fn episode_source_files(
  source_dir : String,
) -> Result[Array[(String, String)], @data_store.DataStoreIssue] {
  let names = match read_sorted_names(source_dir) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let files : Array[(String, String)] = []
  for name in names {
    if !@data_core.data_relative_path_is_safe(name) {
      return Err(
        import_issue(
          "unsafe-robot-episode-path", name, "robot episode source contains an unsafe relative path",
        ),
      )
    }
    let path = join(source_dir, name)
    if !is_directory(path) {
      files.push((path, name))
    }
  }
  Ok(files)
}

///|
fn episode_replay_files(
  source_dir : String,
) -> Result[Array[(String, String)], @data_store.DataStoreIssue] {
  let replay_dir = join(source_dir, "replays")
  if !@fsx.path_exists(replay_dir) {
    return Ok([])
  }
  let names = match read_sorted_names(replay_dir) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let files : Array[(String, String)] = []
  for name in names {
    if !@data_core.data_relative_path_is_safe(name) {
      return Err(
        import_issue(
          "unsafe-robot-replay-path", name, "robot replay source contains an unsafe relative path",
        ),
      )
    }
    let path = join(replay_dir, name)
    if !is_directory(path) {
      files.push((path, name))
    }
  }
  Ok(files)
}

///|
fn episode_quality_files(
  source_dir : String,
) -> Result[Array[(String, String)], @data_store.DataStoreIssue] {
  let quality_dir = join(source_dir, "quality")
  if !@fsx.path_exists(quality_dir) {
    return Ok([])
  }
  let names = match read_sorted_names(quality_dir) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let files : Array[(String, String)] = []
  for name in names {
    if !@data_core.data_relative_path_is_safe(name) {
      return Err(
        import_issue(
          "unsafe-robot-quality-path", name, "robot quality source contains an unsafe relative path",
        ),
      )
    }
    let path = join(quality_dir, name)
    if !is_directory(path) {
      files.push((path, name))
    }
  }
  Ok(files)
}

///|
fn episode_payload_data_ref(
  dataset_id : @data_core.DatasetId,
  relative_path : String,
  body : String,
) -> @data_core.DataRef {
  let payload_path = "payloads/robot_data/episodes/\{dataset_id}/\{relative_path}"
  @data_core.data_ref(
    "robot-episode-\{safe_ref_segment(dataset_id)}-\{safe_ref_segment(relative_path)}",
    "robot-signal-frame",
    @data_core.data_uri(payload_path),
    content_type=if relative_path.has_suffix(".json") {
      "application/json"
    } else {
      "text/plain"
    },
    byte_count=body.length().to_int64(),
    checksum=text_sum(body),
  )
}

///|
fn quality_payload_data_ref(
  dataset_id : @data_core.DatasetId,
  relative_path : String,
  body : String,
) -> @data_core.DataRef {
  let payload_path = "payloads/robot_data/episodes/\{dataset_id}/quality/\{relative_path}"
  @data_core.data_ref(
    "robot-quality-\{safe_ref_segment(dataset_id)}-\{safe_ref_segment(relative_path)}",
    "robot-quality-report",
    @data_core.data_uri(payload_path),
    content_type=if relative_path.has_suffix(".json") {
      "application/json"
    } else {
      "text/plain"
    },
    byte_count=body.length().to_int64(),
    checksum=text_sum(body),
  )
}

///|
fn replay_payload_data_ref(
  dataset_id : @data_core.DatasetId,
  relative_path : String,
  body : String,
) -> @data_core.DataRef {
  let payload_path = "payloads/robot_data/episodes/\{dataset_id}/replays/\{relative_path}"
  @data_core.data_ref(
    "robot-replay-\{safe_ref_segment(dataset_id)}-\{safe_ref_segment(relative_path)}",
    "robot-replay",
    @data_core.data_uri(payload_path),
    content_type=if relative_path.has_suffix(".json") {
      "application/json"
    } else {
      "text/plain"
    },
    byte_count=body.length().to_int64(),
    checksum=text_sum(body),
  )
}

///|
fn payload_from_data_ref(
  data_ref : @data_core.DataRef,
) -> Result[@robot_data.RobotPayloadRef, @data_store.DataStoreIssue] {
  let path = match @data_core.data_payload_uri_relative_path(data_ref.uri) {
    Some(value) => value
    None =>
      return Err(
        import_issue(
          "unsafe-robot-data-ref",
          data_ref.uri,
          "robot data ref is not a safe data-root payload URI",
        ),
      )
  }
  Ok(
    @robot_data.robot_payload_ref_with_checksum(
      data_ref.ref_id,
      data_ref.kind,
      path,
      data_ref.content_type,
      data_ref.byte_count,
      data_ref.checksum,
    ),
  )
}

///|
fn model_urdf_ref(dossier : RobotModelCatalogDossier) -> @data_core.DataRef? {
  for data_ref in dossier.dataset.data_refs {
    if data_ref.kind == "urdf" {
      return Some(data_ref)
    }
  }
  None
}

///|
fn model_ref_from_dossier(
  dossier : RobotModelCatalogDossier,
  robot_id : @robot_data.RobotId,
) -> Result[@robot_data.RobotModelRef, @data_store.DataStoreIssue] {
  let urdf = match model_urdf_ref(dossier) {
    Some(value) => value
    None =>
      return Err(
        import_issue(
          "robot-model-urdf-ref-missing",
          dossier.model_id,
          "robot model dossier has no URDF data ref",
        ),
      )
  }
  let payload = match payload_from_data_ref(urdf) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok(
    @robot_data.robot_model_ref(
      dossier.model_id,
      robot_id,
      dossier.source.source_id,
      dossier.source.label,
      dossier.dataset.kind,
      dossier.source.authority,
      dossier.source.source_url,
      dossier.source.license,
      dossier.source.citation,
      dossier.source.created_at_ms,
      payload,
    ),
  )
}

///|
pub fn import_robot_episode_directory(
  request : RobotEpisodeDirectoryImportRequest,
) -> Result[RobotEpisodeDirectoryImportResult, @data_store.DataStoreIssue] {
  match
    validate_episode_import_id(request.dataset_id, "robot-episode-dataset-id") {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  match validate_episode_import_id(request.episode_id, "robot-episode-id") {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let model_dossier = match
    robot_model_catalog_dossier_from_root(request.root, request.model_id) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let model = match model_ref_from_dossier(model_dossier, request.robot_id) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let files = match episode_source_files(request.source_dir) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if files.length() == 0 {
    return Err(
      import_issue(
        "empty-robot-episode-directory",
        request.source_dir,
        "robot episode source directory has no file payloads",
      ),
    )
  }
  let replay_files = match episode_replay_files(request.source_dir) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let quality_files = match episode_quality_files(request.source_dir) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let signals : Array[ImportedRobotEpisodeSignal] = []
  for index in 0..
        return Err(
          import_issue(
            "read-robot-episode-payload", source_path, "failed to read robot episode payload as text",
          ),
        )
    }
    let data_ref = episode_payload_data_ref(
      request.dataset_id,
      relative_path,
      body,
    )
    let payload_path = match stage_text_data_ref(request.root, data_ref, body) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let payload = match payload_from_data_ref(data_ref) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let signal = @robot_data.robot_signal_ref(
      "signal-\{safe_ref_segment(request.dataset_id)}-\{index}",
      "frame-\{safe_ref_segment(request.episode_id)}-\{index}",
      "raw-file",
      1,
      "payload",
      payload,
    )
    signals.push({ source_path, relative_path, payload_path, signal })
  }
  let replays : Array[ImportedRobotEpisodeReplay] = []
  for index in 0..
        return Err(
          import_issue(
            "read-robot-replay-payload", source_path, "failed to read robot replay payload as text",
          ),
        )
    }
    let data_ref = replay_payload_data_ref(
      request.dataset_id,
      relative_path,
      body,
    )
    let payload_path = match stage_text_data_ref(request.root, data_ref, body) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let payload = match payload_from_data_ref(data_ref) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let replay = @robot_data.robot_replay_artifact(
      "replay-\{safe_ref_segment(request.dataset_id)}-\{index}",
      "robot-replay",
      payload,
    )
    replays.push({ source_path, relative_path, payload_path, replay })
  }
  let quality_reports : Array[ImportedRobotEpisodeQuality] = []
  for index in 0..
        return Err(
          import_issue(
            "read-robot-quality-payload", source_path, "failed to read robot quality payload as text",
          ),
        )
    }
    let data_ref = quality_payload_data_ref(
      request.dataset_id,
      relative_path,
      body,
    )
    let payload_path = match stage_text_data_ref(request.root, data_ref, body) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let payload = match payload_from_data_ref(data_ref) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let report = @robot_data.robot_quality_report(
      "quality-\{safe_ref_segment(request.dataset_id)}-\{index}",
      request.episode_id,
      @data_core.ValidationStatus::Passed,
      request.imported_at_ms,
      signals.length(),
      0,
      findings=[
        @data_core.validation_finding(
          "robot-quality-payload-imported",
          @data_core.FindingSeverity::Info,
          @data_core.artifact_ref(
            "payload",
            payload.ref_id,
            payload.path,
            status=@data_core.DataStatus::Verified,
            summary=payload.role,
          ),
          "robot quality payload imported",
          recommendation="use the domain quality payload for detailed robot acceptance evidence",
        ),
      ],
    )
    quality_reports.push({
      source_path,
      relative_path,
      payload_path,
      payload,
      report,
    })
  }
  let episode = @robot_data.robot_episode_manifest(
    request.episode_id,
    request.robot_id,
    request.dataset_id,
    @data_core.DataStatus::Verified,
    request.imported_at_ms,
    signals.length(),
    [model],
    signals.map(fn(signal) { signal.signal }),
    replays.map(fn(replay) { replay.replay }),
    quality_reports.map(fn(quality) { quality.payload }),
    request.source_label,
  )
  match write_episodes(request.root, [episode]) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  match
    write_quality_reports(
      request.root,
      quality_reports.map(fn(quality) { quality.report }),
    ) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  match @data_store.rebuild_catalog(request.root, request.imported_at_ms) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let validation = match
    @data_validate.validate_and_write_root(request.root, request.imported_at_ms) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let catalog = match @data_store.read_catalog(request.root) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok({
    root: request.root,
    source_dir: request.source_dir,
    dataset_id: request.dataset_id,
    episode_id: request.episode_id,
    robot_id: request.robot_id,
    model_id: request.model_id,
    signal_count: signals.length(),
    replay_count: replays.length(),
    quality_report_count: quality_reports.length(),
    episode,
    signals,
    replays,
    quality_reports,
    materialization: {
      root: request.root,
      catalog_path: validation.catalog_path,
      validation_report_path: validation.report_path,
      source_count: 0,
      dataset_count: 1,
      version_count: 1,
      lineage_count: 1,
      telemetry_stream_count: 0,
      gait_clip_count: 0,
      gait_annotation_count: 0,
      gait_alignment_count: 0,
      task_label_count: 0,
      rollout_count: 0,
      quality_report_count: quality_reports.length(),
      catalog_entry_count: catalog.entries.length(),
      validation_status: validation.status,
      blocker_count: validation.blocker_count,
      warning_count: validation.warning_count,
    },
  })
}