use std::{error::Error, fmt, future::Future, path::PathBuf};

use tokio::task::JoinSet;
use tracing::{debug, error, info};

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct EditPath {
    pub(super) path: PathBuf,
}

/// Represents a pending edit operation that can be written to persistent storage later.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Edit {
    /// An edit that doesn't do anything.
    NoOp,

    /// Write the given content to a file.
    Write(EditPath, Vec<u8>),

    /// Execute a sequence of edits in order.
    Seq(Vec<Edit>),
    /// Execute the provided edits in parallel.
    All(Vec<Edit>),

    /// Makes an edit dry.
    ///
    /// A dry edit only logs what operations would be performed, does not perform the I/O.
    Dry(Box<Edit>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApplyFailed;

impl Edit {
    #[expect(clippy::manual_async_fn)]
    pub fn apply(self) -> impl Future<Output = Result<(), ApplyFailed>> + Send {
        async {
            match self {
                Edit::NoOp => debug!("no op edit"),
                Edit::Write(edit_path, content) => {
                    tokio::fs::write(&edit_path.path, &content)
                        .await
                        .inspect_err(|err| error!("write to {edit_path:?} failed: {err:?}"))
                        .map_err(|_| ApplyFailed)?;
                    debug!("wrote {} bytes to {edit_path:?}", content.len())
                }
                Edit::Seq(vec) => {
                    debug!("begin sequence of {} edits", vec.len());

                    for edit in vec {
                        Box::pin(edit.apply()).await?;
                    }

                    debug!("end sequence");
                }
                Edit::All(vec) => {
                    debug!("begin parallel {} edits", vec.len());

                    let mut set = JoinSet::new();
                    for edit in vec {
                        set.spawn(edit.apply());
                    }
                    while let Some(result) = set.join_next().await {
                        result.map_err(|_| ApplyFailed)??;
                    }

                    debug!("end parallel");
                }
                Edit::Dry(edit) => edit.dry(),
            }

            Ok(())
        }
    }

    pub fn dry(&self) {
        match self {
            Edit::NoOp => (),
            Edit::Write(edit_path, content) => {
                info!("{edit_path:?}: would write {:?} bytes", content.len());
            }
            Edit::Seq(edits) => edits.iter().for_each(Self::dry),
            Edit::All(edits) => edits.iter().for_each(Self::dry),
            Edit::Dry(edit) => edit.dry(),
        }
    }
}

impl fmt::Display for ApplyFailed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("failed to apply some edits")
    }
}

impl Error for ApplyFailed {}

impl fmt::Debug for EditPath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.path, f)
    }
}
