use serde_derive::*;
use std::sync::Arc;
use tokio::sync::mpsc::*;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Update {
    Change {
        repo: uuid::Uuid,
        hash: libpijul::pristine::Hash,
        length: u64,
        is_tag: bool,
    },
    Apply {
        repo: uuid::Uuid,
        channel: String,
        hash: libpijul::pristine::Hash,
    },
    Unrecord {
        repo: uuid::Uuid,
        channel: String,
        hash: libpijul::pristine::Hash,
        deps: (),
    },
    NewChannel {
        repo: uuid::Uuid,
        channel: String,
    },
    Fork {
        repo: uuid::Uuid,
        channel: String,
        new: String,
    },
    Prune {
        repo: uuid::Uuid,
        channel: String,
    },
    Rename {
        repo: uuid::Uuid,
        channel: String,
        new: String,
    },
}

pub trait Handler: Send + Sync {
    type Error: std::fmt::Debug + Send + Sync + 'static;
    type F: futures::Future<Output = Result<Option<Self::Error>, Self::Error>> + Send + 'static;
    fn update(&self, is_init: bool, update: Update) -> Self::F;
}

#[derive(Clone)]
pub struct Worker<H: Handler> {
    pub config: Arc<Config>,
    pub handler: Arc<H>,
}

pub struct Config {
    pub repositories: PathBuf,
}

impl Config {

    pub fn changes_path(&self, id: uuid::Uuid) -> std::path::PathBuf {
        let mut p = std::path::Path::new(&self.repositories).join(&format!("{}", id));
        p.push(libpijul::DOT_DIR);
        p.push("changes");
        p
    }
    pub fn tags_path(&self, id: uuid::Uuid) -> std::path::PathBuf {
        self.changes_path(id)
    }
}

impl<H: Handler> Worker<H> {

    pub fn new(config: Arc<Config>, handler: H) -> Self {
        Worker {
            config,
            handler: Arc::new(handler),
        }
    }

    pub async fn deps(&self, _id: uuid::Uuid) -> Result<(), Error<H::Error>> {
        Ok(())
    }

    pub async fn worker(self, _: UnboundedReceiver<()>) {
        futures::future::pending().await
    }

    pub async fn handle_update(
        &self,
        _: Option<()>,
        _: Option<()>,
        _: Option<()>,
        update: Update,
    ) -> Result<(), Error<H::Error>> {
        self.handler.update(true, update).await?;
        Ok(())
    }

    pub async fn send_change(
        &self,
        repo: uuid::Uuid,
        hash: libpijul::Hash,
        is_tag: bool,
    ) -> Result<(), Error<H::Error>> {
        let mut path = if is_tag {
            self.config.tags_path(repo)
        } else {
            self.config.changes_path(repo)
        };
        libpijul::changestore::filesystem::push_filename(&mut path, &hash);
        if let Ok(m) = std::fs::metadata(&path) {
            self.handle_update(
                None,
                None,
                None,
                Update::Change {
                    repo,
                    hash,
                    length: m.len(),
                    is_tag,
                },
            )
            .await?;
            Ok(())
        } else {
            Err(Error::ChangeNotFound { repo, hash })
        }
    }

}

#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigFile {
    repositories_path: PathBuf,
}

impl ConfigFile {
    pub async fn to_config(self, _blsend: UnboundedSender<()>) -> Config {
        Config {
            repositories: self.repositories_path,
        }
    }
}

use thiserror::*;

#[derive(Debug, Error)]
pub enum Error<E> {
    #[error(transparent)]
    E(#[from]E),
    #[error("Change not found")]
    ChangeNotFound { repo: uuid::Uuid, hash: libpijul::Hash },
}