use crate::prelude::*;

use std::io::Read;
use std::path::Path;
use std::sync::Mutex;

use crate::graphs::{ConfigurableGraph, GraphConfig};
use futures::future::ok;
use taganak_core::default_graph_view;
use taganak_core::graphs::union_graph::UnionView;
use taganak_core::graphs::GraphView;
use taganak_core::sources::parsers::TripleParser;

use taganak_core::sources::serializers::{GraphSerializer, TripleSerializer};
use taganak_core::terms::{file_iri_to_path, TermError};
use taganak_core::triples::TripleRef;
use taganak_core::USizeAlias;
use taganak_orm::GraphORM;
use tempfile::NamedTempFile;
use tokio::io::{BufReader, BufWriter};

use tokio::fs::{File, OpenOptions};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom};

use futures::{Stream, StreamExt};

use iref::{InvalidIri, IriBuf};

use tracing::{debug, error, info};

fn guess_media_type(iri: &IriBuf) -> Option<String> {
    let mt = match Path::new(iri.path().as_str())
        .extension()
        .map(|ext| ext.to_str().unwrap())
    {
        Some("nt") => "application/n-triples",
        Some("ttl") => "text/turtle",
        Some(_) | None => return None,
    };

    Some(mt.to_string())
}

#[derive(Debug, Clone)]
pub struct LocalFileSourceGraph {
    config: LocalFileSourceGraphConfig,
    state: Arc<Mutex<BasicProfile>>,
}

#[derive(Debug, Clone, PartialEq, Eq, GraphORM, Hash)]
#[rdf(
    prefix(ns = "sdk", iri = "http://sdk.taganak.net/vocab/taganak#"),
    rdf_type = "sdk:LocalFileSourceGraphConfig"
)]
pub struct LocalFileSourceGraphConfig {
    #[rdf(predicate = "sdk:sourceIri")]
    source_iri: IriBuf,
    #[rdf(predicate = "sdk:baseIri")]
    base_iri: Option<IriBuf>,
    #[rdf(predicate = "sdk:mediaType")]
    media_type: Option<String>,
    #[rdf(predicate = "sdk:tripleSizeHint")]
    triple_size_hint: Option<USizeAlias>,
    #[rdf(predicate = "sdk:bufSizeHint")]
    buf_size_hint: Option<USizeAlias>,
    #[rdf(predicate = "sdk:assumeBase")]
    assume_base: bool,
}

#[derive(Debug, thiserror::Error)]
pub enum FromGraphError {
    #[error("the provided subject is not of type <https://ns.taganak.net/framework/LocalFileSourceGraph>")]
    NotHttpSourceGraphConfig,
    #[error("the requested subject has no <https://ns.taganak.net/framework/SourceIri> predicate")]
    NoSourceIri,
    #[error("the source iri is invalid: `{_0}`")]
    InvalidSourceIri(
        #[source]
        #[from]
        InvalidIri<String>,
    ),
    #[error("the source iri of the provided subject is no literal")]
    SourceIriNotLiteral,
    #[error("the provided graph config does not have the <https://ns.taganak.net/framework/graphConfig> attribute")]
    NoGraphConfig,
    #[error("the provided graph config is not of type <https://ns.taganak.net/framework/LocalFileSourceGraphConfig>")]
    NotGraphConfig,
    #[error("the provided HttpSourceGraphConfig is missing the mediatype attribute")]
    NoMediaType,
    #[error("the provided media type is not a literal")]
    MediaTypeNotLiteral,
    #[error("invalid media type {0:?}")]
    InvalidMediaType(
        #[source]
        #[from]
        TermError,
    ),
    #[error(transparent)]
    GraphError(#[from] GraphError),
}

impl LocalFileSourceGraphConfig {
    pub fn new(
        source_iri: IriBuf,
        base_iri: Option<IriBuf>,
        media_type: Option<String>,
        assume_base: bool,
    ) -> Self {
        let media_type = match media_type {
            Some(media_type) => Some(media_type),
            None => guess_media_type(&source_iri),
        };

        Self {
            source_iri,
            base_iri,
            media_type,
            triple_size_hint: None,
            buf_size_hint: None,
            assume_base,
        }
    }
}

impl GraphConfig for LocalFileSourceGraphConfig {
    type Graph = LocalFileSourceGraph;
}

impl ConfigurableGraph for LocalFileSourceGraph {
    type Config = LocalFileSourceGraphConfig;

    fn from_config(config: Self::Config) -> Result<Self, SourceError> {
        let mut profile = BasicProfile::new();
        profile.set_base_iri(Some(
            config.base_iri.clone().unwrap_or(config.source_iri.clone()),
        ));
        Ok(Self {
            config,
            state: Arc::new(profile.into()),
        })
    }
}

impl Graph for LocalFileSourceGraph {
    type View<'g> = &'g Self;

    fn view(&self) -> impl core::future::Future<Output = Self::View<'_>> + Send {
        core::future::ready(self)
    }

    async fn add(&mut self, triple: Arc<Triple>) -> Result<(), GraphError> {
        self.merge(triple.as_ref()).await
    }

