#include "tools/jotpluggler/app.h"
#include "tools/jotpluggler/car_fingerprint_to_dbc.h"
#include "tools/jotpluggler/common.h"

#include <kj/exception.h>

#include <chrono>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <initializer_list>
#include <map>
#include <mutex>
#include <limits>
#include <numeric>
#include <regex>
#include <set>
#include <stdexcept>
#include <thread>
#include <tuple>
#include <unordered_set>
#include <utility>

#include "common/util.h"
#include "json11/json11.hpp"
#include "tools/replay/logreader.h"
#include "tools/replay/py_downloader.h"

namespace fs = std::filesystem;

namespace {

struct RouteSelection {
  std::string dongle_id;
  std::string timestamp;
  int begin_segment = 0;
  int end_segment = -1;
  bool slice_explicit = false;
  LogSelector selector = LogSelector::Auto;
  bool selector_explicit = false;
  std::string canonical_name;
};

struct SegmentLogs {
  std::string rlog;
  std::string qlog;
  std::string narrow_road;
  std::string cabin;
  std::string wide_road;
  std::string qcamera;
};

struct SchemaIndex {
  size_t fixed_series_count = 0;
  std::vector<std::string> fixed_paths;

  static const SchemaIndex &instance();
};

constexpr size_t INVALID_DYNAMIC_SLOT = std::numeric_limits<size_t>::max();

struct SeriesAccumulator {
  explicit SeriesAccumulator(size_t fixed_count = 0) : fixed_series(fixed_count) {}

  std::vector<RouteSeries> fixed_series;
  std::vector<RouteSeries> dynamic_series;
  std::vector<CanMessageData> can_messages;
  std::unordered_map<std::string, size_t> dynamic_slots;
  std::unordered_map<std::string, std::vector<size_t>> list_scalar_slots;
  std::unordered_map<CanMessageId, size_t, CanMessageIdHash> can_message_slots;
  std::unordered_map<std::string, EnumInfo> enum_info;
};

void append_fixed_scalar_point(RouteSeries *series, double tm, double value);
void append_dynamic_scalar_point(const std::string &path, double tm, double value, SeriesAccumulator *series);
RouteSeries *ensure_list_scalar_series(const std::string &base_path, size_t index, SeriesAccumulator *series);
void append_can_frame(CanServiceKind service,
                      uint8_t bus,
                      uint32_t address,
                      uint16_t bus_time,
                      capnp::Data::Reader dat,
                      double tm,
                      SeriesAccumulator *series);
void decode_can_frame(const dbc::Database *can_dbc,
                      const std::string &service_name,
                      uint8_t bus,
                      uint32_t address,
                      const uint8_t *raw,
                      size_t data_size,
                      double tm,
                      SeriesAccumulator *series);

#include "tools/jotpluggler/generated_event_extractors.h"

struct LoadedRouteArtifacts {
  std::vector<RouteSeries> series;
  std::vector<CanMessageData> can_messages;
  std::vector<LogEntry> logs;
  std::vector<TimelineEntry> timeline;
  std::vector<ThumbnailFrame> thumbnails;
  std::unordered_map<std::string, EnumInfo> enum_info;
};

struct RouteMetadata {
  std::string car_fingerprint;
};

struct LoadStats {
  using Clock = std::chrono::steady_clock;
  using TimePoint = Clock::time_point;

  struct SegmentStats {
    int segment_number = -1;
    std::string log_path;
    double download_seconds = 0.0;
    double decompress_seconds = 0.0;
    double parse_seconds = 0.0;
    double extract_seconds = 0.0;
    size_t compressed_bytes = 0;
    size_t decompressed_bytes = 0;
    size_t event_count = 0;
    size_t series_count = 0;
    bool failed = false;
  };

  explicit LoadStats(const RouteLoadProgressCallback &callback) : progress(callback) {}

  void publish(RouteLoadStage stage, size_t segment_index, const std::string &segment_name) {
    if (!progress) {
      return;
    }
    RouteLoadProgress update;
    update.stage = stage;
    update.segment_index = segment_index;
    update.segment_count = segment_count;
    update.current = stage == RouteLoadStage::DownloadingSegment
      ? segments_downloaded.load()
      : segments_parsed.load();
    update.total = total_segments.load();
    update.segments_downloaded = segments_downloaded.load();
    update.segments_parsed = segments_parsed.load();
    update.total_segments = total_segments.load();
    update.bytes_downloaded = bytes_downloaded.load();
    update.num_workers = num_workers;
    update.segment_name = segment_name;
    std::lock_guard<std::mutex> lock(progress_mutex);
    progress(update);
  }

  void print_summary(size_t final_series_count) const {
    const auto secs = [](TimePoint a, TimePoint b) { return std::chrono::duration<double>(b - a).count(); };
    const auto mb = [](size_t bytes) { return static_cast<double>(bytes) / (1024.0 * 1024.0); };
    double dl = 0, dc = 0, pa = 0, ex = 0;
    size_t ev = 0, cb = 0, db = 0;
    for (const auto &s : segments) {
      dl += s.download_seconds; dc += s.decompress_seconds;
      pa += s.parse_seconds; ex += s.extract_seconds;
      ev += s.event_count; cb += s.compressed_bytes; db += s.decompressed_bytes;
    }
    std::cerr << std::fixed << std::setprecision(1)
      << "route loaded in " << secs(load_start, load_end) << "s (" << segment_count << " segments, " << num_workers << " workers)\n"
      << "  resolve: " << secs(load_start, resolve_end) << "s  fetch: " << dl << "s (" << mb(cb) << " MB)"
      << "  decompress: " << dc << "s (" << mb(db) << " MB)\n"
      << "  parse: " << pa << "s (" << ev << " events)  extract: " << ex << "s  merge: " << secs(merge_start, merge_end) << "s"
      << "  series: " << final_series_count << " paths\n";
    for (const auto &s : segments) {
      std::cerr << "    seg " << std::setw(2) << s.segment_number << ": "
        << (s.failed ? "FAILED" : std::to_string(s.download_seconds) + "s + " + std::to_string(s.parse_seconds)
          + "s (" + std::to_string(s.event_count) + " ev, " + std::to_string(s.series_count) + " series)") << "\n";
    }
    std::cerr.unsetf(std::ios::floatfield);
  }

