/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use anyhow::Context;
use buck2_core::fs::paths::abs_path::AbsPathBuf;
use buck2_wrapper_common::invocation_id::TraceId;
use dupe::Dupe;
use itertools::Itertools;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;

#[derive(Error, Debug)]
pub(crate) enum EventLogErrors {
    #[error(
        "Trying to write to logfile that hasn't been opened yet - this is an internal error, please report. Unwritten event: {serialized_event}"
    )]
    LogNotOpen { serialized_event: String },

    #[error("Reached End of File before reading BuckEvent in log `{0}`")]
    EndOfFile(String),
    #[error("No event log available for {idx}th last command (have latest {num_logfiles})")]
    RecentIndexOutOfBounds { idx: usize, num_logfiles: usize },
}

#[derive(Copy, Clone, Dupe, Debug)]
pub struct Encoding {
    pub(crate) mode: LogMode,
    pub(crate) compression: Compression,
    /// List of extensions used to detect file type.
    ///
    /// The first extension is the default one, used when writing a file.
    pub extensions: &'static [&'static str],
}

impl Encoding {
    pub(crate) const JSON: Encoding = Encoding {
        mode: LogMode::Json,
        compression: Compression::None,
        extensions: &[".json-lines"],
    };

    pub(crate) const JSON_GZIP: Encoding = Encoding {
        mode: LogMode::Json,
        compression: Compression::Gzip,
        extensions: &[".json-lines.gz"],
    };

    pub(crate) const JSON_ZSTD: Encoding = Encoding {
        mode: LogMode::Json,
        compression: Compression::Zstd,
        extensions: &[".json-lines.zst"],
    };

    pub(crate) const PROTO: Encoding = Encoding {
        mode: LogMode::Protobuf,
        compression: Compression::None,
        extensions: &[".pb", ".proto"],
    };

    pub(crate) const PROTO_GZIP: Encoding = Encoding {
        mode: LogMode::Protobuf,
        compression: Compression::Gzip,
        extensions: &[".pb.gz", ".proto.gz"],
    };

    pub const PROTO_ZSTD: Encoding = Encoding {
        mode: LogMode::Protobuf,
        compression: Compression::Zstd,
        extensions: &[".pb.zst"],
    };
}

pub(crate) const KNOWN_ENCODINGS: &[Encoding] = &[
    // Don't forget to update these lists when this is updated:
    // * https://fburl.com/code/zgdxtryb
    // * https://fburl.com/code/antguytj
    Encoding::JSON_GZIP,
    Encoding::JSON,
    Encoding::JSON_ZSTD,
    Encoding::PROTO,
    Encoding::PROTO_GZIP,
    Encoding::PROTO_ZSTD,
];

#[derive(Error, Debug)]
pub(crate) enum EventLogInferenceError {
    #[error("Event log at path {} has no filename", .0.display())]
    NoFilename(AbsPathBuf),

    #[error("Event log at path {} has a non-utf-8 filename", .0.display())]
    InvalidFilename(AbsPathBuf),

    #[error(
        "Event log at path {} has an extension that was not recognized. Valid extensions are: {}.",
        .0.display(), display_valid_extensions()
    )]
    InvalidExtension(AbsPathBuf),
}

fn display_valid_extensions() -> String {
    let mut exts = KNOWN_ENCODINGS
        .iter()
        .flat_map(|encoding| encoding.extensions);
    exts.join(", ")
}

pub(crate) struct NoInference(pub(crate) AbsPathBuf);

#[derive(Copy, Clone, Dupe, Debug, PartialEq, Eq)]
pub(crate) enum LogMode {
    Json,
    Protobuf,
}

#[derive(Copy, Clone, Dupe, Debug)]
pub(crate) enum Compression {
    None,
    Gzip,
    Zstd,
}

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Invocation {
    pub command_line_args: Vec<String>,
    /// Command line args with expanded `@` args.
    #[serde(default)] // For backwards compatibility. Delete after 2023-08-01.
    pub expanded_command_line_args: Vec<String>,
    /// This is `String` not `AbsPathBuf` because event log is cross-platform
    /// and `AbsPathBuf` is not.
    pub working_dir: String,
    #[serde(default = "TraceId::null")]
    pub trace_id: TraceId,
}

impl Invocation {
    pub fn display_command_line(&self) -> String {
        shlex::join(self.command_line_args.iter().map(|e| e.as_str()))
    }

    pub fn display_expanded_command_line(&self) -> String {
        shlex::join(self.expanded_command_line_args.iter().map(|e| e.as_str()))
    }

