Open source Nest implementation
use std::fmt::Write;
use std::path::PathBuf;

use crate::models::{pijul::changestores::Changestore, users::User, projects::Project};
use crate::database::Database;

use libpijul::pristine::sanakirja::Pristine;
use libpijul::TxnT;
use libpijul::TxnTExt;
use libpijul::{Base32, Hash};
use libpijul::{MutTxnT, MutTxnTExt};

use anyhow::bail;

pub struct Repository<'r> {
    /// full_path is the fully qualified path on disk
    full_path: PathBuf,
    project: &'r Project,
}

const DEFAULT_CHANNEL: &str = "main";
const PRISTINE_DIR: &str = "pristine";

impl <'r>Repository<'r> {
    fn pristine_dir(&self) -> PathBuf {
        self.full_path.join(PRISTINE_DIR)
    }

    fn pristine(&self) -> Result<Pristine, anyhow::Error> {
        // TODO error should be logged, or handled somehow, not like this
        match Pristine::new(self.pristine_dir().join("db")) {
            Ok(p) => Ok(p),
            Err(e) => bail!("Failed to open pristine: {}", e),
        }
    }

    pub fn changestore(&self) -> Changestore {
        Changestore::new(self.full_path.clone())
    }

    // Checks if the change is on disk, and if we can deserialize
    pub fn valid_change(&self, hash: Hash) -> bool {
        libpijul::change::Change::deserialize(
            &self.changestore().change_file(hash).to_string_lossy(),
            Some(&hash),
        )
        .is_ok()
    }

    // TODO maybe make a proper channel struct etc later
    pub fn apply_change_to_channel(&self, chan: &str, change: Hash) -> Result<(), anyhow::Error> {
        let mut txn = self.pristine()?.mut_txn_begin()?;

        let chan = if let Some(chan) = txn.load_channel(chan)? {
            chan
        } else {
            bail!("channel not found")
        };

        {
            let mut write_channel = chan.write();

            txn.apply_change(
                &libpijul::changestore::filesystem::FileSystem::from_changes(
                    self.changestore().dir(),
                ),
                &mut write_channel,
                &change,
            )?;
        }

        txn.commit()?;

        Ok(())
    }

    fn new(project: &Project, full_path: PathBuf) -> Repository {
        Repository {
            project: project,
            full_path: full_path.join(libpijul::DOT_DIR),
        }
    }

    /// init returns an error if there's already something on disk
    fn init(&self) -> Result<(), anyhow::Error> {
        if std::fs::metadata(self.pristine_dir()).is_err() {
            std::fs::create_dir_all(self.pristine_dir())?;
        } else {
            bail!("Already a repository on disk")
        }

        // TODO why the f does libpijul do this? It's properly weird even small
        // actions can just create their own txn
        let mut txn = self.pristine()?.mut_txn_begin()?;

        txn.open_or_create_channel(DEFAULT_CHANNEL)?;
        txn.set_current_channel(DEFAULT_CHANNEL)?;
        txn.commit()?;

        Ok(())
    }

    /// Either initiate a Pijul repository, or open the one already present on
    /// disk. Fails if the pristine cannot be opened correctly.
    pub fn init_or_open(project: &'r Project, root: &PathBuf) -> Result<Self, anyhow::Error> {
        let repo = Self::new(project, root.join(project.repo_path()));

        // If the pristine can't be construsted, assume nothing is on disk
        // TODO make a proper is_valid();
        match repo.pristine() {
            Ok(_) => Ok(repo),
            Err(e) => {
                println!("{}", e);
                repo.init()?;
                Ok(repo)
            }
        }
    }

    pub fn channel_remote_id(&self, channel: String, _id: Option<String>) -> Option<String> {
        let txn = match self.pristine().ok()?.mut_txn_begin() {
            Err(_) => return None,
            Ok(txn) => txn,
        };
        // TODO validate the txn needs closing?
        //
        // Or does the drop function do that?
        let chan = match txn.load_channel(&channel) {
            Ok(c) => c,
            Err(_) => None,
        };

        match chan {
            Some(c) => Some(c.read().id.to_string()),
            None => {
                println!("No chan found");
                None
            }
        }
    }

    // TODO figure out how to stream the response, instead of allocating all now
    pub fn changelist(&self, channel: String, from: u64) -> Result<String, anyhow::Error> {
        let txn = self.pristine()?.mut_txn_begin()?;

        let chan = match txn.load_channel(&channel)? {
            Some(c) => c,
            None => bail!("failed to read channel transaction"),
        };

        let mut out: String = "".to_string();

        for change in txn.log(&chan.read(), from)? {
            let (offset, (hash, merkle)) = change?;
            let h: Hash = hash.into();
            let m: libpijul::Merkle = merkle.into();

            write!(out, "{}.{}.{}\n", offset, h.to_base32(), m.to_base32())?;
        }

        Ok(out)
    }

    pub async fn owner(&self, db: &Database) -> anyhow::Result<User> {
        self.project.owner(db).await
    }
}