use chrono::offset::TimeZone;
use chrono::{DateTime, Utc};
use hashbrown::{HashMap, HashSet};
use intern::intern;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::hash;
use std::ops::{Add, Sub};
use std::sync::Arc;
use std::time::Duration;

pub mod interpolate;
pub mod metric;
pub mod pool;
pub mod selector;
pub mod tests;

pub use pool::{Connection, Pool};

intern!(pub struct Metric);
intern!(pub struct Benchmark);
intern!(pub struct TargetName);
intern!(pub struct TagName);
intern!(pub struct RepoName);

pub fn intern_target_name(target: &str) -> TargetName {
    intern(target)
}

#[derive(Debug, PartialEq, Eq)]
pub struct QueuedCommit {
    pub pr: u32,
    pub sha: String,
    pub parent_sha: String,
    pub include: Option<String>,
    pub exclude: Option<String>,
    pub runs: Option<i32>,
    pub commit_date: Option<Date>,
    pub backends: Option<String>,
    pub repo: String,
}

#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date(pub DateTime<Utc>);

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DateParseError {
    pub input: String,
    pub format: String,
    pub error: chrono::ParseError,
}

impl std::str::FromStr for Date {
    type Err = DateParseError;
    fn from_str(s: &str) -> Result<Date, DateParseError> {
        match DateTime::parse_from_rfc3339(s) {
            Ok(value) => Ok(Date(value.with_timezone(&Utc))),
            Err(error) => Err(DateParseError {
                input: s.to_string(),
                format: "RFC 3339".to_string(),
                error,
            }),
        }
    }
}

impl Date {
    pub fn ymd_hms(year: i32, month: u32, day: u32, h: u32, m: u32, s: u32) -> Date {
        Date(Utc.with_ymd_and_hms(year, month, day, h, m, s).unwrap())
    }

    pub fn empty() -> Date {
        Date::ymd_hms(2000, 1, 1, 1, 1, 1)
    }
}

impl fmt::Display for Date {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0.to_rfc3339())
    }
}

impl From<DateTime<Utc>> for Date {
    fn from(datetime: DateTime<Utc>) -> Date {
        Date(datetime)
    }
}

impl PartialEq<DateTime<Utc>> for Date {
    fn eq(&self, other: &DateTime<Utc>) -> bool {
        self.0 == *other
    }
}

impl Sub<chrono::Duration> for Date {
    type Output = Date;
    fn sub(self, rhs: chrono::Duration) -> Date {
        Date(self.0 - rhs)
    }
}

impl Add<chrono::Duration> for Date {
    type Output = Date;
    fn add(self, rhs: chrono::Duration) -> Date {
        Date(self.0 + rhs)
    }
}

impl Serialize for Date {
    fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        serializer.serialize_str(&self.0.to_rfc3339())
    }
}

impl<'de> Deserialize<'de> for Date {
    fn deserialize<D>(deserializer: D) -> ::std::result::Result<Date, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        struct DateVisitor;

        impl serde::de::Visitor<'_> for DateVisitor {
            type Value = Date;

            fn visit_str<E>(self, value: &str) -> ::std::result::Result<Date, E>
            where
                E: serde::de::Error,
            {
                value.parse::<Date>().map_err(|_| {
                    serde::de::Error::invalid_value(serde::de::Unexpected::Str(value), &self)
                })
            }

            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str("an RFC 3339 date")
            }
        }

        deserializer.deserialize_str(DateVisitor)
    }
}

/// A master-branch commit with parent/PR metadata, loaded from the DB.
///
/// This replaces the old network-fetched `bors-commit-list` data.
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct MasterCommitInfo {
    pub sha: String,
    pub parent_sha: String,
    #[serde(default)]
    pub pr: Option<u32>,
    pub date: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum CommitType {
    Try,
    Master,
}

impl FromStr for CommitType {
    type Err = String;

    fn from_str(ty: &str) -> Result<Self, Self::Err> {
        match ty {
            "try" => Ok(CommitType::Try),
            "master" => Ok(CommitType::Master),
            _ => Err(format!("Wrong commit type {ty}")),
        }
    }
}

#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Commit {
    pub sha: String,
    pub date: Date,
    pub r#type: CommitType,
}

impl Commit {
    pub fn is_try(&self) -> bool {
        matches!(self.r#type, CommitType::Try)
    }
    pub fn is_master(&self) -> bool {
        matches!(self.r#type, CommitType::Master)
    }
}

impl hash::Hash for Commit {
    fn hash<H: hash::Hasher>(&self, hasher: &mut H) {
        self.sha.hash(hasher);
    }
}

impl PartialEq for Commit {
    fn eq(&self, other: &Self) -> bool {
        self.sha == other.sha
    }
}

impl Eq for Commit {}

impl PartialOrd for Commit {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Commit {
    fn cmp(&self, other: &Self) -> Ordering {
        self.date
            .cmp(&other.date)
            .then_with(|| self.sha.cmp(&other.sha))
    }
}

/// The compilation profile (i.e., how the crate was built)
#[derive(
    Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Deserialize, serde::Serialize,
)]
pub enum Profile {
    /// A checked build (i.e., no codegen)
    Check,
    /// A debug build (i.e., low optimizations)
    Debug,
    /// A doc build
    Doc,
    /// A doc build with `--output-format=json` option.
    DocJson,
    /// An optimized "release" build
    Opt,
    /// A Clippy run
    Clippy,
}

impl Profile {
    pub fn as_str(self) -> &'static str {
        match self {
            Profile::Check => "check",
            Profile::Opt => "opt",
            Profile::Debug => "debug",
            Profile::Doc => "doc",
            Profile::DocJson => "doc-json",
            Profile::Clippy => "clippy",
        }
    }

