ccd-cli 1.0.0-alpha.8

Bootstrap and validate Continuous Context Development repositories
use serde::{Deserialize, Serialize};

use crate::content_trust::ContentTrust;
use crate::paths::state::StateLayout;

pub(crate) const DEFAULT_STALE_AFTER_SECS: u64 = 24 * 60 * 60;

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct BacklogRef {
    pub provider: String,
    pub kind: String,
    pub id: String,
    pub url: String,
}

impl BacklogRef {
    pub(crate) fn key(&self) -> String {
        format!("{}:{}:{}", self.provider, self.kind, self.id)
    }

    #[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
    pub(crate) fn is_empty(&self) -> bool {
        self.provider.is_empty() || self.kind.is_empty() || self.id.is_empty()
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct WorkQueueSummary {
    pub open_issues: usize,
    pub queue_scoped: usize,
    pub queue_candidates: usize,
    pub policy_conflicts: usize,
    pub metadata_invalid: usize,
    pub upstream_claimed: usize,
    pub auto_selectable: usize,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct WorkQueueSummaryItem {
    pub ccd_id: u64,
    pub github_issue_number: u64,
    pub backlog_ref: BacklogRef,
    pub content_trust: ContentTrust,
    pub title: String,
    pub url: String,
    pub section: String,
    pub status: String,
    pub queue_state: &'static str,
    pub dispatch_state: &'static str,
    pub metadata_status: &'static str,
    pub upstream_claim: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority_label: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub claimed_by: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority_rank: Option<u64>,
}

impl WorkQueueSummaryItem {
    pub(crate) fn display_ref(&self) -> String {
        if self.ccd_id != 0 {
            format!("ccd#{}", self.ccd_id)
        } else if self
            .backlog_ref
            .provider
            .eq_ignore_ascii_case("github-issues")
        {
            format!("GH#{}", self.github_issue_number)
        } else if self
            .backlog_ref
            .provider
            .eq_ignore_ascii_case("gitlab-issues")
        {
            format!("GL#{}", self.github_issue_number)
        } else {
            format!("#{}", self.github_issue_number)
        }
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct WorkQueueDispatchView {
    pub status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority_label: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub selected: Option<WorkQueueSummaryItem>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct WorkQueueCacheView {
    pub path: String,
    pub rendered_path: String,
    pub status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_trust: Option<ContentTrust>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repo: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fetched_at_epoch_s: Option<u64>,
    pub stale_after_s: u64,
    pub queue_summary: WorkQueueSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dispatch: Option<WorkQueueDispatchView>,
    pub active_items: Vec<WorkQueueSummaryItem>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct WorkQueueSnapshotItem {
    pub ccd_id: u64,
    pub github_issue_number: u64,
    pub backlog_ref: BacklogRef,
    pub content_trust: ContentTrust,
    pub title: String,
    pub url: String,
    pub section: String,
    pub status: String,
    pub queue_state: &'static str,
    pub dispatch_state: &'static str,
    pub metadata_status: &'static str,
    pub upstream_claim: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority_label: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub claimed_by: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority_rank: Option<u64>,
    pub closed: bool,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct WorkQueueSnapshot {
    pub content_trust: ContentTrust,
    pub provider: String,
    pub repo: String,
    pub fetched_at_epoch_s: u64,
    pub stale_after_s: u64,
    pub revalidate_on_refresh: bool,
    pub queue_summary: WorkQueueSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dispatch: Option<WorkQueueDispatchView>,
    pub active_items: Vec<WorkQueueSummaryItem>,
    pub items: Vec<WorkQueueSnapshotItem>,
}

pub(crate) fn cache_view_from_snapshot(
    layout: &StateLayout,
    snapshot: Option<&WorkQueueSnapshot>,
    limit: usize,
) -> WorkQueueCacheView {
    let path = layout.work_queue_cache_path().display().to_string();
    let rendered_path = layout.work_queue_view_path().display().to_string();
    let Some(snapshot) = snapshot else {
        return WorkQueueCacheView {
            path,
            rendered_path,
            status: "missing",
            content_trust: None,
            provider: None,
            repo: None,
            fetched_at_epoch_s: None,
            stale_after_s: DEFAULT_STALE_AFTER_SECS,
            queue_summary: WorkQueueSummary::default(),
            dispatch: None,
            active_items: Vec::new(),
        };
    };

    WorkQueueCacheView {
        path,
        rendered_path,
        status: "loaded",
        content_trust: Some(snapshot.content_trust),
        provider: Some(snapshot.provider.clone()),
        repo: Some(snapshot.repo.clone()),
        fetched_at_epoch_s: Some(snapshot.fetched_at_epoch_s),
        stale_after_s: snapshot.stale_after_s,
        queue_summary: snapshot.queue_summary.clone(),
        dispatch: snapshot.dispatch.clone(),
        active_items: snapshot.active_items.iter().take(limit).cloned().collect(),
    }
}

pub(crate) fn is_snapshot_stale(snapshot: &WorkQueueSnapshot, now_epoch_s: u64) -> bool {
    now_epoch_s.saturating_sub(snapshot.fetched_at_epoch_s) > snapshot.stale_after_s
}