use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use std::process::Command;

use crate::error::SolverError;
use crate::framework::abstract_af::AbstractFramework;
use crate::framework::argument::ArgumentId;

/// Write `contents` to a temporary file and return the handle.
///
/// # Errors
///
/// Returns [`SolverError`] if creating the temp file or writing `contents` fails.
pub fn write_temp_instance(contents: &str) -> Result<tempfile::NamedTempFile, SolverError> {
    let mut file = tempfile::NamedTempFile::new()
        .map_err(|e| SolverError::Failed(format!("failed to create temp file: {e}")))?;
    file.write_all(contents.as_bytes())
        .map_err(|e| SolverError::Failed(format!("failed to write instance: {e}")))?;
    Ok(file)
}

/// Run an external solver command and return its stdout.
///
/// Maps `NotFound` IO errors to [`SolverError::NotFound`] and treats exit
/// code 1 as a solver error (surfacing stderr).
///
/// # Errors
///
/// Returns [`SolverError`] if the executable is missing, the process cannot be
/// started, or it exits with code 1 (stderr is included in the message).
pub fn run_solver_command(cmd: &mut Command, solver_display: &str) -> Result<String, SolverError> {
    let output = cmd.output().map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            let program = cmd.get_program().to_string_lossy().into_owned();
            SolverError::NotFound(program)
        } else {
            SolverError::Failed(format!("failed to run {solver_display}: {e}"))
        }
    })?;

    if output.status.code() == Some(1) {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(SolverError::Failed(format!(
            "{solver_display} error: {stderr}"
        )));
    }

    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}

/// Build a name-to-[`ArgumentId`] map from the framework's arguments,
/// using the given formatter to produce the string key for each ID.
fn build_name_map(
    af: &AbstractFramework,
    formatter: impl Fn(ArgumentId) -> String,
) -> HashMap<String, ArgumentId> {
    af.arguments.iter().map(|&id| (formatter(id), id)).collect()
}

/// Build the ASPARTIX-style name map (`"a0"`, `"a1"`, ...).
pub fn aspartix_name_map(af: &AbstractFramework) -> HashMap<String, ArgumentId> {
    build_name_map(af, |id| format!("a{}", id.0))
}

/// Build the ICCMA/TGF-style name map (`"0"`, `"1"`, ...).
pub fn iccma_name_map(af: &AbstractFramework) -> HashMap<String, ArgumentId> {
    build_name_map(af, |id| id.0.to_string())
}

/// Check that a path exists, returning a descriptive error if not.
///
/// # Errors
///
/// Returns [`SolverError::Failed`] if `path` does not exist.
pub fn require_path(path: &Path, description: &str) -> Result<(), SolverError> {
    if !path.exists() {
        return Err(SolverError::Failed(format!(
            "{description} not found: {}",
            path.display()
        )));
    }
    Ok(())
}
