use std::{fmt, io};

use camino::{Utf8Path, Utf8PathBuf};
use serde::{Deserialize, Serialize};
use thiserror::Error as ThisError;
use tokio::fs;

use super::Status;

#[derive(Debug, ThisError)]
pub(crate) enum Error {
    #[error("unable to link {}->{}: {}", src, path, source)]
    CreateLink {
        path: Utf8PathBuf,
        src: Utf8PathBuf,
        source: io::Error,
    },
    #[error("unable to create {}: {}", path, source)]
    CreatePath {
        path: Utf8PathBuf,
        source: io::Error,
    },
    #[error("{} already exists", path)]
    PathExists { path: Utf8PathBuf },
    #[allow(dead_code)] // TODO: test-only errors should not be here
    #[error("unable to read {}: {}", path, source)]
    ReadPath {
        path: Utf8PathBuf,
        source: io::Error,
    },
    #[error("unable to remove {}: {}", path, source)]
    RemovePath {
        path: Utf8PathBuf,
        source: io::Error,
    },
    #[error("{} not found", src)]
    SrcNotFound { src: Utf8PathBuf },
    #[error("state={} requires src", format!("{state:?}").to_lowercase())]
    StateRequiresSrc { state: FileState },
    #[error("state={} is not yet implemented", format!("{state:?}").to_lowercase())]
    StateNotImplemented { state: FileState },
    #[allow(dead_code)] // TODO: test-only errors should not be here
    #[error(transparent)]
    TempPath { source: io::Error },
    #[error("unable to write {}: {}", path, source)]
    WritePath {
        path: Utf8PathBuf,
        source: io::Error,
    },
}
impl PartialEq for Error {
    fn eq(&self, other: &Error) -> bool {
        format!("{self:?}") == format!("{other:?}")
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum FileState {
    Absent,
    Directory,
    File,
    Hard,
    Link,
    Touch,
}

#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "lowercase", tag = "type")]
pub(crate) struct File {
    #[serde(deserialize_with = "crate::convert::into_option_bool")]
    pub force: Option<bool>,
    pub path: Utf8PathBuf,
    pub src: Option<Utf8PathBuf>,
    pub state: FileState,
}
impl Default for File {
    fn default() -> Self {
        Self {
            force: None,
            path: Utf8PathBuf::new(),
            src: None,
            state: FileState::Touch,
        }
    }
}
impl fmt::Display for File {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let force = self.force.unwrap_or(false);
        match self.state {
            FileState::Absent => write!(f, "rm -r{} {}", if force { "f" } else { "" }, &self.path),
            FileState::Directory => write!(f, "mkdir -p {}", &self.path),
            FileState::Link => write!(
                f,
                "ln -s{} {} {}",
                if force { "f" } else { "" },
                self.src.clone().unwrap_or_default(),
                &self.path
            ),
            FileState::Touch => write!(f, "touch {}", &self.path),
            _ => write!(f, "{self:#?}"),
        }
    }
}
impl File {
    pub async fn execute(&self) -> Result {
        match self.state {
            FileState::Absent => execute_absent(&self.path).await,
            FileState::Directory => {
                execute_directory(&self.path, self.force.unwrap_or(false)).await
            }
            FileState::Link => match &self.src {
                Some(s) => execute_link(s, &self.path, self.force.unwrap_or(false)).await,
                None => Err(Error::StateRequiresSrc { state: self.state }),
            },
            FileState::Touch => execute_touch(&self.path).await,
            _ => Err(Error::StateNotImplemented { state: self.state }),
        }
    }
}

pub(crate) type Result = std::result::Result<Status, Error>;

async fn execute_absent<P>(path: P) -> Result
where
    P: AsRef<Utf8Path>,
{
    let p = path.as_ref();
    if !p.exists() {
        return Ok(Status::no_change(format!("{p}")));
    }

    (if p.is_dir() {
        fs::remove_dir_all(&p).await
    } else {
        fs::remove_file(&p).await
    })
    .map_err(|e| Error::RemovePath {
        path: p.to_path_buf(),
        source: e,
    })?;
    Ok(Status::changed(format!("{p}"), String::from("absent")))
}

async fn execute_directory<P>(path: P, force: bool) -> Result
where
    P: AsRef<Utf8Path>,
{
    let p = path.as_ref();
    let previously;
    if p.is_dir() {
        return Ok(Status::no_change(format!("directory: {p}")));
    } else if p.exists() {
        if !force {
            return Err(Error::PathExists {
                path: p.to_path_buf(),
            });
        }
        previously = String::from("not directory");
        execute_absent(&p).await?;
    } else {
        previously = String::from("absent");
    }

    fs_create_dir_all(&p).await?;
    Ok(Status::changed(previously, format!("directory: {p}")))
}

async fn execute_link<P>(src: P, dest: P, force: bool) -> Result
where
    P: AsRef<Utf8Path>,
{
    let s = src.as_ref();
    if std::fs::symlink_metadata(s).is_err() && !force {
        return Err(Error::SrcNotFound {
            src: s.to_path_buf(),
        });
    }

    let d = dest.as_ref();
    let mut previously = String::from("absent");

    if let Ok(target) = std::fs::read_link(d) {
        previously = format!("{} -> {}", target.display(), d);
        if s == target {
            return Ok(Status::no_change(previously));
        }
        if !force {
            return Err(Error::PathExists {
                path: d.to_path_buf(),
            });
        }
    }
    // dest does not exist, or is wrong symlink, or is not a symlink

    match std::fs::symlink_metadata(d) {
        Ok(attr) => {
            if !attr.file_type().is_symlink() {
                previously = format!("existing: {}", &d);
            }
            if force {
                execute_absent(&d).await?;
            } else {
                return Err(Error::PathExists {
                    path: d.to_path_buf(),
                });
            }
        }
        Err(_) => {
            if let Some(parent) = d.parent() {
                execute_directory(&parent, force).await?;
            }
        }
    }

    symbolic_link(&s, &d).map_err(|e| Error::CreateLink {
        path: d.to_path_buf(),
        src: s.to_path_buf(),
        source: e,
    })?;

    Ok(Status::changed(previously, format!("{s} -> {d}",)))
}

async fn execute_touch<P>(path: P) -> Result
where
    P: AsRef<Utf8Path>,
{
    let p = path.as_ref();
    if p.exists() {
        // TODO: consider bumping access/modify time like real `touch`
        return Ok(Status::no_change(format!("{p}")));
    }
    if let Some(parent) = p.parent() {
        execute_directory(&parent, false).await?;
    }
    fs_write(p, "").await?;
    Ok(Status::changed(String::from("absent"), format!("{p}")))
}

async fn fs_create_dir_all<P>(p: P) -> std::result::Result<(), Error>
where
    P: AsRef<Utf8Path>,
{
    fs::create_dir_all(&p.as_ref().as_std_path())
        .await
        .map_err(|e| Error::CreatePath {
            path: p.as_ref().to_path_buf(),
            source: e,
        })
}

async fn fs_write<P, C>(p: P, c: C) -> std::result::Result<(), Error>
where
    P: AsRef<Utf8Path>,
    C: AsRef<[u8]>,
{
    fs::write(&p.as_ref().as_std_path(), c)
        .await
        .map_err(|e| Error::WritePath {
            path: p.as_ref().to_path_buf(),
            source: e,
        })
}

#[cfg(not(windows))]
fn symbolic_link<P>(src: P, dest: P) -> io::Result<()>
where
    P: AsRef<Utf8Path>,
{
    std::os::unix::fs::symlink(src.as_ref(), dest.as_ref())
}

#[cfg(windows)]
fn symbolic_link<P>(src: P, dest: P) -> io::Result<()>
where
    P: AsRef<Utf8Path>,
{
    let src_attr = std::fs::symlink_metadata(&src.as_ref().as_std_path())?;
    if src_attr.is_dir() {
        return std::os::windows::fs::symlink_dir(&src.as_ref().as_std_path(), dest);
    }

    std::os::windows::fs::symlink_file(&src.as_ref().as_std_path(), dest)
}

#[cfg(test)]
pub(crate) mod tests {
    use mktemp::Temp;