  TimePoint load_start;
  TimePoint resolve_end;
  TimePoint merge_start;
  TimePoint merge_end;
  TimePoint load_end;
  size_t segment_count = 0;
  int num_workers = 1;
  std::vector<SegmentStats> segments;
  std::atomic<size_t> segments_downloaded{0};
  std::atomic<size_t> segments_parsed{0};
  std::atomic<size_t> total_segments{0};
  std::atomic<uint64_t> bytes_downloaded{0};
  RouteLoadProgressCallback progress;
  mutable std::mutex progress_mutex;
};

// Skip individual messages that our local Cap'n Proto schema can't project,
// such as logs recorded by a newer build.
template <typename Fn>
void with_parseable_event(kj::ArrayPtr<const capnp::word> data, Fn &&fn) {
  try {
    capnp::FlatArrayMessageReader event_reader(data);
    fn(event_reader.getRoot<cereal::Event>());
  } catch (const kj::Exception &) {
    return;
  }
}

std::string curve_label(std::string_view series_name) {
  return std::string(series_name.empty() ? std::string_view{"plot"} : series_name);
}

bool parse_segment_number(std::string_view value, int *out) {
  if (value.empty()) return false;
  char *end = nullptr;
  const long parsed = std::strtol(std::string(value).c_str(), &end, 10);
  if (end == nullptr || *end != '\0') return false;
  *out = static_cast<int>(parsed);
  return true;
}

bool is_log_selector_char(char c) {
  return c == 'a' || c == 'r' || c == 'q';
}

LogSelector parse_log_selector_char(char c) {
  switch (c) {
    case 'r': return LogSelector::RLog;
    case 'q': return LogSelector::QLog;
    case 'a':
    default: return LogSelector::Auto;
  }
}

const std::string &selected_log_path(const SegmentLogs &segment, LogSelector selector) {
  switch (selector) {
    case LogSelector::RLog:
      return segment.rlog;
    case LogSelector::QLog:
      return segment.qlog;
    case LogSelector::Auto:
    default:
      return !segment.rlog.empty() ? segment.rlog : segment.qlog;
  }
}

RouteSelection parse_route_selection(std::string route_name) {
  RouteSelection route = {};
  route_name = util::strip(route_name);
  if (route_name.size() >= 2 && route_name[route_name.size() - 2] == '/'
      && is_log_selector_char(static_cast<char>(std::tolower(route_name.back())))) {
    route.selector = parse_log_selector_char(static_cast<char>(std::tolower(route_name.back())));
    route.selector_explicit = true;
    route_name.resize(route_name.size() - 2);
  }
  static const std::regex pattern(R"(^(([a-z0-9]{16})[|_/])?(.{20})((--|/)((-?\d+(:(-?\d+)?)?)|(:-?\d+)))?$)");
  std::smatch match;
  if (!std::regex_match(route_name, match, pattern)) return route;

  route.dongle_id = match[2].str();
  route.timestamp = match[3].str();
  route.canonical_name = route.dongle_id + "|" + route.timestamp;

  const std::string separator = match[5].str();
  const std::string range_str = match[6].str();
  if (!range_str.empty()) {
    route.slice_explicit = true;
    if (separator == "/") {
      size_t pos = range_str.find(':');
      int begin_segment = 0;
      const std::string begin_str = range_str.substr(0, pos);
      if (!begin_str.empty() && !parse_segment_number(begin_str, &begin_segment)) {
        return {};
      }
      route.begin_segment = begin_segment;
      route.end_segment = begin_segment;
      if (pos != std::string::npos) {
        int end_segment = -1;
        const std::string end_str = range_str.substr(pos + 1);
        if (!end_str.empty() && !parse_segment_number(end_str, &end_segment)) {
          return {};
        }
        route.end_segment = end_str.empty() ? -1 : end_segment;
      }
    } else if (separator == "--") {
      int begin_segment = 0;
      if (!parse_segment_number(range_str, &begin_segment)) return {};
      route.begin_segment = begin_segment;
    }
  }
  return route;
}

void add_log_file_to_segments(std::map<int, SegmentLogs> *segments, int segment_number, const std::string &file) {
  std::string name = extractFileName(file);
  const size_t pos = name.find_last_of("--");
  name = pos != std::string::npos ? name.substr(pos + 2) : name;
  SegmentLogs &segment = (*segments)[segment_number];
  if (name == "rlog.bz2" || name == "rlog.zst" || name == "rlog") {
    segment.rlog = file;
  } else if (name == "qlog.bz2" || name == "qlog.zst" || name == "qlog") {
    segment.qlog = file;
  } else if (name == "fcamera.hevc") {
    segment.narrow_road = file;
  } else if (name == "dcamera.hevc") {
    segment.cabin = file;
  } else if (name == "ecamera.hevc") {
    segment.wide_road = file;
  } else if (name == "qcamera.ts") {
    segment.qcamera = file;
  }
}

std::map<int, SegmentLogs> trim_segments(std::map<int, SegmentLogs> segments, const RouteSelection &route) {
  if (route.begin_segment > 0) {
    segments.erase(segments.begin(), segments.lower_bound(route.begin_segment));
  }
  if (route.end_segment >= 0) {
    segments.erase(segments.upper_bound(route.end_segment), segments.end());
  }
  return segments;
}

std::map<int, SegmentLogs> load_segments_from_json(const json11::Json &json) {
  std::map<int, SegmentLogs> segments;
  static const std::regex rx(R"(\/(\d+)\/)");
  for (const auto &value : json.object_items()) {
    for (const auto &url : value.second.array_items()) {
      const std::string url_str = url.string_value();
      std::smatch match;
      if (!std::regex_search(url_str, match, rx)) continue;
      add_log_file_to_segments(&segments, std::stoi(match[1].str()), url_str);
    }
  }
  return segments;
}

std::map<int, SegmentLogs> load_segments_from_server(const RouteSelection &route) {
  const std::string result = PyDownloader::getRouteFiles(route.canonical_name);
  if (result.empty()) throw std::runtime_error("Failed to fetch route files for " + route.canonical_name);

  std::string parse_error;
  const auto json = json11::Json::parse(result, parse_error);
  if (!parse_error.empty()) throw std::runtime_error("Failed to parse route file list for " + route.canonical_name);
  if (json.is_object() && json["error"].is_string()) {
    throw std::runtime_error("Route API error for " + route.canonical_name + ": " + json["error"].string_value());
  }
  return load_segments_from_json(json);
}

std::map<int, SegmentLogs> load_segments_from_local(const RouteSelection &route, const std::string &data_dir) {
  std::map<int, SegmentLogs> segments;
  const std::string pattern = route.timestamp + "--";
  for (const auto &entry : fs::directory_iterator(data_dir)) {
    if (!entry.is_directory()) continue;
    const std::string dirname = entry.path().filename().string();
    if (dirname.find(pattern) == std::string::npos) continue;
    const size_t marker = dirname.rfind("--");
    if (marker == std::string::npos) continue;
    int segment_number = 0;
    if (!parse_segment_number(dirname.substr(marker + 2), &segment_number)) {
      continue;
    }
    for (const auto &file : fs::directory_iterator(entry.path())) {
      if (file.is_regular_file()) {
        add_log_file_to_segments(&segments, segment_number, file.path().string());
      }
    }
  }
  return segments;
}

RouteIdentifier make_route_identifier(const RouteSelection &route, const std::map<int, SegmentLogs> &segments) {
  RouteIdentifier route_id;
  route_id.dongle_id = route.dongle_id;
  route_id.log_id = route.timestamp;
  route_id.slice_begin = route.begin_segment;
  route_id.slice_end = route.end_segment;
  route_id.slice_explicit = route.slice_explicit;
  route_id.selector = route.selector;
  route_id.selector_explicit = route.selector_explicit;
  if (!segments.empty()) {
    route_id.available_begin = segments.begin()->first;
    route_id.available_end = segments.rbegin()->first;
  }
  return route_id;
}

std::string detect_dbc_for_fingerprint(std::string_view car_fingerprint) {
  return std::string(dbc_for_car_fingerprint(car_fingerprint));
}

std::vector<std::string> available_dbc_names_impl() {
  std::set<std::string> names;
  for (const fs::path &dbc_dir : {
         repo_root() / "opendbc_repo" / "opendbc" / "dbc",
         repo_root() / "openpilot" / "tools" / "jotpluggler" / "generated_dbcs",
       }) {
    if (fs::exists(dbc_dir) && fs::is_directory(dbc_dir)) {
      for (const auto &entry : fs::directory_iterator(dbc_dir)) {
        if (!entry.is_regular_file() || entry.path().extension() != ".dbc") {
          continue;
        }
        names.insert(entry.path().stem().string());
      }
    }
  }
  for (const auto &[_, dbc_name] : kCarFingerprintToDbc) {
    if (!dbc_name.empty()) {
      names.insert(std::string(dbc_name));
    }
  }
  return std::vector<std::string>(names.begin(), names.end());
}

fs::path resolve_dbc_path(const std::string &dbc_name) {
  for (const fs::path &candidate : {
         repo_root() / "opendbc_repo" / "opendbc" / "dbc" / (dbc_name + ".dbc"),
         repo_root() / "openpilot" / "tools" / "jotpluggler" / "generated_dbcs" / (dbc_name + ".dbc"),
       }) {
    if (fs::exists(candidate)) return candidate;
  }
  throw std::runtime_error("DBC not found: " + dbc_name);
}

std::array<uint8_t, 3> parse_color(std::string_view color) {
  if (!color.empty() && color.front() == '#') {
    color.remove_prefix(1);
  }
  if (color.size() != 6) return {160, 170, 180};

  std::array<uint8_t, 3> out = {};
  for (size_t i = 0; i < 3; ++i) {
    const std::string byte(color.substr(i * 2, 2));
    char *end = nullptr;
    const long parsed = std::strtol(byte.c_str(), &end, 16);
    if (end == nullptr || *end != '\0' || parsed < 0 || parsed > 255) return {160, 170, 180};
    out[i] = static_cast<uint8_t>(parsed);
  }
  return out;
}

uint8_t operating_system_priority_to_level(uint8_t priority) {
  switch (priority) {
    case 2:
    case 3:
      return 10;
    case 4:
      return 20;
    case 5:
      return 30;
    case 6:
      return 40;
    case 7:
    default:
      return 50;
  }
}

uint8_t alert_status_to_level(cereal::SelfdriveState::AlertStatus status) {
  switch (status) {
    case cereal::SelfdriveState::AlertStatus::NORMAL:
      return 20;
    case cereal::SelfdriveState::AlertStatus::USER_PROMPT:
      return 30;
    case cereal::SelfdriveState::AlertStatus::CRITICAL:
      return 40;
  }
  return 20;
}

TimelineEntry::Type alert_status_to_timeline_type(cereal::SelfdriveState::AlertStatus status, bool enabled) {
  if (!enabled) {
    return TimelineEntry::Type::None;
  }
  switch (status) {
    case cereal::SelfdriveState::AlertStatus::NORMAL:
      return TimelineEntry::Type::Engaged;
    case cereal::SelfdriveState::AlertStatus::USER_PROMPT:
      return TimelineEntry::Type::AlertInfo;
    case cereal::SelfdriveState::AlertStatus::CRITICAL:
      return TimelineEntry::Type::AlertCritical;
  }
  return TimelineEntry::Type::Engaged;
}

void append_timeline_entry(std::vector<TimelineEntry> *timeline, double mono_time, TimelineEntry::Type type) {
  if (timeline == nullptr) {
    return;
  }
  if (!timeline->empty() && timeline->back().type == type) {
    timeline->back().end_time = std::max(timeline->back().end_time, mono_time);
    return;
  }
  timeline->push_back(TimelineEntry{
    .start_time = mono_time,
    .end_time = mono_time,
    .type = type,
  });
}

double operating_system_wall_time_seconds(uint64_t timestamp) {
  if (timestamp == 0) return 0.0;
  if (timestamp > 1000000000000ULL) return static_cast<double>(timestamp) / 1.0e9;
  if (timestamp > 1000000000ULL) return static_cast<double>(timestamp) / 1.0e6;
  return static_cast<double>(timestamp);
}

std::optional<uint64_t> json_u64_value(const json11::Json &value) {
  if (value.is_number()) {
    const double number = value.number_value();
    if (number >= 0.0) return static_cast<uint64_t>(number);
  }
  if (value.is_string()) {
    try {
      return static_cast<uint64_t>(std::stoull(value.string_value()));
    } catch (...) {
    }
  }
  return std::nullopt;
}

std::optional<int> json_int_value(const json11::Json &value) {
  if (value.is_number()) return value.int_value();
  if (value.is_string()) {
    try {
      return std::stoi(value.string_value());
    } catch (...) {
    }
  }
  return std::nullopt;
}

std::string json_value_for_log(const json11::Json &value) {
  if (value.is_string()) return value.string_value();
  if (value.is_bool()) return value.bool_value() ? "true" : "false";
  return value.dump();
}

std::string format_journal_context(const json11::Json &parsed, int pid, int tid) {
  std::vector<std::string> lines;
  if (pid != 0 || tid != 0) {
    lines.push_back("pid=" + std::to_string(pid) + ", tid=" + std::to_string(tid));
  }

  const std::array<const char *, 5> preferred_keys = {
    "_HOSTNAME",
    "_TRANSPORT",
    "PRIORITY",
    "SYSLOG_FACILITY",
    "__MONOTONIC_TIMESTAMP",
  };
  for (const char *key : preferred_keys) {
    const json11::Json &value = parsed[key];
    if (!value.is_null()) {
      lines.push_back(std::string(key) + "=" + json_value_for_log(value));
    }
  }
  return join(lines, "\n");
}

std::string alert_message_text(const cereal::SelfdriveState::Reader &state) {
  std::string text = state.getAlertText1().cStr();
  const std::string text2 = state.getAlertText2().cStr();
  if (!text2.empty()) {
    text += " - " + text2;
  }
  return text;
}

bool same_log_entry(const LogEntry &a, const LogEntry &b) {
  return a.mono_time == b.mono_time
      && a.level == b.level
      && a.source == b.source
      && a.func == b.func
      && a.message == b.message
      && a.context == b.context
      && a.origin == b.origin;
}

void append_log_event(cereal::Event::Which which,
                      const cereal::Event::Reader &event,
                      double time_offset,
                      std::vector<LogEntry> *logs,
                      std::string *last_alert_key) {
  const double boot_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
  const double mono_time = boot_time - time_offset;

  auto make_entry = [&](LogOrigin origin, uint8_t level = 20) {
    LogEntry e;
    e.mono_time = mono_time;
    e.boot_time = boot_time;
    e.origin = origin;
    e.level = level;
    return e;
  };

  switch (which) {
    case cereal::Event::Which::LOG_MESSAGE:
    case cereal::Event::Which::ERROR_LOG_MESSAGE: {
      const std::string raw = which == cereal::Event::Which::LOG_MESSAGE
        ? event.getLogMessage().cStr() : event.getErrorLogMessage().cStr();
      auto entry = make_entry(LogOrigin::Log, which == cereal::Event::Which::ERROR_LOG_MESSAGE ? 40 : 20);
      entry.source = "log";
      entry.message = raw;
      std::string err;
      if (const auto p = json11::Json::parse(raw, err); err.empty() && p.is_object()) {
        entry.wall_time = p["created"].number_value();
        if (p["levelnum"].is_number()) entry.level = static_cast<uint8_t>(p["levelnum"].int_value());
        const std::string fn = p["filename"].string_value();
        const int ln = p["lineno"].is_number() ? p["lineno"].int_value() : 0;
        entry.source = fn.empty() ? "log" : fn + (ln > 0 ? ":" + std::to_string(ln) : "");
        entry.func = p["funcname"].string_value();
        if (p["msg"].is_string()) entry.message = p["msg"].string_value();
        if (!p["ctx"].is_null()) entry.context = p["ctx"].dump();
      }
      logs->push_back(std::move(entry));
      break;
    }
    case cereal::Event::Which::OPERATING_SYSTEM_LOG: {
      const auto operating_system_log = event.getOperatingSystemLog();
      auto entry = make_entry(LogOrigin::OperatingSystem, operating_system_priority_to_level(operating_system_log.getPriority()));
      entry.wall_time = operating_system_wall_time_seconds(operating_system_log.getTs());
      entry.source = operating_system_log.hasTag() ? operating_system_log.getTag().cStr() : "operating_system";
      entry.message = operating_system_log.hasMessage() ? operating_system_log.getMessage().cStr() : std::string();
      entry.context = "pid=" + std::to_string(operating_system_log.getPid()) + ", tid=" + std::to_string(operating_system_log.getTid());
      if (!entry.message.empty()) {
        std::string err;
        if (const auto p = json11::Json::parse(entry.message, err); err.empty() && p.is_object()) {
          if (p["MESSAGE"].is_string()) entry.message = p["MESSAGE"].string_value();
          if (p["SYSLOG_IDENTIFIER"].is_string() && !p["SYSLOG_IDENTIFIER"].string_value().empty())
            entry.source = p["SYSLOG_IDENTIFIER"].string_value();
          if (auto pri = json_int_value(p["PRIORITY"]); pri.has_value())
            entry.level = operating_system_priority_to_level(*pri);
          if (auto ts = json_u64_value(p["__REALTIME_TIMESTAMP"]); ts.has_value())
            entry.wall_time = operating_system_wall_time_seconds(*ts);
          entry.context = format_journal_context(p, operating_system_log.getPid(), operating_system_log.getTid());
        }
      }
      logs->push_back(std::move(entry));
      break;
    }
    case cereal::Event::Which::SELFDRIVE_STATE: {
      const auto sd = event.getSelfdriveState();
      const std::string alert_type = sd.getAlertType().cStr();
      const std::string alert_text1 = sd.getAlertText1().cStr();
      if (alert_text1.empty() && alert_type.empty()) break;
      const std::string key = alert_type + "\n" + alert_text1 + "\n" + std::string(sd.getAlertText2().cStr());
      if (last_alert_key != nullptr && key == *last_alert_key) break;
      if (last_alert_key != nullptr) *last_alert_key = key;
      auto entry = make_entry(LogOrigin::Alert, alert_status_to_level(sd.getAlertStatus()));
      entry.source = "alert";
      entry.func = alert_type;
      entry.message = alert_message_text(sd);
      logs->push_back(std::move(entry));
      break;
    }
    default:
      break;
  }
}

std::vector<TimelineEntry> extract_segment_timeline(const std::vector<Event> &events) {
  std::vector<TimelineEntry> timeline;
  timeline.reserve(events.size() / 16);

  for (const Event &event_record : events) {
    if (event_record.which != cereal::Event::Which::SELFDRIVE_STATE) {
      continue;
    }
    with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
      const auto sd = event.getSelfdriveState();
      const double mono_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
      append_timeline_entry(&timeline, mono_time, alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
    });
  }

