use std::{
    collections::HashSet,
    path::{Path, PathBuf},
    sync::Arc,
};

use futures::StreamExt;
use iref::IriBuf;
use sha2::Digest;

use taganak_core::{
    graphs::{
        bijection::{Bijection, TermBijectionOverlay},
        AndFilter, Graph, JoinedMatches, Matches, TrueFilter, YesMatch,
    },
    impl_default_cbd,
    terms::Term,
    USizeAlias,
};
use taganak_orm::{
    re::{BlankNode, BlankNodeInterface, GraphError, GraphView, SourceError, Triple, TripleFilter},
    GraphORM, GraphORMError,
};

use crate::graphs::{
    local::persistent::local_file::{LocalFileSourceGraph, LocalFileSourceGraphConfig},
    ConfigurableGraph, GraphConfig,
};

use self::streams::FidibatsStream;

mod streams;

#[derive(Debug, Clone, PartialEq, Eq, Hash, GraphORM)]
#[rdf(
    prefix(ns = "fidibats", iri = "http://sdk.taganak.net/vocab/fidibats#"),
    prefix(ns = "owl", iri = "http://www.w3.org/2002/07/owl#"),
    rdf_type = "fidibats:Graph",
    default(prefix = "fidibats", case_conv = true)
)]
pub struct FidibatsMetaData {
    term_hash_algo: TermHashAlgo,
    version: u32,
    #[rdf(predicate = "owl:sameAs")]
    uuid_iri: Term,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, GraphORM)]
#[rdf(
    prefix(ns = "fidibats", iri = "http://sdk.taganak.net/vocab/fidibats#"),
    rdf_type = "fidibats:TermHashAlgo",
    default(prefix = "fidibats", case_conv = true)
)]
pub enum TermHashAlgo {
    #[default]
    Sha256,
    Sha512,
}

impl Default for FidibatsMetaData {
    fn default() -> Self {
        Self {
            term_hash_algo: Default::default(),
            version: 1,
            uuid_iri: Term::NamedNode(
                IriBuf::new(format!("urn:uuid:{}", uuid::Uuid::new_v4())).unwrap(),
            ),
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum FidibatsMetaError {
    #[error("io error occured when trying to open fidibats metadata")]
    Io(
        #[from]
        #[source]
        std::io::Error,
    ),

    #[error("failed to open meta graph")]
    FailedToOpenMetaGraph(
        #[from]
        #[source]
        SourceError,
    ),

    #[error("failed to parse existing graph metadata")]
    FailedToParseExistingMeta(
        #[from]
        #[source]
        GraphORMError,
    ),

    #[error("failed to serialize new graph metadata")]
    GraphError(
        #[from]
        #[source]
        GraphError,
    ),

    #[error("invalid meta uuid")]
    InvalidUuid,

    #[error("fidibats iri must reference a directory")]
    MustBeDir,
}

impl From<FidibatsMetaError> for GraphError {
    fn from(value: FidibatsMetaError) -> Self {
        match value {
            FidibatsMetaError::Io(error) => SourceError::Io(error).into(),
            FidibatsMetaError::FailedToOpenMetaGraph(source_error) => source_error.into(),
            FidibatsMetaError::FailedToParseExistingMeta(_) => {
                SourceError::UnsupportedSourceConfig.into()
            }
            FidibatsMetaError::GraphError(graph_error) => graph_error,
            FidibatsMetaError::InvalidUuid => {
                GraphError::Source(SourceError::UnsupportedSourceConfig)
            }
            FidibatsMetaError::MustBeDir => GraphError::Source(SourceError::UnsupportedSourceIri),
        }
    }
}

impl FidibatsMetaData {
    pub fn uuid(&self) -> uuid::Uuid {
        self.uuid_iri
            .to_iri()
            .unwrap()
            .strip_prefix("urn:uuid:")
            .unwrap()
            .parse()
            .unwrap()
    }
    pub async fn open(path: impl AsRef<Path>) -> Result<FidibatsMetaData, FidibatsMetaError> {
        let path = tokio::fs::canonicalize(path.as_ref()).await?;
        let path_meta = tokio::fs::metadata(&path).await?;
        if !path_meta.is_dir() {
            return Err(FidibatsMetaError::MustBeDir);
        }
        if tokio::fs::try_exists(format!("{}/meta.ttl", path.display())).await? {
            let graph = LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                IriBuf::new(format!("file://{}/meta.ttl", path.display())).unwrap(),
                None,
                None,
                false,
            ))?;
            let metadata = FidibatsMetaData::deserialize(
                graph.view().await,
                &Term::NamedNode(IriBuf::new(format!("file://{}/", path.display())).unwrap()),
            )
            .await?;
            let Ok(uuid_iri) = metadata.uuid_iri.to_iri() else {
                return Err(FidibatsMetaError::InvalidUuid);
            };
            let Some(presumed_uuid) = uuid_iri.strip_prefix("urn:uuid:") else {
                return Err(FidibatsMetaError::InvalidUuid);
            };
            let Ok(_): Result<uuid::Uuid, _> = presumed_uuid.parse() else {
                return Err(FidibatsMetaError::InvalidUuid);
            };
            return Ok(metadata.into_deref());
        }

        let mut metadata = LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
            IriBuf::new(format!("file://{}/meta.ttl", path.display())).unwrap(),
            None,
            Some("text/turtle".to_string()),
            true,
        ))?;