    use crate::status::Satisfying;

    use super::*;

    pub(crate) fn temp_dir() -> std::result::Result<Utf8PathBuf, Error> {
        Ok(Utf8PathBuf::try_from(
            Temp::new_dir()
                .map_err(|e| Error::TempPath { source: e })?
                .to_path_buf(),
        )
        .expect("temporary directory path has invalid characters"))
    }
    pub(crate) fn temp_file() -> std::result::Result<Utf8PathBuf, Error> {
        Ok(Utf8PathBuf::try_from(
            Temp::new_file()
                .map_err(|e| Error::TempPath { source: e })?
                .to_path_buf(),
        )
        .expect("temporary file path has invalid characters"))
    }

    #[tokio::test]
    async fn absent_deletes_existing_file() -> std::result::Result<(), Error> {
        let file = File {
            path: temp_file()?,
            state: FileState::Absent,
            ..Default::default()
        };

        fs_create_dir_all(&file.path.parent().unwrap()).await?;
        fs_write(&file.path, "").await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                format!("{}", file.path),
                String::from("absent")
            ))
        );
        assert!(fs::symlink_metadata(&file.path).await.is_err());
        Ok(())
    }

    #[tokio::test]
    async fn absent_deletes_existing_directory() -> std::result::Result<(), Error> {
        let file = File {
            path: temp_dir()?,
            state: FileState::Absent,
            ..Default::default()
        };

        fs_create_dir_all(&file.path).await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                format!("{}", file.path),
                String::from("absent")
            ))
        );
        assert!(fs::symlink_metadata(&file.path).await.is_err());
        Ok(())
    }

    #[tokio::test]
    async fn absent_makes_nochange_when_already_absent() -> std::result::Result<(), Error> {
        let file = File {
            path: temp_dir()?.join("missing.txt"),
            state: FileState::Absent,
            ..Default::default()
        };

        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::NoChange(format!("{}", file.path)))
        );
        Ok(())
    }

    #[tokio::test]
    async fn link_symlinks_src_to_path() -> std::result::Result<(), Error> {
        let src = temp_file()?.to_path_buf();
        let file = File {
            path: temp_file()?.to_path_buf(),
            src: Some(src.clone()),
            state: FileState::Link,
            ..Default::default()
        };

        fs_write(&src, "hello").await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                String::from("absent"),
                format!("{} -> {}", &src, file.path)
            ))
        );
        assert_eq!(fs_read(&file.path).await?, "hello");
        Ok(())
    }

    #[tokio::test]
    async fn link_symlinks_src_to_path_in_new_directory() -> std::result::Result<(), Error> {
        let src = temp_file()?.to_path_buf();
        let file = File {
            path: temp_dir()?.join("symlink.txt"),
            src: Some(src.clone()),
            state: FileState::Link,
            ..Default::default()
        };

        fs_create_dir_all(file.path.parent().unwrap()).await?;
        fs_write(&src, "hello").await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                String::from("absent"),
                format!("{} -> {}", &src, file.path)
            ))
        );
        assert_eq!(fs_read(&file.path).await?, "hello");
        Ok(())
    }

    #[tokio::test]
    async fn link_corrects_existing_symlink() -> std::result::Result<(), Error> {
        let src_old = temp_file()?.to_path_buf();
        let file_old = File {
            path: temp_dir()?.join("symlink.txt"),
            src: Some(src_old.clone()),
            state: FileState::Link,
            ..Default::default()
        };
        fs_write(&src_old, "hello_old").await?;
        file_old.execute().await?;

        let src = temp_file()?.to_path_buf();
        let file = File {
            force: Some(true),
            path: file_old.path,
            src: Some(src.clone()),
            state: FileState::Link,
        };

        fs_write(&src, "hello").await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                format!("{} -> {}", &src_old, file.path),
                format!("{} -> {}", &src, file.path)
            ))
        );
        assert_eq!(fs_read(&file.path).await?, "hello");
        Ok(())
    }

    #[tokio::test]
    async fn link_removes_existing_file_at_path() -> std::result::Result<(), Error> {
        let src = temp_file()?.to_path_buf();
        let file = File {
            force: Some(true),
            path: temp_file()?.to_path_buf(),
            src: Some(src.clone()),
            state: FileState::Link,
        };

        fs_write(&src, "hello").await?;
        fs_write(&file.path, "existing").await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                format!("existing: {}", file.path),
                format!("{} -> {}", &src, file.path)
            ))
        );
        assert_eq!(fs_read(&file.path).await?, "hello");
        Ok(())
    }

    #[tokio::test]
    async fn link_removes_existing_directory_at_path() -> std::result::Result<(), Error> {
        let src = temp_file()?.to_path_buf();
        let file = File {
            force: Some(true),
            path: temp_dir()?.to_path_buf(),
            src: Some(src.clone()),
            state: FileState::Link,
        };

        fs_write(&src, "hello").await?;
        fs_create_dir_all(&file.path).await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                format!("existing: {}", file.path),
                format!("{} -> {}", &src, file.path)
            ))
        );
        assert_eq!(fs_read(&file.path).await?, "hello");
        Ok(())
    }

    #[tokio::test]
    async fn link_without_force_requires_src_to_exist() -> std::result::Result<(), Error> {
        let src = temp_file()?.to_path_buf();
        let file = File {
            path: temp_dir()?.to_path_buf(),
            src: Some(src.clone()),
            state: FileState::Link,
            ..Default::default()
        };

        let got = file.execute().await;

        assert!(got.is_err());
        assert_eq!(got.err().unwrap(), Error::SrcNotFound { src },);
        Ok(())
    }

    #[tokio::test]
    async fn link_without_force_requires_path_to_not_exist() -> std::result::Result<(), Error> {
        let src = temp_file()?.to_path_buf();
        let file = File {
            path: temp_dir()?.to_path_buf(),
            src: Some(src.clone()),
            state: FileState::Link,
            ..Default::default()
        };

        fs_write(&src, "hello").await?;
        fs_create_dir_all(&file.path).await?;
        let got = file.execute().await;

        assert!(got.is_err());
        assert_eq!(got.err().unwrap(), Error::PathExists { path: file.path },);
        Ok(())
    }

    #[tokio::test]
    async fn name_absent() {
        let file = File {
            path: Utf8PathBuf::from("foo"),
            state: FileState::Absent,
            ..Default::default()
        };
        let got = format!("{file}");
        let want = "rm -r foo";
        assert_eq!(got, want);
    }

    #[tokio::test]
    async fn name_absent_force() {
        let file = File {
            force: Some(true),
            path: Utf8PathBuf::from("foo"),
            state: FileState::Absent,
            ..Default::default()
        };
        let got = format!("{file}");
        let want = "rm -rf foo";
        assert_eq!(got, want);
    }

    #[tokio::test]
    async fn name_directory() {
        let file = File {
            path: Utf8PathBuf::from("foo"),
            state: FileState::Directory,
            ..Default::default()
        };
        let got = format!("{file}");
        let want = "mkdir -p foo";
        assert_eq!(got, want);
    }

    #[test]
    fn name_link() {
        let file = File {
            path: Utf8PathBuf::from("foo"),
            src: Some(Utf8PathBuf::from("bar")),
            state: FileState::Link,
            ..Default::default()
        };
        let got = format!("{file}");
        let want = "ln -s bar foo";
        assert_eq!(got, want);
    }

    #[test]
    fn name_link_force() {
        let file = File {
            force: Some(true),
            path: Utf8PathBuf::from("foo"),
            src: Some(Utf8PathBuf::from("bar")),
            state: FileState::Link,
        };
        let got = format!("{file}");
        let want = "ln -sf bar foo";
        assert_eq!(got, want);
    }

    #[test]
    fn name_touch() {
        let file = File {
            path: Utf8PathBuf::from("foo"),
            state: FileState::Touch,
            ..Default::default()
        };
        let got = format!("{file}");
        let want = "touch foo";
        assert_eq!(got, want);
    }

    #[tokio::test]
    async fn touch_creates_new_empty_file() -> std::result::Result<(), Error> {
        let file = File {
            path: temp_dir()?.join("new.txt"),
            state: FileState::Touch,
            ..Default::default()
        };

        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::Changed(
                String::from("absent"),
                format!("{}", file.path)
            ))
        );
        Ok(())
    }

    #[tokio::test]
    async fn touch_makes_nochange_for_existing_path() -> std::result::Result<(), Error> {
        let file = File {
            path: temp_file()?.to_path_buf(),
            state: FileState::Touch,
            ..Default::default()
        };

        fs_create_dir_all(file.path.parent().unwrap()).await?;
        fs_write(&file.path, "").await?;
        let got = file.execute().await?;

        assert_eq!(
            got,
            Status::Satisfying(Satisfying::NoChange(format!("{}", file.path)))
        );
        Ok(())
    }

    async fn fs_read<P>(p: P) -> std::result::Result<String, Error>
    where
        P: AsRef<Utf8Path>,
    {
        let pb = p.as_ref().to_path_buf();
        fs::read_to_string(&pb).await.map_err(|e| Error::ReadPath {
            path: pb,
            source: e,
        })
    }
}
