use std::path::PathBuf;
use std::sync::Arc;

use libpijul::pristine::{Base32, Hash, Merkle, MutTxnT, TxnT};
use libpijul::{MutTxnTExt, TxnTExt};
use log::debug;

use super::RemoteRef;

#[derive(Clone)]
pub struct Local {
    pub channel: String,
    pub root: std::path::PathBuf,
    pub changes_dir: std::path::PathBuf,
    pub pristine: Arc<libpijul::pristine::sanakirja::Pristine>,
    pub name: String,
}

pub fn get_state<T: TxnTExt>(
    txn: &T,
    channel: &libpijul::pristine::ChannelRef<T>,
    mid: Option<u64>,
) -> Option<(u64, Merkle)> {
    if let Some(mid) = mid {
        txn.get_changes(&channel, mid).map(|(_, m)| (mid, m))
    } else {
        txn.reverse_log(&channel.borrow(), None)
            .next()
            .map(|(n, (_, m))| (n, m))
    }
}

impl Local {
    pub fn get_state(&mut self, mid: Option<u64>) -> Result<Option<(u64, Merkle)>, anyhow::Error> {
        let txn = self.pristine.txn_begin()?;
        let channel = txn.load_channel(&self.channel).unwrap();
        Ok(get_state(&txn, &channel, mid))
    }

    pub fn download_changelist<T: MutTxnT>(
        &mut self,
        txn: &mut T,
        remote: &mut RemoteRef<T>,
        from: u64,
        paths: &[String],
    ) -> Result<(), anyhow::Error> {
        let store = libpijul::changestore::filesystem::FileSystem::from_root(&self.root);
        let remote_txn = self.pristine.txn_begin()?;
        let remote_channel = if let Some(channel) = remote_txn.load_channel(&self.channel) {
            channel
        } else {
            debug!("no remote channel named {:?}", self.channel);
            return Ok(());
        };
        let mut paths_ = Vec::new();
        for s in paths {
            if let Ok((p, _ambiguous)) = remote_txn.follow_oldest_path(&store, &remote_channel, s) {
                paths_.push(p)
            }
        }
        for (n, (h, m)) in remote_txn.log(&remote_channel.borrow(), from) {
            if n >= from {
                debug!(
                    "downloading changelist item {:?} {:?} {:?}",
                    n,
                    h.to_base32(),
                    m.to_base32()
                );
                let h_int = remote_txn.get_internal(h).unwrap();
                if paths_.is_empty()
                    || paths_
                        .iter()
                        .any(|x| remote_txn.get_touched_files(*x, Some(h_int)).is_some())
                {
                    txn.put_remote(remote, n, (h, m))?;
                }
            }
        }
        Ok(())
    }

    pub fn upload_changes(
        &mut self,
        mut local: PathBuf,
        to_channel: Option<&str>,
        changes: &[Hash],
    ) -> Result<(), anyhow::Error> {
        let store = libpijul::changestore::filesystem::FileSystem::from_root(&self.root);
        let mut txn = self.pristine.mut_txn_begin();
        let mut channel = txn.open_or_create_channel(to_channel.unwrap_or(&self.channel))?;
        for c in changes {
            libpijul::changestore::filesystem::push_filename(&mut local, &c);
            libpijul::changestore::filesystem::push_filename(&mut self.changes_dir, &c);
            std::fs::create_dir_all(&self.changes_dir.parent().unwrap())?;
            debug!("hard link {:?} {:?}", local, self.changes_dir);
            if std::fs::metadata(&self.changes_dir).is_err() {
                if std::fs::hard_link(&local, &self.changes_dir).is_err() {
                    std::fs::copy(&local, &self.changes_dir)?;
                }
            }
            debug!("hard link done");
            libpijul::changestore::filesystem::pop_filename(&mut local);
            libpijul::changestore::filesystem::pop_filename(&mut self.changes_dir);
        }
        let mut repo = libpijul::working_copy::filesystem::FileSystem::from_root(&self.root);
        upload_changes(&store, &mut txn, &mut channel, changes)?;
        txn.output_repository_no_pending(&mut repo, &store, &mut channel, "", true)?;
        txn.commit()?;
        Ok(())
    }

    pub async fn download_changes(
        &mut self,
        c: &[libpijul::pristine::Hash],
        send: &mut tokio::sync::mpsc::Sender<libpijul::pristine::Hash>,
        mut path: &mut PathBuf,
    ) -> Result<(), anyhow::Error> {
        for c in c {
            libpijul::changestore::filesystem::push_filename(&mut self.changes_dir, c);
            libpijul::changestore::filesystem::push_filename(&mut path, c);
            if std::fs::metadata(&path).is_ok() {
                debug!("metadata {:?} ok", path);
                libpijul::changestore::filesystem::pop_filename(&mut path);
                continue;
            }
            std::fs::create_dir_all(&path.parent().unwrap())?;
            if std::fs::hard_link(&self.changes_dir, &path).is_err() {
                std::fs::copy(&self.changes_dir, &path)?;
            }
            debug!("hard link done");
            libpijul::changestore::filesystem::pop_filename(&mut self.changes_dir);
            libpijul::changestore::filesystem::pop_filename(&mut path);
            debug!("sent");
            send.send(*c).await.unwrap();
        }
        Ok(())
    }
}

pub fn upload_changes<T: MutTxnTExt, C: libpijul::changestore::ChangeStore>(
    store: &C,
    txn: &mut T,
    channel: &mut libpijul::pristine::ChannelRef<T>,
    changes: &[Hash],
) -> Result<(), anyhow::Error> {
    let mut ws = libpijul::ApplyWorkspace::new();
    for c in changes {
        txn.apply_change_ws(store, channel, *c, &mut ws)?;
    }
    Ok(())
}