    pub(crate) fn parse_json_line(json: &str) -> anyhow::Result<Invocation> {
        serde_json::from_str::<Invocation>(json)
            .with_context(|| format!("Invalid header: {}", json.trim_end()))
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use buck2_wrapper_common::invocation_id::TraceId;

    use crate::subscribers::event_log::utils::Invocation;

    #[test]
    fn test_parse_json_line() {
        // Make sure serialization format is backwards compatible.
        let line = r#"{"command_line_args":["/some/path/buck2","test","@//mode/mac","app/..."],"working_dir":"/Users/nga/dir45","trace_id":"281d1c16-8930-40cd-8fc1-7d71355c20f5"}"#;
        let line = Invocation::parse_json_line(line).unwrap();
        let expected = Invocation {
            command_line_args: vec![
                "/some/path/buck2".to_owned(),
                "test".to_owned(),
                "@//mode/mac".to_owned(),
                "app/...".to_owned(),
            ],
            working_dir: "/Users/nga/dir45".to_owned(),
            expanded_command_line_args: Vec::new(),
            trace_id: TraceId::from_str("281d1c16-8930-40cd-8fc1-7d71355c20f5").unwrap(),
        };
        assert_eq!(expected, line);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::io;
use std::mem;
use std::pin::Pin;
use std::process::Stdio;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;

use anyhow::Context as _;
use async_compression::tokio::write::GzipEncoder;
use async_compression::tokio::write::ZstdEncoder;
use buck2_cli_proto::*;
use buck2_core::env_helper::EnvHelper;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::paths::abs_path::AbsPathBuf;
use buck2_core::fs::working_dir::WorkingDir;
use buck2_events::BuckEvent;
use buck2_wrapper_common::invocation_id::TraceId;
use futures::future::Future;
use futures::FutureExt;
use pin_project::pin_project;
use prost::Message;
use serde::Serialize;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWrite;
use tokio::io::AsyncWriteExt;

use crate::cleanup_ctx::AsyncCleanupContext;
use crate::subscribers::event_log::file_names::get_logfile_name;
use crate::subscribers::event_log::file_names::remove_old_logs;
use crate::subscribers::event_log::read::EventLogPathBuf;
use crate::subscribers::event_log::utils::Compression;
use crate::subscribers::event_log::utils::Encoding;
use crate::subscribers::event_log::utils::EventLogErrors;
use crate::subscribers::event_log::utils::Invocation;
use crate::subscribers::event_log::utils::LogMode;
use crate::subscribers::event_log::utils::NoInference;
use crate::subscribers::should_upload_log;

type EventLogWriter = Box<dyn AsyncWrite + Send + Sync + Unpin + 'static>;

mod counting_reader {
    use super::*;

    #[pin_project]
    pub struct CountingReader<T> {
        #[pin]
        pub(super) inner: T,
        pub(super) stats: Option<Arc<AtomicU64>>,
    }
}

use counting_reader::CountingReader;

use super::user_event_types::try_get_user_event;
use crate::argv::SanitizedArgv;

impl<T> CountingReader<T> {
    fn new(inner: T, stats: Option<Arc<AtomicU64>>) -> Self {
        Self { inner, stats }
    }
}

impl<T> AsyncWrite for CountingReader<T>
where
    T: AsyncWrite,
{
    fn poll_write(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        let this = self.project();
        let bytes = futures::ready!(this.inner.poll_write(cx, buf))?;
        if let Some(stats) = this.stats {
            stats.fetch_add(bytes as u64, Ordering::Relaxed);
        }

        Poll::Ready(Ok(bytes))
    }

    fn poll_flush(
        self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
        self.project().inner.poll_flush(cx)
    }

    fn poll_shutdown(
        self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<std::result::Result<(), std::io::Error>> {
        self.project().inner.poll_shutdown(cx)
    }
}

#[derive(Eq, PartialEq, Copy, Clone)]
pub(crate) enum EventLogType {
    System,
    User,
}

pub(crate) struct NamedEventLogWriter {
    path: EventLogPathBuf,
    file: EventLogWriter,
    event_log_type: EventLogType,
}

pub(crate) enum LogWriterState {
    Unopened {
        logdir: AbsNormPathBuf,
        extra_path: Option<AbsPathBuf>,
        extra_user_event_log_path: Option<AbsPathBuf>,
    },
    Opened {
        writers: Vec<NamedEventLogWriter>,
    },
    Closed,
}

pub(crate) struct WriteEventLog<'a> {
    state: LogWriterState,
    async_cleanup_context: Option<AsyncCleanupContext<'a>>,
    sanitized_argv: SanitizedArgv,
    command_name: String,
    working_dir: WorkingDir,
    /// Allocation cache. Must be cleaned before use.
    buf: Vec<u8>,
    log_size_counter_bytes: Option<Arc<AtomicU64>>,
}

impl<'a> WriteEventLog<'a> {
    pub(crate) fn new(
        logdir: AbsNormPathBuf,
        working_dir: WorkingDir,
        extra_path: Option<AbsPathBuf>,
        extra_user_event_log_path: Option<AbsPathBuf>,
        sanitized_argv: SanitizedArgv,
        async_cleanup_context: AsyncCleanupContext<'a>,
        command_name: String,
        log_size_counter_bytes: Option<Arc<AtomicU64>>,
    ) -> anyhow::Result<Self> {
        Ok(Self {
            state: LogWriterState::Unopened {
                logdir,
                extra_path,
                extra_user_event_log_path,
            },
            async_cleanup_context: Some(async_cleanup_context),
            sanitized_argv,
            command_name,
            working_dir,
            buf: Vec::new(),
            log_size_counter_bytes,
        })
    }

    /// Get the command line arguments and cwd and serialize them for replaying later.
    async fn log_invocation(&mut self, trace_id: TraceId) -> anyhow::Result<()> {
        let command_line_args = self.sanitized_argv.argv.clone();
        let expanded_command_line_args = self.sanitized_argv.expanded_argv.clone();
        let invocation = Invocation {
            command_line_args,
            expanded_command_line_args,
            working_dir: self.working_dir.to_string(),
            trace_id,
        };
        self.write_ln(&[invocation]).await
    }

    async fn write_ln<'b, T, I>(&'b mut self, events: I) -> anyhow::Result<()>
    where
        T: SerializeForLog + 'b,
        I: IntoIterator<Item = &'b T> + Clone + 'b,
    {
        match &mut self.state {
            LogWriterState::Opened { writers, .. } => {
                for writer in writers {
                    self.buf.clear();

                    for event in events.clone() {
                        match writer.event_log_type {
                            EventLogType::System => {
                                match writer.path.encoding.mode {
                                    LogMode::Json => {
                                        event.serialize_to_json(&mut self.buf)?;
                                        self.buf.push(b'\n');
                                    }
                                    LogMode::Protobuf => event
                                        .serialize_to_protobuf_length_delimited(&mut self.buf)?,
                                };
                            }
                            EventLogType::User => {
                                if event.maybe_serialize_user_event(&mut self.buf)? {
                                    self.buf.push(b'\n');
                                }
                            }
                        }
                    }

                    writer
                        .file
                        .write_all(&self.buf)
                        .await
                        .context("Failed to write event")?;

                    if self.buf.len() > 1_000_000 {
                        // Make sure we don't keep too much memory if encountered one large event.
                        self.buf = Vec::new();
                    }
                }
                Ok(())
            }
            LogWriterState::Unopened { .. } | LogWriterState::Closed => {
                self.buf.clear();
                if let Some(event) = events.into_iter().next() {
                    event.serialize_to_json(&mut self.buf)?;
                } else {
                    // Unreachable.
                }
                Err(EventLogErrors::LogNotOpen {
                    serialized_event: String::from_utf8(mem::take(&mut self.buf))
                        .context("Failed to serialize event for debug")?,
                }
                .into())
            }
        }
    }

    async fn ensure_log_writers_opened(&mut self, event: &BuckEvent) -> anyhow::Result<()> {
        let (logdir, maybe_extra_path, maybe_extra_user_event_log_path) = match &self.state {
            LogWriterState::Unopened {
                logdir,
                extra_path,
                extra_user_event_log_path,
            } => (logdir, extra_path, extra_user_event_log_path),
            LogWriterState::Opened { .. } => return Ok(()),
            LogWriterState::Closed => {
                return Err(anyhow::anyhow!("Received events after logs were closed"));
            }
        };
        tokio::fs::create_dir_all(logdir)
            .await
            .with_context(|| format!("Error creating event log directory: `{}`", logdir))?;
        remove_old_logs(logdir).await;

        // The event-log is going to be written to file containing the build uuid.
        // But we don't know the build uuid until we've gotten the CommandStart event.
        // So we'll just create it when we know where to put it.
        let mut log_mode = LogMode::Protobuf;
        static JSON_LOG: EnvHelper<bool> = EnvHelper::new("BUCK2_JSON_LOG");
        if JSON_LOG.get_copied()?.unwrap_or(false) {
            log_mode = LogMode::Json;
        }

        // Open our log fie, gzip encoded.
        let encoding = match log_mode {
            LogMode::Json => Encoding::JSON_GZIP,
            LogMode::Protobuf => Encoding::PROTO_ZSTD,
        };

        let file_name = &get_logfile_name(event, encoding, &self.command_name)?;
        let path = EventLogPathBuf {
            path: logdir.as_abs_path().join(file_name),
            encoding,
        };
        let writer = start_persist_subprocess(
            path,
            event.trace_id()?.clone(),
            self.log_size_counter_bytes.clone(),
        )
        .await?;
        let mut writers = vec![writer];

        // Also open the user's log file, if any as provided, with no encoding.
        if let Some(extra_path) = maybe_extra_path {
            writers.push(
                open_event_log_for_writing(
                    EventLogPathBuf::infer_opt(extra_path.clone())?.unwrap_or_else(
                        |NoInference(path)| EventLogPathBuf {
                            path,
                            encoding: Encoding::JSON_GZIP,
                        },
                    ),
                    self.log_size_counter_bytes.clone(),
                    EventLogType::System,
                )
                .await?,
            );
        }

        // Also open the user's simple log file, if any as provided, json-line formatted with no compression if no extensions are detected.
        if let Some(extra_user_event_log_path) = maybe_extra_user_event_log_path {
            writers.push(
                open_event_log_for_writing(
                    EventLogPathBuf::infer_opt(extra_user_event_log_path.clone())?.unwrap_or_else(
                        |NoInference(path)| EventLogPathBuf {
                            path,
                            encoding: Encoding::JSON,
                        },
                    ),
                    self.log_size_counter_bytes.clone(),
                    EventLogType::User,
                )
                .await?,
            );
        }

        self.state = LogWriterState::Opened { writers };
        self.log_invocation(event.trace_id()?).await
    }

    pub(crate) fn exit(
        &mut self,
    ) -> impl Future<Output = anyhow::Result<()>> + 'static + Send + Sync {
        // Shut down writers, flush all our files before exiting.
        let state = std::mem::replace(&mut self.state, LogWriterState::Closed);

        async move {
            let mut writers = match state {
                LogWriterState::Opened { writers } => writers,
                LogWriterState::Unopened { .. } | LogWriterState::Closed => {
                    // Nothing to do in this case, though this should be unreachable
                    // since we just did a write_ln.
                    return Ok(());
                }
            };
            for writer in writers.iter_mut() {
                writer.file.shutdown().await?;
            }
            Ok(())
        }
    }
}

impl<'a> Drop for WriteEventLog<'a> {
    fn drop(&mut self) {
        let exit = self.exit();
        match self.async_cleanup_context.as_ref() {
            Some(async_cleanup_context) => {
                async_cleanup_context.register(
                    "event log upload",
                    async move {
                        if let Err(e) = exit.await {
                            tracing::warn!("Failed to cleanup EventLog: {:#}", e);
                        }
                    }
                    .boxed(),
                );
            }
            None => (),
        }
    }
}

async fn start_persist_subprocess(
    path: EventLogPathBuf,
    trace_id: TraceId,
    bytes_written: Option<Arc<AtomicU64>>,
) -> anyhow::Result<NamedEventLogWriter> {
    let current_exe = std::env::current_exe().context("No current_exe")?;
    let mut command = buck2_util::process::async_background_command(current_exe);
    // @oss-disable: #[cfg(unix)]
    #[cfg(all(tokio_unstable, unix))] // @oss-enable
    {
        // Ensure that if we get CTRL-C, the persist-event-logs process does not get it.
        command.process_group(0);
    }
    let manifold_name = &format!("{}{}", trace_id, path.extension());
    command
        .args(["debug", "persist-event-logs"])
        .args(["--manifold-name", manifold_name])
        .args(["--local-path".as_ref(), path.path.as_os_str()]);
    if !should_upload_log()? {
        command.arg("--no-upload");
    };
    let child = command
        .stderr(Stdio::null())
        .stdin(Stdio::piped())
        .spawn()
        .with_context(|| {
            format!(
                "Failed to open event log subprocess for writing at `{}`",
                path.path.display()
            )
        })?;
    let pipe = child.stdin.expect("stdin was piped");
    get_writer(path, pipe, bytes_written, EventLogType::System)
}

async fn open_event_log_for_writing(
    path: EventLogPathBuf,
    bytes_written: Option<Arc<AtomicU64>>,
    event_log_type: EventLogType,
) -> anyhow::Result<NamedEventLogWriter> {
    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&path.path)
        .await
        .with_context(|| {
            format!(
                "Failed to open event log for writing at `{}`",
                path.path.display()
            )
        })?;

    get_writer(path, file, bytes_written, event_log_type)
}

fn get_writer(
    path: EventLogPathBuf,
    file: impl AsyncWrite + std::marker::Send + std::marker::Unpin + std::marker::Sync + 'static,
    bytes_written: Option<Arc<AtomicU64>>,
    event_log_type: EventLogType,
) -> Result<NamedEventLogWriter, anyhow::Error> {
    let file = match path.encoding.compression {
        Compression::None => Box::new(CountingReader::new(file, bytes_written)) as EventLogWriter,
        Compression::Gzip => Box::new(GzipEncoder::with_quality(
            CountingReader::new(file, bytes_written),
            async_compression::Level::Fastest,
        )) as EventLogWriter,
        Compression::Zstd => Box::new(ZstdEncoder::with_quality(
            CountingReader::new(file, bytes_written),
            async_compression::Level::Default,
        )) as EventLogWriter,
    };
    Ok(NamedEventLogWriter {
        path,
        file,
        event_log_type,
    })
}

impl<'a> WriteEventLog<'a> {
    pub(crate) async fn write_events(&mut self, events: &[Arc<BuckEvent>]) -> anyhow::Result<()> {
        let mut event_refs = Vec::new();
        let mut first = true;
        for event in events {
            if first {
                self.ensure_log_writers_opened(event).await?;
                first = false;
            }

            event_refs.push(StreamValueForWrite::Event(event.event()));
        }

        if event_refs.is_empty() {
            return Ok(());
        }

        self.write_ln(&event_refs).await
    }

    pub(crate) async fn write_result(
        &mut self,
        result: &buck2_cli_proto::CommandResult,
    ) -> anyhow::Result<()> {
        match &self.state {
            LogWriterState::Opened { .. } | LogWriterState::Closed => {}
            LogWriterState::Unopened { .. } => {
                // This is a bit wonky. We can receive a CommandResult before we opened log files
                // if the command crashed before it started. That can happen if the daemon
                // initialization is what fails, since we need the daemon to initialize in order to
                // access request metadata, which we need for the command start event. To keep
                // things simple, just tolerate this happening.
                return Ok(());
            }
        }

        let event = StreamValueForWrite::Result(result);

        self.write_ln(&[event]).await
    }

    pub(crate) async fn flush_files(&mut self) -> anyhow::Result<()> {
        let writers = match &mut self.state {
            LogWriterState::Opened { writers } => writers,
            LogWriterState::Unopened { .. } | LogWriterState::Closed => return Ok(()),
        };

        for writer in writers {
            writer.file.flush().await.with_context(|| {
                format!("Error flushing log file at {}", writer.path.path.display())
            })?;
        }

        Ok(())
    }
}

pub(crate) trait SerializeForLog {
    fn serialize_to_json(&self, buf: &mut Vec<u8>) -> anyhow::Result<()>;
    fn serialize_to_protobuf_length_delimited(&self, buf: &mut Vec<u8>) -> anyhow::Result<()>;
    fn maybe_serialize_user_event(&self, buf: &mut Vec<u8>) -> anyhow::Result<bool>;
}

impl SerializeForLog for Invocation {
    fn serialize_to_json(&self, buf: &mut Vec<u8>) -> anyhow::Result<()> {
        serde_json::to_writer(buf, &self).context("Failed to serialize event")
    }