    /// Set of default profiles that should be benchmarked for a master/try artifact.
    pub fn default_profiles() -> Vec<Self> {
        vec![Profile::Check, Profile::Debug, Profile::Opt, Profile::Doc]
    }

    pub fn all_values() -> &'static [Self] {
        &[
            Self::Check,
            Self::Debug,
            Self::Opt,
            Self::Doc,
            Self::DocJson,
            Self::Clippy,
        ]
    }
}

impl std::str::FromStr for Profile {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s.to_ascii_lowercase().as_str() {
            "check" => Profile::Check,
            "debug" => Profile::Debug,
            "doc" => Profile::Doc,
            "doc-json" => Profile::DocJson,
            "opt" => Profile::Opt,
            "clippy" => Profile::Clippy,
            _ => return Err(format!("{s} is not a profile")),
        })
    }
}

impl fmt::Display for Profile {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// The scenario under test - composed of incremental cache state
/// and sometimes a code change.
///
/// These are usually reported to users in a "flipped" way. For example,
/// `Cache::Empty` means we're doing a "full" build. We present this to users as "full".
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Scenario {
    /// Empty cache (i.e., full build)
    Empty,
    /// Empty cache but still incremental (i.e., a full incremental build)
    IncrementalEmpty,
    /// Cache is fully up-to-date (i.e., no code has changed)
    IncrementalFresh,
    /// Cache is mostly up-to-date but some code has been changed
    IncrementalPatch(PatchName),
}

intern!(pub struct PatchName);

impl std::str::FromStr for Scenario {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s.to_ascii_lowercase().as_str() {
            "full" => Scenario::Empty,
            "incr-full" => Scenario::IncrementalEmpty,
            "incr-unchanged" => Scenario::IncrementalFresh,
            _ => {
                if let Some(stripped) = s.strip_prefix("incr-patched: ") {
                    Scenario::IncrementalPatch(PatchName::from(stripped))
                } else {
                    return Err(format!("{s} is not a scenario"));
                }
            }
        })
    }
}

impl fmt::Display for Scenario {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Scenario::Empty => write!(f, "full"),
            Scenario::IncrementalEmpty => write!(f, "incr-full"),
            Scenario::IncrementalFresh => write!(f, "incr-unchanged"),
            Scenario::IncrementalPatch(name) => write!(f, "incr-patched: {name}"),
        }
    }
}

impl Scenario {
    pub fn to_id(&self) -> String {
        match self {
            Scenario::Empty => "full".to_string(),
            Scenario::IncrementalEmpty => "incr-full".to_string(),
            Scenario::IncrementalFresh => "incr-unchanged".to_string(),
            Scenario::IncrementalPatch(name) => format!("incr-patched-{name}"),
        }
    }
}

use anyhow::anyhow;
use std::cmp::Ordering;
use std::str::FromStr;

// We sort println before all other patches.
impl Ord for Scenario {
    fn cmp(&self, other: &Scenario) -> Ordering {
        match (self, other) {
            (a, b) if a == b => Ordering::Equal,
            (Scenario::Empty, _) => Ordering::Less,
            (Scenario::IncrementalEmpty, Scenario::Empty) => Ordering::Greater,
            (Scenario::IncrementalEmpty, _) => Ordering::Less,
            (Scenario::IncrementalFresh, Scenario::Empty) => Ordering::Greater,
            (Scenario::IncrementalFresh, Scenario::IncrementalEmpty) => Ordering::Greater,
            (Scenario::IncrementalFresh, _) => Ordering::Less,
            (Scenario::IncrementalPatch(_), Scenario::Empty) => Ordering::Greater,
            (Scenario::IncrementalPatch(_), Scenario::IncrementalEmpty) => Ordering::Greater,
            (Scenario::IncrementalPatch(_), Scenario::IncrementalFresh) => Ordering::Greater,
            (Scenario::IncrementalPatch(a), Scenario::IncrementalPatch(b)) => {
                if a == "println" {
                    Ordering::Less
                } else if b == "println" {
                    Ordering::Greater
                } else {
                    a.cmp(b)
                }
            }
        }
    }
}

impl PartialOrd for Scenario {
    fn partial_cmp(&self, other: &Scenario) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

/// Target representing an Rust target triple, for a full list of targets and
/// their support see;
/// https://doc.rust-lang.org/nightly/rustc/platform-support.html
///
/// We separate "first-party" targets for which we have additional support, and a fallback
/// that can be used to use rustc-perf for any target.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Target {
    /// `x86_64-unknown-linux-gnu`
    X86_64UnknownLinuxGnu,

    /// `aarch64-unknown-linux-gnu`
    AArch64UnknownLinuxGnu,
    Custom(TargetName),
}

impl Target {
    pub fn as_str(self) -> &'static str {
        match self {
            Target::X86_64UnknownLinuxGnu => "x86_64-unknown-linux-gnu",
            Target::AArch64UnknownLinuxGnu => "aarch64-unknown-linux-gnu",
            Target::Custom(name) => name.as_str(),
        }
    }

    /// Targets that we primarily support in the website.
    pub fn primary_targets() -> Vec<Self> {
        vec![Self::X86_64UnknownLinuxGnu, Self::AArch64UnknownLinuxGnu]
    }

    /// Targets that will be benchmarked by default in benchmark requests when no explicit target
    /// is specified.
    pub fn default_targets() -> Vec<Self> {
        vec![Self::X86_64UnknownLinuxGnu]
    }

    // 2026/01/12 (Jamesbarford) - presently rustc-perf only support `x86_64`'s
    // in the pipeline and has two machines running. AArch64 will be run in
    // the background and we do not want to wait for it to complete benchmarking
    pub fn is_optional(&self) -> bool {
        *self != Target::X86_64UnknownLinuxGnu
    }
}