  return timeline;
}

std::vector<LogEntry> extract_segment_logs(const std::vector<Event> &events) {
  std::vector<LogEntry> logs;
  logs.reserve(events.size() / 8);
  std::string last_alert_key;

  for (const Event &event_record : events) {
    with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
      append_log_event(event_record.which, event, 0.0, &logs, &last_alert_key);
    });
  }

  return logs;
}

std::vector<ThumbnailFrame> extract_segment_thumbnails(const std::vector<Event> &events, int segment) {
  std::vector<ThumbnailFrame> thumbnails;
  for (const Event &event_record : events) {
    if (event_record.which != cereal::Event::Which::THUMBNAIL) continue;
    with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
      const auto thumbnail = event.getThumbnail();
      const auto jpeg = thumbnail.getThumbnail();
      if (jpeg.size() == 0) return;
      const uint64_t timestamp = thumbnail.getTimestampEof();
      ThumbnailFrame frame;
      frame.timestamp = static_cast<double>(timestamp != 0 ? timestamp : event.getLogMonoTime()) / 1.0e9;
      frame.segment = segment;
      frame.jpeg.assign(jpeg.begin(), jpeg.end());
      thumbnails.push_back(std::move(frame));
    });
  }
  return thumbnails;
}

RouteMetadata extract_segment_metadata(const std::vector<Event> &events) {
  RouteMetadata metadata;
  for (const Event &event_record : events) {
    if (event_record.which != cereal::Event::Which::CAR_PARAMS) continue;
    with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
      metadata.car_fingerprint = event.getCarParams().getCarFingerprint().cStr();
    });
    if (!metadata.car_fingerprint.empty()) break;
  }
  return metadata;
}

RouteMetadata detect_route_metadata(const std::map<int, SegmentLogs> &segments, LogSelector selector) {
  for (const auto &[_, segment] : segments) {
    const std::string &log_path = selector == LogSelector::Auto
      ? (!segment.qlog.empty() ? segment.qlog : segment.rlog)
      : selected_log_path(segment, selector);
    if (log_path.empty()) {
      continue;
    }
    LogReader reader;
    if (!reader.load(log_path, nullptr, true)) continue;
    RouteMetadata metadata = extract_segment_metadata(reader.events);
    if (!metadata.car_fingerprint.empty()) return metadata;
  }
  return {};
}

std::vector<double> normalize_sizes(const json11::Json &sizes_json, size_t child_count) {
  std::vector<double> parsed;
  if (sizes_json.is_array()) {
    for (const json11::Json &value : sizes_json.array_items()) {
      if (value.is_number()) {
        parsed.push_back(std::max(value.number_value(), 0.0));
      }
    }
  }

  if (parsed.size() != child_count || child_count == 0) return std::vector<double>(child_count, child_count == 0 ? 0.0 : 1.0 / static_cast<double>(child_count));

  const double total = std::accumulate(parsed.begin(), parsed.end(), 0.0);
  if (total <= 0.0) return std::vector<double>(child_count, 1.0 / static_cast<double>(child_count));
  for (double &value : parsed) {
    value /= total;
  }
  return parsed;
}