    fn serialize_to_protobuf_length_delimited(&self, buf: &mut Vec<u8>) -> anyhow::Result<()> {
        let invocation = buck2_data::Invocation {
            command_line_args: self.command_line_args.clone(),
            expanded_command_line_args: self.expanded_command_line_args.clone(),
            working_dir: self.working_dir.clone(),
            trace_id: Some(self.trace_id.to_string()),
        };
        invocation.encode_length_delimited(buf)?;
        Ok(())
    }

    // Always log invocation record to user event log for `buck2 log show` compatibility
    fn maybe_serialize_user_event(&self, buf: &mut Vec<u8>) -> anyhow::Result<bool> {
        serde_json::to_writer(buf, &self).context("Failed to serialize event")?;
        Ok(true)
    }
}

#[derive(Serialize)]
pub enum StreamValueForWrite<'a> {
    Result(&'a CommandResult),
    Event(&'a buck2_data::BuckEvent),
}

impl<'a> SerializeForLog for StreamValueForWrite<'a> {
    fn serialize_to_json(&self, buf: &mut Vec<u8>) -> anyhow::Result<()> {
        serde_json::to_writer(buf, &self).context("Failed to serialize event")
    }

    fn serialize_to_protobuf_length_delimited(&self, buf: &mut Vec<u8>) -> anyhow::Result<()> {
        // We use `CommandProgressForWrite` here to avoid cloning `BuckEvent`.
        // `CommandProgressForWrite` serialization is bitwise identical to `CommandProgress`.
        // See the protobuf spec
        // https://developers.google.com/protocol-buffers/docs/encoding#length-types
        // for the details about protobuf wire format.
        let progress = match self {
            Self::Event(e) => command_progress_for_write::Progress::Event(e.encode_to_vec()),
            Self::Result(res) => command_progress_for_write::Progress::Result((*res).clone()),
        };
        let stream_val = buck2_cli_proto::CommandProgressForWrite {
            progress: Some(progress),
        };
        stream_val.encode_length_delimited(buf)?;
        Ok(())
    }

    fn maybe_serialize_user_event(&self, buf: &mut Vec<u8>) -> anyhow::Result<bool> {
        if let StreamValueForWrite::Event(event) = self {
            if let Some(user_event) = try_get_user_event(event)? {
                serde_json::to_writer(buf, &user_event).context("Failed to serialize event")?;
                return Ok(true);
            }
        }

        Ok(false)
    }
}

#[cfg(test)]
mod tests {
    use std::time::SystemTime;

    use buck2_core::fs::paths::abs_path::AbsPathBuf;
    use buck2_data::LoadBuildFileStart;
    use buck2_data::SpanStartEvent;
    use buck2_events::span::SpanId;
    use buck2_events::BuckEvent;
    use buck2_wrapper_common::invocation_id::TraceId;
    use futures::TryStreamExt;
    use tempfile::TempDir;

    use super::*;
    use crate::stream_value::StreamValue;

    impl WriteEventLog<'static> {
        async fn new_test(log: EventLogPathBuf) -> anyhow::Result<Self> {
            Ok(Self {
                state: LogWriterState::Opened {
                    writers: vec![
                        open_event_log_for_writing(log, None, EventLogType::System).await?,
                    ],
                },
                sanitized_argv: SanitizedArgv {
                    argv: vec!["buck2".to_owned()],
                    expanded_argv: vec!["buck2".to_owned()],
                },
                async_cleanup_context: None,
                command_name: "testtest".to_owned(),
                working_dir: WorkingDir::current_dir()?,
                buf: Vec::new(),
                log_size_counter_bytes: None,
            })
        }
    }

    fn make_event() -> BuckEvent {
        BuckEvent::new(
            SystemTime::now(),
            TraceId::new(),
            Some(SpanId::new()),
            None,
            buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                data: Some(buck2_data::span_start_event::Data::Load(
                    LoadBuildFileStart {
                        module_id: "foo".to_owned(),
                        cell: "bar".to_owned(),
                    },
                )),
            }),
        )
    }

    #[tokio::test]
    async fn test_protobuf_decoding_gzip() -> anyhow::Result<()> {
        test_protobuf_decoding(Encoding::PROTO_GZIP).await
    }

    #[tokio::test]
    async fn test_protobuf_decoding_zstd() -> anyhow::Result<()> {
        test_protobuf_decoding(Encoding::PROTO_ZSTD).await
    }

    async fn test_protobuf_decoding(encoding: Encoding) -> anyhow::Result<()> {
        //Create log dir
        let tmp_dir = TempDir::new()?;

        //Create mock event
        let event = make_event();

        // Create event log
        let log = EventLogPathBuf {
            path: AbsPathBuf::try_from(tmp_dir.path().join("log")).unwrap(),
            encoding,
        };

        let mut write_event_log = WriteEventLog::new_test(log.clone()).await?;

        //Log event
        let value = StreamValueForWrite::Event(event.event());
        write_event_log.log_invocation(event.trace_id()?).await?;
        write_event_log.write_ln(&[value]).await?;
        write_event_log.exit().await?;

        //Get and decode log
        let (_invocation, mut events) = log.unpack_stream().await?;

        //Get event
        let retrieved_event = match events.try_next().await?.expect("Failed getting log") {
            StreamValue::Event(e) => BuckEvent::try_from(e),
            _ => panic!("expected event"),
        }?;

        //Assert it's the same event created in the beginning
        assert_eq!(retrieved_event.timestamp(), event.timestamp());
        assert_eq!(
            retrieved_event.trace_id().unwrap(),
            event.trace_id().unwrap()
        );
        assert_eq!(retrieved_event.span_id().unwrap(), event.span_id().unwrap());
        assert_eq!(retrieved_event.data(), event.data());

        assert!(
            events.try_next().await.unwrap().is_none(),
            "expecting no more events"
        );

        Ok(())
    }

    #[tokio::test]
    async fn test_tick_makes_valid_log_zstd() -> anyhow::Result<()> {
        test_tick_makes_valid_log(Encoding::PROTO_ZSTD).await
    }

    async fn test_tick_makes_valid_log(encoding: Encoding) -> anyhow::Result<()> {
        if cfg!(windows) {
            // Do not want to deal with exclusivity issues on Windows.
            return Ok(());
        }

        let tmp_dir = TempDir::new()?;

        let log = EventLogPathBuf {
            path: AbsPathBuf::try_from(tmp_dir.path().join("test_tick_makes_valid_log.pb.gz"))
                .unwrap(),
            encoding,
        };

        let mut write_event_log = WriteEventLog::new_test(log.clone()).await?;

        let event = make_event();
        let value = StreamValueForWrite::Event(event.event());
        write_event_log.log_invocation(event.trace_id()?).await?;
        write_event_log.write_ln(&[value]).await?;

        assert!(
            log.unpack_stream().await.is_err(),
            "Sanity check: gzip was not flushed, so the log is invalid"
        );

        // Now flush the gzip stream.
        write_event_log.flush_files().await?;

        // Do not close the log, and open it.
        let (_invocation, mut events) = log.unpack_stream().await?;

        let retrieved_event = match events.try_next().await?.expect("Failed getting log") {
            StreamValue::Event(e) => BuckEvent::try_from(e).unwrap(),
            _ => panic!("expecting event"),
        };

        assert_eq!(retrieved_event.timestamp(), event.timestamp());
        assert_eq!(
            retrieved_event.trace_id().unwrap(),
            event.trace_id().unwrap()
        );
        assert_eq!(retrieved_event.span_id(), event.span_id());
        assert_eq!(retrieved_event.data(), event.data());

        match encoding.compression {
            Compression::Gzip => {
                // TODO(nga): `tick` does not write gzip footer, so even after `tick`
                //   generated file is not a valid gzip file.
                // assert!(events.try_next().await.unwrap().is_none(), "expecting no more events");
                assert!(events.try_next().await.is_err());
            }
            Compression::Zstd => {
                assert!(
                    events.try_next().await.unwrap().is_none(),
                    "expecting no more events"
                );
            }
            Compression::None => unreachable!(),
        }

        Ok(())
    }

    #[test]
    fn test_stream_value_serialize_to_protobuf_length_delimited() {
        let event = make_event();
        let mut actual = Vec::new();
        StreamValueForWrite::Event(event.event())
            .serialize_to_protobuf_length_delimited(&mut actual)
            .unwrap();
        let expected = buck2_cli_proto::CommandProgress {
            progress: Some(command_progress::Progress::Event(event.into())),
        }
        .encode_length_delimited_to_vec();
        assert_eq!(expected, actual);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_event_observer::action_stats::ActionStats;
use superconsole::Component;
use superconsole::Line;
use superconsole::Lines;

use crate::subscribers::superconsole::SuperConsoleConfig;

pub(crate) struct CommandsComponent<'a> {
    pub(crate) super_console_config: &'a SuperConsoleConfig,
    pub(crate) action_stats: &'a ActionStats,
}

