// SPDX-License-Identifier: AGPL-3.0-or-later
//! Logic for writing reports of process completion

use std::{
    borrow::Cow,
    fmt::{Display, Write},
};

use crate::{cmd::CmdOutput, stats::Stats};

/// Options for reporting on server stats at the end of a run.
pub struct ReportOptions {
    target: ReportTarget,
    format: ReportFormat,
}
impl Default for ReportOptions {
    fn default() -> Self {
        Self {
            target: ReportTarget::Logs,
            format: ReportFormat::Plain,
        }
    }
}
impl ReportOptions {
    pub fn report_stats(&self, stats: &Stats) {
        let report = self.format.format_stats(stats);
        match self.target {
            ReportTarget::Logs => {
                tracing::info!("{report}")
            }
        }
    }
}

/// Report output target.
pub enum ReportTarget {
    /// Output the report to wherever logs are going.
    ///
    /// Note this means report visibility is subject to the configured verbosity level.
    Logs,
}

/// The format for the report.
pub enum ReportFormat {
    /// Format as plaintext.
    Plain,
}
impl ReportFormat {
    fn format_stats(&self, stats: &Stats) -> String {
        match self {
            Self::Plain => Self::format_plain(stats)
                .unwrap_or_else(|e| format!("Unexpected error creating report: {e}")),
        }
    }

    fn format_plain(stats: &Stats) -> Result<String, std::fmt::Error> {
        let mut result = String::new();
        result.push_str("Server Run Statistics:\n");
        result.write_fmt(format_args!("- elapsed: {} s\n", stats.elapsed()))?;
        result.write_fmt(format_args!(
            "- total handled processes: {}\n",
            stats.handled()
        ))?;
        result.write_fmt(format_args!("- command failures: {}\n", stats.errors()))?;

        result.write_fmt(format_args!(
            "- total command duration: {} ms\n",
            stats.total_cmd_duration()
        ))?;

        if !stats.exit_codes().is_empty() {
            result.write_fmt(format_args!("- average duration by command:\n"))?;
            for (cmd, avg_duration) in stats.average_durations() {
                result.write_fmt(format_args!("  - {cmd}: {avg_duration} ms\n"))?;
            }

            result.write_fmt(format_args!("- result counts by exit code:\n"))?;
            for (code, count) in stats.exit_codes().iter() {
                let code = code.map(|i| i.to_string()).unwrap_or("<none>".into());
                result.write_fmt(format_args!("  - {code}: {count}\n"))?;
            }
        }

        if !stats.nonzero_status_detail().is_empty() {
            result.write_fmt(format_args!(
                "- non-zero exit code details (most recent {})\n",
                stats.nonzero_status_detail().max_capacity()
            ))?;
            for (cmd, output) in stats.nonzero_status_detail().iter() {
                // just include the first line of stdout
                let StdoutFirstLine {
                    stdout,
                    but_wait_theres_more,
                } = StdoutFirstLine::from_output(&output.stdout);

                let mut stderr = String::from_utf8_lossy(&output.stderr);
                truncate_trailing_newlines(&mut stderr);

                result.write_fmt(format_args!("  - command: {cmd}\n"))?;

                if stdout.is_empty() && stderr.is_empty() {
                    result.write_str("    output: <no output>\n")?;
                } else {
                    if !stdout.is_empty() {
                        result.write_fmt(format_args!(
                            "    stdout: {stdout}{but_wait_theres_more}\n"
                        ))?;
                    }
                    if !stderr.is_empty() {
                        result.write_fmt(format_args!("    stderr: {stderr}\n"))?;
                    }
                }
            }
        }

        if !stats.error_detail().is_empty() {
            result.write_fmt(format_args!(
                "- command failure details (most recent {}):\n",
                stats.error_detail().max_capacity(),
            ))?;
            for (cmd, error) in stats.error_detail().iter() {
                result.write_fmt(format_args!("  - command: {cmd}\n"))?;
                result.write_fmt(format_args!("    error: {error}\n"))?;
            }
        }

        if !stats.recent_results().is_empty() {
            result.write_fmt(format_args!(
                "- most recent {} successful commands:\n",
                stats.recent_results().max_capacity()
            ))?;

            for CmdOutput {
                cmd,
                output,
                duration,
            } in stats.recent_results().iter()
            {
                let StdoutFirstLine {
                    stdout,
                    but_wait_theres_more,
                } = StdoutFirstLine::from_output(&output.stdout);
                let mut stderr = String::from_utf8_lossy(&output.stderr);
                truncate_trailing_newlines(&mut stderr);

                result.write_fmt(format_args!("  - command: {cmd}\n"))?;
                result.write_fmt(format_args!("    duration: {} ms\n", duration.as_millis()))?;

                if stdout.is_empty() && stderr.is_empty() {
                    result.write_str("    output: <no output>\n")?;
                } else {
                    if !stdout.is_empty() {
                        result.write_fmt(format_args!(
                            "    stdout: {stdout}{but_wait_theres_more}\n"
                        ))?;
                    }
                    if !stderr.is_empty() {
                        result.write_fmt(format_args!("    stderr: {stderr}\n"))?;
                    }
                }
            }
        }

        // Remove the trailing newline.
        result.truncate(result.len() - 1);
        Ok(result)
    }
}

struct StdoutFirstLine<'a> {
    stdout: Cow<'a, str>,
    but_wait_theres_more: ButWaitTheresMore,
}
impl<'a> StdoutFirstLine<'a> {
    fn from_output(output: &'a [u8]) -> Self {
        let mut but_wait_theres_more = "";
        let mut stdout = String::from_utf8_lossy(
            output
                .split(|b| b == &b'\n')
                .next()
                .inspect(|_| {
                    but_wait_theres_more = " ...";
                })
                .unwrap_or(output),
        );
        truncate_trailing_newlines(&mut stdout);

        Self {
            stdout,
            but_wait_theres_more: ButWaitTheresMore::from_output(output),
        }
    }
}

struct ButWaitTheresMore {
    total_lines: usize,
}
impl ButWaitTheresMore {
    fn from_output(output: &[u8]) -> Self {
        let total_lines = output.split(|b| b == &b'\n').count();

        Self { total_lines }
    }
}
impl Display for ButWaitTheresMore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.total_lines <= 1 {
            f.write_str("")
        } else {
            write!(f, " ({} more lines)...", self.total_lines - 1)
        }
    }
}

fn truncate_trailing_newlines(val: &mut Cow<'_, str>) {
    while val.ends_with('\n') {
        match val {
            Cow::Owned(string) => {
                string.truncate(string.len() - 1);
            }
            Cow::Borrowed(slice) => {
                *slice = &slice[0..slice.len() - 1];
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn trunc_newlines() {
        let cases = [
            ("okay", "okay"),
            ("okay\n", "okay"),
            ("okay\n\n", "okay"),
            ("okay\n\n\n", "okay"),
            ("", ""),
            ("\n", ""),
            ("\nokay", "\nokay"),
        ];

        for (input, exp) in cases {
            let mut owned: Cow<'_, str> = String::from(input).into();
            let mut borrowed: Cow<'_, str> = input.into();

            truncate_trailing_newlines(&mut owned);
            assert_eq!(exp, owned);

            truncate_trailing_newlines(&mut borrowed);
            assert_eq!(exp, borrowed);
        }
    }
}