PlotRange parse_range(const json11::Json &pane_node) {
  PlotRange range;
  const json11::Json &range_node = pane_node["range"];
  if (range_node.is_object()) {
    range.valid = true;
    range.left = range_node["left"].number_value();
    range.right = range_node["right"].number_value();
    range.bottom = range_node["bottom"].number_value();
    range.top = range_node["top"].is_number() ? range_node["top"].number_value() : 1.0;
  }
  const json11::Json &limit_y_node = pane_node["y_limits"];
  if (limit_y_node.is_object()) {
    if (limit_y_node["min"].is_number()) {
      range.has_y_limit_min = true;
      range.y_limit_min = limit_y_node["min"].number_value();
    }
    if (limit_y_node["max"].is_number()) {
      range.has_y_limit_max = true;
      range.y_limit_max = limit_y_node["max"].number_value();
    }
  }
  return range;
}

Curve parse_curve(const json11::Json &curve_node) {
  Curve curve;
  curve.name = curve_node["name"].string_value();
  curve.label = curve_label(curve.name);
  curve.color = parse_color(curve_node["color"].string_value());

  const std::string transform_name = curve_node["transform"].string_value();
  if (transform_name == "derivative") {
    curve.derivative = true;
    curve.derivative_dt = curve_node["derivative_dt"].is_number() ? curve_node["derivative_dt"].number_value() : 0.0;
  } else if (transform_name == "scale") {
    curve.value_scale = curve_node["scale"].is_number() ? curve_node["scale"].number_value() : 1.0;
    curve.value_offset = curve_node["offset"].is_number() ? curve_node["offset"].number_value() : 0.0;
  }
  const json11::Json &custom_node = curve_node["custom_python"];
  if (custom_node.is_object()) {
    CustomPythonSeries spec;
    spec.linked_source = custom_node["linked_source"].string_value();
    spec.globals_code = custom_node["globals_code"].string_value();
    spec.function_code = custom_node["function_code"].string_value();
    for (const json11::Json &source : custom_node["additional_sources"].array_items()) {
      if (source.is_string()) {
        spec.additional_sources.push_back(source.string_value());
      }
    }
    curve.custom_python = std::move(spec);
  }
  return curve;
}

std::string pane_title(const json11::Json &dock_area_node) {
  const std::string raw = dock_area_node["title"].string_value();
  return raw.empty() ? "..." : raw;
}

Pane parse_dock_area(const json11::Json &dock_area_node) {
  Pane pane;
  const std::string kind = dock_area_node["kind"].string_value();
  if (kind == "map") {
    pane.kind = PaneKind::Map;
  } else if (kind == "thumbnail") {
    pane.kind = PaneKind::Thumbnail;
  } else if (kind == "camera") {
    pane.kind = PaneKind::Camera;
    const std::string camera_view = dock_area_node["camera_view"].string_value();
    if (const CameraViewSpec *spec = camera_view_spec_from_layout_name(camera_view)) {
      pane.camera_view = spec->view;
    } else {
      pane.camera_view = CameraViewKind::Road;
    }
  }
  pane.range = parse_range(dock_area_node);
  const json11::Json &curves_node = dock_area_node["curves"];
  if (curves_node.is_array()) {
    for (const json11::Json &curve_node : curves_node.array_items()) {
      if (curve_node.is_object()) {
        pane.curves.push_back(parse_curve(curve_node));
      }
    }
  }
  pane.title = pane_title(dock_area_node);
  return pane;
}

WorkspaceNode parse_workspace_node(const json11::Json &node, WorkspaceTab *tab) {
  WorkspaceNode workspace_node;
  if (!node.is_object()) return workspace_node;

  if (node["curves"].is_array()) {
    workspace_node.is_pane = true;
    workspace_node.pane_index = static_cast<int>(tab->panes.size());
    tab->panes.push_back(parse_dock_area(node));
    return workspace_node;
  }

  const json11::Json &children_node = node["children"];
  if (!children_node.is_array()) return workspace_node;

  const std::vector<json11::Json> children = children_node.array_items();
  if (children.empty()) return workspace_node;

  const std::string split = node["split"].string_value();
  workspace_node.orientation = split == "vertical" ? SplitOrientation::Vertical : SplitOrientation::Horizontal;
  const std::vector<double> sizes = normalize_sizes(node["sizes"], children.size());
  workspace_node.sizes.reserve(sizes.size());
  workspace_node.children.reserve(children.size());
  for (size_t i = 0; i < children.size(); ++i) {
    workspace_node.sizes.push_back(static_cast<float>(sizes[i]));
    workspace_node.children.push_back(parse_workspace_node(children[i], tab));
  }
  return workspace_node;
}

WorkspaceTab parse_tab(const json11::Json &tab, const fs::path &layout_path) {
  WorkspaceTab workspace_tab;
  workspace_tab.tab_name = tab["name"].string_value().empty() ? "tab1" : tab["name"].string_value();
  const json11::Json &dock_root = tab["root"];
  if (!dock_root.is_object()) throw std::runtime_error("Layout tab has no dock content: " + layout_path.string());
  workspace_tab.root = parse_workspace_node(dock_root, &workspace_tab);
  return workspace_tab;
}

SketchLayout parse_layout(const fs::path &layout_path) {
  const std::string text = util::read_file(layout_path.string());
  if (text.empty()) throw std::runtime_error("Failed to read layout JSON: " + layout_path.string());

  std::string parse_error;
  const json11::Json root = json11::Json::parse(text, parse_error);
  if (!parse_error.empty() || !root.is_object()) {
    throw std::runtime_error("Failed to parse layout JSON: " + layout_path.string());
  }
  SketchLayout layout;
  for (const json11::Json &tab : root["tabs"].array_items()) {
    if (tab.is_object()) {
      layout.tabs.push_back(parse_tab(tab, layout_path));
    }
  }
  if (layout.tabs.empty()) throw std::runtime_error("Layout has no tabs: " + layout_path.string());
  const json11::Json &tab_index = root["current_tab_index"].is_number() ? root["current_tab_index"] : root["currentTabIndex"];
  layout.current_tab_index = std::clamp(tab_index.is_number() ? tab_index.int_value() : 0,
                                        0,
                                        static_cast<int>(layout.tabs.size()) - 1);
  return layout;
}

const SchemaIndex &SchemaIndex::instance() {
  static const SchemaIndex index = [] {
    SchemaIndex out;
    out.fixed_paths = static_event_fixed_paths();
    out.fixed_series_count = out.fixed_paths.size();
    return out;
  }();
  return index;
}

bool is_absolute_curve(const std::string &name) {
  return !name.empty() && name.front() == '/';
}

void append_scalar_point(RouteSeries *series,
                         const std::string &path,
                         double tm,
                         double value) {
  if (series->path.empty()) {
    series->path = path;
  }
  series->times.push_back(tm);
  series->values.push_back(value);
}

// This has thousands of generated call sites. Inlining it duplicates vector
// growth logic throughout the extractor and is slower both to compile and run.
__attribute__((noinline)) void append_fixed_scalar_point(RouteSeries *series, double tm, double value) {
  series->times.push_back(tm);
  series->values.push_back(value);
}

CanMessageData *ensure_can_message(CanServiceKind service, uint8_t bus, uint32_t address, SeriesAccumulator *series) {
  const CanMessageId id{service, bus, address};
  auto [it, inserted] = series->can_message_slots.try_emplace(id, series->can_messages.size());
  if (inserted) {
    series->can_messages.push_back(CanMessageData{.id = id});
  }
  return &series->can_messages[it->second];
}

void append_can_frame(CanServiceKind service,
                      uint8_t bus,
                      uint32_t address,
                      uint16_t bus_time,
                      capnp::Data::Reader dat,
                      double tm,
                      SeriesAccumulator *series) {
  CanMessageData *message = ensure_can_message(service, bus, address, series);
  message->samples.push_back(CanFrameSample{
    .mono_time = tm,
    .bus_time = bus_time,
    .data = std::string(reinterpret_cast<const char *>(dat.begin()), dat.size()),
  });
}

void append_dynamic_scalar_point(const std::string &path, double tm, double value, SeriesAccumulator *series);

void decode_can_frame(const dbc::Database *can_dbc,
                      const std::string &service_name,
                      uint8_t bus,
                      uint32_t address,
                      const uint8_t *raw,
                      size_t data_size,
                      double tm,
                      SeriesAccumulator *series) {
  if (can_dbc == nullptr) {
    return;
  }
  const dbc::Message *message = can_dbc->message(address);
  if (message == nullptr) {
    return;
  }
  const std::string base_path = "/" + service_name + "/" + std::to_string(bus) + "/" + message->name;
  for (const dbc::Signal &signal : message->signals) {
    std::optional<double> value = dbc::signalValue(signal, *message, raw, data_size);
    if (!value.has_value()) continue;
    const std::string path = base_path + "/" + signal.name;
    append_dynamic_scalar_point(path, tm, *value, series);
    if (series->enum_info.find(path) == series->enum_info.end()) {
      std::vector<std::string> enum_names = can_dbc->enumNames(signal);
      if (!enum_names.empty()) {
        series->enum_info.emplace(path, EnumInfo{.names = std::move(enum_names)});
      }
    }
  }
}