impl<'a> Component for CommandsComponent<'a> {
    fn draw_unchecked(
        &self,

        _dimensions: superconsole::Dimensions,
        _mode: superconsole::DrawMode,
    ) -> anyhow::Result<superconsole::Lines> {
        if !self.super_console_config.enable_commands {
            return Ok(Lines::new());
        }

        Ok(Lines(vec![Line::unstyled(&self.action_stats.to_string())?]))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::fmt::Debug;

use superconsole::components::alignment::HorizontalAlignmentKind;
use superconsole::components::alignment::VerticalAlignmentKind;
use superconsole::components::Aligned;
use superconsole::components::DrawHorizontal;
use superconsole::Component;
use superconsole::Dimensions;
use superconsole::DrawMode;
use superconsole::Line;
use superconsole::Lines;

#[derive(Debug)]
pub(crate) struct HeaderLineComponent<A: Component, B: Component> {
    lhs: A,
    rhs: B,
}

impl<A: Component, B: Component> HeaderLineComponent<A, B> {
    pub(crate) fn new(lhs: A, rhs: B) -> Self {
        HeaderLineComponent { lhs, rhs }
    }
}

impl<A: Component, B: Component> Component for HeaderLineComponent<A, B> {
    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        let mut draw = DrawHorizontal::new(dimensions);
        draw.draw(&self.lhs, mode)?;
        draw.draw(
            &Aligned {
                child: &self.rhs,
                horizontal: HorizontalAlignmentKind::Right,
                vertical: VerticalAlignmentKind::Top,
            },
            mode,
        )?;
        Ok(draw.finish())
    }
}

/// This component is part of the header line and displays a hardcoded message.
#[derive(Debug)]
pub(crate) struct StaticStringComponent<S: AsRef<str>> {
    pub header: S,
}

impl<S: AsRef<str>> Component for StaticStringComponent<S> {
    fn draw_unchecked(&self, _dimensions: Dimensions, _mode: DrawMode) -> anyhow::Result<Lines> {
        Ok(Lines(vec![Line::unstyled(self.header.as_ref())?]))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_event_observer::starlark_debug::StarlarkDebuggerState;
use gazebo::prelude::*;
use superconsole::Component;
use superconsole::Lines;

#[derive(Debug)]
pub(crate) struct StarlarkDebuggerComponent<'s> {
    pub(crate) starlark_debugger_state: &'s StarlarkDebuggerState,
}

impl<'s> Component for StarlarkDebuggerComponent<'s> {
    fn draw_unchecked(
        &self,

        _dimensions: superconsole::Dimensions,
        _mode: superconsole::DrawMode,
    ) -> anyhow::Result<superconsole::Lines> {
        let state = self.starlark_debugger_state;

        if !state.debugger_attached {
            return Ok(Lines::new());
        }

        let mut lines: Vec<String> = Vec::new();
        lines.push("     ******** Buck2 Starlark Debugger is attached **********".to_owned());

        let total = state.this_stopped_evals.len() + state.other_stopped_evals.len();
        for stopped in &state.this_stopped_evals {
            if lines.len() < 7 {
                lines.push(format!(
                    "( stopped)   {:<60} @ {}",
                    &stopped.description, &stopped.stopped_at
                ));
            }
        }
        for stopped in &state.other_stopped_evals {
            if lines.len() < 7 {
                lines.push(format!(
                    "(*stopped) {:<60} @ {}",
                    &stopped.description, &stopped.stopped_at
                ));
            }
        }

        let shown = lines.len() - 1;
        if total > shown {
            lines.push(format!("{} more paused evaluations", total - shown));
        }

        lines.push("      *******************************************************".to_owned());

        Ok(Lines(lines.into_try_map(|v| vec![v].try_into())?))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::time::Duration;

use buck2_event_observer::debug_events::DebugEventsState;
use gazebo::prelude::*;
use superconsole::Component;
use superconsole::Lines;

use crate::subscribers::superconsole::SuperConsoleConfig;

pub(crate) struct DebugEventsComponent<'s> {
    pub(crate) super_console_config: &'s SuperConsoleConfig,
    pub(crate) debug_events_state: &'s DebugEventsState,
}

impl<'s> Component for DebugEventsComponent<'s> {
    fn draw_unchecked(
        &self,

        _dimensions: superconsole::Dimensions,
        _mode: superconsole::DrawMode,
    ) -> anyhow::Result<superconsole::Lines> {
        if !self.super_console_config.enable_debug_events {
            return Ok(Lines::new());
        }

        let mut lines: Vec<String> = Vec::new();
        lines.push(format!(
            "Events...  total: {} maximum delay: {:.3}ms average delay last {}: {:.3}ms",
            self.debug_events_state.event_count,
            self.debug_events_state.max_delay.as_secs_f64() * 1000.0,
            self.debug_events_state.recent_delays.len(),
            if self.debug_events_state.recent_delays.is_empty() {
                0.0
            } else {
                self.debug_events_state
                    .recent_delays
                    .iter()
                    .sum::<Duration>()
                    .as_secs_f64()
                    / (self.debug_events_state.recent_delays.len() as f64)
                    * 1000.0
            }
        ));

        if !self.debug_events_state.spans.is_empty() {
            let header_line = format!(
                "  {:<32}  {:>10}  {:>10}  {:>9}  {:>13}  {:>14}",
                "Span Events", "started", "finished", "duration", "poll time", "avg max poll time"
            );
            let header_len = header_line.len();
            lines.push(header_line);
            lines.push("-".repeat(header_len));
            for (k, v) in self.debug_events_state.spans.iter() {
                lines.push(format!(
                    "    {:<30} |{:>10} |{:>10} |{:8.3}s |{:>12.3}s |{:>12}us",
                    k,
                    v.started,
                    v.finished,
                    v.total_duration.as_secs_f64(),
                    v.total_poll_time.as_secs_f64(),
                    if v.finished == 0 {
                        0
                    } else {
                        v.total_max_poll_time.as_micros() / (v.finished as u128)
                    },
                ));
            }
            lines.push("-".repeat(header_len));
        }

        if !self.debug_events_state.instants.is_empty() {
            let header_line = format!("  {:<32}  {:>12}", "Instant Events", "count");
            let header_len = header_line.len();
            lines.push(header_line);
            lines.push("-".repeat(header_len));
            for (k, v) in self.debug_events_state.instants.iter() {
                lines.push(format!("    {:<30} |{:>12}", k, v.count));
            }
            lines.push("-".repeat(header_len));
        }

        Ok(Lines(lines.into_try_map(|v| vec![v].try_into())?))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_event_observer::dice_state::DiceState;
use gazebo::prelude::*;
use superconsole::Component;
use superconsole::Lines;

use crate::subscribers::superconsole::SuperConsoleConfig;

pub(crate) struct DiceComponent<'s> {
    pub(crate) super_console_config: &'s SuperConsoleConfig,
    pub(crate) dice_state: &'s DiceState,
}

impl<'s> Component for DiceComponent<'s> {
    fn draw_unchecked(
        &self,

        _dimensions: superconsole::Dimensions,
        mode: superconsole::DrawMode,
    ) -> anyhow::Result<superconsole::Lines> {
        if !self.super_console_config.enable_dice {
            return Ok(Lines::new());
        }

        let mut lines = vec!["Dice Key States".to_owned()];

        let header = format!("  {:<42}  {:>12}  {:>12}", "  Key", "Pending", "Finished");
        let header_len = header.len();
        lines.push(header);
        lines.push("-".repeat(header_len));
        for (k, v) in self.dice_state.key_states() {
            // We aren't guaranteed to get a final DiceStateUpdate and so we just assume all dice nodes that we
            // know about finished so that the final rendering doesn't look silly.
            let (pending, finished) = match mode {
                superconsole::DrawMode::Normal => (v.started - v.finished, v.finished),
                superconsole::DrawMode::Final => (0, v.started),
            };
            lines.push(format!(
                "    {:<40} |{:>12} |{:>12}",
                // Dice key states are all ascii
                if k.len() > 40 { &k[..40] } else { k },
                pending,
                finished
            ));
        }
        lines.push("-".repeat(header_len));
        Ok(Lines(lines.into_try_map(|v| vec![v].try_into())?))
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_event_observer::io_state::IoState;
use superconsole::Component;
use superconsole::Dimensions;
use superconsole::DrawMode;
use superconsole::Lines;

use crate::subscribers::superconsole::SuperConsoleConfig;

pub(crate) struct IoHeader<'s> {
    pub(crate) super_console_config: &'s SuperConsoleConfig,
    pub(crate) io_state: &'s IoState,
}

impl<'s> Component for IoHeader<'s> {
    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        self.io_state
            .render(mode, dimensions.width, self.super_console_config.enable_io)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use buck2_event_observer::re_state::ReState;
use superconsole::Component;

use crate::subscribers::superconsole::SuperConsoleConfig;

/// Draw the test summary line above the `timed_list`
pub(crate) struct ReHeader<'a> {
    pub(crate) super_console_config: &'a SuperConsoleConfig,
    pub(crate) re_state: &'a ReState,
}

impl<'a> Component for ReHeader<'a> {
    fn draw_unchecked(
        &self,

        _dimensions: superconsole::Dimensions,
        mode: superconsole::DrawMode,
    ) -> anyhow::Result<superconsole::Lines> {
        self.re_state
            .render(self.super_console_config.enable_detailed_re, mode)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::iter;

use buck2_event_observer::session_info::SessionInfo;
use superconsole::Component;
use superconsole::Dimensions;
use superconsole::DrawMode;
use superconsole::Line;
use superconsole::Lines;
use superconsole::Span;

/// This component is used to display session information for a command e.g. RE session ID
pub struct SessionInfoComponent<'s> {
    pub session_info: &'s SessionInfo,
}

impl<'s> Component for SessionInfoComponent<'s> {
    fn draw_unchecked(&self, dimensions: Dimensions, _mode: DrawMode) -> anyhow::Result<Lines> {
        let mut headers = Lines::new();
        let mut ids = vec![];
        if cfg!(fbcode_build) {
            headers.push(Line::unstyled("Buck UI:")?);
            ids.push(Span::new_unstyled(format!(
                "https://www.internalfb.com/buck2/{}",
                self.session_info.trace_id
            ))?);
        } else {
            headers.push(Line::unstyled("Build ID:")?);
            ids.push(Span::new_unstyled(&self.session_info.trace_id)?);
        }
        if let Some(buck2_data::TestSessionInfo { info }) = &self.session_info.test_session {
            headers.push(Line::unstyled("Test UI:")?);
            ids.push(Span::new_unstyled(info)?);
        }
        // pad all headers to the max width.
        headers.justify();
        headers.pad_lines_right(1);

        let max_len = headers
            .iter()
            .zip(ids.iter())
            .map(|(header, id)| header.len() + id.len())
            .max()
            .unwrap_or(0);

        let lines = if max_len > dimensions.width {
            headers
                .into_iter()
                .zip(ids.into_iter())
                .flat_map(|(header, id)| {
                    iter::once(header).chain(iter::once(Line::from_iter([id])))
                })
                .collect()
        } else {
            headers
                .iter_mut()
                .zip(ids.into_iter())
                .for_each(|(header, id)| header.push(id));
            headers
        };

        let max_len = lines.iter().map(|line| line.len()).max().unwrap_or(0);

        Ok(if max_len > dimensions.width {
            Lines(vec![Line::unstyled(
                "<Terminal too small for build details>",
            )?])
        } else {
            lines
        })
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use anyhow::Context;
use buck2_event_observer::test_state::TestState;
use crossterm::style::Color;
use crossterm::style::ContentStyle;
use superconsole::Component;
use superconsole::Dimensions;
use superconsole::DrawMode;
use superconsole::Line;
use superconsole::Lines;
use superconsole::Span;

use crate::subscribers::superconsole::SessionInfo;

struct TestCounterComponent;

pub struct TestCounterColumn {
    label: &'static str,
    color: Option<Color>,
    get_from_test_state: fn(&TestState) -> u64,
    get_from_test_statues: fn(
        &buck2_cli_proto::test_response::TestStatuses,
    ) -> &Option<buck2_cli_proto::CounterWithExamples>,
}

impl TestCounterColumn {
    pub const LISTING_FAIL: TestCounterColumn = TestCounterColumn {
        label: "Listing Fail",
        color: Some(Color::Red),
        get_from_test_state: |test_state| test_state.listing_failed,
        get_from_test_statues: |test_statuses| &test_statuses.listing_failed,
    };
    const DISCOVERED: TestCounterColumn = TestCounterColumn {
        label: "Discovered",
        color: None,
        get_from_test_state: |test_state| test_state.discovered,
        get_from_test_statues: |_test_statuses| &None,
    };
    pub const PASS: TestCounterColumn = TestCounterColumn {
        label: "Pass",
        color: Some(Color::Green),
        get_from_test_state: |test_state| test_state.pass,
        get_from_test_statues: |test_statuses| &test_statuses.passed,
    };
    pub const FAIL: TestCounterColumn = TestCounterColumn {
        label: "Fail",
        color: Some(Color::Red),
        get_from_test_state: |test_state| test_state.fail,
        get_from_test_statues: |test_statuses| &test_statuses.failed,
    };
    pub const FATAL: TestCounterColumn = TestCounterColumn {
        label: "Fatal",
        color: Some(Color::Red),
        get_from_test_state: |test_state| test_state.fatal,
        get_from_test_statues: |test_statuses| &test_statuses.fatals,
    };
    pub const SKIP: TestCounterColumn = TestCounterColumn {
        label: "Skip",
        color: Some(Color::Yellow),
        get_from_test_state: |test_state| test_state.skipped,
        get_from_test_statues: |test_statuses| &test_statuses.skipped,
    };
    const TIMEOUT: TestCounterColumn = TestCounterColumn {
        label: "Timeout",
        color: Some(Color::Yellow),
        get_from_test_state: |test_state| test_state.timeout,
        get_from_test_statues: |_test_statuses| &None,
    };

    fn to_span_from_test_state(&self, test_state: &TestState) -> anyhow::Result<Span> {
        StylizedCount {
            label: self.label,
            count: (self.get_from_test_state)(test_state),
            color: self.color,
        }
        .to_span()
    }

    pub fn to_span_from_test_statuses(
        &self,
        test_statuses: &buck2_cli_proto::test_response::TestStatuses,
    ) -> anyhow::Result<Span> {
        StylizedCount {
            label: self.label,
            count: (self.get_from_test_statues)(test_statuses)
                .as_ref()
                .with_context(|| format!("Missing {} in TestStatuses", self.label))?
                .count,
            color: self.color,
        }
        .to_span()
    }
}

impl TestCounterComponent {
    fn draw_unchecked(
        &self,
        test_state: &TestState,
        _dimensions: Dimensions,
        mode: DrawMode,
    ) -> anyhow::Result<Lines> {
        if matches!(mode, DrawMode::Final) {
            return Ok(Lines::new());
        }

        // TODO(brasselsprouts): use the outer try_into conversion on Lines.

        let mut spans = Vec::new();
        if test_state.listing_failed > 0 {
            spans.push(TestCounterColumn::LISTING_FAIL.to_span_from_test_state(test_state)?);
            spans.push(". ".try_into()?);
        }
        spans.push(TestCounterColumn::DISCOVERED.to_span_from_test_state(test_state)?);
        spans.push(". ".try_into()?);
        spans.push(TestCounterColumn::PASS.to_span_from_test_state(test_state)?);
        spans.push(". ".try_into()?);
        spans.push(TestCounterColumn::FAIL.to_span_from_test_state(test_state)?);
        spans.push(". ".try_into()?);
        spans.push(TestCounterColumn::FATAL.to_span_from_test_state(test_state)?);
        spans.push(". ".try_into()?);
        spans.push(TestCounterColumn::SKIP.to_span_from_test_state(test_state)?);
        spans.push(". ".try_into()?);
        spans.push(TestCounterColumn::TIMEOUT.to_span_from_test_state(test_state)?);
        Ok(Lines::from_iter([Line::from_iter(spans)]))
    }
}

/// Draw the test summary line above the `timed_list`
pub(crate) struct TestHeader<'a> {
    pub(crate) session_info: &'a SessionInfo,
    pub(crate) test_state: &'a TestState,
}

impl<'a> Component for TestHeader<'a> {
    fn draw_unchecked(
        &self,

        dimensions: superconsole::Dimensions,
        mode: superconsole::DrawMode,
    ) -> anyhow::Result<superconsole::Lines> {
        if self.session_info.test_session.is_some() {
            TestCounterComponent.draw_unchecked(self.test_state, dimensions, mode)
        } else {
            Ok(Lines::new())
        }
    }
}

pub fn span_from_build_failure_count(count: usize) -> anyhow::Result<Span> {
    StylizedCount {
        label: "Build failure",
        count: count.try_into()?,
        color: Some(Color::Red),
    }
    .to_span()
}

/// A count that receives color if and only if it's > 0
struct StylizedCount {
    label: &'static str,
    count: u64,
    color: Option<Color>,
}

impl StylizedCount {
    /// Turn this StylizedCount into a Superconsole Span.
    fn to_span(&self) -> anyhow::Result<superconsole::Span> {
        let mut style = ContentStyle::default();
        if self.count > 0 {
            style.foreground_color = self.color;
        }
        style
            .apply(format!("{} {}", self.label, self.count))
            .try_into()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::fmt::Write;
use std::time::Duration;
use std::time::Instant;

use buck2_event_observer::display;
use buck2_event_observer::display::TargetDisplayOptions;
use buck2_event_observer::fmt_duration;
use buck2_event_observer::humanized::HumanizedCount;
use buck2_event_observer::pending_estimate::pending_estimate;
use buck2_event_observer::span_tracker::BuckEventSpanHandle;
use buck2_event_observer::span_tracker::BuckEventSpanTracker;
use superconsole::components::bordering::BorderedSpec;
use superconsole::components::Bordered;
use superconsole::components::DrawVertical;
use superconsole::style::Stylize;
use superconsole::Component;
use superconsole::Dimensions;
use superconsole::DrawMode;
use superconsole::Line;
use superconsole::Lines;
use superconsole::Span;

use self::table_builder::Table;
use crate::subscribers::superconsole::common::HeaderLineComponent;
use crate::subscribers::superconsole::common::StaticStringComponent;
use crate::subscribers::superconsole::timed_list::table_builder::Row;
use crate::subscribers::superconsole::timed_list::table_builder::TimedRow;
use crate::subscribers::superconsole::SuperConsoleState;

mod table_builder;

/// The minimum time disparity between a single subaction and a target elapsed time
/// before the former will be displayed separately from the latter's time.
/// Heuristic uses a percent difference to normalize for long running actions.
const DISPLAY_SUBACTION_CUTOFF: f64 = 0.9;

/// Information about notable event durations.
#[derive(Debug)]
pub struct Cutoffs {
    /// Cutoff for normal execution time.
    pub inform: Duration,
    /// Cutoff for abnormal but still OK execution time.
    pub warn: Duration,
    /// Minimum time an event must be alive before it is worth displaying.
    pub _notable: Duration,
}

/// This component renders each event and a timer indicating for how long the event has been ongoing.

struct TimedListBody<'c> {
    cutoffs: &'c Cutoffs,
    state: &'c SuperConsoleState,
}

impl<'c> TimedListBody<'c> {
    /// Render a root  as `root [first child + remaining children]`
    fn draw_root_first_child(
        &self,
        root: &BuckEventSpanHandle,
        single_child: BuckEventSpanHandle,
        remaining_children: usize,
        display_platform: bool,
    ) -> anyhow::Result<TimedRow> {
        let time_speed = self.state.time_speed;
        let info = root.info();
        let child_info = single_child.info();

        // always display the event and subaction
        let mut event_string = format!(
            "{} [{}",
            display::display_event(
                &info.event,
                TargetDisplayOptions::for_console(display_platform)
            )?,
            display::display_event(
                &child_info.event,
                TargetDisplayOptions::for_console(display_platform)
            )?
        );

        let now = Instant::now();
        let child_info_elapsed = now - child_info.start;
        let info_elapsed = now - info.start;
        let subaction_ratio = child_info_elapsed.as_secs_f64() / info_elapsed.as_secs_f64();

        // but only display the time of the subaction if it differs significantly.
        if subaction_ratio < DISPLAY_SUBACTION_CUTOFF {
            let subaction_time = fmt_duration::fmt_duration(child_info_elapsed, time_speed.speed());
            event_string.push(' ');
            event_string.push_str(&subaction_time);
        }

        if remaining_children > 0 {
            write!(event_string, " + {}", remaining_children)
                .expect("Write to String is not fallible");
        }

        event_string.push(']');

        TimedRow::text(
            0,
            event_string,
            fmt_duration::fmt_duration(info_elapsed, time_speed.speed()),
            info_elapsed.mul_f64(time_speed.speed()),
            self.cutoffs,
        )
    }