impl FromStr for Target {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "x86_64-unknown-linux-gnu" => Target::X86_64UnknownLinuxGnu,
            "aarch64-unknown-linux-gnu" => Target::AArch64UnknownLinuxGnu,
            other => Target::Custom(intern(other)),
        })
    }
}

impl fmt::Display for Target {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

pub const DEFAULT_TAG: &str = "default";

/// A tag identifying the machine/environment that produced benchmark results.
/// This is a free-form string, defaulting to `"default"`.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Tag(pub TagName);

impl Tag {
    pub fn as_str(self) -> &'static str {
        self.0.as_str()
    }

    pub fn default_tag() -> Self {
        Tag(intern(DEFAULT_TAG))
    }
}

impl fmt::Display for Tag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl std::str::FromStr for Tag {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Tag(intern(s)))
    }
}

impl serde::Serialize for Tag {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> serde::Deserialize<'de> for Tag {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        Ok(Tag(intern(&s)))
    }
}

/// A repository identifier (e.g. "gitcode/xuanwu/Daft", "github/rust-lang/rust").
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Repo(pub RepoName);

impl Repo {
    pub fn as_str(self) -> &'static str {
        self.0.as_str()
    }
}

impl fmt::Display for Repo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl std::str::FromStr for Repo {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Repo(intern(s)))
    }
}

impl From<&str> for Repo {
    fn from(s: &str) -> Self {
        Repo(intern(s))
    }
}

impl serde::Serialize for Repo {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> serde::Deserialize<'de> for Repo {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        Ok(Repo(intern(&s)))
    }
}

/// The codegen backend used for compilation.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CodegenBackend {
    /// The default LLVM backend
    Llvm,
    /// Cranelift codegen backend
    Cranelift,
}

impl CodegenBackend {
    pub fn as_str(self) -> &'static str {
        match self {
            CodegenBackend::Llvm => "llvm",
            CodegenBackend::Cranelift => "cranelift",
        }
    }

    pub fn all_values() -> &'static [Self] {
        &[Self::Llvm, Self::Cranelift]
    }
}

impl FromStr for CodegenBackend {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s.to_ascii_lowercase().as_str() {
            "llvm" => CodegenBackend::Llvm,
            "cranelift" => CodegenBackend::Cranelift,
            _ => return Err(format!("{s} is not a codegen backend")),
        })
    }
}

impl fmt::Display for CodegenBackend {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// An identifier for a built version of the compiler
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ArtifactId {
    /// A built version of the compiler at an exact commit
    Commit { repo: String, commit: Commit },
    /// A symbolic tag for a built compiler like "1.51.0"
    Tag { repo: String, tag: String },
}

impl fmt::Display for ArtifactId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ArtifactId::Commit { repo, commit } => {
                write!(f, "{repo}: {} ({})", commit.sha, commit.date)
            }
            ArtifactId::Tag { repo, tag } => write!(f, "{repo}: {tag}"),
        }
    }
}

struct ArtifactInfo<'a> {
    name: &'a str,
    date: Option<DateTime<Utc>>,
    kind: &'static str,
}

impl ArtifactId {
    pub fn repo(&self) -> &str {
        match self {
            Self::Commit { repo, .. } | Self::Tag { repo, .. } => repo,
        }
    }

    pub fn as_name(&self) -> &str {
        match self {
            Self::Commit { commit, .. } => commit.sha.as_str(),
            Self::Tag { tag, .. } => tag.as_str(),
        }
    }

    pub fn commit(&self) -> Option<&Commit> {
        match self {
            Self::Commit { commit, .. } => Some(commit),
            Self::Tag { .. } => None,
        }
    }

    fn info(&self) -> ArtifactInfo<'_> {
        let (name, date, ty) = match self {
            Self::Commit { commit, .. } => (
                commit.sha.as_str(),
                Some(commit.date.0),
                if commit.is_try() { "try" } else { "master" },
            ),
            Self::Tag { tag, .. } => (tag.as_str(), None, "release"),
        };
        ArtifactInfo {
            name,
            date,
            kind: ty,
        }
    }
}

intern!(pub struct QueryLabel);

/// A database row ID for an artifact in the artifact table
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct ArtifactIdNumber(pub u32);

#[derive(Debug)]
pub struct ArtifactIdIter {
    ids: Arc<Vec<(ArtifactId, Tag)>>,
    idx: usize,
}

impl ArtifactIdIter {
    pub fn new(artifact_ids: Arc<Vec<(ArtifactId, Tag)>>) -> ArtifactIdIter {
        ArtifactIdIter {
            ids: artifact_ids,
            idx: 0,
        }
    }
}

impl Iterator for ArtifactIdIter {
    type Item = ArtifactId;
    fn next(&mut self) -> Option<Self::Item> {
        let (aid, _tag) = self.ids.get(self.idx)?;
        self.idx += 1;
        Some(aid.clone())
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        (self.ids.len(), Some(self.ids.len()))
    }
}

/// Cached Id lookups for many database tables.
///
/// This is a quick way to find what the database id for something.
/// Essentially duplicates of the various database tables (artifacts,
/// error_series, pstat_series, etc.) so that we can avoid a network round-trip.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Index {
    /// Id lookup for a (repo, commit, tag) triple
    commits: Indexed<(Repo, Commit, Tag)>,
    /// Id lookup of published release artifacts (repo, name, tag)
    artifacts: Indexed<(Repo, Box<str>, Tag)>,
    /// Id lookup of compile stat description ids
    /// For legacy reasons called `pstat_series` in the database, and so the name is kept here.
    pstat_series: Indexed<(Benchmark, Profile, Scenario, CodegenBackend, Target, Metric)>,
    /// Id lookup of runtime stat description ids
    runtime_pstat_series: Indexed<(Benchmark, Target, Metric)>,
}