    async fn merge<G>(&mut self, source_graph: G) -> Result<(), GraphError>
    where
        G: GraphView,
    {
        let path = file_iri_to_path(&self.config.source_iri)
            .ok_or(GraphError::Source(SourceError::UnsupportedSourceIri))?;
        let tmp = NamedTempFile::new_in(path.parent().unwrap()).map_err(SourceError::Io)?;
        let file = tmp.reopen().unwrap();
        let mut writer = tokio::io::BufWriter::new(tokio::fs::File::from_std(file));
        let source = UnionView((self.view().await, source_graph));
        TripleSerializer::from_media_type_and_profile(
            self.config
                .media_type
                .as_deref()
                .ok_or(SourceError::UnsupportedSourceConfig)?,
            self.state.clone(),
            if self.config.assume_base {
                Some(self.config.source_iri.clone())
            } else {
                None
            },
        )?
        .write_all(source, &mut writer)
        .await?;

        writer.flush().await.map_err(SourceError::Io)?;
        tmp.persist(path).map_err(|e| SourceError::Io(e.error))?;
        Ok(())
    }

    async fn clear(&mut self) -> Result<(), GraphError> {
        let path = file_iri_to_path(&self.config.source_iri)
            .ok_or(GraphError::Source(SourceError::UnsupportedSourceIri))?;
        tokio::fs::OpenOptions::new()
            .truncate(true)
            .open(path)
            .await
            .map_err(SourceError::Io)?;
        Ok(())
    }

    async fn remove(&mut self, triple: TripleRef<'_>) -> Result<(), GraphError> {
        let path = file_iri_to_path(&self.config.source_iri)
            .ok_or(GraphError::Source(SourceError::UnsupportedSourceIri))?;
        let tmp = NamedTempFile::new_in(path.parent().unwrap()).map_err(SourceError::Io)?;
        let file = tmp.reopen().unwrap();
        let mut writer = tokio::io::BufWriter::new(tokio::fs::File::from_std(file));
        let source = self
            .view()
            .await
            .filter(|t: TripleRef<'_>| t != triple)
            .await?;
        TripleSerializer::from_media_type_and_profile(
            self.config
                .media_type
                .as_deref()
                .ok_or(SourceError::UnsupportedSourceConfig)?,
            self.state.clone(),
            if self.config.assume_base {
                Some(self.config.source_iri.clone())
            } else {
                None
            },
        )?
        .write_all(source, &mut writer)
        .await?;

        writer.flush().await.map_err(SourceError::Io)?;
        tmp.persist(path).map_err(|e| SourceError::Io(e.error))?;
        Ok(())
    }
}

fn map_err(a: Result<Arc<Triple>, SourceError>) -> Result<Arc<Triple>, GraphError> {
    a.map_err(From::from)
}

impl<'g> GraphView for &'g LocalFileSourceGraph {
    type Stream = futures::stream::Map<
        TripleParser<BufReader<tokio::fs::File>>,
        fn(Result<Arc<Triple>, SourceError>) -> Result<Arc<Triple>, GraphError>,
    >;

    async fn stream(self) -> Result<Self::Stream, GraphError> {
        let path = file_iri_to_path(&self.config.source_iri)
            .ok_or(GraphError::Source(SourceError::UnsupportedSourceIri))?;
        info!(
            "Getting local file data from {}",
            path.to_str().expect("came from a valid IRI")
        );

        let _create_file_if_not_exists = tokio::fs::OpenOptions::new()
            .create(true)
            .truncate(false)
            .write(true)
            .open(&path)
            .await
            .map_err(SourceError::Io)?;

        let reader = BufReader::with_capacity(
            self.buf_size_hint(),
            File::open(path).await.map_err(SourceError::Io)?,
        );
        let media_type = match self.config.media_type.clone() {
            Some(mt) => {
                debug!(" -> media type was provided as {}", mt);
                mt
            }
            None => {
                debug!(" -> media type not provided, guessing.");
                // FIXME implement or remove guessing
                let guessed_mt = guess_media_type(&self.config.source_iri);
                match guessed_mt {
                    Some(mt) => {
                        debug!(" -> media type guessed as {}", mt);
                        mt
                    }
                    None => {
                        error!(" -> failed to guess media type");
                        return Err(GraphError::Source(SourceError::UnsupportedSourceConfig));
                    }
                }
            }
        };
        // FIXME: RETHINK EVERYTHING ABOUT PROFILES,... LIKE EVERYTHING
        let parser_state = (*self.state).lock().unwrap().clone();
        TripleParser::from_media_type(&media_type, reader, Arc::new(Mutex::new(parser_state)))
            .map_err(GraphError::Source)
            .map(|s| s.map(map_err as _))
    }

    default_graph_view!();
}

impl GraphView for LocalFileSourceGraph {
    type Stream = futures::stream::Map<
        TripleParser<BufReader<tokio::fs::File>>,
        fn(Result<Arc<Triple>, SourceError>) -> Result<Arc<Triple>, GraphError>,
    >;
    async fn stream(self) -> Result<Self::Stream, GraphError> {
        let stream = <&LocalFileSourceGraph as GraphView>::stream(&self).await?;
        Ok(stream)
    }

    default_graph_view!();
}