    fn draw_root(&self, root: &BuckEventSpanHandle) -> anyhow::Result<Vec<TimedRow>> {
        let time_speed = self.state.time_speed;
        let config = &self.state.config;
        let two_lines = config.two_lines;
        let display_platform = config.display_platform;
        let info = root.info();

        let mut it = root.children();

        match it.next() {
            Some(first) if !two_lines => Ok(vec![self.draw_root_first_child(
                root,
                first,
                it.len(),
                display_platform,
            )?]),
            first => {
                let mut rows = Vec::new();
                rows.push(TimedRow::span(
                    0,
                    info,
                    time_speed.speed(),
                    self.cutoffs,
                    display_platform,
                )?);

                for child in first.into_iter().chain(it) {
                    rows.push(TimedRow::span(
                        2,
                        child.info(),
                        time_speed.speed(),
                        self.cutoffs,
                        display_platform,
                    )?);
                }
                Ok(rows)
            }
        }
    }
}

impl<'c> Component for TimedListBody<'c> {
    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        let config = &self.state.config;
        let max_lines = config.max_lines;

        let observer = self.state.simple_console.observer();

        let spans = observer.spans();

        let mut roots = spans.iter_roots();

        let mut builder = Table::new();

        let mut first_not_rendered = None;

        for root in &mut roots {
            let rows = self.draw_root(&root)?;

            if builder.len() + rows.len() >= max_lines {
                first_not_rendered = Some(root);
                break;
            }

            builder.rows.extend(rows.into_iter().map(Row::from));
        }

        // Add remaining unshown tasks, if any.
        let more = roots.len() as u64 + first_not_rendered.map_or(0, |_| 1);

        if more > 0 {
            let remaining = format!("... and {} more currently executing", more);
            builder.rows.push(
                std::iter::once(Span::new_styled(remaining.italic())?)
                    .collect::<Line>()
                    .into(),
            );
        }

