#![allow(dead_code)] // kept for `[[link]]` block support — wired in a later phase

use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LinkMode {
    Symlink,
    Copy,
}

pub fn install(src: &Path, dest: &Path, mode: LinkMode) -> Result<()> {
    if let Some(parent) = dest.parent() {
        if !parent.as_os_str().is_empty() {
            fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
        }
    }

    let tmp = tmp_path(dest);
    let _ = fs::remove_file(&tmp);

    match mode {
        LinkMode::Symlink => {
            std::os::unix::fs::symlink(src, &tmp)
                .with_context(|| format!("symlinking {} -> {}", tmp.display(), src.display()))?;
        }
        LinkMode::Copy => {
            fs::copy(src, &tmp)
                .with_context(|| format!("copying {} to {}", src.display(), tmp.display()))?;
        }
    }

    fs::rename(&tmp, dest)
        .with_context(|| format!("renaming {} to {}", tmp.display(), dest.display()))?;
    Ok(())
}

pub fn remove(dest: &Path) -> Result<()> {
    match fs::symlink_metadata(dest) {
        Ok(meta) => {
            if meta.file_type().is_dir() {
                anyhow::bail!("{} is a directory, refusing to remove", dest.display());
            }
            fs::remove_file(dest).with_context(|| format!("removing {}", dest.display()))
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(e) => Err(e).with_context(|| format!("stat {}", dest.display())),
    }
}

fn tmp_path(dest: &Path) -> PathBuf {
    let mut s = OsString::from(dest.as_os_str());
    s.push(".aimgr-tmp");
    PathBuf::from(s)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    fn write_src(dir: &Path, name: &str, body: &str) -> PathBuf {
        let path = dir.join(name);
        let mut f = fs::File::create(&path).unwrap();
        f.write_all(body.as_bytes()).unwrap();
        path
    }

    #[test]
    fn symlink_installs_and_points_at_src() {
        let dir = tempdir().unwrap();
        let src = write_src(dir.path(), "source.md", "hello");
        let dest = dir.path().join("nested/dest.md");

        install(&src, &dest, LinkMode::Symlink).unwrap();

        assert!(fs::symlink_metadata(&dest)
            .unwrap()
            .file_type()
            .is_symlink());
        assert_eq!(fs::read_to_string(&dest).unwrap(), "hello");
    }

    #[test]
    fn symlink_creates_missing_parent_dirs() {
        let dir = tempdir().unwrap();
        let src = write_src(dir.path(), "source.md", "x");
        let dest = dir.path().join("a/b/c/out.md");

        install(&src, &dest, LinkMode::Symlink).unwrap();

        assert!(dest.exists());
    }

    #[test]
    fn symlink_overwrites_existing_file() {
        let dir = tempdir().unwrap();
        let src = write_src(dir.path(), "source.md", "new");
        let dest = write_src(dir.path(), "dest.md", "old");

        install(&src, &dest, LinkMode::Symlink).unwrap();

        assert!(fs::symlink_metadata(&dest)
            .unwrap()
            .file_type()
            .is_symlink());
        assert_eq!(fs::read_to_string(&dest).unwrap(), "new");
    }

    #[test]
    fn copy_installs_independent_file() {
        let dir = tempdir().unwrap();
        let src = write_src(dir.path(), "source.md", "original");
        let dest = dir.path().join("dest.md");

        install(&src, &dest, LinkMode::Copy).unwrap();

        assert!(!fs::symlink_metadata(&dest)
            .unwrap()
            .file_type()
            .is_symlink());

        // Mutate src; dest must not follow.
        write_src(dir.path(), "source.md", "mutated");
        assert_eq!(fs::read_to_string(&dest).unwrap(), "original");
    }

    #[test]
    fn remove_deletes_symlink() {
        let dir = tempdir().unwrap();
        let src = write_src(dir.path(), "source.md", "x");
        let dest = dir.path().join("dest.md");
        install(&src, &dest, LinkMode::Symlink).unwrap();

        remove(&dest).unwrap();

        assert!(!dest.exists());
        assert!(src.exists(), "remove must not touch the source");
    }

    #[test]
    fn remove_deletes_copy() {
        let dir = tempdir().unwrap();
        let src = write_src(dir.path(), "source.md", "x");
        let dest = dir.path().join("dest.md");
        install(&src, &dest, LinkMode::Copy).unwrap();

        remove(&dest).unwrap();

        assert!(!dest.exists());
    }

    #[test]
    fn remove_missing_file_is_noop() {
        let dir = tempdir().unwrap();
        remove(&dir.path().join("never-existed.md")).unwrap();
    }

    #[test]
    fn remove_directory_errors() {
        let dir = tempdir().unwrap();
        let sub = dir.path().join("sub");
        fs::create_dir(&sub).unwrap();
        assert!(remove(&sub).is_err());
        assert!(sub.exists(), "directory must remain after refusal");
    }

    #[test]
    fn reinstall_symlink_over_existing_symlink() {
        let dir = tempdir().unwrap();
        let src1 = write_src(dir.path(), "s1.md", "one");
        let src2 = write_src(dir.path(), "s2.md", "two");
        let dest = dir.path().join("dest.md");

        install(&src1, &dest, LinkMode::Symlink).unwrap();
        install(&src2, &dest, LinkMode::Symlink).unwrap();

        assert_eq!(fs::read_to_string(&dest).unwrap(), "two");
    }
}
