regy 0.1.4

Private-by-default desktop agent for the Regy web interface
use serde::Serialize;

use crate::{
    config::app::ProviderId,
    providers::{descriptor::descriptor, probe::ProviderProbeResult},
};

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CheckId {
    System,
    Pi,
    Providers,
    Pairing,
    Tunnel,
}

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CheckStatus {
    // The TUI models an in-progress check even though the current CLI publishes completed reports.
    #[allow(dead_code)]
    Checking,
    Ready,
    /// Reserved by the serialized readiness protocol and TUI state model for non-blocking issues.
    #[allow(dead_code)]
    Warning,
    ActionRequired,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct CheckResult {
    pub id: CheckId,
    pub status: CheckStatus,
    pub required: bool,
    pub detail: Option<String>,
    pub action: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<ReadinessAction>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct ReadinessAction {
    pub(crate) title: String,
    pub(crate) steps: Vec<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct ReadinessReport {
    pub checks: Vec<CheckResult>,
    pub start_allowed: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iroh_endpoint_id: Option<String>,
}

impl ReadinessReport {
    pub(crate) fn from_checks(checks: Vec<CheckResult>) -> Self {
        let start_allowed = checks.iter().all(|check| {
            !check.required || matches!(check.status, CheckStatus::Ready | CheckStatus::Warning)
        });
        Self {
            checks,
            start_allowed,
            iroh_endpoint_id: None,
        }
    }
}

#[cfg(test)]
pub(crate) fn unselected_provider_action() -> ReadinessAction {
    ReadinessAction {
        title: "Select a model provider.".to_owned(),
        steps: vec![
            "Run `regy-pc-agent setup` in a terminal.".to_owned(),
            "Select OpenAI Codex, Claude Code, or Antigravity.".to_owned(),
            "Follow the displayed browser/login instructions.".to_owned(),
            "Run `regy-pc-agent doctor` and confirm Providers is ready.".to_owned(),
            "Run `regy-pc-agent start`, then open https://regy.one.".to_owned(),
        ],
    }
}

pub(crate) fn provider_authentication_summary(provider: ProviderId) -> String {
    descriptor(provider).setup_steps.join(" ")
}

pub(crate) struct ProviderSetupGuidance {
    pub(crate) display_name: &'static str,
    pub(crate) summary: String,
    #[cfg(test)]
    pub(crate) action: ReadinessAction,
}

pub(crate) fn provider_setup_guidance(result: &ProviderProbeResult) -> ProviderSetupGuidance {
    let descriptor = descriptor(result.id);
    let (summary, _title, _steps) = if !result.executable_ready {
        let summary = "Install the Claude Code executable.".to_owned();
        (
            summary.clone(),
            format!("{} requires setup.", descriptor.display_name),
            vec![summary],
        )
    } else if !result.extension_ready {
        let summary = match result.id {
            ProviderId::ClaudeCode => "Install the reviewed Claude Code Pi extension.",
            ProviderId::Antigravity => "Install the reviewed Antigravity Pi extension.",
            ProviderId::OpenaiCodex => "Configure the OpenAI Codex provider.",
        }
        .to_owned();
        (
            summary.clone(),
            format!("{} requires setup.", descriptor.display_name),
            vec![summary],
        )
    } else if !result.authenticated {
        (
            provider_authentication_summary(result.id),
            format!("{} requires authentication.", descriptor.display_name),
            descriptor
                .setup_steps
                .iter()
                .map(|step| (*step).to_owned())
                .collect(),
        )
    } else {
        let summary = "Complete provider setup.".to_owned();
        (
            summary.clone(),
            format!("{} requires setup.", descriptor.display_name),
            vec![summary],
        )
    };

    ProviderSetupGuidance {
        display_name: descriptor.display_name,
        summary,
        #[cfg(test)]
        action: ReadinessAction {
            title: _title,
            steps: _steps,
        },
    }
}

#[cfg(test)]
pub(crate) fn provider_readiness(results: &[ProviderProbeResult]) -> CheckResult {
    if results.iter().any(ProviderProbeResult::ready) {
        return CheckResult {
            id: CheckId::Providers,
            status: CheckStatus::Ready,
            required: true,
            detail: Some("At least one model provider is ready.".to_owned()),
            action: None,
            actions: Vec::new(),
        };
    }
    let guidance = results.first().map(provider_setup_guidance);
    CheckResult {
        id: CheckId::Providers,
        status: CheckStatus::ActionRequired,
        required: true,
        detail: Some("No model provider is ready.".to_owned()),
        action: guidance
            .as_ref()
            .map(|guidance| guidance.summary.clone())
            .or_else(|| Some("Configure and log in to a model provider.".to_owned())),
        actions: guidance
            .map(|guidance| guidance.action)
            .into_iter()
            .collect(),
    }
}