        builder.draw(dimensions, mode)
    }
}

/// This component is used to display summary counts about the number of jobs.
struct CountComponent<'s> {
    state: &'s SuperConsoleState,
}

impl<'s> Component for CountComponent<'s> {
    fn draw_unchecked(&self, _dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        let observer = self.state.simple_console.observer();
        let spans = observer.spans();
        let action_stats = self.state.simple_console.observer().action_stats();
        let time_speed = self.state.time_speed;

        let finished = spans.roots_completed() as u64;

        let elapsed =
            fmt_duration::fmt_duration(self.state.current_tick.elapsed_time, time_speed.speed());

        match mode {
            DrawMode::Normal => {
                let pending = pending_estimate(spans.roots(), observer.extra().dice_state());

                let remaining = spans.iter_roots().len() as u64 + pending;
                let total = remaining + finished;

                let remaining = HumanizedCount::new(remaining);
                let total = HumanizedCount::new(total);

                let contents = if action_stats.log_stats() {
                    let mut actions_summary = format!(
                        "Remaining: {}/{}. Cache hits: {}%. ",
                        remaining,
                        total,
                        action_stats.action_cache_hit_percentage()
                    );
                    if action_stats.fallback_actions > 0 {
                        actions_summary += format!(
                            "Fallback: {}/{}. ",
                            HumanizedCount::new(action_stats.fallback_actions),
                            HumanizedCount::new(action_stats.total_executed_actions())
                        )
                        .as_str();
                    }
                    actions_summary += format!("Time elapsed: {}", elapsed).as_str();
                    actions_summary
                } else {
                    format!(
                        "Remaining: {}/{}. Time elapsed: {}",
                        remaining, total, elapsed
                    )
                };
                Ok(Lines(vec![Line::unstyled(&contents)?]))
            }
            DrawMode::Final => {
                let mut lines = vec![Line::unstyled(&format!(
                    "Jobs completed: {}. Time elapsed: {}.",
                    finished, elapsed,
                ))?];
                if action_stats.log_stats() {
                    lines.push(Line::unstyled(&action_stats.to_string())?);
                }
                Ok(Lines(lines))
            }
        }
    }
}

/// Wrapper component for Header + Count
struct TimedListHeader<'s> {
    state: &'s SuperConsoleState,
    header: &'s str,
}

impl<'s> Component for TimedListHeader<'s> {
    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        let info = StaticStringComponent {
            header: self.header,
        };
        let header_split = HeaderLineComponent::new(info, CountComponent { state: self.state });
        let header_box = Bordered::new(
            header_split,
            BorderedSpec {
                bottom: Some(Span::dash()),
                top: None,
                left: None,
                right: None,
            },
        );

        header_box.draw(dimensions, mode)
    }
}

/// Component that displays ongoing events and their durations + summary stats.
pub struct TimedList<'a> {
    header: &'a str,
    cutoffs: &'a Cutoffs,
    state: &'a SuperConsoleState,
}

impl<'a> TimedList<'a> {
    /// * `cutoffs` determines durations for warnings, time-outs, and baseline notability.
    /// * `header` is the string displayed at the top of the list.
    pub fn new(cutoffs: &'a Cutoffs, header: &'a str, state: &'a SuperConsoleState) -> Self {
        Self {
            header,
            cutoffs,
            state,
        }
    }
}

impl<'a> Component for TimedList<'a> {
    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        let span_tracker: &BuckEventSpanTracker = self.state.simple_console.observer().spans();

        match mode {
            DrawMode::Normal if !span_tracker.is_unused() => {
                let header = TimedListHeader {
                    header: self.header,
                    state: self.state,
                };
                let body = TimedListBody {
                    cutoffs: self.cutoffs,
                    state: self.state,
                };

                let mut draw = DrawVertical::new(dimensions);
                draw.draw(&header, mode)?;
                draw.draw(&body, mode)?;
                Ok(draw.finish())
            }
            // show a summary at the end
            DrawMode::Final => {
                CountComponent { state: self.state }.draw(dimensions, DrawMode::Final)
            }
            _ => Ok(Lines::new()),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Arc;
    use std::time::UNIX_EPOCH;

    use buck2_core::fs::paths::file_name::FileNameBuf;
    use buck2_data::FakeStart;
    use buck2_data::SpanStartEvent;
    use buck2_event_observer::action_stats::ActionStats;
    use buck2_event_observer::verbosity::Verbosity;
    use buck2_events::span::SpanId;
    use buck2_events::BuckEvent;
    use buck2_wrapper_common::invocation_id::TraceId;
    use dupe::Dupe;
    use itertools::Itertools;

    use super::*;
    use crate::subscribers::subscriber::Tick;
    use crate::subscribers::superconsole::SuperConsoleConfig;
    use crate::subscribers::superconsole::TimeSpeed;

    const CUTOFFS: Cutoffs = Cutoffs {
        inform: Duration::from_secs(2),
        warn: Duration::from_secs(4),
        _notable: Duration::from_millis(200),
    };

    const TIME_DILATION: u64 = 10;

    fn fake_time_speed() -> TimeSpeed {
        // We run time 10x slower so that any time occurring due to the
        // test running on an overloaded server is ignored.
        //
        // Note that going to 100x slower causes Windows CI to fail, because
        // the `Instant` can't go below the time when the VM was booted, or you get an
        // underflow of `Instant`.
        TimeSpeed::new(Some(1.0 / (TIME_DILATION as f64))).unwrap()
    }

    fn fake_time(tick: &Tick, secs: u64) -> Instant {
        tick.start_time
            .checked_sub(Duration::from_secs(secs * TIME_DILATION))
            .unwrap_or_else(|| {
                panic!(
                    "Instant went too low: {:?} - ({secs} * {TIME_DILATION}",
                    tick.start_time
                )
            })
            // We add 50ms to give us a 100ms window where we round down correctly
            .checked_add(Duration::from_millis(50 * TIME_DILATION))
            .unwrap()
    }

    fn super_console_state_for_test(
        span_tracker: BuckEventSpanTracker,
        action_stats: ActionStats,
        tick: Tick,
        time_speed: TimeSpeed,
        timed_list_state: SuperConsoleConfig,
    ) -> SuperConsoleState {
        let mut state = SuperConsoleState::new(
            None,
            TraceId::null(),
            FileNameBuf::unchecked_new("ignore"),
            Verbosity::Default,
            false,
            timed_list_state,
        )
        .unwrap();
        state.simple_console.observer.span_tracker = span_tracker;
        state.simple_console.observer.action_stats = action_stats;
        state.current_tick = tick;
        state.time_speed = time_speed;
        state
    }

    #[test]
    fn test_normal() -> anyhow::Result<()> {
        let tick = Tick::now();

        let label = Arc::new(BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            None,
            buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                data: Some(buck2_data::span_start_event::Data::Fake(FakeStart {
                    caramba: "test".to_owned(),
                })),
            }),
        ));