/// An index lookup
///
/// Given a `T` find what its database id is
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Indexed<T> {
    #[serde(with = "index_serde")]
    #[serde(bound = "T: Serialize + serde::de::DeserializeOwned + std::hash::Hash + Eq")]
    map: HashMap<T, u32>,
}

impl<T> std::iter::FromIterator<(u32, T)> for Indexed<T>
where
    T: std::hash::Hash + Eq,
{
    fn from_iter<I: IntoIterator<Item = (u32, T)>>(iter: I) -> Self {
        Self {
            map: iter.into_iter().map(|(idx, v)| (v, idx)).collect(),
        }
    }
}

impl<T> PartialEq for Indexed<T>
where
    T: PartialEq + Eq + std::hash::Hash,
{
    fn eq(&self, other: &Self) -> bool {
        self.map == other.map
    }
}
impl<T> Eq for Indexed<T> where T: PartialEq + Eq + std::hash::Hash {}

impl<T> Default for Indexed<T> {
    fn default() -> Self {
        Indexed {
            map: Default::default(),
        }
    }
}

impl<T> Indexed<T>
where
    T: Clone,
    T: Eq + std::hash::Hash,
{
    pub fn get<U>(&self, value: &U) -> Option<u32>
    where
        T: std::borrow::Borrow<U>,
        U: std::hash::Hash + Eq + ?Sized,
    {
        self.map.get(value).copied()
    }
}

mod index_serde {
    use hashbrown::HashMap;
    use serde::de::{DeserializeOwned, MapAccess, Visitor};
    use serde::ser::SerializeMap;
    use serde::{Deserializer, Serialize, Serializer};
    use std::marker::PhantomData;

    pub fn serialize<T, S>(map: &HashMap<T, u32>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: Serialize,
    {
        let mut writer = serializer.serialize_map(Some(map.len()))?;
        for (value, idx) in map.iter() {
            writer.serialize_entry(idx, value)?;
        }
        writer.end()
    }

    struct MapVisitor<T>(PhantomData<T>);

    impl<'de, T> Visitor<'de> for MapVisitor<T>
    where
        T: std::hash::Hash + Eq + DeserializeOwned,
    {
        type Value = HashMap<T, u32>;
        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(formatter, "map u32 -> T")
        }

        fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
        where
            A: MapAccess<'de>,
        {
            let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(64));
            while let Some((idx, value)) = access.next_entry()? {
                if map.insert(value, idx).is_some() {
                    return Err(serde::de::Error::invalid_value(
                        serde::de::Unexpected::Other("duplicate"),
                        &"no duplicates",
                    ));
                }
            }
            Ok(map)
        }
    }

    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<HashMap<T, u32>, D::Error>
    where
        D: Deserializer<'de>,
        T: DeserializeOwned + Eq + std::hash::Hash,
    {
        deserializer.deserialize_map(MapVisitor(PhantomData))
    }
}

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum DbLabel {
    StatisticDescription {
        benchmark: Benchmark,
        profile: Profile,
        scenario: Scenario,
        backend: CodegenBackend,
        target: Target,
        metric: Metric,
    },
}

pub trait Lookup {
    type Id;
    fn lookup(&self, index: &Index) -> Option<Self::Id>;
}

impl Lookup for DbLabel {
    type Id = u32;
    fn lookup(&self, index: &Index) -> Option<Self::Id> {
        match self {
            DbLabel::StatisticDescription {
                benchmark,
                profile,
                scenario,
                backend,
                metric,
                target,
            } => index
                .pstat_series
                .get(&(*benchmark, *profile, *scenario, *backend, *target, *metric)),
        }
    }
}

impl Lookup for ArtifactId {
    type Id = ArtifactIdNumber;
    /// Looks up the artifact using the default tag.
    /// For tag-aware lookups, use `Index::lookup_artifact_with_tag`.
    fn lookup(&self, index: &Index) -> Option<Self::Id> {
        index.lookup_artifact_with_tag(self, Tag::default_tag())
    }
}

pub type StatisticalDescriptionId = u32;

impl Index {
    pub async fn load(conn: &mut dyn pool::Connection) -> Index {
        conn.load_index().await
    }

    /// Look up an artifact by its ID and tag.
    pub fn lookup_artifact_with_tag(
        &self,
        artifact_id: &ArtifactId,
        tag: Tag,
    ) -> Option<ArtifactIdNumber> {
        Some(match artifact_id {
            ArtifactId::Commit { repo, commit, .. } => {
                let repo_key = Repo::from(repo.as_str());
                ArtifactIdNumber(*self.commits.map.get(&(repo_key, commit.clone(), tag))?)
            }
            ArtifactId::Tag {
                repo,
                tag: artifact,
                ..
            } => {
                let repo_key = Repo::from(repo.as_str());
                ArtifactIdNumber(*self.artifacts.map.get(&(
                    repo_key,
                    artifact.as_str().into(),
                    tag,
                ))?)
            }
        })
    }

    pub fn lookup(
        &self,
        label: &DbLabel,
        artifact_id: &ArtifactId,
    ) -> Option<(u32, ArtifactIdNumber)> {
        let artifact_row_id = artifact_id.lookup(self)?;
        let stat_description_row_id = label.lookup(self)?;
        Some((stat_description_row_id, artifact_row_id))
    }

    pub fn artifacts(&self) -> impl Iterator<Item = &'_ str> + '_ {
        self.artifacts.map.keys().map(|(_, s, _)| &**s)
    }

    pub fn artifacts_for_repo(&self, repo: &str) -> impl Iterator<Item = &'_ str> + '_ {
        let repo_key = Repo::from(repo);
        self.artifacts
            .map
            .keys()
            .filter(move |(r, _, _)| *r == repo_key)
            .map(|(_, s, _)| &**s)
    }

