//! Dung-style abstract argumentation frameworks.
//!
//! An [`AbstractFramework`] is the bridge between structured ASPIC+
//! argumentation and classical Dung-style semantics evaluation. After the
//! pipeline computes defeats (attacks filtered through preference orderings),
//! the resulting directed graph of arguments and defeat relations is
//! represented as an `AbstractFramework`.
//!
//! The framework provides:
//!
//! - Efficient adjacency queries ([`attackers_of`](AbstractFramework::attackers_of),
//!   [`attacked_by`](AbstractFramework::attacked_by), [`defeats`](AbstractFramework::defeats)).
//! - Dung-theoretic property checks ([`is_conflict_free`](AbstractFramework::is_conflict_free),
//!   [`is_admissible`](AbstractFramework::is_admissible),
//!   [`is_complete`](AbstractFramework::is_complete),
//!   [`is_stable`](AbstractFramework::is_stable)).
//! - Graph decomposition ([`sccs`](AbstractFramework::sccs),
//!   [`sub_framework`](AbstractFramework::sub_framework)).
//! - Import from ASPARTIX format ([`from_aspartix`](AbstractFramework::from_aspartix)).
//! - Export to TGF, ASPARTIX, DOT, and ICCMA via methods in [`crate::io::export`].

use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

use crate::error::{AspicError, SolverError};
use crate::framework::argument::ArgumentId;
use crate::framework::argument_store::ArgumentStore;
use crate::framework::attack::Defeat;

/// A Dung-style abstract argumentation framework derived from ASPIC+.
///
/// Consists of a set of arguments and a binary defeat relation over those
/// arguments. Internally maintains hash-based adjacency lists for efficient
/// attacker/attacked queries. Construct via
/// [`from_defeats`](Self::from_defeats) or [`from_aspartix`](Self::from_aspartix).
///
/// See the [module-level documentation](self) for the full API surface.
///
/// # Example
///
/// ```
/// use pickaxe_rs::prelude::*;
///
/// // Parse from ASPARTIX format.
/// let input = "arg(a).\narg(b).\narg(c).\natt(a,b).\natt(b,c).\n";
/// let (af, names) = AbstractFramework::from_aspartix(input).unwrap();
///
/// assert_eq!(af.arguments.len(), 3);
/// assert!(af.defeats(names["a"], names["b"]));
/// assert!(af.is_conflict_free(&[names["a"], names["c"]].into_iter().collect()));
///
/// // Export to TGF and DOT formats.
/// let tgf = af.to_tgf();
/// assert!(tgf.contains("#"));
/// let aspartix = af.to_aspartix();
/// assert!(aspartix.contains("att("));
/// ```
#[derive(Clone, Debug)]
pub struct AbstractFramework {
    /// The set of all argument IDs in this framework.
    pub arguments: BTreeSet<ArgumentId>,
    /// Directed defeat pairs (attacker, target) forming the attack relation.
    pub defeats: Vec<(ArgumentId, ArgumentId)>,
    defeat_set: HashSet<(ArgumentId, ArgumentId)>,
    attackers_map: HashMap<ArgumentId, Vec<ArgumentId>>,
    attacked_map: HashMap<ArgumentId, Vec<ArgumentId>>,
}

impl AbstractFramework {
    /// Build from an argument store and its computed defeats.
    pub fn from_defeats(store: &ArgumentStore, defeats: &[Defeat]) -> Self {
        let mut arguments = BTreeSet::new();
        let mut defeat_pairs = Vec::new();
        let mut attackers_map: HashMap<ArgumentId, Vec<ArgumentId>> = HashMap::new();
        let mut attacked_map: HashMap<ArgumentId, Vec<ArgumentId>> = HashMap::new();
        let mut seen = HashSet::new();

        for (id, _) in store.all_arguments() {
            arguments.insert(id);
        }

        for defeat in defeats {
            let pair = (defeat.attacker, defeat.target);
            if seen.insert(pair) {
                defeat_pairs.push(pair);
                attackers_map
                    .entry(defeat.target)
                    .or_default()
                    .push(defeat.attacker);
                attacked_map
                    .entry(defeat.attacker)
                    .or_default()
                    .push(defeat.target);
            }
        }

        let defeat_set: HashSet<(ArgumentId, ArgumentId)> = defeat_pairs.iter().copied().collect();

        AbstractFramework {
            arguments,
            defeats: defeat_pairs,
            defeat_set,
            attackers_map,
            attacked_map,
        }
    }

