use std::collections::{BTreeSet, HashMap};
use std::path::PathBuf;
use std::process::Command;

use crate::error::{AspicError, SolverError};
use crate::framework::abstract_af::AbstractFramework;
use crate::framework::argument::ArgumentId;
use crate::framework::semantics::{Extension, Semantics};
use crate::pipeline::traits::SemanticsSolver;

use super::external;

/// Solver that delegates to an external ICCMA-compliant binary.
///
/// Uses the ICCMA'23 interface: `solver -p <problem> -f <file> -fo tgf`.
/// The framework is written as TGF (preserving original argument IDs) and
/// the solver's output is parsed in the ICCMA'23 `w`-prefix format.
///
/// Currently used for mu-toksia; any solver supporting the same protocol
/// (TGF input, `w`-prefix output) can be plugged in.
pub struct IccmaSolver {
    pub solver_path: PathBuf,
    pub solver_name: String,
}

impl IccmaSolver {
    fn problem_code(sem: &Semantics) -> Result<&'static str, AspicError> {
        match sem {
            Semantics::Complete => Ok("EE-CO"),
            Semantics::Preferred => Ok("EE-PR"),
            Semantics::Stable => Ok("EE-ST"),
            Semantics::SemiStable => Ok("EE-SST"),
            Semantics::Ideal => Ok("SE-ID"),
            Semantics::Grounded | Semantics::Eager | Semantics::Admissible => {
                Err(SolverError::UnsupportedSemantics(format!("{sem:?}")).into())
            }
        }
    }

    fn find_one_problem_code(sem: &Semantics) -> Result<&'static str, AspicError> {
        match sem {
            Semantics::Complete => Ok("SE-CO"),
            Semantics::Preferred => Ok("SE-PR"),
            Semantics::Stable => Ok("SE-ST"),
            Semantics::SemiStable => Ok("SE-SST"),
            Semantics::Ideal => Ok("SE-ID"),
            Semantics::Grounded | Semantics::Eager | Semantics::Admissible => {
                Err(SolverError::UnsupportedSemantics(format!("{sem:?}")).into())
            }
        }
    }

    /// Run the external ICCMA solver and return all extensions.
    ///
    /// # Errors
    ///
    /// Returns [`AspicError`] if the solver binary is not found, the
    /// semantics is unsupported, or the solver produces unparseable output.
    pub fn try_extensions(
        &self,
        af: &AbstractFramework,
        sem: &Semantics,
    ) -> Result<Vec<Extension>, AspicError> {
        let problem = Self::problem_code(sem)?;
        let instance_file = external::write_temp_instance(&af.to_tgf())?;

        let mut cmd = Command::new(&self.solver_path);
        cmd.arg("-p")
            .arg(problem)
            .arg("-f")
            .arg(instance_file.path())
            .arg("-fo")
            .arg("tgf");

        let stdout = external::run_solver_command(&mut cmd, &self.solver_name)?;

        if stdout.trim().is_empty() {
            return Err(
                SolverError::Failed(format!("{}: no output produced", self.solver_name)).into(),
            );
        }

        let name_to_id = external::iccma_name_map(af);
        parse_iccma_output(&stdout, &name_to_id)
    }

    /// Run the external ICCMA solver and return at most one extension.
    ///
    /// # Errors
    ///
    /// Returns [`AspicError`] if the solver binary is not found, the
    /// semantics is unsupported, or the solver produces unparseable output.
    pub fn try_find_one(
        &self,
        af: &AbstractFramework,
        sem: &Semantics,
    ) -> Result<Option<Extension>, AspicError> {
        let problem = Self::find_one_problem_code(sem)?;
        let instance_file = external::write_temp_instance(&af.to_tgf())?;

        let mut cmd = Command::new(&self.solver_path);
        cmd.arg("-p")
            .arg(problem)
            .arg("-f")
            .arg(instance_file.path())
            .arg("-fo")
            .arg("tgf");

        let stdout = external::run_solver_command(&mut cmd, &self.solver_name)?;

        if stdout.trim().is_empty() {
            return Err(
                SolverError::Failed(format!("{}: no output produced", self.solver_name)).into(),
            );
        }

        let name_to_id = external::iccma_name_map(af);
        let exts = parse_iccma_output(&stdout, &name_to_id)?;
        Ok(exts.into_iter().next())
    }
}

impl SemanticsSolver for IccmaSolver {
    fn extensions(&self, af: &AbstractFramework, sem: Semantics) -> Vec<Extension> {
        match self.try_extensions(af, &sem) {
            Ok(exts) => exts,
            Err(e) => {
                eprintln!("IccmaSolver ({}): {e}", self.solver_name);
                Vec::new()
            }
        }
    }

    fn find_one(&self, af: &AbstractFramework, sem: Semantics) -> Option<Extension> {
        match self.try_find_one(af, &sem) {
            Ok(opt) => opt,
            Err(e) => {
                eprintln!("IccmaSolver ({}): {e}", self.solver_name);
                None
            }
        }
    }
}

/// Parse ICCMA'23 output format.
///
/// Each `w` line lists the arguments in one extension, e.g. `w 1 3`.
/// A bare `w` (no arguments) represents the empty extension.
/// `NO` means no extension exists (valid for e.g. stable semantics).
fn parse_iccma_output(
    output: &str,
    name_to_id: &HashMap<String, ArgumentId>,
) -> Result<Vec<Extension>, AspicError> {
    let mut extensions = Vec::new();

    for line in output.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }

        if trimmed == "NO" {
            return Ok(Vec::new());
        }

        if !trimmed.starts_with("w ") && trimmed != "w" {
            continue;
        }

        let args_part = trimmed.strip_prefix("w").unwrap().trim();
        let mut ext = BTreeSet::new();

        if !args_part.is_empty() {
            for token in args_part.split_whitespace() {
                if let Some(&id) = name_to_id.get(token) {
                    ext.insert(id);
                } else {
                    return Err(SolverError::Failed(format!(
                        "unknown argument in solver output: {token}"
                    ))
                    .into());
                }
            }
        }

        extensions.push(ext);
    }

    Ok(extensions)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_ee_output() {
        let mut name_map = HashMap::new();
        name_map.insert("0".into(), ArgumentId(0));
        name_map.insert("1".into(), ArgumentId(1));
        name_map.insert("2".into(), ArgumentId(2));

        let output = "w 0 2 \nw 1 \n";
        let result = parse_iccma_output(output, &name_map).unwrap();

        assert_eq!(result.len(), 2);
        assert_eq!(result[0], BTreeSet::from([ArgumentId(0), ArgumentId(2)]));
        assert_eq!(result[1], BTreeSet::from([ArgumentId(1)]));
    }

    #[test]
    fn parse_se_output() {
        let mut name_map = HashMap::new();
        name_map.insert("0".into(), ArgumentId(0));
        name_map.insert("1".into(), ArgumentId(1));

        let output = "w 0 1\n";
        let result = parse_iccma_output(output, &name_map).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0], BTreeSet::from([ArgumentId(0), ArgumentId(1)]));
    }

    #[test]
    fn parse_no_output() {
        let name_map = HashMap::new();
        let output = "NO\n";
        let result = parse_iccma_output(output, &name_map).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn parse_empty_extension() {
        let name_map = HashMap::new();
        let output = "w\n";
        let result = parse_iccma_output(output, &name_map).unwrap();
        assert_eq!(result.len(), 1);
        assert!(result[0].is_empty());
    }
}
