#[derive(Debug, Clone)]
enum DriftSignal {
    Drifted([u8; 32]),
    AlreadyIndexed(#[allow(dead_code)] [u8; 32]),
}

fn emit_indexed_line(packed: &PackedCookArchive, origin: Option<&str>) {
    let mib = packed.size_bytes as f64 / 1024.0 / 1024.0;
    let origin_field = origin.unwrap_or("none");
    eprintln!(
        "{}  sha256={}  size={mib:.1} MiB  origin={origin_field}",
        green_indexed_prefix(),
        sha_abbrev(&packed.sha256),
    );
}

fn build_cook_cmd_summary(args: &CookArgs) -> String {
    let mut parts = vec!["cook".to_string()];
    if args.release {
        parts.push("--release".to_string());
    }
    if let Some(p) = args.profile.as_deref() {
        parts.push(format!("--profile={p}"));
    }
    if let Some(t) = args.target.as_deref() {
        parts.push(format!("--target={t}"));
    }
    if args.workspace {
        parts.push("--workspace".to_string());
    }
    for pkg in &args.packages {
        parts.push(format!("-p {pkg}"));
    }
    parts.join(" ")
}

/// Remove cargo-chef-generated `plugin = false` / `plugin = true`
/// target fields from the prepared recipe's embedded TOML manifests.
/// Newer Cargo warns on these obsolete keys during `cargo chef cook`,
/// but they are generated by cargo-chef's metadata round-trip rather
/// than by the user's checked-in manifests.
fn sanitize_cargo_chef_recipe(recipe_path: &Path) -> Result<usize, SoldrError> {
    let raw = std::fs::read_to_string(recipe_path).map_err(|e| {
        SoldrError::Other(format!(
            "soldr cook: failed to read cargo-chef recipe {}: {e}",
            recipe_path.display()
        ))
    })?;
    let mut recipe: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
        SoldrError::Other(format!(
            "soldr cook: failed to parse cargo-chef recipe {}: {e}",
            recipe_path.display()
        ))
    })?;

    let mut removed = 0usize;
    let Some(manifests) = recipe
        .get_mut("skeleton")
        .and_then(|value| value.get_mut("manifests"))
        .and_then(|value| value.as_array_mut())
    else {
        return Ok(0);
    };

    for manifest in manifests {
        let Some(contents_value) = manifest.get_mut("contents") else {
            continue;
        };
        let Some(contents) = contents_value.as_str() else {
            continue;
        };
        let (sanitized, count) = strip_generated_plugin_lines(contents);
        if count > 0 {
            *contents_value = serde_json::Value::String(sanitized);
            removed += count;
        }
    }

    if removed > 0 {
        let encoded = serde_json::to_vec(&recipe).map_err(|e| {
            SoldrError::Other(format!(
                "soldr cook: failed to serialize sanitized recipe {}: {e}",
                recipe_path.display()
            ))
        })?;
        std::fs::write(recipe_path, encoded).map_err(|e| {
            SoldrError::Other(format!(
                "soldr cook: failed to write sanitized recipe {}: {e}",
                recipe_path.display()
            ))
        })?;
    }

    Ok(removed)
}

fn strip_generated_plugin_lines(contents: &str) -> (String, usize) {
    let mut out = String::with_capacity(contents.len());
    let mut removed = 0usize;
    for line in contents.split_inclusive('\n') {
        let trimmed = line.trim();
        if matches!(trimmed, "plugin = false" | "plugin = true") {
            removed += 1;
            continue;
        }
        out.push_str(line);
    }
    (out, removed)
}

/// Run the `cook` strip preset against the workspace's `target/` directory.
///
/// Best-effort: a missing or partially-populated `target/` (the heavy
/// case is when cargo wrote into `target/<triple>/...` and the
/// stripper just finds nothing) is not an error — the trim only ever
/// reduces output. Failures are surfaced on stderr but never abort
/// the cook command (the artifacts are already built and valid).
fn run_cook_target_trim(manifest_dir: &Path) {
    let target_dir = manifest_dir.join("target");
    if !target_dir.is_dir() {
        return;
    }
    let opts = StripTargetOptions::cook(target_dir.clone());
    match strip_target(&opts) {
        Ok(report) => {
            if report.deleted == 0 {
                return;
            }
            let mib = report.reclaimed_bytes as f64 / 1024.0 / 1024.0;
            eprintln!(
                "soldr cook: trimmed {} cargo-recreatable entries from target/ ({mib:.1} MiB reclaimed)",
                report.deleted,
            );
        }
        Err(err) => {
            eprintln!(
                "soldr cook: target/ trim failed at {}: {err} (cook output is still valid)",
                target_dir.display()
            );
        }
    }
}

/// Argv for `cargo chef prepare --recipe-path <path>`, ready to feed
/// through the cargo front door.
pub(crate) fn build_chef_prepare_args(ctx: &CookContext) -> Vec<String> {
    vec![
        "chef".to_string(),
        "prepare".to_string(),
        "--recipe-path".to_string(),
        ctx.recipe_path.display().to_string(),
    ]
}

/// Argv for `cargo chef cook ...`, ready to feed through the cargo front
/// door.
pub(crate) fn build_chef_cook_args(ctx: &CookContext, args: &CookArgs) -> Vec<String> {
    let mut out = vec![
        "chef".to_string(),
        "cook".to_string(),
        "--recipe-path".to_string(),
        ctx.recipe_path.display().to_string(),
    ];
    if args.release {
        out.push("--release".to_string());
    }
    if let Some(profile) = args.profile.as_ref() {
        out.push("--profile".to_string());
        out.push(profile.clone());
    }
    if let Some(target) = args.target.as_ref() {
        out.push("--target".to_string());
        out.push(target.clone());
    }
    if args.workspace {
        out.push("--workspace".to_string());
    }
    for pkg in &args.packages {
        out.push("--package".to_string());
        out.push(pkg.clone());
    }
    if !args.passthrough.is_empty() {
        out.push("--".to_string());
        out.extend(args.passthrough.iter().cloned());
    }
    out
}