    /// Whether `attacker` defeats `target`.
    pub fn defeats(&self, attacker: ArgumentId, target: ArgumentId) -> bool {
        self.defeat_set.contains(&(attacker, target))
    }

    /// All arguments that defeat `target`.
    #[must_use]
    pub fn attackers_of(&self, target: ArgumentId) -> &[ArgumentId] {
        self.attackers_map
            .get(&target)
            .map_or(&[], std::vec::Vec::as_slice)
    }

    /// Add a defeat relation to the framework (idempotent).
    pub fn add_defeat(&mut self, from: ArgumentId, to: ArgumentId) {
        if !self.defeat_set.insert((from, to)) {
            return;
        }
        self.defeats.push((from, to));
        self.attackers_map.entry(to).or_default().push(from);
        self.attacked_map.entry(from).or_default().push(to);
    }

    /// All arguments defeated by `attacker`.
    #[must_use]
    pub fn attacked_by(&self, attacker: ArgumentId) -> &[ArgumentId] {
        self.attacked_map
            .get(&attacker)
            .map_or(&[], std::vec::Vec::as_slice)
    }

    /// Check if a set defends an argument (all attackers of the argument
    /// are attacked by some member of the set).
    pub fn defends(&self, set: &BTreeSet<ArgumentId>, arg: ArgumentId) -> bool {
        for &attacker in self.attackers_of(arg) {
            let defended = set.iter().any(|&defender| self.defeats(defender, attacker));
            if !defended {
                return false;
            }
        }
        true
    }

    /// Check if a set is conflict-free (no member attacks another member).
    ///
    /// Traverses each member's adjacency list rather than checking all pairs,
    /// giving O(Σ outdegree) complexity instead of O(|S|²).
    pub fn is_conflict_free(&self, set: &BTreeSet<ArgumentId>) -> bool {
        for &a in set {
            for &target in self.attacked_by(a) {
                if set.contains(&target) {
                    return false;
                }
            }
        }
        true
    }

    /// Check if a set defends all its members (without checking conflict-freeness).
    ///
    /// Use this when the set is already known to be conflict-free (e.g. produced
    /// by conflict-free enumeration). For any conflict-free set S,
    /// `defends_all_members(S) == is_admissible(S)`.
    pub fn defends_all_members(&self, set: &BTreeSet<ArgumentId>) -> bool {
        set.iter().all(|&arg| self.defends(set, arg))
    }

    /// Check if a set is admissible (conflict-free and defends all its members).
    pub fn is_admissible(&self, set: &BTreeSet<ArgumentId>) -> bool {
        if !self.is_conflict_free(set) {
            return false;
        }
        for &arg in set {
            if !self.defends(set, arg) {
                return false;
            }
        }
        true
    }

    /// Check if a set is a complete extension (admissible and contains all
    /// arguments it defends).
    pub fn is_complete(&self, set: &BTreeSet<ArgumentId>) -> bool {
        if !self.is_admissible(set) {
            return false;
        }
        for &arg in &self.arguments {
            if !set.contains(&arg) && self.defends(set, arg) {
                return false;
            }
        }
        true
    }

    /// Check if a set is a stable extension (conflict-free and attacks
    /// all non-members).
    pub fn is_stable(&self, set: &BTreeSet<ArgumentId>) -> bool {
        if !self.is_conflict_free(set) {
            return false;
        }
        for &arg in &self.arguments {
            if !set.contains(&arg) {
                let attacked = set.iter().any(|&s| self.defeats(s, arg));
                if !attacked {
                    return false;
                }
            }
        }
        true
    }

    /// The set of unattacked arguments.
    pub fn unattacked(&self) -> BTreeSet<ArgumentId> {
        self.arguments
            .iter()
            .filter(|&&arg| self.attackers_of(arg).is_empty())
            .copied()
            .collect()
    }

    /// Compute the strongly connected components using Tarjan's algorithm.
    ///
    /// Returns SCCs in reverse topological order (leaf components first).
    /// Callers that need topological order (e.g. SCC-recursive solvers)
    /// should reverse the result.
    pub fn sccs(&self) -> Vec<BTreeSet<ArgumentId>> {
        crate::algorithms::tarjan::strongly_connected_components(
            &self.arguments,
            &self.attacked_map,
        )
    }