        let meta = Self::default();

        let metadata_stream = meta
            .serialize_stream(Arc::new(Term::NamedNode(
                IriBuf::new(format!("file://{}/", path.display())).unwrap(),
            )))?
            .map(Ok::<_, GraphError>);

        metadata.merge_stream(metadata_stream).await?;
        Ok(meta)
    }
}

async fn delete_up_dirs(path: impl AsRef<Path>, n: u32) -> std::io::Result<()> {
    let mut path = path.as_ref();
    for _ in 0..n {
        let is_empty = tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(path).await?)
            .next()
            .await
            .is_none();
        if !is_empty {
            return Ok(());
        }
        tokio::fs::remove_dir(path).await?;

        path = path.parent().unwrap();
    }
    Ok(())
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, GraphORM)]
#[rdf(
    prefix(ns = "fidibats", iri = "http://sdk.taganak.net/vocab/fidibats#"),
    rdf_type = "fidibats:Config",
    default(prefix = "fidibats", case_conv = true)
)]
pub struct FidibatsSourceGraphConfig {
    pub source_iri: IriBuf,
    pub triple_size_hint: Option<USizeAlias>,
    pub buf_size_hint: Option<USizeAlias>,
}

#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct FidibatsSourceGraph {
    pub config: FidibatsSourceGraphConfig,
}

#[derive(Clone, Hash, PartialEq, Eq)]
pub struct FidibatsSourceGraphView<F, M> {
    pub config: FidibatsSourceGraphConfig,
    filter: F,
    matches: M,
    limit: Option<usize>,
}

impl FidibatsSourceGraphConfig {
    pub fn new(root_dir: &Path) -> Self {
        Self {
            source_iri: IriBuf::new(format!("file+fidibats://{}", root_dir.display())).unwrap(),
            triple_size_hint: None,
            buf_size_hint: None,
        }
    }
    pub fn from_iri(iri: iref::IriBuf) -> Result<Self, SourceError> {
        if iri.scheme().as_str() != "file+fidibats" {
            return Err(SourceError::UnsupportedSourceIri);
        }
        Ok(Self {
            source_iri: iri,
            triple_size_hint: None,
            buf_size_hint: None,
        })
    }
}

impl ConfigurableGraph for FidibatsSourceGraph {
    type Config = FidibatsSourceGraphConfig;

    fn from_config(config: Self::Config) -> Result<Self, taganak_core::prelude::SourceError>
    where
        Self: Sized,
    {
        Ok(Self { config })
    }
}

impl GraphConfig for FidibatsSourceGraphConfig {
    type Graph = FidibatsSourceGraph;
}

impl<F, M> GraphView for FidibatsSourceGraphView<F, M>
where
    M: Clone + Send + Matches + Unpin,
    F: Clone + Unpin + Send + TripleFilter,
{
    type Stream = FidibatsStream<F, M>;
    fn stream(
        self,
    ) -> impl core::future::Future<Output = Result<Self::Stream, taganak_core::prelude::GraphError>> + Send
    {
        FidibatsStream::new(self)
    }

    type Filter<T>
        = FidibatsSourceGraphView<AndFilter<T, F>, M>
    where
        T: TripleFilter + Unpin;

    async fn filter<T>(self, filter: T) -> Result<Self::Filter<T>, GraphError>
    where
        T: TripleFilter + Unpin,
    {
        let FidibatsSourceGraphView {
            config,
            matches,
            filter: prev_filter,
            limit,
        } = self;
        Ok(FidibatsSourceGraphView {
            filter: AndFilter::new(filter, prev_filter),
            config,
            matches,
            limit,
        })
    }

    type Matches<M2>
        = FidibatsSourceGraphView<F, JoinedMatches<M2, M>>
    where
        M2: Matches + Unpin;

    async fn matches<M2>(
        self,
        matches: M2,
        limit: Option<usize>,
    ) -> Result<Self::Matches<M2>, GraphError>
    where
        M2: Matches + Unpin,
    {
        let FidibatsSourceGraphView {
            config,
            matches: old_matches,
            filter,
            limit: old_limit,
        } = self;

        let new_limit = match (old_limit, limit) {
            (None, Some(l)) | (Some(l), None) => Some(l),
            (Some(prev), Some(new)) => Some(core::cmp::min(prev, new)),
            (None, None) => None,
        };
        Ok(FidibatsSourceGraphView {
            config,
            matches: JoinedMatches::new(matches, old_matches),
            filter,
            limit: new_limit,
        })
    }

    impl_default_cbd!();

    taganak_core::impl_default_streams!();

    type SubjectStreamDedup<'g> = Self::SubjectStream<'g>;

    fn subjects_dedup<'g>(
        self,
        predicate: Option<&'g Term>,
        object: Option<&'g Term>,
        limit: Option<usize>,
    ) -> impl core::future::Future<Output = Result<Self::SubjectStreamDedup<'g>, GraphError>> + Send {
        self.subjects(predicate, object, limit)
    }

    taganak_core::impl_predicate_stream_dedup!();
    taganak_core::impl_object_stream_dedup!();
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct SkolemizeBijection {
    graph_uuid: uuid::Uuid,
}