void append_live_can_frame(CanServiceKind service,
                           const LiveCanFrame &frame,
                           double time_offset,
                           const dbc::Database *can_dbc,
                           SeriesAccumulator *series) {
  const double tm = frame.mono_time - time_offset;
  CanMessageData *message = ensure_can_message(service, frame.bus, frame.address, series);
  message->samples.push_back(CanFrameSample{
    .mono_time = tm,
    .bus_time = frame.bus_time,
    .data = frame.data,
  });
  decode_can_frame(can_dbc,
                   service == CanServiceKind::Can ? "can" : "sendcan",
                   frame.bus,
                   frame.address,
                   reinterpret_cast<const uint8_t *>(frame.data.data()),
                   frame.data.size(),
                   tm,
                   series);
}

SeriesAccumulator make_series_accumulator(const SchemaIndex &schema) {
  SeriesAccumulator out(schema.fixed_series_count);
  for (size_t i = 0; i < schema.fixed_paths.size(); ++i) {
    out.fixed_series[i].path = schema.fixed_paths[i];
  }
  return out;
}

size_t ensure_dynamic_slot(const std::string &path, SeriesAccumulator *series) {
  auto [it, inserted] = series->dynamic_slots.try_emplace(path, series->dynamic_series.size());
  if (inserted) {
    series->dynamic_series.push_back(RouteSeries{it->first});
  }
  return it->second;
}

RouteSeries *ensure_dynamic_series(const std::string &path, SeriesAccumulator *series) {
  return &series->dynamic_series[ensure_dynamic_slot(path, series)];
}

RouteSeries *ensure_list_scalar_series(const std::string &base_path, size_t index, SeriesAccumulator *series) {
  auto [it, _] = series->list_scalar_slots.try_emplace(base_path);
  std::vector<size_t> &slots = it->second;
  if (slots.size() <= index) {
    slots.resize(index + 1, INVALID_DYNAMIC_SLOT);
  }
  if (slots[index] == INVALID_DYNAMIC_SLOT) {
    slots[index] = ensure_dynamic_slot(base_path + "/" + std::to_string(index), series);
  }
  return &series->dynamic_series[slots[index]];
}

void append_dynamic_scalar_point(const std::string &path, double tm, double value, SeriesAccumulator *series) {
  append_scalar_point(ensure_dynamic_series(path, series), path, tm, value);
}

void append_event_fast(cereal::Event::Which which,
                       int32_t eidx_segnum,
                       kj::ArrayPtr<const capnp::word> data,
                       const dbc::Database *can_dbc,
                       bool skip_raw_can,
                       double time_offset,
                       SeriesAccumulator *series) {
  if (eidx_segnum != -1) {
    return;
  }
  with_parseable_event(data, [&](const cereal::Event::Reader &event) {
    append_event_static_reader(which, event, can_dbc, skip_raw_can, time_offset, series);
  });
}

void append_events_fast_range(const std::vector<Event> &events,
                              size_t begin,
                              size_t end,
                              const dbc::Database *can_dbc,
                              bool skip_raw_can,
                              SeriesAccumulator *series) {
  for (size_t i = begin; i < end; ++i) {
    const Event &event_record = events[i];
    append_event_fast(event_record.which,
                      event_record.eidx_segnum,
                      event_record.data,
                      can_dbc,
                      skip_raw_can,
                      0.0,
                      series);
  }
}

void merge_route_series(RouteSeries *dst, RouteSeries *src) {
  if (src->times.empty()) {
    return;
  }
  if (dst->times.empty()) {
    *dst = std::move(*src);
    return;
  }

  dst->times.reserve(dst->times.size() + src->times.size());
  dst->values.reserve(dst->values.size() + src->values.size());
  dst->times.insert(dst->times.end(), src->times.begin(), src->times.end());
  dst->values.insert(dst->values.end(), src->values.begin(), src->values.end());
}

void merge_can_message_data(CanMessageData *dst, CanMessageData *src) {
  if (src->samples.empty()) {
    return;
  }
  if (dst->samples.empty()) {
    *dst = std::move(*src);
    return;
  }
  dst->samples.reserve(dst->samples.size() + src->samples.size());
  dst->samples.insert(dst->samples.end(),
                      std::make_move_iterator(src->samples.begin()),
                      std::make_move_iterator(src->samples.end()));
}

void merge_series_accumulator(SeriesAccumulator *dst, SeriesAccumulator *src) {
  if (dst->fixed_series.size() != src->fixed_series.size()) {
    throw std::runtime_error("Fixed-series slot count mismatch during merge");
  }

  for (size_t i = 0; i < dst->fixed_series.size(); ++i) {
    merge_route_series(&dst->fixed_series[i], &src->fixed_series[i]);
  }
  for (auto &series : src->dynamic_series) {
    if (series.path.empty()) continue;
    RouteSeries &dst_series = dst->dynamic_series[ensure_dynamic_slot(series.path, dst)];
    merge_route_series(&dst_series, &series);
  }
  for (auto &message : src->can_messages) {
    CanMessageData &dst_message = *ensure_can_message(message.id.service, message.id.bus, message.id.address, dst);
    merge_can_message_data(&dst_message, &message);
  }
  for (auto &[path, info] : src->enum_info) {
    dst->enum_info.try_emplace(path, std::move(info));
  }
}

size_t populated_series_count(const SeriesAccumulator &series) {
  size_t count = 0;
  for (const RouteSeries &fixed : series.fixed_series) {
    count += !fixed.times.empty();
  }
  for (const RouteSeries &dynamic : series.dynamic_series) {
    count += !dynamic.times.empty();
  }
  return count;
}

bool series_is_sorted(const RouteSeries &series) {
  for (size_t i = 1; i < series.times.size(); ++i) {
    if (series.times[i] < series.times[i - 1]) return false;
  }
  return true;
}

void sort_series_by_time(RouteSeries *series) {
  if (series->times.size() <= 1 || series_is_sorted(*series)) {
    return;
  }
  std::vector<size_t> order(series->times.size());
  std::iota(order.begin(), order.end(), 0);
  std::sort(order.begin(), order.end(), [&](size_t a, size_t b) {
    return series->times[a] < series->times[b];
  });

  std::vector<double> sorted_times(series->times.size());
  std::vector<double> sorted_values(series->values.size());
  for (size_t i = 0; i < order.size(); ++i) {
    sorted_times[i] = series->times[order[i]];
    sorted_values[i] = series->values[order[i]];
  }
  series->times = std::move(sorted_times);
  series->values = std::move(sorted_values);
}

std::vector<RouteSeries> collect_series(SeriesAccumulator &&series) {
  std::vector<RouteSeries> out;
  out.reserve(series.fixed_series.size() + series.dynamic_series.size());
  for (auto &fixed : series.fixed_series) {
    sort_series_by_time(&fixed);
    if (!fixed.times.empty()) {
      out.push_back(std::move(fixed));
    }
  }
  for (auto &dynamic : series.dynamic_series) {
    sort_series_by_time(&dynamic);
    if (!dynamic.times.empty()) {
      out.push_back(std::move(dynamic));
    }
  }
  return out;
}