    /// Extract a sub-framework restricted to the given set of arguments.
    ///
    /// Only defeats between arguments in the given set are included.
    pub fn sub_framework(&self, args: &BTreeSet<ArgumentId>) -> AbstractFramework {
        let mut defeat_pairs = Vec::new();
        let mut attackers_map: HashMap<ArgumentId, Vec<ArgumentId>> = HashMap::new();
        let mut attacked_map: HashMap<ArgumentId, Vec<ArgumentId>> = HashMap::new();

        for &(from, to) in &self.defeats {
            if args.contains(&from) && args.contains(&to) {
                defeat_pairs.push((from, to));
                attackers_map.entry(to).or_default().push(from);
                attacked_map.entry(from).or_default().push(to);
            }
        }

        let defeat_set = defeat_pairs.iter().copied().collect();

        AbstractFramework {
            arguments: args.clone(),
            defeats: defeat_pairs,
            defeat_set,
            attackers_map,
            attacked_map,
        }
    }

    /// Parse an ASPARTIX format string into an abstract argumentation framework.
    ///
    /// Accepts `arg(NAME).` and `att(NAME1,NAME2).` declarations in any order.
    /// Returns the framework and a mapping from argument names to IDs.
    ///
    /// # Errors
    ///
    /// Returns [`AspicError`] if an `att(...)` line is malformed or references an
    /// argument name that was never declared with `arg(...)`.
    pub fn from_aspartix(input: &str) -> Result<(Self, BTreeMap<String, ArgumentId>), AspicError> {
        let mut name_to_id: BTreeMap<String, ArgumentId> = BTreeMap::new();
        let mut next_id = 0usize;
        let mut att_lines: Vec<&str> = Vec::new();

        // Pass 1: collect all arg declarations
        for line in input.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }

            if let Some(inner) = line.strip_prefix("arg(").and_then(|s| s.strip_suffix(").")) {
                let name = inner.trim().to_string();
                if let std::collections::btree_map::Entry::Vacant(e) = name_to_id.entry(name) {
                    e.insert(ArgumentId(next_id));
                    next_id += 1;
                }
            } else if line.starts_with("att(") {
                att_lines.push(line);
            }
        }

        // Pass 2: process att declarations
        let mut defeat_pairs = Vec::new();
        for line in att_lines {
            let inner = line
                .strip_prefix("att(")
                .and_then(|s| s.strip_suffix(")."))
                .ok_or_else(|| {
                    AspicError::from(SolverError::AspartixParse(format!(
                        "invalid att declaration: {line}"
                    )))
                })?;
            let parts: Vec<&str> = inner.splitn(2, ',').collect();
            if parts.len() != 2 {
                return Err(
                    SolverError::AspartixParse(format!("invalid att declaration: {line}")).into(),
                );
            }
            let from_name = parts[0].trim();
            let to_name = parts[1].trim();

            let from_id = *name_to_id.get(from_name).ok_or_else(|| {
                AspicError::from(SolverError::AspartixParse(format!(
                    "undefined argument: {from_name}"
                )))
            })?;
            let to_id = *name_to_id.get(to_name).ok_or_else(|| {
                AspicError::from(SolverError::AspartixParse(format!(
                    "undefined argument: {to_name}"
                )))
            })?;

            defeat_pairs.push((from_id, to_id));
        }

        let arguments: BTreeSet<ArgumentId> = name_to_id.values().copied().collect();
        let mut attackers_map: HashMap<ArgumentId, Vec<ArgumentId>> = HashMap::new();
        let mut attacked_map: HashMap<ArgumentId, Vec<ArgumentId>> = HashMap::new();

        for &(from, to) in &defeat_pairs {
            attackers_map.entry(to).or_default().push(from);
            attacked_map.entry(from).or_default().push(to);
        }

        let defeat_set = defeat_pairs.iter().copied().collect();

        Ok((
            AbstractFramework {
                arguments,
                defeats: defeat_pairs,
                defeat_set,
                attackers_map,
                attacked_map,
            },
            name_to_id,
        ))
    }
}