        let module = Arc::new(BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            None,
            buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                data: Some(buck2_data::span_start_event::Data::Fake(FakeStart {
                    caramba: "foo".to_owned(),
                })),
            }),
        ));

        let mut state = BuckEventSpanTracker::new();
        state.start_at(&label, fake_time(&tick, 3)).unwrap();
        state.start_at(&module, fake_time(&tick, 1)).unwrap();

        let time_speed = fake_time_speed();
        let action_stats = ActionStats {
            local_actions: 0,
            remote_actions: 0,
            cached_actions: 1,
            fallback_actions: 0,
        };

        let timed_list_state = SuperConsoleConfig {
            max_lines: 5,
            ..Default::default()
        };

        let output = TimedList::new(
            &CUTOFFS,
            "test",
            &super_console_state_for_test(state, action_stats, tick, time_speed, timed_list_state),
        )
        .draw(
            Dimensions {
                width: 40,
                height: 10,
            },
            DrawMode::Normal,
        )?;
        let expected = [
            "testRemaining: 2/2. Cache hits: 100%. Ti",
            "----------------------------------------",
            "test -- speak of the devil          3.0s",
            "foo -- speak of the devil           1.0s",
        ].iter().map(|l| format!("{}\n", l)).join("");

        pretty_assertions::assert_eq!(output.fmt_for_test().to_string(), expected);

        Ok(())
    }

    #[test]
    fn test_remaining() -> anyhow::Result<()> {
        let tick = Tick::now();

        let e1 = BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            None,
            buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                data: Some(buck2_data::span_start_event::Data::Fake(FakeStart {
                    caramba: "e1".to_owned(),
                })),
            }),
        );

        let e2 = BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            None,
            buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                data: Some(buck2_data::span_start_event::Data::Fake(FakeStart {
                    caramba: "e2".to_owned(),
                })),
            }),
        );

        let e3 = BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            None,
            buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                data: Some(buck2_data::span_start_event::Data::Fake(FakeStart {
                    caramba: "e3".to_owned(),
                })),
            }),
        );

        let mut state = BuckEventSpanTracker::new();

        for e in vec![e1, e2, e3] {
            state
                .start_at(&Arc::new(e.clone()), fake_time(&tick, 1))
                .unwrap();
        }

        let time_speed = fake_time_speed();
        let action_stats = ActionStats {
            local_actions: 0,
            remote_actions: 0,
            cached_actions: 1,
            fallback_actions: 0,
        };

        let timed_list_state = SuperConsoleConfig {
            max_lines: 2,
            ..Default::default()
        };

        let output = TimedList::new(
            &CUTOFFS,
            "test",
            &super_console_state_for_test(state, action_stats, tick, time_speed, timed_list_state),
        )
        .draw(
            Dimensions {
                width: 40,
                height: 10,
            },
            DrawMode::Normal,
        )?;
        let expected = [
            "testRemaining: 3/3. Cache hits: 100%. Ti",
            "----------------------------------------",
            "e1 -- speak of the devil            1.0s",
            "... and 2 more currently executing",
        ]
        .iter()
        .map(|l| format!("{}\n", l))
        .join("");

        pretty_assertions::assert_eq!(output.fmt_for_test().to_string(), expected);

        Ok(())
    }

    #[test]
    fn test_remaining_with_pending() -> anyhow::Result<()> {
        let tick = Tick::now();

        let mut state = SuperConsoleState::new(
            None,
            TraceId::null(),
            FileNameBuf::unchecked_new("ignore"),
            Verbosity::Default,
            false,
            SuperConsoleConfig {
                max_lines: 2,
                ..Default::default()
            },
        )?;

        state.time_speed = fake_time_speed();
        state.current_tick = tick.clone();

        state.simple_console.observer.observe(
            fake_time(&tick, 10),
            &Arc::new(BuckEvent::new(
                UNIX_EPOCH,
                TraceId::new(),
                Some(SpanId::new()),
                None,
                SpanStartEvent {
                    data: Some(
                        buck2_data::ActionExecutionStart {
                            key: Some(buck2_data::ActionKey {
                                id: Default::default(),
                                owner: Some(buck2_data::action_key::Owner::TargetLabel(
                                    buck2_data::ConfiguredTargetLabel {
                                        label: Some(buck2_data::TargetLabel {
                                            package: "pkg".into(),
                                            name: "target".into(),
                                        }),
                                        configuration: Some(buck2_data::Configuration {
                                            full_name: "conf".into(),
                                        }),
                                        execution_configuration: None,
                                    },
                                )),
                                key: "".to_owned(),
                            }),
                            name: Some(buck2_data::ActionName {
                                category: "category".into(),
                                identifier: "identifier".into(),
                            }),
                            kind: buck2_data::ActionKind::NotSet as i32,
                        }
                        .into(),
                    ),
                }
                .into(),
            )),
        )?;

        state.simple_console.observer.observe(
            fake_time(&tick, 1),
            &Arc::new(BuckEvent::new(
                UNIX_EPOCH,
                TraceId::new(),
                None,
                None,
                buck2_data::InstantEvent {
                    data: Some(
                        buck2_data::DiceStateSnapshot {
                            key_states: {
                                let mut map = HashMap::new();
                                map.insert(
                                    "BuildKey".to_owned(),
                                    buck2_data::DiceKeyState {
                                        started: 4,
                                        finished: 2,
                                        check_deps_started: 2,
                                        check_deps_finished: 1,
                                    },
                                );
                                map
                            },
                        }
                        .into(),
                    ),
                }
                .into(),
            )),
        )?;

        {
            let output = TimedList::new(&CUTOFFS, "test", &state).draw(
                Dimensions {
                    width: 60,
                    height: 10,
                },
                DrawMode::Normal,
            )?;

            let expected = [
                "test                      Remaining: 3/3. Time elapsed: 0.0s",
                "------------------------------------------------------------",
                "pkg:target -- action (category identifier)             10.0s",
            ].iter().map(|l| format!("{}\n", l)).join("");

            pretty_assertions::assert_eq!(output.fmt_for_test().to_string(), expected);
        }

        {
            state.config.max_lines = 1; // With fewer lines now

            let output = TimedList::new(&CUTOFFS, "test", &state).draw(
                Dimensions {
                    width: 60,
                    height: 10,
                },
                DrawMode::Normal,
            )?;

            let expected = [
                "test                      Remaining: 3/3. Time elapsed: 0.0s",
                "------------------------------------------------------------",
                "... and 1 more currently executing",
            ]
            .iter()
            .map(|l| format!("{}\n", l))
            .join("");

            pretty_assertions::assert_eq!(output.fmt_for_test().to_string(), expected);
        }

        Ok(())
    }

    #[test]
    fn test_children() -> anyhow::Result<()> {
        let tick = Tick::now();

        let parent = SpanId::new();

        let action = Arc::new(BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(parent),
            None,
            SpanStartEvent {
                data: Some(
                    buck2_data::ActionExecutionStart {
                        key: Some(buck2_data::ActionKey {
                            id: Default::default(),
                            owner: Some(buck2_data::action_key::Owner::TargetLabel(
                                buck2_data::ConfiguredTargetLabel {
                                    label: Some(buck2_data::TargetLabel {
                                        package: "pkg".into(),
                                        name: "target".into(),
                                    }),
                                    configuration: Some(buck2_data::Configuration {
                                        full_name: "conf".into(),
                                    }),
                                    execution_configuration: None,
                                },
                            )),
                            key: "".to_owned(),
                        }),
                        name: Some(buck2_data::ActionName {
                            category: "category".into(),
                            identifier: "identifier".into(),
                        }),
                        kind: buck2_data::ActionKind::NotSet as i32,
                    }
                    .into(),
                ),
            }
            .into(),
        ));

        let prepare = Arc::new(BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            Some(parent),
            SpanStartEvent {
                data: Some(
                    buck2_data::ExecutorStageStart {
                        stage: Some(buck2_data::PrepareAction {}.into()),
                    }
                    .into(),
                ),
            }
            .into(),
        ));

        let mut state = BuckEventSpanTracker::new();
        state.start_at(&action, fake_time(&tick, 10)).unwrap();
        state.start_at(&prepare, fake_time(&tick, 5)).unwrap();

        let time_speed = fake_time_speed();

        let action_stats = ActionStats {
            local_actions: 0,
            remote_actions: 0,
            cached_actions: 1,
            fallback_actions: 0,
        };

        let timed_list_state = SuperConsoleConfig {
            max_lines: 5,
            ..Default::default()
        };

        let output = TimedList::new(
            &CUTOFFS,
            "test",
            &super_console_state_for_test(
                state.clone(),
                action_stats.dupe(),
                tick.dupe(),
                time_speed,
                timed_list_state.clone(),
            ),
        )
        .draw(
            Dimensions {
                width: 80,
                height: 10,
            },
            DrawMode::Normal,
        )?;
        let expected = [
            "test                        Remaining: 1/1. Cache hits: 100%. Time elapsed: 0.0s",
            "--------------------------------------------------------------------------------",
            "pkg:target -- action (category identifier) [prepare 5.0s]                  10.0s",
        ].iter().map(|l| format!("{}\n", l)).join("");

        pretty_assertions::assert_eq!(output.fmt_for_test().to_string(), expected);

        // Now, add another action. Normally we don't have multiple stages actually running
        // concurrently but this is a test!

        let re_download = Arc::new(BuckEvent::new(
            UNIX_EPOCH,
            TraceId::new(),
            Some(SpanId::new()),
            Some(parent),
            SpanStartEvent {
                data: Some(
                    buck2_data::ExecutorStageStart {
                        stage: Some(
                            buck2_data::ReStage {
                                stage: Some(buck2_data::ReDownload {}.into()),
                            }
                            .into(),
                        ),
                    }
                    .into(),
                ),
            }
            .into(),
        ));

        state.start_at(&re_download, fake_time(&tick, 2)).unwrap();

        let output = TimedList::new(
            &CUTOFFS,
            "test",
            &super_console_state_for_test(state, action_stats, tick, time_speed, timed_list_state),
        )
        .draw(
            Dimensions {
                width: 80,
                height: 10,
            },
            DrawMode::Normal,
        )?;
        let expected = [
            "test                        Remaining: 1/1. Cache hits: 100%. Time elapsed: 0.0s",
            "--------------------------------------------------------------------------------",
            "pkg:target -- action (category identifier) [prepare 5.0s + 1]              10.0s",
        ].iter().map(|l| format!("{}\n", l)).join("");

        pretty_assertions::assert_eq!(output.fmt_for_test().to_string(), expected);

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::time::Duration;

use buck2_event_observer::display;
use buck2_event_observer::display::TargetDisplayOptions;
use buck2_event_observer::fmt_duration;
use buck2_event_observer::span_tracker::BuckEventSpanInfo;
use derive_more::From;
use superconsole::style::style;
use superconsole::style::StyledContent;
use superconsole::style::Stylize;
use superconsole::Component;
use superconsole::Dimensions;
use superconsole::DrawMode;
use superconsole::Line;
use superconsole::Lines;
use superconsole::Span;

use crate::subscribers::superconsole::timed_list::Cutoffs;

#[derive(Debug, Clone, From)]
pub(crate) enum Row {
    Timed(TimedRow),
    Line(Line),
}

#[derive(Debug)]
pub(crate) struct Table {
    pub(crate) rows: Vec<Row>,
}

impl Table {
    pub(crate) fn new() -> Self {
        Self { rows: Vec::new() }
    }

    pub(crate) fn len(&self) -> usize {
        self.rows.len()
    }
}

impl Component for Table {
    /// Zips together each time and label lines, but gives the times preferential treatment.
    fn draw_unchecked(
        &self,
        Dimensions { width, .. }: Dimensions,
        _mode: DrawMode,
    ) -> anyhow::Result<Lines> {
        let combined = self
            .rows
            .iter()
            .cloned()
            .map(|row| {
                match row {
                    Row::Timed(row) => {
                        let mut label = row.event;
                        let time = row.time;
                        let time_len = time.len();
                        let padding = 1;
                        let maximum_label_width = width.saturating_sub(time_len + padding);
                        let original_label_len = label.len();
                        let will_be_truncated = original_label_len > maximum_label_width;
                        if will_be_truncated {
                            // make space for ellipses
                            let style = label.iter().last().unwrap().style;
                            label.truncate_line(maximum_label_width.saturating_sub(3));
                            label.push(Span::new_styled_lossy(StyledContent::new(
                                style,
                                "...".to_owned(),
                            )));
                        }

                        // add extra padding to compensate for missing spaces between label and time
                        label.pad_right(width.saturating_sub(time_len + label.len()));
                        let mut combined = label;
                        combined.extend(time);
                        combined
                    }
                    Row::Line(line) => line,
                }
            })
            .collect();

        Ok(combined)
    }
}

#[derive(Debug, Clone)]
pub(crate) struct TimedRow {
    event: Line,
    time: Line,
}

impl TimedRow {
    pub(crate) fn span(
        padding: usize,
        span: &BuckEventSpanInfo,
        time_speed: f64,
        cutoffs: &Cutoffs,
        display_platform: bool,
    ) -> anyhow::Result<Self> {
        let event = display::display_event(
            &span.event,
            TargetDisplayOptions::for_console(display_platform),
        )?;
        let time = fmt_duration::fmt_duration(span.start.elapsed(), time_speed);
        let age = span.start.elapsed().mul_f64(time_speed);
        Self::text(padding, event, time, age, cutoffs)
    }

    pub(crate) fn text(
        padding: usize,
        event: String,
        time: String,
        age: Duration,
        cutoffs: &Cutoffs,
    ) -> anyhow::Result<Self> {
        Self::styled(padding, style(event), style(time), age, cutoffs)
    }

    pub(crate) fn styled(
        padding: usize,
        event: StyledContent<String>,
        time: StyledContent<String>,
        age: Duration,
        cutoffs: &Cutoffs,
    ) -> anyhow::Result<Self> {
        let event = Span::new_styled(styled_for_delay(event, age, cutoffs))?;

        let line = if padding > 0 {
            Line::from_iter([Span::padding(padding), event])
        } else {
            Line::from_iter([event])
        };

        let time = Line::from_iter([Span::new_styled(styled_for_delay(time, age, cutoffs))?]);
        Ok(Self { event: line, time })
    }
}

/// This component echoes the `Lines` that have been stored in it.
#[derive(Debug)]
struct LinesComponent(Lines);

impl Component for LinesComponent {
    fn draw_unchecked(&self, _dimensions: Dimensions, _mode: DrawMode) -> anyhow::Result<Lines> {
        Ok(self.0.clone())
    }
}