RouteData build_route_data(std::vector<RouteSeries> &&series_list,
                           std::vector<CanMessageData> &&can_messages,
                           std::vector<LogEntry> &&logs,
                           std::vector<TimelineEntry> &&timeline,
                           std::vector<ThumbnailFrame> &&thumbnails,
                           std::unordered_map<std::string, EnumInfo> &&enum_info,
                           std::string car_fingerprint,
                           std::string dbc_name) {
  RouteData route_data;
  route_data.series.reserve(series_list.size());
  route_data.paths.reserve(series_list.size());
  for (RouteSeries &series : series_list) {
    if (series.times.empty()) continue;
    route_data.has_time_range = true;
    route_data.x_min = route_data.series.empty() ? series.times.front() : std::min(route_data.x_min, series.times.front());
    route_data.x_max = route_data.series.empty() ? series.times.back() : std::max(route_data.x_max, series.times.back());
    route_data.paths.push_back(series.path);
    route_data.series.push_back(std::move(series));
  }

  std::sort(route_data.paths.begin(), route_data.paths.end());
  std::sort(route_data.series.begin(), route_data.series.end(), [](const RouteSeries &a, const RouteSeries &b) {
    return a.path < b.path;
  });
  std::sort(logs.begin(), logs.end(), [](const LogEntry &a, const LogEntry &b) {
    return a.mono_time < b.mono_time;
  });
  logs.erase(std::unique(logs.begin(), logs.end(), [](const LogEntry &a, const LogEntry &b) {
               return same_log_entry(a, b);
             }),
             logs.end());

  std::vector<LogEntry> deduped_logs;
  deduped_logs.reserve(logs.size());
  for (LogEntry &entry : logs) {
    if (!deduped_logs.empty()
        && entry.origin == LogOrigin::Alert
        && deduped_logs.back().origin == LogOrigin::Alert
        && deduped_logs.back().func == entry.func
        && deduped_logs.back().message == entry.message) {
      continue;
    }
    deduped_logs.push_back(std::move(entry));
  }
  route_data.logs = std::move(deduped_logs);

  if (!route_data.has_time_range && !route_data.logs.empty()) {
    route_data.has_time_range = true;
    route_data.x_min = route_data.logs.front().mono_time;
    route_data.x_max = route_data.logs.back().mono_time;
  }
  if (!route_data.has_time_range) {
    bool initialized = false;
    for (const CanMessageData &message : can_messages) {
      if (message.samples.empty()) continue;
      if (!initialized) {
        route_data.x_min = message.samples.front().mono_time;
        route_data.x_max = message.samples.back().mono_time;
        initialized = true;
      } else {
        route_data.x_min = std::min(route_data.x_min, message.samples.front().mono_time);
        route_data.x_max = std::max(route_data.x_max, message.samples.back().mono_time);
      }
    }
    route_data.has_time_range = initialized;
  }
  if (!route_data.has_time_range && !timeline.empty()) {
    route_data.has_time_range = true;
    route_data.x_min = timeline.front().start_time;
    route_data.x_max = timeline.back().end_time;
  }
  std::sort(thumbnails.begin(), thumbnails.end(), [](const ThumbnailFrame &a, const ThumbnailFrame &b) {
    return a.timestamp < b.timestamp;
  });
  if (!route_data.has_time_range && !thumbnails.empty()) {
    route_data.has_time_range = true;
    route_data.x_min = thumbnails.front().timestamp;
    route_data.x_max = thumbnails.back().timestamp;
  }

  if (route_data.has_time_range) {
    const double time_offset = route_data.x_min;
    for (RouteSeries &series : route_data.series) {
      for (double &tm : series.times) {
        tm -= time_offset;
      }
    }
    for (LogEntry &entry : route_data.logs) {
      entry.boot_time = entry.mono_time;
      entry.mono_time -= time_offset;
    }
    for (CanMessageData &message : can_messages) {
      for (CanFrameSample &sample : message.samples) {
        sample.mono_time -= time_offset;
      }
    }
    for (TimelineEntry &entry : timeline) {
      entry.start_time -= time_offset;
      entry.end_time -= time_offset;
    }
    for (ThumbnailFrame &thumbnail : thumbnails) {
      thumbnail.timestamp -= time_offset;
    }
    route_data.x_max -= time_offset;
    route_data.x_min = 0.0;
  }

  std::sort(timeline.begin(), timeline.end(), [](const TimelineEntry &a, const TimelineEntry &b) {
    return a.start_time < b.start_time;
  });
  std::vector<TimelineEntry> merged_timeline;
  merged_timeline.reserve(timeline.size());
  for (TimelineEntry &entry : timeline) {
    if (!merged_timeline.empty() && merged_timeline.back().type == entry.type) {
      merged_timeline.back().end_time = std::max(merged_timeline.back().end_time, entry.end_time);
      continue;
    }
    merged_timeline.push_back(std::move(entry));
  }
  route_data.timeline = std::move(merged_timeline);
  route_data.thumbnails = std::move(thumbnails);
  std::sort(can_messages.begin(), can_messages.end(), [](const CanMessageData &a, const CanMessageData &b) {
    return std::make_tuple(a.id.service, a.id.bus, a.id.address)
         < std::make_tuple(b.id.service, b.id.bus, b.id.address);
  });
  route_data.can_messages = std::move(can_messages);

  route_data.enum_info = std::move(enum_info);
  route_data.car_fingerprint = std::move(car_fingerprint);
  route_data.dbc_name = std::move(dbc_name);
  rebuild_gps_trace(&route_data);
  route_data.roots = collect_route_roots_for_paths(route_data.paths);
  return route_data;
}

const RouteSeries *find_route_series(const RouteData &route_data, std::string_view path) {
  auto it = std::lower_bound(route_data.series.begin(), route_data.series.end(), path,
                             [](const RouteSeries &series, std::string_view target) {
                               return series.path < target;
                             });
  if (it == route_data.series.end() || it->path != path) return nullptr;
  return &(*it);
}

std::optional<double> sample_series_at_time(const RouteSeries &series, double tm) {
  if (series.times.empty() || series.times.size() != series.values.size()) {
    return std::nullopt;
  }
  if (tm <= series.times.front()) {
    return series.values.front();
  }
  if (tm >= series.times.back()) {
    return series.values.back();
  }
  auto upper = std::lower_bound(series.times.begin(), series.times.end(), tm);
  if (upper == series.times.begin()) {
    return series.values.front();
  }
  if (upper == series.times.end()) {
    return series.values.back();
  }
  const size_t upper_index = static_cast<size_t>(std::distance(series.times.begin(), upper));
  const size_t lower_index = upper_index - 1;
  const double t0 = series.times[lower_index];
  const double t1 = series.times[upper_index];
  const double v0 = series.values[lower_index];
  const double v1 = series.values[upper_index];
  if (t1 <= t0) {
    return v0;
  }
  const double alpha = (tm - t0) / (t1 - t0);
  return v0 + (v1 - v0) * alpha;
}

}  // namespace

void rebuild_gps_trace(RouteData *route_data) {
  route_data->gps_trace = {};
  const RouteSeries *latitude = find_route_series(*route_data, "/gpsLocationExternal/latitude");
  const RouteSeries *longitude = find_route_series(*route_data, "/gpsLocationExternal/longitude");
  const RouteSeries *has_fix = find_route_series(*route_data, "/gpsLocationExternal/hasFix");
  if (latitude == nullptr || longitude == nullptr || has_fix == nullptr) {
    return;
  }

  const RouteSeries *bearing = find_route_series(*route_data, "/gpsLocationExternal/bearingDeg");
  size_t count = std::min({latitude->times.size(), latitude->values.size(),
                           longitude->times.size(), longitude->values.size(),
                           has_fix->times.size(), has_fix->values.size()});
  if (count == 0) {
    return;
  }

  bool found = false;
  route_data->gps_trace.points.reserve(count);
  for (size_t i = 0; i < count; ++i) {
    if (has_fix->values[i] < 0.5) {
      continue;
    }
    const double lat = latitude->values[i];
    const double lon = longitude->values[i];
    if (!std::isfinite(lat) || !std::isfinite(lon)) {
      continue;
    }
    const double tm = latitude->times[i];
    const float bearing_value = bearing != nullptr
      ? static_cast<float>(sample_series_at_time(*bearing, tm).value_or(0.0))
      : 0.0f;
    route_data->gps_trace.points.push_back(GpsPoint{
      .time = tm,
      .lat = lat,
      .lon = lon,
      .bearing = bearing_value,
      .type = timeline_type_at_time(route_data->timeline, tm),
    });
    if (!found) {
      route_data->gps_trace.min_lat = route_data->gps_trace.max_lat = lat;
      route_data->gps_trace.min_lon = route_data->gps_trace.max_lon = lon;
      found = true;
    } else {
      route_data->gps_trace.min_lat = std::min(route_data->gps_trace.min_lat, lat);
      route_data->gps_trace.max_lat = std::max(route_data->gps_trace.max_lat, lat);
      route_data->gps_trace.min_lon = std::min(route_data->gps_trace.min_lon, lon);
      route_data->gps_trace.max_lon = std::max(route_data->gps_trace.max_lon, lon);
    }
  }
  if (!found) {
    route_data->gps_trace = {};
  }
}

