//! Code-generates `baked_pricing()` from `data/pricing.json` so the runtime
//! pricing table is a release-pinned snapshot of the model catalog rather than
//! a hand-edited literal. The same `pricing.json` is served live from
//! `https://blazen.dev/api/pricing.json` and is overwritten by the daily
//! check-pricing workflow on the `models-data` branch (then pulled into main
//! at release time by the auto-tag job).

use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

use serde::Deserialize;

/// Mirrors [`crate::pricing::PricingEntry`] field-for-field. Kept private to
/// build.rs so the codegen stays a single point of truth without forcing
/// callers to share types across the build / runtime boundary.
#[derive(Deserialize)]
struct Entry {
    #[serde(default)]
    input_per_million: f64,
    #[serde(default)]
    output_per_million: f64,
    #[serde(default)]
    per_image: Option<f64>,
    #[serde(default)]
    per_second: Option<f64>,
}

#[derive(Deserialize)]
struct PricingFile {
    /// `BTreeMap` so the generated code is deterministic across builds.
    models: BTreeMap<String, Entry>,
}

fn main() {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
    let json_path = PathBuf::from(&manifest_dir).join("data/pricing.json");
    println!("cargo:rerun-if-changed={}", json_path.display());

    let json = fs::read_to_string(&json_path)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", json_path.display()));
    let parsed: PricingFile = serde_json::from_str(&json)
        .unwrap_or_else(|e| panic!("failed to parse {}: {e}", json_path.display()));

    let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
    let out_path = PathBuf::from(&out_dir).join("baked_pricing.rs");
    let mut out = fs::File::create(&out_path)
        .unwrap_or_else(|e| panic!("failed to create {}: {e}", out_path.display()));

    writeln!(
        out,
        "/// Generated by build.rs from data/pricing.json. Do not edit by hand."
    )
    .unwrap();
    writeln!(
        out,
        "fn baked_pricing() -> HashMap<String, PricingEntry> {{"
    )
    .unwrap();
    writeln!(
        out,
        "    let mut m = HashMap::with_capacity({});",
        parsed.models.len()
    )
    .unwrap();

    for (id, entry) in &parsed.models {
        writeln!(
            out,
            "    m.insert({id:?}.to_owned(), PricingEntry {{ input_per_million: {input}_f64, output_per_million: {output}_f64, per_image: {per_image}, per_second: {per_second} }});",
            input = format_f64(entry.input_per_million),
            output = format_f64(entry.output_per_million),
            per_image = format_opt(entry.per_image),
            per_second = format_opt(entry.per_second),
        )
        .unwrap();
    }

    writeln!(out, "    m").unwrap();
    writeln!(out, "}}").unwrap();
}

/// Format an `f64` as a Rust literal that round-trips. Always emit at least
/// one decimal digit so the literal stays an `f64` and not an integer.
fn format_f64(v: f64) -> String {
    if v.fract() == 0.0 {
        format!("{v:.1}")
    } else {
        // Rust's default Display for f64 round-trips for finite values.
        format!("{v}")
    }
}

fn format_opt(v: Option<f64>) -> String {
    match v {
        Some(x) => format!("Some({}_f64)", format_f64(x)),
        None => "None".to_owned(),
    }
}