impl SkolemizeBijection {
    pub fn biject_triple(&self, triple: Arc<Triple>) -> Arc<Triple> {
        let subject = self.f(triple.subject_arc());
        let predicate = self.f(triple.predicate_arc());
        let object = self.f(triple.object_arc());

        Triple::new(subject, predicate, object).unwrap()
    }
    pub fn inv_biject_triple(&self, triple: Arc<Triple>) -> Arc<Triple> {
        let subject = self.inv_f(triple.subject_arc());
        let predicate = self.inv_f(triple.predicate_arc());
        let object = self.inv_f(triple.object_arc());

        Triple::new(subject, predicate, object).unwrap()
    }
}

impl Bijection for SkolemizeBijection {
    type Domain = Arc<Term>;

    type Image = Arc<Term>;

    fn f(&self, input: Self::Domain) -> Self::Image {
        match &*input {
            Term::BlankNode(blank_node) => {
                let skolem_base = IriBuf::new(format!(
                    "https://sdk.taganak.org/fidibats/{}/",
                    self.graph_uuid
                ))
                .unwrap();
                Arc::new(
                    Term::NamedNode(
                        iref::IriRef::new(&blank_node.id())
                            .unwrap()
                            .resolved(&skolem_base),
                    )
                    .to_owned(),
                )
            }
            _ => input,
        }
    }

    fn inv_f(&self, input: Self::Image) -> Self::Domain {
        match &*input {
            Term::NamedNode(iri_buf) => {
                let skolem_base = format!("https://sdk.taganak.org/fidibats/{}/", self.graph_uuid);
                let Some(blank_node_id) = iri_buf.strip_prefix(&skolem_base) else {
                    return input;
                };
                Arc::new(Term::BlankNode(BlankNode::from_string(
                    blank_node_id.to_string(),
                )))
            }
            _ => input,
        }
    }
}

impl FidibatsSourceGraphConfig {
    fn subject_file_path(
        &self,
        subject: &Term,
        meta: &FidibatsMetaData,
    ) -> Result<PathBuf, GraphError> {
        match subject {
            taganak_orm::re::Term::NamedNode(iri_buf) => {
                let mut path: Vec<&Path> = vec![
                    self.source_iri.path().as_str().as_ref(),
                    "named_nodes".as_ref(),
                ];
                let scheme = iri_buf.scheme().as_str();
                path.push(scheme.as_ref());
                match scheme {
                    "http" | "https" => path.push(
                        iri_buf
                            .authority()
                            .ok_or(GraphError::UnsupportedOp)?
                            .as_str()
                            .as_ref(),
                    ),
                    _ => path.push("_".as_ref()),
                }
                let subject_hash = format!(
                    "{}.ttl",
                    hash_bytes(iri_buf.as_bytes(), meta.term_hash_algo)
                );
                to_final_subdirs(&subject_hash, &mut path);
                Ok(path.into_iter().collect::<PathBuf>())
            }
            taganak_orm::re::Term::BlankNode(blank_node) => {
                let mut path: Vec<&Path> = vec![
                    self.source_iri.path().as_str().as_ref(),
                    "blank_nodes".as_ref(),
                ];
                let subject_hash = format!(
                    "{}.ttl",
                    hash_bytes(blank_node.id().as_bytes(), meta.term_hash_algo)
                );
                to_final_subdirs(&subject_hash, &mut path);
                Ok(path.into_iter().collect::<PathBuf>())
            }
            _ => Err(GraphError::UnsupportedOp),
        }
    }
}