    pub fn commits(&self) -> Vec<Commit> {
        let mut commits: Vec<Commit> = self
            .commits
            .map
            .keys()
            .map(|(_, c, _)| c)
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .cloned()
            .collect();
        commits.sort();
        commits
    }

    pub fn commits_for_repo(&self, repo: &str) -> Vec<Commit> {
        let repo_key = Repo::from(repo);
        let mut commits: Vec<Commit> = self
            .commits
            .map
            .keys()
            .filter(|(r, _, _)| *r == repo_key)
            .map(|(_, c, _)| c)
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .cloned()
            .collect();
        commits.sort();
        commits
    }

    /// Returns the set of tags associated with a specific commit.
    pub fn tags_for_commit(&self, commit: &Commit) -> Vec<Tag> {
        self.commits
            .map
            .keys()
            .filter_map(|(_, c, tag)| if c == commit { Some(*tag) } else { None })
            .collect()
    }

    /// Returns the set of tags associated with a specific commit in a specific repo.
    pub fn tags_for_commit_in_repo(&self, repo: &str, commit: &Commit) -> Vec<Tag> {
        let repo_key = Repo::from(repo);
        self.commits
            .map
            .keys()
            .filter_map(|(r, c, tag)| {
                if *r == repo_key && c == commit {
                    Some(*tag)
                } else {
                    None
                }
            })
            .collect()
    }

    /// Returns the set of tags associated with a specific artifact.
    pub fn tags_for_artifact(&self, artifact_id: &ArtifactId) -> Vec<Tag> {
        match artifact_id {
            ArtifactId::Commit { repo, commit, .. } => self.tags_for_commit_in_repo(repo, commit),
            ArtifactId::Tag { repo, tag, .. } => {
                let repo_key = Repo::from(repo.as_str());
                self.artifacts
                    .map
                    .keys()
                    .filter_map(|(r, name, t)| {
                        if *r == repo_key && name.as_ref() == tag.as_str() {
                            Some(*t)
                        } else {
                            None
                        }
                    })
                    .collect()
            }
        }
    }

    // Note: this function (same as the functions below) iterate through all possible combinations
    // of benchmark parameters, so they should ideally not be called very often.
    pub fn compile_metrics(&self) -> Vec<String> {
        self.pstat_series
            .map
            .keys()
            .map(|(_, _, _, _, _, metric)| metric)
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .map(|s| s.to_string())
            .collect()
    }

    pub fn runtime_metrics(&self) -> Vec<String> {
        self.runtime_pstat_series
            .map
            .keys()
            .map(|(_, _, metric)| metric)
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .map(|s| s.to_string())
            .collect()
    }

    /// All targets for which we have some compile-time benchmark result data.
    pub fn compile_targets(&self) -> Vec<Target> {
        self.pstat_series
            .map
            .keys()
            .map(|(_, _, _, _, target, _)| target)
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .cloned()
            .collect()
    }

    /// All targets for which we have some runtime benchmark result data.
    pub fn runtime_targets(&self) -> Vec<Target> {
        self.runtime_pstat_series
            .map
            .keys()
            .map(|(_, target, _)| target)
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .cloned()
            .collect()
    }

    /// All tags for which we have benchmark data.
    /// Tags are now sourced from the artifact table (commits + release artifacts).
    pub fn tags(&self) -> Vec<Tag> {
        let mut tags: std::collections::HashSet<Tag> =
            self.commits.map.keys().map(|(_, _, tag)| *tag).collect();
        tags.extend(self.artifacts.map.keys().map(|(_, _, tag)| *tag));
        tags.into_iter().collect()
    }

    pub fn tags_for_repo(&self, repo: &str) -> Vec<Tag> {
        let repo_key = Repo::from(repo);
        let mut tags: std::collections::HashSet<Tag> = self
            .commits
            .map
            .keys()
            .filter(|(r, _, _)| *r == repo_key)
            .map(|(_, _, tag)| *tag)
            .collect();
        tags.extend(
            self.artifacts
                .map
                .keys()
                .filter(|(r, _, _)| *r == repo_key)
                .map(|(_, _, tag)| *tag),
        );
        tags.into_iter().collect()
    }

    // FIXME: in theory this won't scale indefinitely as there's potentially
    // millions of queries and labels and iterating all of them is eventually
    // going to be impractical. But for now it performs quite well, so we'll go
    // for it as keeping indices around would be annoying.
    pub fn compile_statistic_descriptions(
        &self,
    ) -> impl Iterator<
        Item = (
            &(Benchmark, Profile, Scenario, CodegenBackend, Target, Metric),
            StatisticalDescriptionId,
        ),
    > + '_ {
        self.pstat_series
            .map
            .iter()
            .map(|(test_case, &row_id)| (test_case, row_id))
    }

    pub fn runtime_statistic_descriptions(
        &self,
    ) -> impl Iterator<Item = (&(Benchmark, Target, Metric), StatisticalDescriptionId)> + '_ {
        self.runtime_pstat_series
            .map
            .iter()
            .map(|(test_case, &row_id)| (test_case, row_id))
    }

    pub fn artifact_id_for_commit(&self, commit: &str, repo: &str) -> Option<ArtifactId> {
        self.commits_for_repo(repo)
            .into_iter()
            .find(|c| c.sha == *commit)
            .map(|c| ArtifactId::Commit {
                repo: repo.to_string(),
                commit: c,
            })
            .or_else(|| {
                self.artifacts_for_repo(repo)
                    .find(|a| *a == commit)
                    .map(|a| ArtifactId::Tag {
                        repo: repo.to_string(),
                        tag: a.to_owned(),
                    })
            })
    }
}

#[derive(Debug)]
pub struct Step {
    pub name: String,
    pub is_done: bool,
    // The amount of time this step has been ongoing (or took, if completed).
    pub duration: Duration,
    pub expected: Duration,
}