namespace {

void build_camera_index(const std::map<int, SegmentLogs> &segments,
                        const RouteData &route_data,
                        std::string SegmentLogs::*file_member,
                        std::string_view index_name,
                        CameraFeedIndex *out) {
  *out = {};
  out->segment_files.reserve(segments.size());

  std::unordered_set<int> available_segments;
  available_segments.reserve(segments.size());
  for (const auto &[segment_number, segment] : segments) {
    const std::string &path = segment.*file_member;
    if (path.empty()) continue;
    out->segment_files.push_back(CameraSegmentFile{
      .segment = segment_number,
      .path = path,
    });
    available_segments.insert(segment_number);
  }
  if (out->segment_files.empty()) {
    return;
  }

  const std::string prefix = "/" + std::string(index_name);
  const RouteSeries *segment_numbers = find_route_series(route_data, prefix + "/segmentNum");
  const RouteSeries *decode_indices = find_route_series(route_data, prefix + "/segmentId");
  if (decode_indices == nullptr) {
    decode_indices = find_route_series(route_data, prefix + "/segmentIdEncode");
  }
  const RouteSeries *frame_ids = find_route_series(route_data, prefix + "/frameId");
  if (segment_numbers == nullptr || decode_indices == nullptr) {
    return;
  }

  size_t count = std::min(segment_numbers->times.size(), segment_numbers->values.size());
  count = std::min(count, decode_indices->values.size());
  if (frame_ids != nullptr) {
    count = std::min(count, frame_ids->values.size());
  }
  out->entries.reserve(count);
  for (size_t i = 0; i < count; ++i) {
    const int segment_number = static_cast<int>(std::llround(segment_numbers->values[i]));
    if (available_segments.find(segment_number) == available_segments.end()) {
      continue;
    }
    const int decode_index = static_cast<int>(std::llround(decode_indices->values[i]));
    const uint32_t frame_id = frame_ids != nullptr
      ? static_cast<uint32_t>(std::llround(frame_ids->values[i]))
      : static_cast<uint32_t>(std::max(0, decode_index));
    out->entries.push_back(CameraFrameIndexEntry{
      .timestamp = segment_numbers->times[i],
      .segment = segment_number,
      .decode_index = decode_index,
      .frame_id = frame_id,
    });
  }

  std::sort(out->entries.begin(), out->entries.end(),
            [](const CameraFrameIndexEntry &a, const CameraFrameIndexEntry &b) {
              return a.timestamp < b.timestamp;
            });
}

size_t load_worker_budget() {
  size_t worker_count = std::thread::hardware_concurrency();
  if (worker_count == 0) {
    worker_count = 1;
  }
  if (const char *raw = std::getenv("JOTP_LOAD_WORKERS"); raw != nullptr && std::strlen(raw) > 0) {
    char *end = nullptr;
    const unsigned long parsed = std::strtoul(raw, &end, 10);
    if (end != nullptr && *end == '\0' && parsed > 0) {
      worker_count = static_cast<size_t>(parsed);
    }
  }
  return std::max<size_t>(1, worker_count);
}

size_t segment_worker_count(size_t segment_count, size_t worker_budget) {
  return std::max<size_t>(1, std::min(worker_budget, segment_count));
}

size_t extract_chunk_count(size_t event_count, size_t worker_budget, size_t segment_workers) {
  if (event_count < 4096) return 1;
  const size_t per_segment_budget = std::max<size_t>(1, worker_budget / std::max<size_t>(1, segment_workers));
  const size_t chunk_target = std::max<size_t>(1, (event_count + 14999) / 15000);
  return std::max<size_t>(1, std::min({per_segment_budget, chunk_target, static_cast<size_t>(8)}));
}

SeriesAccumulator extract_segment_series(const std::vector<Event> &events,
                                         const SchemaIndex &schema,
                                         const dbc::Database *can_dbc,
                                         bool skip_raw_can,
                                         size_t worker_budget,
                                         size_t segment_workers) {
  const size_t chunk_count = extract_chunk_count(events.size(), worker_budget, segment_workers);
  if (chunk_count <= 1 || events.empty()) {
    SeriesAccumulator series = make_series_accumulator(schema);
    append_events_fast_range(events, 0, events.size(), can_dbc, skip_raw_can, &series);
    return series;
  }

  const size_t events_per_chunk = (events.size() + chunk_count - 1) / chunk_count;
  std::vector<SeriesAccumulator> chunk_results;
  chunk_results.reserve(chunk_count);
  for (size_t i = 0; i < chunk_count; ++i) {
    chunk_results.push_back(make_series_accumulator(schema));
  }

  std::vector<std::thread> workers;
  workers.reserve(chunk_count > 0 ? chunk_count - 1 : 0);
  for (size_t chunk = 1; chunk < chunk_count; ++chunk) {
    workers.emplace_back([&, chunk]() {
      const size_t begin = chunk * events_per_chunk;
      const size_t end = std::min(events.size(), begin + events_per_chunk);
      append_events_fast_range(events, begin, end, can_dbc, skip_raw_can, &chunk_results[chunk]);
    });
  }
  append_events_fast_range(events, 0, std::min(events.size(), events_per_chunk), can_dbc, skip_raw_can, &chunk_results[0]);
  for (std::thread &worker : workers) {
    worker.join();
  }

  SeriesAccumulator merged = make_series_accumulator(schema);
  for (SeriesAccumulator &chunk : chunk_results) {
    merge_series_accumulator(&merged, &chunk);
  }
  return merged;
}

LoadedRouteArtifacts load_route_series_parallel(
    const std::map<int, SegmentLogs> &segments,
    const SchemaIndex &schema,
    const dbc::Database *can_dbc,
    LogSelector selector,
    bool skip_raw_can,
    LoadStats *stats) {
  struct SegmentResult {
    SeriesAccumulator series;
    std::vector<LogEntry> logs;
    std::vector<TimelineEntry> timeline;
    std::vector<ThumbnailFrame> thumbnails;
  };

  const std::vector<std::pair<int, SegmentLogs>> segment_list(segments.begin(), segments.end());
  std::vector<SegmentResult> results;
  results.reserve(segment_list.size());
  for (size_t i = 0; i < segment_list.size(); ++i) {
    results.emplace_back(SegmentResult{make_series_accumulator(schema)});
  }
  std::atomic<size_t> next_segment{0};
  std::mutex error_mutex;
  std::string first_error;
  const size_t worker_budget = static_cast<size_t>(stats->num_workers);
  const size_t segment_workers = segment_worker_count(segment_list.size(), worker_budget);

  auto worker = [&]() {
    while (true) {
      const size_t index = next_segment.fetch_add(1);
      if (index >= segment_list.size()) {
        return;
      }

      const auto &[segment_number, segment] = segment_list[index];
      const std::string &log_path = selected_log_path(segment, selector);
      LoadStats::SegmentStats &segment_stats = stats->segments[index];
      segment_stats.segment_number = segment_number;
      segment_stats.log_path = log_path;
      if (log_path.empty()) {
        segment_stats.failed = true;
        std::lock_guard<std::mutex> lock(error_mutex);
        if (first_error.empty()) {
          first_error = "Missing log path for segment " + std::to_string(segment_number);
        }
        stats->publish(RouteLoadStage::DownloadingSegment, index, std::to_string(segment_number));
        stats->publish(RouteLoadStage::ParsingSegment, index, std::to_string(segment_number));
        continue;
      }

      LogReader reader;
      if (!reader.load(log_path, nullptr, true)) {
        segment_stats.failed = true;
        std::lock_guard<std::mutex> lock(error_mutex);
        if (first_error.empty()) {
          first_error = "Failed to load log segment: " + log_path;
        }
        stats->publish(RouteLoadStage::DownloadingSegment, index, std::to_string(segment_number));
        stats->publish(RouteLoadStage::ParsingSegment, index, std::to_string(segment_number));
        continue;
      }

      segment_stats.download_seconds = reader.download_seconds();
      segment_stats.decompress_seconds = reader.decompress_seconds();
      segment_stats.parse_seconds = reader.parse_seconds();
      segment_stats.compressed_bytes = reader.compressed_size();
      segment_stats.decompressed_bytes = reader.decompressed_size();
      stats->bytes_downloaded.fetch_add(reader.compressed_size());
      stats->segments_downloaded.fetch_add(1);
      stats->publish(RouteLoadStage::DownloadingSegment, index, std::to_string(segment_number));

      const auto extract_start = LoadStats::Clock::now();
      results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers);
      results[index].logs = extract_segment_logs(reader.events);
      results[index].timeline = extract_segment_timeline(reader.events);
      results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number);
      segment_stats.extract_seconds = std::chrono::duration<double>(LoadStats::Clock::now() - extract_start).count();
      segment_stats.event_count = reader.events.size();
      segment_stats.series_count = populated_series_count(results[index].series);
      stats->segments_parsed.fetch_add(1);
      stats->publish(RouteLoadStage::ParsingSegment, index, std::to_string(segment_number));
    }
  };

  std::vector<std::thread> workers;
  workers.reserve(segment_workers);
  for (size_t i = 0; i < segment_workers; ++i) {
    workers.emplace_back(worker);
  }
  for (std::thread &thread : workers) {
    thread.join();
  }

  if (!first_error.empty()) throw std::runtime_error(first_error);

  stats->merge_start = LoadStats::Clock::now();
  SeriesAccumulator merged = make_series_accumulator(schema);
  for (size_t i = 0; i < results.size(); ++i) {
    merge_series_accumulator(&merged, &results[i].series);
  }
  std::vector<LogEntry> logs;
  std::vector<TimelineEntry> timeline;
  std::vector<ThumbnailFrame> thumbnails;
  for (SegmentResult &result : results) {
    if (!result.logs.empty()) {
      logs.insert(logs.end(),
                  std::make_move_iterator(result.logs.begin()),
                  std::make_move_iterator(result.logs.end()));
    }
    if (!result.timeline.empty()) {
      timeline.insert(timeline.end(),
                      std::make_move_iterator(result.timeline.begin()),
                      std::make_move_iterator(result.timeline.end()));
    }
    if (!result.thumbnails.empty()) {
      thumbnails.insert(thumbnails.end(),
                        std::make_move_iterator(result.thumbnails.begin()),
                        std::make_move_iterator(result.thumbnails.end()));
    }
  }
  LoadedRouteArtifacts artifacts;
  artifacts.series = collect_series(std::move(merged));
  artifacts.can_messages = std::move(merged.can_messages);
  artifacts.logs = std::move(logs);
  artifacts.timeline = std::move(timeline);
  artifacts.thumbnails = std::move(thumbnails);
  artifacts.enum_info = std::move(merged.enum_info);
  stats->merge_end = LoadStats::Clock::now();
  return artifacts;
}

std::vector<std::string> collect_layout_roots(const SketchLayout &layout) {
  std::vector<std::string> roots;
  for (const auto &tab : layout.tabs) {
    for (const auto &pane : tab.panes) {
      for (const auto &curve : pane.curves) {
        std::string root = "custom";
        if (is_absolute_curve(curve.name)) {
          const size_t slash = curve.name.find('/', 1);
          root = curve.name.substr(1, slash == std::string::npos ? std::string::npos : slash - 1);
        }
        if (std::find(roots.begin(), roots.end(), root) == roots.end()) {
          roots.push_back(root);
        }
      }
    }
  }
  if (roots.empty()) {
    roots.push_back("layout");
  }
  return roots;
}

}  // namespace

