use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;

use oxrdf::vocab::rdfs;
use oxrdf::{NamedOrBlankNode, Term};
use oxttl::{TurtleParseError, TurtleParser};

use crate::model::{Edge, Graph, Node};

#[derive(Debug)]
pub enum TtlImportError {
    Io(std::io::Error),
    Parse(TurtleParseError),
    NoSubclassTriples,
}

impl Display for TtlImportError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            TtlImportError::Io(error) => write!(f, "failed to read Turtle input: {error}"),
            TtlImportError::Parse(error) => write!(f, "failed to parse Turtle input: {error}"),
            TtlImportError::NoSubclassTriples => {
                write!(
                    f,
                    "no usable rdfs:subClassOf triples were found in the Turtle input"
                )
            }
        }
    }
}

impl Error for TtlImportError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            TtlImportError::Io(error) => Some(error),
            TtlImportError::Parse(error) => Some(error),
            TtlImportError::NoSubclassTriples => None,
        }
    }
}

impl From<std::io::Error> for TtlImportError {
    fn from(error: std::io::Error) -> Self {
        Self::Io(error)
    }
}

impl From<TurtleParseError> for TtlImportError {
    fn from(error: TurtleParseError) -> Self {
        Self::Parse(error)
    }
}

pub fn graph_from_ttl_reader<R: Read>(reader: R) -> Result<Graph, TtlImportError> {
    let mut nodes = Vec::new();
    let mut node_indices = HashMap::new();
    let mut edges = Vec::new();
    let mut seen_edges = HashSet::new();

    for triple in TurtleParser::new().for_reader(reader) {
        let triple = triple?;
        if triple.predicate.as_ref() != rdfs::SUB_CLASS_OF {
            continue;
        }

        let NamedOrBlankNode::NamedNode(subject) = triple.subject else {
            continue;
        };
        let Term::NamedNode(object) = triple.object else {
            continue;
        };

        let subclass = get_or_insert_node(&mut nodes, &mut node_indices, subject.as_str());
        let superclass = get_or_insert_node(&mut nodes, &mut node_indices, object.as_str());
        if seen_edges.insert((superclass, subclass)) {
            edges.push(Edge::new(superclass, subclass));
        }
    }

    if edges.is_empty() {
        return Err(TtlImportError::NoSubclassTriples);
    }

    Ok(Graph::new(nodes, edges))
}

pub fn graph_from_ttl_path(path: impl AsRef<Path>) -> Result<Graph, TtlImportError> {
    let file = File::open(path)?;
    graph_from_ttl_reader(BufReader::new(file))
}

fn get_or_insert_node(
    nodes: &mut Vec<Node>,
    node_indices: &mut HashMap<String, usize>,
    iri: &str,
) -> usize {
    if let Some(&index) = node_indices.get(iri) {
        return index;
    }

    let index = nodes.len();
    nodes.push(Node {
        label: Some(iri.to_owned()),
        ..Node::default()
    });
    node_indices.insert(iri.to_owned(), index);
    index
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{layout_radial_hierarchy, LayoutConfig};

    const TTL_PREFIXES: &str = "@prefix ex: <http://example.com/> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix schema: <http://schema.org/> .\n";

    #[test]
    fn imports_only_subclass_triples() {
        let ttl = format!(
            "{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:A schema:name \"Alpha\" .\nex:B schema:name \"Beta\" .\n"
        );

        let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();

        assert_eq!(graph.nodes.len(), 2);
        assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
        assert_eq!(
            graph.nodes[0].label.as_deref(),
            Some("http://example.com/A")
        );
        assert_eq!(
            graph.nodes[1].label.as_deref(),
            Some("http://example.com/B")
        );
    }

    #[test]
    fn deduplicates_repeated_subclass_triples() {
        let ttl =
            format!("{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:A rdfs:subClassOf ex:B .\n");

        let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();

        assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
    }

    #[test]
    fn ignores_blank_node_and_literal_targets() {
        let ttl = format!(
            "{TTL_PREFIXES}ex:A rdfs:subClassOf [ a ex:Anonymous ] .\nex:B rdfs:subClassOf \"Literal\" .\nex:C rdfs:subClassOf ex:D .\n"
        );

        let graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();

        assert_eq!(graph.nodes.len(), 2);
        assert_eq!(graph.edges, vec![Edge::new(1, 0)]);
        assert_eq!(
            graph.nodes[0].label.as_deref(),
            Some("http://example.com/C")
        );
        assert_eq!(
            graph.nodes[1].label.as_deref(),
            Some("http://example.com/D")
        );
    }

    #[test]
    fn imports_can_flow_into_layout() {
        let ttl =
            format!("{TTL_PREFIXES}ex:A rdfs:subClassOf ex:B .\nex:B rdfs:subClassOf ex:C .\n");

        let mut graph = graph_from_ttl_reader(ttl.as_bytes()).unwrap();
        layout_radial_hierarchy(&mut graph, LayoutConfig::default()).unwrap();

        assert!(graph
            .nodes
            .iter()
            .all(|node| node.x.is_finite() && node.y.is_finite()));
    }

    #[test]
    fn returns_clear_error_for_invalid_turtle() {
        let ttl = "@prefix ex: <http://example.com/> .\nex:A rdfs:subClassOf .\n";
        let error = graph_from_ttl_reader(ttl.as_bytes()).unwrap_err();

        assert!(matches!(error, TtlImportError::Parse(_)));
    }

    #[test]
    fn returns_clear_error_when_no_subclass_triples_exist() {
        let ttl = format!("{TTL_PREFIXES}ex:A schema:name \"Alpha\" .\n");
        let error = graph_from_ttl_reader(ttl.as_bytes()).unwrap_err();

        assert!(matches!(error, TtlImportError::NoSubclassTriples));
    }
}
