genome-sh 0.1.0

The jq of genomics. Fast, local, human-readable variant analysis.
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::db::config::Config;
use crate::output::Format;

const ALPHAGENOME_API_BASE: &str = "https://alphagenome.deepmind.com/v1";

pub struct AlphaGenomeClient {
    api_key: String,
    client: reqwest::Client,
}

#[derive(Debug, Serialize)]
struct PredictRequest {
    chromosome: String,
    position: u64,
    reference: String,
    alternate: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Prediction {
    pub variant: String,
    pub expression_impact: Option<ExpressionImpact>,
    pub splicing_impact: Option<SplicingImpact>,
    pub chromatin_impact: Option<ChromatinImpact>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ExpressionImpact {
    pub gene: String,
    pub effect_size: f64,
    pub tissue: String,
    pub confidence: f64,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct SplicingImpact {
    pub splice_site_affected: bool,
    pub exon_skipping_risk: f64,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ChromatinImpact {
    pub accessibility_change: String,
    pub regulatory_region: String,
}

impl AlphaGenomeClient {
    pub fn new() -> Result<Self> {
        let config = Config::load()?;
        let api_key = config
            .get("alphagenome-api-key")
            .map(String::from)
            .context(
                "AlphaGenome API key not set. Run:\n  genome config set alphagenome-api-key <key>",
            )?;

        eprintln!("Warning: Variant data will be sent to Google DeepMind's AlphaGenome API.");
        eprintln!("No personal identifiers are included, only genomic coordinates.");
        eprintln!();

        Ok(Self {
            api_key,
            client: reqwest::Client::new(),
        })
    }

    pub async fn predict_variant(&self, variant_str: &str) -> Result<Prediction> {
        // Parse variant string (chr:pos:ref:alt format).
        let parts: Vec<&str> = variant_str.split(':').collect();
        if parts.len() < 4 {
            anyhow::bail!(
                "Variant must be in chr:pos:ref:alt format (e.g., chr17:43092919:G:A)"
            );
        }

        let request = PredictRequest {
            chromosome: parts[0].to_string(),
            position: parts[1].parse().context("Invalid position")?,
            reference: parts[2].to_string(),
            alternate: parts[3].to_string(),
        };

        let response = self
            .client
            .post(format!("{ALPHAGENOME_API_BASE}/predict/variant"))
            .bearer_auth(&self.api_key)
            .json(&request)
            .send()
            .await
            .context("Failed to reach AlphaGenome API")?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("AlphaGenome API error ({status}): {body}");
        }

        let prediction: Prediction = response
            .json()
            .await
            .context("Failed to parse AlphaGenome response")?;

        Ok(prediction)
    }
}

impl Prediction {
    pub fn print(&self, format: Format) -> Result<()> {
        match format {
            Format::Json => {
                println!("{}", serde_json::to_string_pretty(self)?);
            }
            _ => {
                println!("AlphaGenome Prediction: {}", self.variant);
                println!();

                if let Some(expr) = &self.expression_impact {
                    println!("  Gene Expression Impact");
                    let direction = if expr.effect_size < 0.0 {
                        "decrease"
                    } else {
                        "increase"
                    };
                    println!(
                        "    {} expression: {:.1} SD ({direction})",
                        expr.gene,
                        expr.effect_size.abs()
                    );
                    println!("    Tissue most affected: {}", expr.tissue);
                    println!("    Confidence: {:.2}", expr.confidence);
                    println!();
                }

                if let Some(splice) = &self.splicing_impact {
                    println!("  Splicing Impact");
                    println!("    Splice site affected: {}", splice.splice_site_affected);
                    println!(
                        "    Exon skipping risk: {:.2}",
                        splice.exon_skipping_risk
                    );
                    println!();
                }

                if let Some(chrom) = &self.chromatin_impact {
                    println!("  Chromatin Impact");
                    println!("    Accessibility change: {}", chrom.accessibility_change);
                    println!("    Regulatory region: {}", chrom.regulatory_region);
                    println!();
                }
            }
        }

        Ok(())
    }
}