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

use libpijul::pristine::{Hash, Merkle, MutTxnT, Position, TxnT};
use libpijul::*;
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>,
) -> Result<Option<(u64, Merkle)>, anyhow::Error> {
    if let Some(mid) = mid {
        Ok(txn.get_changes(&channel, mid)?.map(|(_, m)| (mid, m)))
    } else {
        Ok(txn.reverse_log(&channel.borrow(), None)?.next().map(|n| {
            let (n, (_, m)) = n.unwrap();
            (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<HashSet<Position<Hash>>, 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(HashSet::new());
        };
        let mut paths_ = HashSet::new();
        let mut result = HashSet::new();
        for s in paths {
            if let Ok((p, _ambiguous)) = remote_txn.follow_oldest_path(&store, &remote_channel, s) {
                debug!("p = {:?}", p);
                result.insert(Position {
                    change: remote_txn.get_external(p.change)?.unwrap(),
                    pos: p.pos,
                });
                paths_.insert(p);
                paths_.extend(
                    libpijul::fs::iter_graph_descendants(
                        &remote_txn,
                        &remote_channel.borrow().graph,
                        p,
                    )?
                    .map(|x| x.unwrap()),
                );
            }
        }
        debug!("paths_ = {:?}", paths_);
        for x in remote_txn.log(&remote_channel.borrow(), from)? {
            let (n, (h, m)) = x?;
            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))
                            .unwrap()
                            .is_some()
                    })
                {
                    txn.put_remote(remote, n, (h, m))?;
                }
            }
        }
        Ok(result)
    }

    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,
            &mut HashMap::new(),
            "",
            true,
            None,
        )?;
        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(())
}