impl Graph for FidibatsSourceGraph {
    type View<'g> = FidibatsSourceGraphView<TrueFilter, YesMatch>;
    async fn view(&self) -> Self::View<'static> {
        FidibatsSourceGraphView {
            config: self.config.clone(),
            filter: TrueFilter,
            matches: YesMatch,
            limit: None,
        }
    }

    async fn add(
        &mut self,
        triple: Arc<taganak_core::prelude::Triple>,
    ) -> Result<(), taganak_core::prelude::GraphError> {
        let meta = FidibatsMetaData::open(self.config.source_iri.path().as_str()).await?;
        let subject = triple.subject();
        let file_path = self.config.subject_file_path(subject, &meta)?;
        let base_dir = file_path.parent().unwrap();
        tokio::fs::create_dir_all(base_dir).await.unwrap();

        let skolem_biject = SkolemizeBijection {
            graph_uuid: meta.uuid(),
        };

        let mut triple_file_graph =
            LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                IriBuf::new(format!("file://{}", file_path.display())).unwrap(),
                triple.subject().to_iri().ok().cloned(),
                Some("text/turtle".to_string()),
                false,
            ))
            .unwrap();

        triple_file_graph
            .add(skolem_biject.biject_triple(triple))
            .await?;
        Ok(())
    }

    async fn merge<G>(&mut self, source_graph: G) -> Result<(), GraphError>
    where
        G: GraphView,
    {
        let meta = FidibatsMetaData::open(self.config.source_iri.path().as_str()).await?;
        let source_graph = TermBijectionOverlay::new(
            source_graph,
            SkolemizeBijection {
                graph_uuid: meta.uuid(),
            },
        );
        let mut subjects = source_graph.clone().subjects_dedup(None, None, None).await?;

        while let Some(Ok(subject)) = subjects.next().await {
            let subject: Arc<Term> = subject;
            let subject_path = self.config.subject_file_path(&subject, &meta)?;
            tokio::fs::create_dir_all(subject_path.parent().unwrap())
                .await
                .map_err(SourceError::Io)?;
            tracing::info!(?subject, ?subject_path, "found subject");
            let mut subject_graph =
                LocalFileSourceGraph::from_config(LocalFileSourceGraphConfig::new(
                    IriBuf::new(format!("file://{}", subject_path.display())).unwrap(),
                    subject.to_iri().ok().cloned(),
                    Some("text/turtle".to_string()),
                    false,
                ))
                .unwrap();
            let description = source_graph.clone().describe(&subject, None).await?;
            subject_graph.merge(description).await?;
        }
        Ok(())
    }

    async fn remove(
        &mut self,
        triple: taganak_core::prelude::TripleRef<'_>,
    ) -> Result<(), GraphError> {
        let meta = FidibatsMetaData::open(self.config.source_iri.path().as_str()).await?;
        let triple = SkolemizeBijection {
            graph_uuid: meta.uuid(),
        }
        .biject_triple(triple.to_triple());
        let subject = triple.subject();
        let file_path = self.config.subject_file_path(subject, &meta)?;

        if !tokio::fs::try_exists(&file_path)
            .await
            .map_err(SourceError::Io)?
        {
            tracing::info!(?file_path, "aborting remove as triple not in graph");
            return Ok(());
        }

        let mut triple_file_graph = LocalFileSourceGraphConfig::new(
            IriBuf::new(format!("file://{}", file_path.display())).unwrap(),
            None,
            Some("text/turtle".to_string()),
            false,
        )
        .into_graph()?;

        triple_file_graph.remove(triple.as_triple_ref()).await?;
        let len = triple_file_graph
            .matches(YesMatch, Some(1))
            .await?
            .stream()
            .await?
            .count()
            .await;

        if len != 0 {
            return Ok(());
        }

        tokio::fs::remove_file(&file_path)
            .await
            .map_err(SourceError::Io)?;

        let n = if subject.is_named_node() { 5 } else { 3 };

        delete_up_dirs(file_path.parent().unwrap(), n)
            .await
            .map_err(SourceError::Io)?;
        Ok(())
    }
}

fn to_hex(slice: &[u8]) -> String {
    use std::fmt::Write;
    let mut out = String::with_capacity(slice.len() * 2);
    for byte in slice {
        write!(&mut out, "{byte:02x}").unwrap()
    }
    out
}

fn hash_bytes(subject: &[u8], term_hash_algo: TermHashAlgo) -> String {
    match term_hash_algo {
        TermHashAlgo::Sha256 => to_hex(sha2::Sha256::digest(subject).as_slice()),
        TermHashAlgo::Sha512 => to_hex(sha2::Sha512::digest(subject).as_slice()),
    }
}

fn to_final_subdirs<'a>(hash: &'a str, out: &mut Vec<&'a Path>) {
    out.push(hash[0..1].as_ref());
    out.push(hash[1..2].as_ref());
    out.push(hash.as_ref());
}
