use std::fmt;
use std::io;
use std::process::ExitStatus;

use crate::node::NodeId;
use crate::prelude::RepoId;

/// Events that can occur when an upload-pack process is running.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum UploadPack {
    /// The upload-pack process finished with `status`.
    Done {
        /// The repository being fetched.
        rid: RepoId,
        /// The node being fetched from.
        remote: NodeId,
        /// The status code of the upload-pack process.
        ///
        /// N.b. `ExitStatus` cannot be de/serialized, so the `Display` of the
        /// status is used instead.
        status: String,
    },
    /// The upload-pack process emitted some [`Progress`] data.
    Write {
        /// The repository being fetched.
        rid: RepoId,
        /// The node being fetched from.
        remote: NodeId,
        /// The progress metadata of the upload-pack.
        progress: Progress,
    },
    /// An error occurred during the upload-pack process.
    Error {
        /// The repository being fetched.
        rid: RepoId,
        /// The node being fetched from.
        remote: NodeId,
        /// The error that occurred during the upload-pack.
        err: String,
    },
    /// The upload-pack packfile transmission progress.
    PackProgress {
        /// The repository being fetched.
        rid: RepoId,
        /// The node being fetched from.
        remote: NodeId,
        /// The total number of bytes transmitted.
        transmitted: usize,
    },
}

impl UploadPack {
    /// Construct a `UploadPack::PackProgress` event.
    pub fn pack_progress(rid: RepoId, remote: NodeId, transmitted: usize) -> Self {
        Self::PackProgress {
            rid,
            remote,
            transmitted,
        }
    }

    /// Construct a `UploadPack::Write` event.
    pub fn write(rid: RepoId, remote: NodeId, progress: Progress) -> Self {
        Self::Write {
            rid,
            remote,
            progress,
        }
    }

    /// Construct a `UploadPack::Done` event.
    ///
    /// If `error` is `None` the process finished successfully; otherwise, it
    /// finished with an error.
    pub fn done(rid: RepoId, remote: NodeId, status: ExitStatus) -> Self {
        Self::Done {
            rid,
            remote,
            status: status.to_string(),
        }
    }

    pub fn error(rid: RepoId, remote: NodeId, err: io::Error) -> Self {
        Self::Error {
            rid,
            remote,
            err: err.to_string(),
        }
    }
}

/// Progress updates emitted from the `git-upload-pack` process.
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Progress {
    Enumerating { total: usize },
    Counting { processed: usize, total: usize },
    Compressing { processed: usize, total: usize },
}

impl fmt::Display for Progress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Progress::Enumerating { total } => write!(f, "Enumerating objects: {total}"),
            Progress::Counting { processed, total } => {
                let percent = (processed / total) * 100;
                write!(f, "Counting objects: {percent}% ({processed}/{total})")
            }
            Progress::Compressing { processed, total } => {
                let percent = (processed / total) * 100;
                write!(f, "Compressing objects: {percent}% ({processed}/{total})")
            }
        }
    }
}
