use std::io::Write;
use std::path::{Path, PathBuf};

use canonical_path::CanonicalPathBuf;
use clap::{Parser, ValueHint};
use log::{debug, info};

use crate::commands::common_opts::RepoPath;
use pijul_core::{MutTxnT, MutTxnTExt, TxnTExt};

#[derive(Parser, Debug)]
pub struct Move {
    #[clap(flatten)]
    base: RepoPath,
    #[clap(hide = true, long = "salt")]
    salt: Option<u64>,
    /// Paths which need to be moved
    ///
    /// The last argument to this option is considered the
    /// destination
    #[clap(value_hint = ValueHint::AnyPath)]
    #[clap(required = true)]
    paths: Vec<PathBuf>,
}

impl Move {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub fn run(mut self) -> Result<(), anyhow::Error> {
        let repo = self.base.find_root()?;
        let to = if let Some(to) = self.paths.pop() {
            to
        } else {
            return Ok(());
        };
        let is_dir = if let Ok(m) = std::fs::metadata(&to) {
            m.is_dir()
        } else {
            false
        };
        if !is_dir && self.paths.len() > 1 {
            return Ok(());
        }

        let mut txn = repo.pristine.mut_txn_begin()?;
        let repo_path = CanonicalPathBuf::canonicalize(&repo.path)?;
        for p in self.paths {
            debug!("p = {:?}", p);
            let source = std::fs::canonicalize(&p.clone())?;
            debug!("source = {:?}", source);
            let target = if is_dir {
                to.join(source.file_name().unwrap())
            } else {
                to.clone()
            };
            debug!("target = {:?}", target);

            let r = Rename {
                source: &source,
                target: &target,
            };
            std::fs::rename(r.source, r.target)?;
            let target = std::fs::canonicalize(r.target)?;
            debug!("target = {:?}", target);
            {
                let source = source.strip_prefix(&repo_path)?;
                use path_slash::PathExt;
                let source = source.to_slash_lossy();
                let target = target.strip_prefix(&repo_path)?;
                let target = target.to_slash_lossy();
                debug!("moving {:?} -> {:?}", source, target);
                txn.move_file(&source, &target, self.salt.unwrap_or(0))?;
            }
            std::mem::forget(r);
        }
        txn.commit()?;
        Ok(())
    }
}

struct Rename<'a> {
    source: &'a Path,
    target: &'a Path,
}

impl<'a> Drop for Rename<'a> {
    fn drop(&mut self) {
        std::fs::rename(self.target, self.source).unwrap_or(())
    }
}

#[derive(Parser, Debug)]
pub struct List {
    #[clap(flatten)]
    base: RepoPath,
}

impl List {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub fn run(mut self) -> Result<(), anyhow::Error> {
        let repo = self.base.find_root()?;
        let txn = repo.pristine.txn_begin()?;
        let mut stdout = std::io::stdout();

        let mut working_copy = txn.iter_working_copy().peekable();
        if working_copy.peek().is_none() {
            writeln!(stdout, "No tracked files")?;
            return Ok(());
        }

        for p in working_copy {
            let p = p?.1;
            writeln!(stdout, "{}", p)?;
        }
        Ok(())
    }
}

#[derive(Parser, Debug)]
pub struct Add {
    #[clap(flatten)]
    base: RepoPath,
    #[clap(short = 'r', long = "recursive")]
    recursive: bool,
    #[clap(short = 'f', long = "force")]
    force: bool,
    #[clap(hide = true, long = "salt")]
    salt: Option<u64>,
    /// Paths to add to the internal tree.
    #[clap(required = true)]
    paths: Vec<PathBuf>,
}

impl Add {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub fn run(mut self) -> Result<(), anyhow::Error> {
        let repo = self.base.find_root()?;
        let txn = repo.pristine.arc_txn_begin()?;
        let threads = std::thread::available_parallelism()?.get();
        let repo_path = CanonicalPathBuf::canonicalize(&repo.path)?;
        let mut stderr = std::io::stderr();
        for path in self.paths.iter() {
            info!("Adding {:?}", path);
            let path = CanonicalPathBuf::canonicalize(&path)?;
            debug!("{:?}", path);
            let meta = std::fs::metadata(&path)?;
            debug!("{:?}", meta);
            if !self.force
                && !pijul_core::working_copy::filesystem::filter_ignore(
                    repo_path.as_ref(),
                    path.as_ref(),
                    meta.is_dir(),
                )
            {
                continue;
            }
            if self.recursive {
                use pijul_core::working_copy::filesystem::*;
                let (full, _) = get_prefix(Some(repo_path.as_ref()), path.as_path())?;
                let full = CanonicalPathBuf::new(&full)?;
                repo.working_copy.add_prefix_rec(
                    &txn,
                    repo_path.clone(),
                    full.clone(),
                    self.force,
                    threads,
                    self.salt.unwrap_or(0),
                )?
            } else {
                let mut txn = txn.write();
                let path = if let Ok(path) = path.as_path().strip_prefix(&repo_path.as_path()) {
                    path
                } else {
                    continue;
                };

                use path_slash::PathExt;
                let path_str = path.to_slash_lossy();

                // If the user calls `pijul add .`, the entire path will be stripped, so we must account for this edge case
                if path.is_dir() || path_str == "" {
                    let display_str = if path.is_dir() {
                        path_str.as_ref()
                    } else {
                        "."
                    };

                    writeln!(
                        stderr,
                        "The directory `{}` has been recorded, but its contents will not be tracked (use --recursive to override)",
                        display_str
                    )?;
                }

                if !txn.is_tracked(&path_str)? {
                    if let Err(e) = txn.add(&path_str, meta.is_dir(), self.salt.unwrap_or(0)) {
                        writeln!(stderr, "{}", e)?;
                    }
                }
            }
        }
        txn.commit()?;
        writeln!(stderr, "Tracked {} path(s)", self.paths.iter().count())?;

        Ok(())
    }
}

#[derive(Parser, Debug)]
pub struct Remove {
    #[clap(flatten)]
    base: RepoPath,
    /// The paths to remove
    #[clap(required = true)]
    paths: Vec<PathBuf>,
}

impl Remove {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub fn run(mut self) -> Result<(), anyhow::Error> {
        let repo = self.base.find_root()?;
        let mut txn = repo.pristine.mut_txn_begin()?;
        let repo_path = CanonicalPathBuf::canonicalize(&repo.path)?;
        for path in self.paths.iter() {
            debug!("{:?}", path);

            if let Some(p) = path.file_name() {
                if let Some(p) = p.to_str() {
                    if p.ends_with('~') || (p.starts_with('#') && p.ends_with('#')) {
                        continue;
                    }
                }
            }

            let path = path.canonicalize()?;
            let path = if let Ok(path) = path.strip_prefix(&repo_path.as_path()) {
                path
            } else {
                continue;
            };
            use path_slash::PathExt;
            let path_str = path.to_slash_lossy();
            if txn.is_tracked(&path_str)? {
                txn.remove_file(&path_str)?;
            }
        }
        txn.commit()?;
        Ok(())
    }
}
