use std::collections::BTreeSet;
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;

/// Semantics solver that delegates to clingo via the ASPARTIX encodings.
///
/// This solver shells out to an external clingo binary, passing the framework
/// in ASPARTIX format along with the appropriate encoding file. Its primary
/// value is as a test oracle: results can be compared against the SAT solver
/// for correctness validation.
///
/// Requires clingo to be installed and accessible at `clingo_path`.
pub struct AspartixSolver {
    pub clingo_path: PathBuf,
    pub encodings_dir: PathBuf,
}

impl AspartixSolver {
    /// Create a solver that looks for clingo on PATH and uses encodings
    /// from the given directory.
    pub fn new(encodings_dir: PathBuf) -> Self {
        AspartixSolver {
            clingo_path: PathBuf::from("clingo"),
            encodings_dir,
        }
    }

    /// Create a solver with an explicit clingo path.
    pub fn with_clingo(clingo_path: PathBuf, encodings_dir: PathBuf) -> Self {
        AspartixSolver {
            clingo_path,
            encodings_dir,
        }
    }

    fn encoding_file(&self, sem: &Semantics) -> Result<PathBuf, AspicError> {
        let filename = match sem {
            Semantics::Stable => "stable.dl",
            Semantics::Complete => "comp.dl",
            Semantics::Grounded => "ground.dl",
            Semantics::Preferred => "prefex_gringo.lp",
            Semantics::SemiStable => "semi_stable_gringo.lp",
            Semantics::Ideal | Semantics::Eager | Semantics::Admissible => {
                return Err(SolverError::Failed(format!(
                    "ASPARTIX encoding not available for {sem:?} (requires multi-shot solving)"
                ))
                .into());
            }
        };
        let path = self.encodings_dir.join(filename);
        external::require_path(&path, "encoding file")?;
        Ok(path)
    }

    /// Run clingo and return extensions, or an error if clingo is not
    /// available or the solve fails.
    ///
    /// # Errors
    ///
    /// Returns [`AspicError`] if `clingo` is not found on `PATH`, the
    /// encoding file is missing, or clingo exits with an error.
    pub fn try_extensions(
        &self,
        af: &AbstractFramework,
        sem: &Semantics,
    ) -> Result<Vec<Extension>, AspicError> {
        let encoding_path = self.encoding_file(sem)?;
        let filter_path = self.encodings_dir.join("filter.lp");

        let instance_file = external::write_temp_instance(&af.to_aspartix())?;

        let mut cmd = Command::new(&self.clingo_path);
        cmd.arg(instance_file.path()).arg(&encoding_path).arg("0");

        if filter_path.exists() {
            cmd.arg(&filter_path);
        }

        let stdout = external::run_solver_command(&mut cmd, "clingo")?;
        let name_to_id = external::aspartix_name_map(af);
        parse_clingo_output(&stdout, &name_to_id)
    }
}

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

/// Parse clingo output into extensions.
///
/// Clingo outputs answer sets as lines like:
///   Answer: 1
///   in(a0) in(a2)
///   Answer: 2
///   in(a1)
///   SATISFIABLE
#[allow(clippy::unnecessary_wraps)]
fn parse_clingo_output(
    output: &str,
    name_to_id: &std::collections::HashMap<String, ArgumentId>,
) -> Result<Vec<Extension>, AspicError> {
    let mut extensions = Vec::new();
    let mut in_answer = false;

    for line in output.lines() {
        let trimmed = line.trim();

        if trimmed.starts_with("Answer:") {
            in_answer = true;
            continue;
        }

        if in_answer {
            in_answer = false;
            let mut ext = BTreeSet::new();

            for token in trimmed.split_whitespace() {
                if let Some(inner) = token.strip_prefix("in(").and_then(|s| s.strip_suffix(')')) {
                    if let Some(&id) = name_to_id.get(inner) {
                        ext.insert(id);
                    }
                }
            }

            extensions.push(ext);
        }
    }

    Ok(extensions)
}