#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct CollectionId(i32);

impl fmt::Display for CollectionId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl CollectionId {
    pub fn from_inner(cid: i32) -> Self {
        Self(cid)
    }
    pub fn as_inner(&self) -> i32 {
        self.0
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct CompileBenchmark {
    pub name: String,
    pub category: String,
}

#[derive(Debug)]
pub struct ArtifactCollection {
    pub artifact: ArtifactId,
    pub duration: Duration,
    pub end_time: DateTime<Utc>,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum BenchmarkRequestStatus {
    WaitingForArtifacts,
    ArtifactsReady,
    InProgress,
    Completed {
        completed_at: DateTime<Utc>,
        duration: Duration,
    },
}

const BENCHMARK_REQUEST_STATUS_WAITING_FOR_ARTIFACTS_STR: &str = "waiting_for_artifacts";
const BENCHMARK_REQUEST_STATUS_ARTIFACTS_READY_STR: &str = "artifacts_ready";
const BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR: &str = "in_progress";
const BENCHMARK_REQUEST_STATUS_COMPLETED_STR: &str = "completed";

impl BenchmarkRequestStatus {
    pub fn as_str(&self) -> &str {
        match self {
            Self::WaitingForArtifacts => BENCHMARK_REQUEST_STATUS_WAITING_FOR_ARTIFACTS_STR,
            Self::ArtifactsReady => BENCHMARK_REQUEST_STATUS_ARTIFACTS_READY_STR,
            Self::InProgress => BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR,
            Self::Completed { .. } => BENCHMARK_REQUEST_STATUS_COMPLETED_STR,
        }
    }

    pub(crate) fn from_str_and_completion_date(
        text: &str,
        completion_date: Option<DateTime<Utc>>,
        duration_ms: Option<i32>,
    ) -> anyhow::Result<Self> {
        match text {
            BENCHMARK_REQUEST_STATUS_WAITING_FOR_ARTIFACTS_STR => Ok(Self::WaitingForArtifacts),
            BENCHMARK_REQUEST_STATUS_ARTIFACTS_READY_STR => Ok(Self::ArtifactsReady),
            BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR => Ok(Self::InProgress),
            BENCHMARK_REQUEST_STATUS_COMPLETED_STR => Ok(Self::Completed {
                completed_at: completion_date.ok_or_else(|| {
                    anyhow!("No completion date for a completed BenchmarkRequestStatus")
                })?,
                duration: Duration::from_millis(duration_ms.ok_or_else(|| {
                    anyhow!("No completion duration for a completed BenchmarkRequestStatus")
                })? as u64),
            }),
            _ => Err(anyhow!("Unknown BenchmarkRequestStatus `{text}`")),
        }
    }
}

impl fmt::Display for BenchmarkRequestStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

const BENCHMARK_REQUEST_TRY_STR: &str = "try";
const BENCHMARK_REQUEST_MASTER_STR: &str = "master";
const BENCHMARK_REQUEST_RELEASE_STR: &str = "release";

#[derive(Debug, Clone, PartialEq)]
pub enum BenchmarkRequestType {
    /// A Try commit
    Try {
        sha: Option<String>,
        parent_sha: Option<String>,
        pr: u32,
        /// The main/upstream repository identifier (e.g. "gitcode/xuanwu/Daft")
        repo: String,
    },
    /// A Master commit
    Master {
        sha: String,
        parent_sha: String,
        pr: u32,
        /// The main repository identifier
        repo: String,
    },
    /// A release
    Release {
        tag: String,
        /// The main repository identifier
        repo: String,
    },
}

impl fmt::Display for BenchmarkRequestType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BenchmarkRequestType::Try { .. } => write!(f, "{BENCHMARK_REQUEST_TRY_STR}"),
            BenchmarkRequestType::Master { .. } => write!(f, "{BENCHMARK_REQUEST_MASTER_STR}"),
            BenchmarkRequestType::Release { .. } => write!(f, "{BENCHMARK_REQUEST_RELEASE_STR}"),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct BenchmarkRequest {
    commit_type: BenchmarkRequestType,
    // When was the compiler artifact created
    commit_date: Option<DateTime<Utc>>,
    created_at: DateTime<Utc>,
    status: BenchmarkRequestStatus,
    benchmark_groups: String,
}

impl BenchmarkRequest {
    /// Create a release benchmark request that is in the `ArtifactsReady` status.
    pub fn create_release(tag: &str, commit_date: DateTime<Utc>, repo: &str) -> Self {
        Self {
            commit_type: BenchmarkRequestType::Release {
                tag: tag.to_string(),
                repo: repo.to_string(),
            },
            commit_date: Some(commit_date),
            created_at: Utc::now(),
            status: BenchmarkRequestStatus::ArtifactsReady,
            benchmark_groups: String::new(),
        }
    }

    /// Create a try request that is in the `WaitingForArtifacts` status.
    /// `repo` is the main/upstream repo identifier.
    pub fn create_try_without_artifacts(pr: u32, repo: &str) -> Self {
        Self {
            commit_type: BenchmarkRequestType::Try {
                pr,
                sha: None,
                parent_sha: None,
                repo: repo.to_string(),
            },
            commit_date: None,
            created_at: Utc::now(),
            status: BenchmarkRequestStatus::WaitingForArtifacts,
            benchmark_groups: String::new(),
        }
    }

    /// Create a try request that is in the `ArtifactsReady` status.
    /// `repo` is the main/upstream repo identifier.
    pub fn create_try_with_artifacts(
        pr: u32,
        repo: &str,
        sha: &str,
        parent_sha: &str,
        commit_date: DateTime<Utc>,
    ) -> Self {
        Self {
            commit_type: BenchmarkRequestType::Try {
                pr,
                sha: Some(sha.to_string()),
                parent_sha: Some(parent_sha.to_string()),
                repo: repo.to_string(),
            },
            commit_date: Some(commit_date),
            created_at: Utc::now(),
            status: BenchmarkRequestStatus::ArtifactsReady,
            benchmark_groups: String::new(),
        }
    }

    /// Create a master benchmark request that is in the `ArtifactsReady` status.
    pub fn create_master(
        sha: &str,
        parent_sha: &str,
        pr: u32,
        commit_date: DateTime<Utc>,
        repo: &str,
    ) -> Self {
        Self {
            commit_type: BenchmarkRequestType::Master {
                pr,
                sha: sha.to_string(),
                parent_sha: parent_sha.to_string(),
                repo: repo.to_string(),
            },
            commit_date: Some(commit_date),
            created_at: Utc::now(),
            status: BenchmarkRequestStatus::ArtifactsReady,
            benchmark_groups: String::new(),
        }
    }

    /// Set the benchmark groups for this request.
    pub fn with_benchmark_groups(mut self, groups: &str) -> Self {
        self.benchmark_groups = groups.to_string();
        self
    }

    /// Get either the `sha` for a `try` or `master` commit or a `tag` for a
    /// `release`
    pub fn tag(&self) -> Option<&str> {
        match &self.commit_type {
            BenchmarkRequestType::Try { sha, .. } => sha.as_deref(),
            BenchmarkRequestType::Master { sha, .. } => Some(sha),
            BenchmarkRequestType::Release { tag, .. } => Some(tag),
        }
    }

    pub fn pr(&self) -> Option<u32> {
        match &self.commit_type {
            BenchmarkRequestType::Try { pr, .. } | BenchmarkRequestType::Master { pr, .. } => {
                Some(*pr)
            }
            BenchmarkRequestType::Release { .. } => None,
        }
    }

    pub fn parent_sha(&self) -> Option<&str> {
        match &self.commit_type {
            BenchmarkRequestType::Try { parent_sha, .. } => parent_sha.as_deref(),
            BenchmarkRequestType::Master { parent_sha, .. } => Some(parent_sha),
            BenchmarkRequestType::Release { .. } => None,
        }
    }

    /// Returns the main/upstream repository identifier.
    pub fn repo(&self) -> Option<&str> {
        match &self.commit_type {
            BenchmarkRequestType::Try { repo, .. }
            | BenchmarkRequestType::Master { repo, .. }
            | BenchmarkRequestType::Release { repo, .. } => {
                (!repo.is_empty()).then_some(repo.as_str())
            }
        }
    }

    pub fn status(&self) -> BenchmarkRequestStatus {
        self.status
    }

    pub fn created_at(&self) -> DateTime<Utc> {
        self.created_at
    }

    pub fn commit_date(&self) -> Option<DateTime<Utc>> {
        self.commit_date
    }

    pub fn commit_type(&self) -> &BenchmarkRequestType {
        &self.commit_type
    }

    pub fn is_master(&self) -> bool {
        matches!(self.commit_type, BenchmarkRequestType::Master { .. })
    }

    pub fn is_try(&self) -> bool {
        matches!(self.commit_type, BenchmarkRequestType::Try { .. })
    }

    pub fn is_release(&self) -> bool {
        matches!(self.commit_type, BenchmarkRequestType::Release { .. })
    }

    /// Get the benchmark groups for the request (comma-separated string).
    /// Returns the raw string; the caller is responsible for applying defaults
    /// from the site config if this is empty.
    pub fn benchmark_groups_raw(&self) -> &str {
        &self.benchmark_groups
    }

    pub fn is_completed(&self) -> bool {
        matches!(self.status, BenchmarkRequestStatus::Completed { .. })
    }

    pub fn is_in_progress(&self) -> bool {
        matches!(self.status, BenchmarkRequestStatus::InProgress)
    }
}

/// Result of inserting into the database
#[derive(Debug, Clone, PartialEq)]
pub enum BenchmarkRequestInsertResult {
    Inserted,
    NothingInserted,
    Conflict {
        existing_tag: String,
        existing_status: String,
    },
}

fn parse_comma_separated<T>(raw_string: &str, name: &str) -> Result<Vec<T>, String>
where
    T: FromStr,
{
    raw_string
        .split(',')
        .map(|s| T::from_str(s).map_err(|_| format!("Invalid {name}: {s}")))
        .collect()
}

pub fn parse_backends(backends: &str) -> Result<Vec<CodegenBackend>, String> {
    parse_comma_separated(backends, "backend")
}

pub fn parse_profiles(profiles: &str) -> Result<Vec<Profile>, String> {
    parse_comma_separated(profiles, "profile")
}

pub fn parse_targets(targets: &str) -> Result<Vec<Target>, String> {
    let primary = Target::primary_targets();
    let targets = parse_comma_separated(targets, "target")?;
    if !targets.iter().all(|t| primary.contains(t)) {
        return Err("Only primary targets can be specified".to_string());
    }
    Ok(targets)
}

/// Cached information about benchmark requests in the DB
pub struct BenchmarkRequestIndex {
    /// Tags (SHA or release name) of all known benchmark requests
    all: HashSet<String>,
}

impl BenchmarkRequestIndex {
    /// Do we already have a benchmark request for the passed `tag`?
    pub fn contains_tag(&self, tag: &str) -> bool {
        self.all.contains(tag)
    }
}

/// Contains pending (ArtifactsReady or InProgress) benchmark requests, and a set of their parents
/// that are already completed.
#[derive(Debug)]
pub struct PendingBenchmarkRequests {
    pub requests: Vec<BenchmarkRequest>,
    pub completed_parent_tags: HashSet<String>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum BenchmarkJobStatus {
    Queued,
    InProgress {
        started_at: DateTime<Utc>,
        collector_name: String,
    },
    Completed {
        started_at: DateTime<Utc>,
        completed_at: DateTime<Utc>,
        collector_name: String,
        success: bool,
    },
}

const BENCHMARK_JOB_STATUS_QUEUED_STR: &str = "queued";
const BENCHMARK_JOB_STATUS_IN_PROGRESS_STR: &str = "in_progress";
const BENCHMARK_JOB_STATUS_SUCCESS_STR: &str = "success";
const BENCHMARK_JOB_STATUS_FAILURE_STR: &str = "failure";

impl BenchmarkJobStatus {
    pub fn as_str(&self) -> &str {
        match self {
            BenchmarkJobStatus::Queued => BENCHMARK_JOB_STATUS_QUEUED_STR,
            BenchmarkJobStatus::InProgress { .. } => BENCHMARK_JOB_STATUS_IN_PROGRESS_STR,
            BenchmarkJobStatus::Completed { success, .. } => {
                if *success {
                    BENCHMARK_JOB_STATUS_SUCCESS_STR
                } else {
                    BENCHMARK_JOB_STATUS_FAILURE_STR
                }
            }
        }
    }
}

impl fmt::Display for BenchmarkJobStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// A single unit of work generated from a benchmark request.
/// Each request is split into several `BenchmarkJob`s, one per (benchmark_group, tag) combination.
/// Collectors poll the queue and claim a job only when its `tag` matches.
#[derive(Debug, Clone, PartialEq)]
pub struct BenchmarkJob {
    id: u32,
    benchmark_group: String,
    runtime_config: QueueRuntimeConfig,
    request_tag: String,
    created_at: DateTime<Utc>,
    status: BenchmarkJobStatus,
    deque_counter: u32,
    is_optional: bool,
    tag: String,
}

impl BenchmarkJob {
    pub fn id(&self) -> u32 {
        self.id
    }

    pub fn benchmark_group(&self) -> &str {
        &self.benchmark_group
    }

    pub fn runtime_config(&self) -> &QueueRuntimeConfig {
        &self.runtime_config
    }

    pub fn request_tag(&self) -> &str {
        &self.request_tag
    }

    pub fn collector_name(&self) -> Option<&str> {
        match &self.status {
            BenchmarkJobStatus::Queued => None,
            BenchmarkJobStatus::InProgress { collector_name, .. }
            | BenchmarkJobStatus::Completed { collector_name, .. } => Some(collector_name),
        }
    }

    /// How many times was the job already dequed?
    pub fn deque_count(&self) -> u32 {
        self.deque_counter
    }

    pub fn status(&self) -> &BenchmarkJobStatus {
        &self.status
    }

    pub fn created_at(&self) -> DateTime<Utc> {
        self.created_at
    }

    pub fn is_optional(&self) -> bool {
        self.is_optional
    }

    /// The environment tag used to store results for this job.
    pub fn tag(&self) -> &str {
        &self.tag
    }
}

/// Describes the final state of a job
#[derive(Debug, Clone, PartialEq)]
pub enum BenchmarkJobConclusion {
    Failure,
    Success,
}

impl BenchmarkJobConclusion {
    pub fn as_str(&self) -> &str {
        match self {
            BenchmarkJobConclusion::Failure => BENCHMARK_JOB_STATUS_FAILURE_STR,
            BenchmarkJobConclusion::Success => BENCHMARK_JOB_STATUS_SUCCESS_STR,
        }
    }
}

/// The configuration for a collector
#[derive(Debug, PartialEq)]
pub struct CollectorConfig {
    name: String,
    is_active: bool,
    last_heartbeat_at: DateTime<Utc>,
    date_added: DateTime<Utc>,
    /// The commit SHA of `rustc-perf` that the collector currently has checked out.
    commit_sha: Option<String>,
}

impl CollectorConfig {
    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn is_active(&self) -> bool {
        self.is_active
    }

    pub fn last_heartbeat_at(&self) -> DateTime<Utc> {
        self.last_heartbeat_at
    }

    pub fn date_added(&self) -> DateTime<Utc> {
        self.date_added
    }

    pub fn commit_sha(&self) -> Option<&str> {
        self.commit_sha.as_deref()
    }
}

/// Mapping of a request to its parent along with all jobs
#[derive(Debug, PartialEq)]
pub struct InProgressRequestWithJobs {
    /// In progress requests along with their associated jobs
    pub request: (BenchmarkRequest, Vec<BenchmarkJob>),
    /// Optionally the parent of the above request with _all_ their associated
    /// jobs
    pub parent: Option<(BenchmarkRequest, Vec<BenchmarkJob>)>,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct QueueRuntimeConfig {
    pub group: String,
    #[serde(default)]
    pub repo_key: String,
    /// The main/upstream repository identifier (e.g. "gitcode/xuanwu/Daft").
    /// The collector clones from this repo and fetches the SHA directly.
    #[serde(default)]
    pub repo: String,
    #[serde(default)]
    pub build_cmd: String,
    #[serde(default)]
    pub build_dir: String,
    #[serde(default)]
    pub is_rust: bool,
    #[serde(default)]
    pub post_build: String,
    #[serde(default)]
    pub iterations: Option<u32>,
    #[serde(default)]
    pub no_isolate: Option<bool>,
    #[serde(default)]
    pub exclude: Vec<String>,
    #[serde(default)]
    pub exclude_suffix: Vec<String>,
    #[serde(default)]
    pub include: Vec<String>,
    #[serde(default)]
    pub exact_match: Vec<String>,
    #[serde(default, alias = "bench_args")]
    pub extra_args: Vec<String>,
}

#[derive(Debug, PartialEq)]
pub struct BenchmarkRequestWithErrors {
    pub request: BenchmarkRequest,
    /// Benchmark (name) -> error
    pub errors: HashMap<String, String>,
}