/// Colorize based on time.
fn styled_for_delay(
    content: StyledContent<String>,
    elapsed: Duration,
    cutoffs: &Cutoffs,
) -> StyledContent<String> {
    if elapsed < cutoffs.inform {
        content
    } else if elapsed < cutoffs.warn {
        content.dark_yellow()
    } else {
        content.dark_red()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

use std::env;
use std::io;
use std::path::PathBuf;

fn main() -> io::Result<()> {
    let proto_files = &["daemon.proto"];

    let data_include = if let Ok(value) = env::var("BUCK_HACK_DATA_PROTOC_INCLUDE") {
        let path = PathBuf::from(value);
        path.parent().unwrap().to_str().unwrap().to_owned()
    } else {
        "../buck2_data".to_owned()
    };

    let subscription_include = if let Ok(value) = env::var("BUCK_HACK_SUBSCRIPTION_PROTOC_INCLUDE")
    {
        let path = PathBuf::from(value);
        path.parent().unwrap().to_str().unwrap().to_owned()
    } else {
        "../buck2_subscription_proto".to_owned()
    };

    buck2_protoc_dev::configure()
        .setup_protoc()
        .type_attribute(".", "#[derive(::serde::Serialize, ::serde::Deserialize)] #[serde(rename_all = \"snake_case\")]")
        .type_attribute(".", "#[derive(::allocative::Allocative)]")
        .field_attribute("start_time", "#[serde(with = \"serialize_timestamp\")]")
        .field_attribute("timeout", "#[serde(rename = \"timeout_us\", with = \"buck2_data::serialize_duration_as_micros\")]")
        .field_attribute("uptime", "#[serde(rename = \"uptime_us\", with = \"buck2_data::serialize_duration_as_micros\")]")
        .field_attribute("delay", "#[serde(rename = \"delay_us\", with = \"buck2_data::serialize_duration_as_micros\")]")
        .field_attribute("ProfileResponse.elapsed", "#[serde(rename = \"elapsed_us\", with = \"buck2_data::serialize_duration_as_micros\")]")
        .boxed("CommandProgress.progress.event")
        .boxed("CommandProgress.progress.result")
        .boxed("CommandProgress.progress.partial_result")
        .extern_path(".buck.data", "::buck2_data")
        .extern_path(".buck.subscription", "::buck2_subscription_proto")
        .compile(proto_files, &[".", &data_include, &subscription_include])
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

#![feature(min_specialization)]
#![allow(clippy::large_enum_variant)]

use thiserror::Error;

use crate::BuckDaemonProtoError::MissingClientContext;

pub mod protobuf_util;

tonic::include_proto!("buck.daemon");

#[derive(Debug, Error)]
enum BuckDaemonProtoError {
    #[error("daemon request was missing client context")]
    MissingClientContext,
    #[error("wrong gRPC request message type, expecting {0} (internal error)")]
    WrongRequestType(&'static str),
}

pub trait HasClientContext {
    fn client_context(&self) -> anyhow::Result<&ClientContext>;
}

impl HasClientContext for StreamingRequest {
    fn client_context(&self) -> anyhow::Result<&ClientContext> {
        match self.request.as_ref() {
            Some(streaming_request::Request::Context(ctx)) => Ok(ctx),
            _ => Err(MissingClientContext.into()),
        }
    }
}

impl HasBuildOptions for StreamingRequest {
    fn build_options(&self) -> Option<&CommonBuildOptions> {
        None
    }
}

impl TryFrom<StreamingRequest> for LspRequest {
    type Error = anyhow::Error;

    fn try_from(value: StreamingRequest) -> Result<Self, Self::Error> {
        match value.request {
            Some(streaming_request::Request::Lsp(req)) => Ok(req),
            _ => Err(BuckDaemonProtoError::WrongRequestType("LspRequest").into()),
        }
    }
}

impl From<LspRequest> for StreamingRequest {
    fn from(request: LspRequest) -> Self {
        Self {
            request: Some(streaming_request::Request::Lsp(request)),
        }
    }
}

impl TryFrom<StreamingRequest> for SubscriptionRequestWrapper {
    type Error = anyhow::Error;

    fn try_from(value: StreamingRequest) -> Result<Self, Self::Error> {
        match value.request {
            Some(streaming_request::Request::Subscription(req)) => Ok(req),
            _ => Err(BuckDaemonProtoError::WrongRequestType("SubscriptionRequestWrapper").into()),
        }
    }
}

impl From<SubscriptionRequestWrapper> for StreamingRequest {
    fn from(request: SubscriptionRequestWrapper) -> Self {
        Self {
            request: Some(streaming_request::Request::Subscription(request)),
        }
    }
}

impl TryFrom<StreamingRequest> for DapRequest {
    type Error = anyhow::Error;

    fn try_from(value: StreamingRequest) -> Result<Self, Self::Error> {
        match value.request {
            Some(streaming_request::Request::Dap(req)) => Ok(req),
            _ => Err(BuckDaemonProtoError::WrongRequestType("DapRequest").into()),
        }
    }
}

impl From<DapRequest> for StreamingRequest {
    fn from(request: DapRequest) -> Self {
        Self {
            request: Some(streaming_request::Request::Dap(request)),
        }
    }
}

/// Trait for requests that have CommonBuildOptions.
pub trait HasBuildOptions {
    fn build_options(&self) -> Option<&CommonBuildOptions>;
}

macro_rules! result_convert {
    ( $name:ident ) => {
        impl From<$name> for command_result::Result {
            fn from(v: $name) -> Self {
                Self::$name(v)
            }
        }

        impl TryFrom<command_result::Result> for $name {
            type Error = command_result::Result;
            fn try_from(r: command_result::Result) -> Result<Self, Self::Error> {
                if let command_result::Result::$name(res) = r {
                    Ok(res)
                } else {
                    Err(r)
                }
            }
        }
    };
}

macro_rules! partial_result_convert {
    ( $name:ident ) => {
        impl From<$name> for partial_result::PartialResult {
            fn from(v: $name) -> Self {
                Self::$name(v)
            }
        }

        impl TryFrom<partial_result::PartialResult> for $name {
            type Error = partial_result::PartialResult;

            fn try_from(r: partial_result::PartialResult) -> Result<Self, Self::Error> {
                #![allow(irrefutable_let_patterns)]

                if let partial_result::PartialResult::$name(res) = r {
                    Ok(res)
                } else {
                    Err(r)
                }
            }
        }
    };
}

/// Helper macro for defining a request and implementing common traits for said request. See the bottom of this file
/// for usage examples.
macro_rules! define_request {
    // define_request! expects an invocation of the form `define_request!(MyRequest, has(foo, bar, baz))`. The bulk of
    // this macro comes from parsing the contents of the `has` token tree. This is done by parsing the comma-delimited
    // list of identifiers tail-recursively while passing down parsing state as parameters to rules prefixed by `@has`.
    //
    // There are three state parameters in each of the non-public rules:
    //  1) @has, which marks the rule as non-public. The public rules pass @has as the first tokens when recursively
    //     invoking this rule.
    //  2) $name:ident, the name of the request.
    //  3) $has_buildopts:ident, a `true` or `false` token indicating whether or not we've emitted an impl of
    //     HasBuildOptions yet.
    //
    // If we reach the end of the token stream without emitting a HasBuildOptions impl, we'll emit a trivial one that
    // returns None.

    ( @has $name:ident $has_buildopts:ident context $($tail:ident)* ) => {
        impl HasClientContext for $name {
            fn client_context(&self) -> anyhow::Result<&ClientContext> {
                // A request that has a client context field should always set the context.
                match &self.context {
                    Some(v) => Ok(v),
                    None => Err(BuckDaemonProtoError::MissingClientContext.into()),
                }
            }
        }

        define_request!(@has $name $has_buildopts $($tail)*);
    };

    ( @has $name:ident $has_buildopts:ident build_options $($tail:ident)* ) => {
        impl HasBuildOptions for $name {
            fn build_options(&self) -> Option<&CommonBuildOptions> {
                self.build_opts.as_ref()
            }
        }

        define_request!(@has $name true $($tail)*);
    };

    ( @has $name:ident false) => {
        impl HasBuildOptions for $name {
            fn build_options(&self) -> Option<&CommonBuildOptions> {
                None
            }
        }
    };

    ( @has $name:ident true) => {};

    // ------ Public API begins here.

    ( $name:ident, has ($($tail:ident),*)) => {
        define_request!(@has $name false $($tail)*);
    };

    ( $name:ident ) => {};
}

mod serialize_timestamp {
    use serde::Deserialize;
    use serde::Deserializer;
    use serde::Serialize;
    use serde::Serializer;

    pub fn serialize<S>(
        value: &Option<::prost_types::Timestamp>,
        serializer: S,
    ) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let d = value.as_ref().map(|v| (v.seconds, v.nanos));
        d.serialize(serializer)
    }

    pub fn deserialize<'de, D>(
        deserializer: D,
    ) -> Result<Option<::prost_types::Timestamp>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let d = Option::<(i64, i32)>::deserialize(deserializer)?;
        let d = d.map(|(seconds, nanos)| ::prost_types::Timestamp { seconds, nanos });
        Ok(d)
    }
}

result_convert!(AqueryResponse);
result_convert!(KillResponse);
result_convert!(PingResponse);
result_convert!(StatusResponse);
result_convert!(BuildResponse);
result_convert!(BxlResponse);
result_convert!(TestResponse);
result_convert!(UqueryResponse);
result_convert!(CqueryResponse);
result_convert!(TargetsResponse);
result_convert!(TargetsShowOutputsResponse);
result_convert!(ConfiguredTargetsResponse);
result_convert!(GenericResponse);
result_convert!(UnstableDocsResponse);
result_convert!(ProfileResponse);
result_convert!(InstallResponse);
result_convert!(MaterializeResponse);
result_convert!(CleanStaleResponse);
result_convert!(LspResponse);
result_convert!(DapResponse);
result_convert!(AllocativeResponse);
result_convert!(SubscriptionCommandResponse);
result_convert!(TraceIoResponse);

partial_result_convert!(StdoutBytes);
partial_result_convert!(LspMessage);
partial_result_convert!(SubscriptionResponseWrapper);
partial_result_convert!(DapMessage);

define_request!(KillRequest);
define_request!(StatusRequest);
define_request!(PingRequest);

define_request!(BuildRequest, has(context, build_options));
define_request!(BxlRequest, has(context, build_options));
define_request!(TargetsRequest, has(context));
define_request!(ConfiguredTargetsRequest, has(context));
define_request!(AqueryRequest, has(context));
define_request!(CqueryRequest, has(context));
define_request!(UqueryRequest, has(context));
define_request!(TestRequest, has(context, build_options));
define_request!(GenericRequest, has(context));
define_request!(UnstableDocsRequest, has(context));
define_request!(ProfileRequest, has(context));
define_request!(MaterializeRequest, has(context));
define_request!(AllocativeRequest, has(context));
define_request!(CleanStaleRequest, has(context));
define_request!(FileStatusRequest, has(context));
define_request!(TraceIoRequest, has(context));

define_request!(InstallRequest, has(context, build_options));