std::vector<std::string> collect_route_roots_for_paths(const std::vector<std::string> &paths) {
  std::vector<std::string> roots;
  for (const std::string &path : paths) {
    if (!is_absolute_curve(path)) continue;
    const size_t slash = path.find('/', 1);
    const std::string root = path.substr(1, slash == std::string::npos ? std::string::npos : slash - 1);
    if (!root.empty() && std::find(roots.begin(), roots.end(), root) == roots.end()) {
      roots.push_back(root);
    }
  }
  std::sort(roots.begin(), roots.end());
  return roots;
}

struct StreamAccumulator::Impl {
  const SchemaIndex &schema = SchemaIndex::instance();
  SeriesAccumulator series = make_series_accumulator(schema);
  std::vector<LogEntry> logs;
  std::vector<TimelineEntry> timeline;
  std::string last_alert_key;
  std::string manual_dbc_name;
  std::string detected_dbc_name;
  std::string car_fingerprint;
  std::optional<dbc::Database> can_dbc;
  std::optional<double> time_offset;

  void refresh_dbc() {
    const std::string next_dbc = !manual_dbc_name.empty() ? manual_dbc_name : detect_dbc_for_fingerprint(car_fingerprint);
    if (next_dbc == detected_dbc_name) {
      return;
    }
    detected_dbc_name = next_dbc;
    can_dbc = load_dbc_by_name(detected_dbc_name);
  }
};

StreamAccumulator::StreamAccumulator(const std::string &dbc_name, std::optional<double> time_offset)
  : impl_(std::make_unique<Impl>()) {
  impl_->manual_dbc_name = dbc_name;
  impl_->time_offset = time_offset;
  impl_->refresh_dbc();
}

StreamAccumulator::~StreamAccumulator() = default;

void StreamAccumulator::setDbcName(const std::string &dbc_name) {
  impl_->manual_dbc_name = dbc_name;
  impl_->refresh_dbc();
}

void StreamAccumulator::appendEvent(kj::ArrayPtr<const capnp::word> data) {
  with_parseable_event(data, [&](const cereal::Event::Reader &event) {
    const cereal::Event::Which which = event.which();
    const double boot_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
    if (!impl_->time_offset.has_value()) {
      impl_->time_offset = boot_time;
    }
    if (which == cereal::Event::Which::CAR_PARAMS) {
      const std::string fingerprint = event.getCarParams().getCarFingerprint().cStr();
      if (!fingerprint.empty() && fingerprint != impl_->car_fingerprint) {
        impl_->car_fingerprint = fingerprint;
        impl_->refresh_dbc();
      }
    }

    append_event_static_reader(which,
                               event,
                               impl_->can_dbc ? &*impl_->can_dbc : nullptr,
                               impl_->can_dbc.has_value(),
                               *impl_->time_offset,
                               &impl_->series);
    append_log_event(which, event, *impl_->time_offset, &impl_->logs, &impl_->last_alert_key);
    if (which == cereal::Event::Which::SELFDRIVE_STATE) {
      const auto sd = event.getSelfdriveState();
      append_timeline_entry(&impl_->timeline, boot_time - *impl_->time_offset,
                            alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
    }
  });
}

void StreamAccumulator::appendCanFrames(CanServiceKind service, const std::vector<LiveCanFrame> &frames) {
  if (frames.empty()) {
    return;
  }
  if (!impl_->time_offset.has_value()) {
    impl_->time_offset = frames.front().mono_time;
  }
  for (const LiveCanFrame &frame : frames) {
    append_live_can_frame(service,
                          frame,
                          *impl_->time_offset,
                          impl_->can_dbc ? &*impl_->can_dbc : nullptr,
                          &impl_->series);
  }
}

StreamExtractBatch StreamAccumulator::takeBatch() {
  StreamExtractBatch batch;
  batch.car_fingerprint = impl_->car_fingerprint;
  batch.dbc_name = impl_->detected_dbc_name;
  if (impl_->time_offset.has_value()) {
    batch.has_time_offset = true;
    batch.time_offset = *impl_->time_offset;
  }
  if (impl_->logs.empty() && impl_->timeline.empty()
      && populated_series_count(impl_->series) == 0
      && impl_->series.enum_info.empty()
      && impl_->series.can_messages.empty()) {
    return batch;
  }

  SeriesAccumulator emitted = std::move(impl_->series);
  batch.can_messages = std::move(emitted.can_messages);
  batch.enum_info = std::move(emitted.enum_info);
  batch.series = collect_series(std::move(emitted));
  batch.logs = std::move(impl_->logs);
  batch.timeline = std::move(impl_->timeline);
  impl_->series = make_series_accumulator(impl_->schema);
  impl_->logs.clear();
  impl_->timeline.clear();
  return batch;
}

const std::string &StreamAccumulator::carFingerprint() const {
  return impl_->car_fingerprint;
}

const std::string &StreamAccumulator::dbc_name() const {
  return impl_->detected_dbc_name;
}

std::optional<double> StreamAccumulator::timeOffset() const {
  return impl_->time_offset;
}

SketchLayout load_sketch_layout(const fs::path &layout_path) {
  SketchLayout layout = parse_layout(layout_path);
  layout.roots = collect_layout_roots(layout);
  return layout;
}

RouteData load_route_data(const std::string &route_name,
                          const std::string &data_dir,
                          const std::string &dbc_name,
                          const RouteLoadProgressCallback &progress) {
  if (route_name.empty()) return RouteData{};

  const RouteSelection route = parse_route_selection(route_name);
  if (route.canonical_name.empty() || (data_dir.empty() && route.dongle_id.empty())) {
    throw std::runtime_error("Invalid route format: " + route_name);
  }
  LoadStats stats(progress);
  stats.load_start = LoadStats::Clock::now();
  std::map<int, SegmentLogs> segments = data_dir.empty()
    ? load_segments_from_server(route)
    : load_segments_from_local(route, data_dir);
  segments = trim_segments(std::move(segments), route);
  if (segments.empty()) throw std::runtime_error("No log segments found for " + route_name);
  stats.resolve_end = LoadStats::Clock::now();
  stats.segment_count = segments.size();
  stats.total_segments.store(segments.size());
  stats.num_workers = static_cast<int>(load_worker_budget());
  stats.segments.resize(segments.size());
  stats.publish(RouteLoadStage::Resolving, 0, {});

  const RouteMetadata metadata = detect_route_metadata(segments, route.selector);
  const std::string resolved_dbc = !dbc_name.empty() ? dbc_name : detect_dbc_for_fingerprint(metadata.car_fingerprint);
  const std::optional<dbc::Database> can_dbc = load_dbc_by_name(resolved_dbc);

  const SchemaIndex &schema = SchemaIndex::instance();
  LoadedRouteArtifacts artifacts = load_route_series_parallel(segments, schema, can_dbc ? &*can_dbc : nullptr,
                                                             route.selector, can_dbc.has_value(), &stats);
  RouteData route_data = build_route_data(std::move(artifacts.series),
                                          std::move(artifacts.can_messages),
                                          std::move(artifacts.logs),
                                          std::move(artifacts.timeline),
                                          std::move(artifacts.thumbnails),
                                          std::move(artifacts.enum_info),
                                          metadata.car_fingerprint,
                                          resolved_dbc);
  route_data.route_id = make_route_identifier(route, segments);
  build_camera_index(segments, route_data, &SegmentLogs::narrow_road, "narrowRoadEncodeIdx", &route_data.road_camera);
  build_camera_index(segments, route_data, &SegmentLogs::cabin, "cabinEncodeIdx", &route_data.cabin_camera);
  build_camera_index(segments, route_data, &SegmentLogs::wide_road, "wideRoadEncodeIdx", &route_data.wide_road_camera);
  build_camera_index(segments, route_data, &SegmentLogs::qcamera, "qNarrowRoadEncodeIdx", &route_data.qroad_camera);
  stats.load_end = LoadStats::Clock::now();
  stats.publish(RouteLoadStage::Finished, segments.size(), {});
  stats.print_summary(route_data.series.size());
  return route_data;
}

RouteIdentifier parse_route_identifier(std::string_view route_name) {
  return make_route_identifier(parse_route_selection(std::string(route_name)), {});
}

std::vector<std::string> available_dbc_names() {
  return available_dbc_names_impl();
}

std::optional<dbc::Database> load_dbc_by_name(const std::string &dbc_name) {
  if (dbc_name.empty()) {
    return std::nullopt;
  }
  try {
    return std::optional<dbc::Database>(std::in_place, resolve_dbc_path(dbc_name));
  } catch (...) {
    return std::nullopt;
  }
}

std::vector<RouteSeries> decode_can_messages(const std::vector<CanMessageData> &can_messages,
                                             const std::string &dbc_name,
                                             std::unordered_map<std::string, EnumInfo> *enum_info) {
  if (enum_info != nullptr) {
    enum_info->clear();
  }
  const std::optional<dbc::Database> can_dbc = load_dbc_by_name(dbc_name);
  if (!can_dbc.has_value()) {
    return {};
  }

  SeriesAccumulator series;
  for (const CanMessageData &message : can_messages) {
    const char *service_name = message.id.service == CanServiceKind::Can ? "can" : "sendcan";
    for (const CanFrameSample &sample : message.samples) {
      decode_can_frame(&*can_dbc,
                       service_name,
                       message.id.bus,
                       message.id.address,
                       reinterpret_cast<const uint8_t *>(sample.data.data()),
                       sample.data.size(),
                       sample.mono_time,
                       &series);
    }
  }
  if (enum_info != nullptr) {
    *enum_info = std::move(series.enum_info);
  }
  return collect_series(std::move(series));
}
