/*
 * 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::borrow::Cow;
use std::fmt::Display;
use std::fmt::Write as _;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;

use anyhow::Context;
use async_trait::async_trait;
use buck2_core::fs::paths::file_name::FileNameBuf;
use buck2_data::CommandExecutionDetails;
use buck2_event_observer::display;
use buck2_event_observer::display::display_file_watcher_end;
use buck2_event_observer::display::TargetDisplayOptions;
use buck2_event_observer::event_observer::EventObserver;
use buck2_event_observer::event_observer::EventObserverExtra;
use buck2_event_observer::humanized::HumanizedBytes;
use buck2_event_observer::io_state::io_in_flight_non_zero_counters;
use buck2_event_observer::verbosity::Verbosity;
use buck2_event_observer::what_ran;
use buck2_event_observer::what_ran::command_to_string;
use buck2_event_observer::what_ran::worker_command_as_fallback_to_string;
use buck2_event_observer::what_ran::WhatRanCommandConsoleFormat;
use buck2_event_observer::what_ran::WhatRanOptions;
use buck2_event_observer::what_ran::WhatRanOutputCommand;
use buck2_event_observer::what_ran::WhatRanOutputWriter;
use buck2_events::BuckEvent;
use buck2_wrapper_common::invocation_id::TraceId;
use dupe::Dupe;
use superconsole::DrawMode;
use superconsole::SuperConsole;
use termwiz::escape::Action;
use termwiz::escape::ControlCode;

use crate::subscribers::subscriber::Tick;
use crate::subscribers::subscriber_unpack::UnpackingEventSubscriber;

/// buck2 daemon info is printed to stderr if there are no other updates available
/// within this duration.
const KEEPALIVE_TIME_LIMIT: Duration = Duration::from_secs(7);

fn now_display() -> impl Display {
    chrono::Local::now().to_rfc3339_opts(::chrono::SecondsFormat::Millis, false)
}

fn echo_impl(message: &str) -> anyhow::Result<()> {
    let now = now_display();
    for line in message.lines() {
        if line.is_empty() {
            // patternlint-disable-next-line buck2-cli-simpleconsole-echo
            crate::eprintln!("[{}]", now)?;
        } else {
            // patternlint-disable-next-line buck2-cli-simpleconsole-echo
            crate::eprintln!("[{}] {}", now, line)?;
        }
    }
    Ok(())
}

// Echoes a message to stderr, along with a timestamp.
macro_rules! echo {
    () => {
        {
            // patternlint-disable-next-line buck2-cli-simpleconsole-echo
            crate::eprintln!("[{}]", now_display())
        }
    };
    ($fmt:expr $(, $args:expr)*) => {
        {
            let message = format!($fmt $(, $args)*);
            echo_impl(&message)
        }
    };
}

#[derive(Copy, Clone, Dupe, Debug, PartialEq)]
enum TtyMode {
    Enabled,
    Disabled,
}

struct ActionError {
    display: display::ActionErrorDisplay<'static>,
}

impl ActionError {
    fn print(&self, tty_mode: TtyMode) -> anyhow::Result<()> {
        echo!("Action failed: {}", self.display.action_id)?;
        echo!("{}", self.display.reason)?;
        if let Some(command) = &self.display.command {
            eprint_command_details(command, tty_mode)?;
        }
        Ok(())
    }
}

fn eprint_command_details(
    command_failed: &CommandExecutionDetails,
    tty_mode: TtyMode,
) -> anyhow::Result<()> {
    use buck2_data::command_execution_details::Command;

    match command_failed.command.as_ref() {
        Some(Command::LocalCommand(local_command)) => {
            echo!("Local command: {}", command_to_string(local_command))?;
        }
        Some(Command::WorkerCommand(worker_command)) => {
            echo!(
                "Local worker command: {}",
                worker_command_as_fallback_to_string(worker_command)
            )?;
        }
        Some(Command::WorkerInitCommand(worker_init_command)) => {
            echo!(
                "Local worker initialization command: {}",
                command_to_string(worker_init_command)
            )?;
        }
        Some(Command::RemoteCommand(remote_command)) => {
            echo!(
                "Remote action{}, reproduce with: `frecli cas download-action {}`",
                if remote_command.cache_hit {
                    " cache hit"
                } else {
                    ""
                },
                remote_command.action_digest
            )?;
        }
        Some(Command::OmittedLocalCommand(..)) | None => {
            // Nothing to show in this case.
        }
    };
    let (stdout, stderr) = if tty_mode == TtyMode::Disabled {
        (
            sanitize_output_colors(command_failed.stdout.as_bytes()),
            sanitize_output_colors(command_failed.stderr.as_bytes()),
        )
    } else {
        (command_failed.stdout.clone(), command_failed.stderr.clone())
    };
    echo!("Stdout: {}", stdout)?;
    echo!("Stderr: {}", stderr)?;
    Ok(())
}

/// Just repeats stdout and stderr to client process.
pub(crate) struct SimpleConsole<E> {
    tty_mode: TtyMode,
    verbosity: Verbosity,
    // Whether to show "Waiting for daemon..." when no root spans are received
    show_waiting_message: bool,
    pub(crate) observer: EventObserver<E>,
    action_errors: Vec<ActionError>,
    last_print_time: Instant,
    last_had_open_spans: Instant, // Used to detect hangs
    already_raged: bool,
    isolation_dir: FileNameBuf,
    last_shown_snapshot_ts: Option<SystemTime>,
}

impl<E> SimpleConsole<E>
where
    E: EventObserverExtra,
{
    pub(crate) fn with_tty(
        trace_id: TraceId,
        isolation_dir: FileNameBuf,
        verbosity: Verbosity,
        show_waiting_message: bool,
    ) -> Self {
        SimpleConsole {
            tty_mode: TtyMode::Enabled,
            verbosity,
            show_waiting_message,
            observer: EventObserver::new(trace_id),
            action_errors: Vec::new(),
            last_print_time: Instant::now(),
            last_had_open_spans: Instant::now(),
            already_raged: false,
            isolation_dir,
            last_shown_snapshot_ts: None,
        }
    }

    pub(crate) fn without_tty(
        trace_id: TraceId,
        isolation_dir: FileNameBuf,
        verbosity: Verbosity,
        show_waiting_message: bool,
    ) -> Self {
        SimpleConsole {
            tty_mode: TtyMode::Disabled,
            verbosity,
            show_waiting_message,
            observer: EventObserver::new(trace_id),
            action_errors: Vec::new(),
            last_print_time: Instant::now(),
            last_had_open_spans: Instant::now(),
            already_raged: false,
            isolation_dir,
            last_shown_snapshot_ts: None,
        }
    }

    /// Create a SimpleConsole that auto detects whether it has a TTY or not.
    pub(crate) fn autodetect(
        trace_id: TraceId,
        isolation_dir: FileNameBuf,
        verbosity: Verbosity,
        show_waiting_message: bool,
    ) -> Self {
        match SuperConsole::compatible() {
            true => Self::with_tty(trace_id, isolation_dir, verbosity, show_waiting_message),
            false => Self::without_tty(trace_id, isolation_dir, verbosity, show_waiting_message),
        }
    }

    pub(crate) fn observer(&self) -> &EventObserver<E> {
        &self.observer
    }

    pub(crate) fn update_event_observer(&mut self, event: &Arc<BuckEvent>) -> anyhow::Result<()> {
        self.observer
            .observe(Instant::now(), event)
            .context("Error tracking event")
    }

    fn notify_printed(&mut self) {
        self.last_print_time = Instant::now();
    }

    fn print_stats_while_waiting(&mut self) -> anyhow::Result<()> {
        if let Some(h) = self.observer().re_state().render_header(DrawMode::Normal) {
            echo!("{}", h)?;
        }

        let snapshots = self.observer().two_snapshots();
        let last_snapshot_ts = snapshots.last.as_ref().map(|(ts, _)| *ts);

        // We normally send snapshots more often than we print this, so we'd expect the
        // snapshot to change on every call.
        let is_snapshot_stale = match (&self.last_shown_snapshot_ts, last_snapshot_ts) {
            (Some(x), Some(y)) => *x == y,
            _ => false,
        };

        if is_snapshot_stale {
            echo!("Resource usage: <snapshot is stale>")?;
        } else {
            let mut parts = Vec::with_capacity(2);
            if let Some((_, snapshot)) = &snapshots.last {
                if let Some(buck2_rss) = snapshot.buck2_rss {
                    parts.push(format!("RSS: {}", HumanizedBytes::new(buck2_rss)));
                }
            }
            if let Some(cpu) = snapshots.cpu_percents() {
                parts.push(format!("CPU: {}%", cpu));
            }
            if !parts.is_empty() {
                echo!("Resource usage: {}", parts.join(" "))?;
            }

            if let Some((_ts, snapshot)) = &snapshots.last {
                let mut parts = Vec::new();
                for (key, value) in io_in_flight_non_zero_counters(snapshot) {
                    parts.push(format!("{:?}: {}", key, value));
                }
                if !parts.is_empty() {
                    echo!("IO: {}", parts.join(" "))?;
                } else {
                    echo!("IO: none")?;
                }
            }

            self.last_shown_snapshot_ts = last_snapshot_ts;
        }

        Ok(())
    }

    pub(crate) async fn detect_hangs(&mut self) -> anyhow::Result<()> {
        if !self.show_waiting_message {
            // No open spans are to be expected in this case (D37658796)
            return Ok(());
        };
        if self.observer().spans().iter_roots().len() > 0 {
            self.last_had_open_spans = Instant::now();
            return Ok(());
        }
        // Do nothing if less than 1 minute since last open span
        if self.last_had_open_spans.elapsed().as_secs() < 60 {
            return Ok(());
        }
        // When command is stuck we call `rage` to gather debugging information
        if !self.already_raged {
            self.already_raged = true;
            spawn_rage(
                self.isolation_dir.clone(),
                self.observer().session_info().trace_id.dupe(),
            );
        }
        Ok(())
    }
}

#[async_trait]
impl<E> UnpackingEventSubscriber for SimpleConsole<E>
where
    E: EventObserverExtra,
{
    async fn handle_output(&mut self, raw_output: &[u8]) -> anyhow::Result<()> {
        // We expect output that gets here to already have been buffered if possible (because it
        // primarily gets to us through a GRPC layer that already needs buffering), so we
        // unconditionally flush it.
        crate::stdio::print_bytes(raw_output)?;
        crate::stdio::flush()?;
        self.notify_printed();
        Ok(())
    }

    async fn handle_stderr(&mut self, stderr: &str) -> anyhow::Result<()> {
        echo!("{}", stderr)?;
        self.notify_printed();
        Ok(())
    }

    async fn handle_structured_error(
        &mut self,
        err: &buck2_data::StructuredError,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if err.quiet {
            return Ok(());
        }
        echo!("{}", err.payload)?;
        self.notify_printed();
        Ok(())
    }

    async fn handle_file_watcher_end(
        &mut self,
        file_watcher: &buck2_data::FileWatcherEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if self.verbosity.print_status() {
            for x in display_file_watcher_end(file_watcher) {
                echo!("{}", x)?;
            }
            self.notify_printed();
        }
        Ok(())
    }

    async fn handle_event(&mut self, event: &Arc<BuckEvent>) -> anyhow::Result<()> {
        self.update_event_observer(event)?;
        self.handle_inner_event(event)
            .await
            .with_context(|| display::InvalidBuckEvent(event.dupe()))?;

        if self.verbosity.print_all_commands() {
            what_ran::emit_event_if_relevant(
                event.parent_id().into(),
                event.data(),
                self.observer().spans(),
                &mut PrintDebugCommandToStderr,
                &WhatRanOptions::default(),
            )?;
        }

        Ok(())
    }

    async fn handle_command_start(
        &mut self,
        _command: &buck2_data::CommandStart,
        event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if cfg!(fbcode_build) {
            echo!(
                "Buck UI: https://www.internalfb.com/buck2/{}",
                event.trace_id()?
            )?;
        } else {
            echo!("Build ID: {}", event.trace_id()?)?;
        }
        self.notify_printed();
        Ok(())
    }

    async fn handle_command_result(
        &mut self,
        result: &buck2_cli_proto::CommandResult,
    ) -> anyhow::Result<()> {
        if let buck2_cli_proto::CommandResult {
            result: Some(buck2_cli_proto::command_result::Result::Error(e)),
        } = result
        {
            echo!("Command failed: ")?;
            for message in &e.messages {
                echo!("{}", message)?;
            }
            self.notify_printed();
        }

        let errors = std::mem::take(&mut self.action_errors);

        if !errors.is_empty() {
            echo!()?;
            echo!("BUILD ERRORS ({})", errors.len())?;
            echo!("The following actions failed during the execution of this command:")?;
            for error in errors.iter() {
                error.print(self.tty_mode)?;
            }
            echo!()?;
            self.notify_printed();
        }

        Ok(())
    }

    async fn handle_command_end(
        &mut self,
        _command: &buck2_data::CommandEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if self.verbosity.print_status() && self.observer().action_stats().log_stats() {
            let cache_hit_percentage = self.observer().action_stats().action_cache_hit_percentage();
            echo!("Cache hits: {}%", cache_hit_percentage)?;
            echo!(
                "Commands: {} (cached: {}, remote: {}, local: {})",
                self.observer()
                    .action_stats()
                    .total_executed_and_cached_actions(),
                self.observer().action_stats().cached_actions,
                self.observer().action_stats().remote_actions,
                self.observer().action_stats().local_actions
            )?;
            if self.observer().action_stats().fallback_actions > 0 {
                echo!(
                    "Fallback: {}/{}",
                    self.observer().action_stats().fallback_actions,
                    self.observer().action_stats().total_executed_actions()
                )?;
            }
        }

        if let Some(re) = &self.observer().re_state().render_header(DrawMode::Final) {
            echo!("{}", re)?;
        }

        if let Some(test_session) = &self.observer().session_info().test_session {
            echo!("Test session: {}", test_session.info)?;
        }

        Ok(())
    }

    async fn handle_console_message(
        &mut self,
        message: &buck2_data::ConsoleMessage,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        self.handle_stderr(&message.message).await
    }

    async fn handle_re_session_created(
        &mut self,
        session: &buck2_data::RemoteExecutionSessionCreated,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        let message = format!("RE Session: {}", session.session_id);
        self.handle_stderr(&message).await
    }

    async fn handle_action_execution_end(
        &mut self,
        action: &buck2_data::ActionExecutionEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        let action_id = display::display_action_identity(
            action.key.as_ref(),
            action.name.as_ref(),
            TargetDisplayOptions::for_log(),
        )?;

        let stderr = display::success_stderr(action, self.verbosity)?;

        if self.verbosity.print_all_actions() || stderr.is_some() {
            let complete = self.observer().spans().roots_completed();
            let incomplete = self.observer().spans().roots_ongoing();
            echo!("{} / {}: {}", complete, complete + incomplete, action_id)?;
            if let Some(stderr) = stderr {
                // TODO(nmj): Factor out behavior here so that handling ttymode isn't ad hoc.  i.e. write a method that formats text based on tty mode
                match self.tty_mode {
                    TtyMode::Enabled => {
                        // Add the extra control character so that users' stderr messages can't
                        // mess up the terminal
                        echo!("stderr:{}\x1b[0m", stderr)?;
                    }
                    TtyMode::Disabled => {
                        echo!("stderr:\n{}", sanitize_output_colors(stderr.as_bytes()))?;
                    }
                }
            }
            self.notify_printed();
        }

        if let Some(error) = &action.error {
            let action_error = ActionError {
                display: display::display_action_error(
                    action,
                    error,
                    TargetDisplayOptions::for_log(),
                )?
                .to_static(),
            };

            action_error.print(self.tty_mode)?;
            self.action_errors.push(action_error);
            self.notify_printed();
        }

        Ok(())
    }

    async fn handle_test_discovery(
        &mut self,
        test_info: &buck2_data::TestDiscovery,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if let Some(data) = &test_info.data {
            match data {
                buck2_data::test_discovery::Data::Session(buck2_data::TestSessionInfo { info }) => {
                    echo!("Test session: {}", info)?;
                    self.notify_printed();
                }
                buck2_data::test_discovery::Data::Tests(..) => {}
            }
        }

        Ok(())
    }

    async fn handle_test_result(
        &mut self,
        result: &buck2_data::TestResult,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if let Some(msg) = display::format_test_result(result)? {
            let mut buffer = String::new();

            for line in msg {
                writeln!(buffer, "{}", line.to_unstyled())?;
            }
            //Printing the test output in multiple lines. It makes easier for the user to read.
            echo!("{}", buffer)?;
        }

        Ok(())
    }

    async fn tick(&mut self, _: &Tick) -> anyhow::Result<()> {
        self.detect_hangs().await?;
        if self.verbosity.print_status() && self.last_print_time.elapsed() > KEEPALIVE_TIME_LIMIT {
            let mut show_stats = self.show_waiting_message;

            let mut roots = self.observer().spans().iter_roots();
            let sample_event = roots.next();
            match sample_event {
                Some(sample_event) => {
                    let child = match sample_event.children().next() {
                        Some(c) => Cow::Owned(format!(
                            " [{}]",
                            display::display_event(
                                &c.info().event,
                                TargetDisplayOptions::for_log()
                            )?
                        )),
                        None => Cow::Borrowed(""),
                    };

                    echo!(
                        "Waiting on {}{}, and {} other actions",
                        display::display_event(
                            &sample_event.info().event,
                            TargetDisplayOptions::for_log()
                        )?,
                        child,
                        roots.len()
                    )?;

                    show_stats = self.verbosity.always_print_stats_in_status();
                }
                None => {
                    if self.show_waiting_message {
                        echo!(
                            "Waiting on buck2 daemon {}...",
                            self.observer.session_info().trace_id
                        )?;
                    }
                }
            }
            // roots must be dropped here because it mutably borrows `self`
            // and doesn't get dropped until the end of this scope otherwise.
            std::mem::drop(roots);

            if show_stats {
                self.print_stats_while_waiting()?;
            }

            self.notify_printed();
        }

        Ok(())
    }

    async fn handle_error(&mut self, _error: &anyhow::Error) -> anyhow::Result<()> {
        // We don't need to do any cleanup to exit.
        Ok(())
    }

    async fn handle_console_preferences(
        &mut self,
        _prefs: &buck2_data::ConsolePreferences,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        // Those are only used by the Superconsole at the moment.
        Ok(())
    }
}

struct PrintDebugCommandToStderr;

impl WhatRanOutputWriter for PrintDebugCommandToStderr {
    fn emit_command(&mut self, command: WhatRanOutputCommand<'_>) -> anyhow::Result<()> {
        echo!(
            "{}",
            WhatRanCommandConsoleFormat {
                reason: command.reason(),
                identity: command.identity(),
                repro: command.repro(),
            }
        )?;
        Ok(())
    }
}

fn sanitize_output_colors(stderr: &[u8]) -> String {
    let mut sanitized = String::with_capacity(stderr.len());
    let mut parser = termwiz::escape::parser::Parser::new();
    parser.parse(stderr, |a| match a {
        Action::Print(c) => sanitized.push(c),
        Action::Control(cc) => match cc {
            ControlCode::CarriageReturn => sanitized.push('\r'),
            ControlCode::LineFeed => sanitized.push('\n'),
            ControlCode::HorizontalTab => sanitized.push('\t'),
            _ => {}
        },
        _ => {}
    });
    sanitized
}

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

    #[test]
    fn removes_color_characters() {
        let message = "\x1b[0mFoo\t\x1b[34mBar\n\x1b[DBaz\r\nQuz";

        let sanitized = sanitize_output_colors(message.as_bytes());

        assert_eq!("Foo\tBar\nBaz\r\nQuz", sanitized);
    }
}

fn spawn_rage(isolation_dir: FileNameBuf, trace_id: TraceId) {
    match spawn_rage_impl(isolation_dir, trace_id) {
        Ok(_) => {}
        Err(e) => tracing::warn!("Error calling buck2 rage: {:#}", e),
    };
}

fn spawn_rage_impl(isolation_dir: FileNameBuf, trace_id: TraceId) -> anyhow::Result<()> {
    let current_exe = std::env::current_exe().context("Not current_exe")?;
    let mut command = buck2_util::process::async_background_command(current_exe);
    command
        .args(["--isolation-dir", isolation_dir.as_str()])
        .arg("rage")
        .arg("--timeout")
        .arg("3600")
        .arg("--no-paste")
        .args(["--origin", "hang-detector"])
        .args(["--invocation-id", &trace_id.to_string()]);
    let mut spawned = command
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;

    // Running rage command in an async task. We don't want to block
    // the main thread while we wait for rage command to finish.
    tokio::spawn(async move { spawned.wait().await });
    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 async_trait::async_trait;

use crate::subscribers::subscriber::EventSubscriber;

/// The only purpose of this subscriber is to print out stderr/stdout
/// during buckd connection bootstrapping.
pub struct StdoutStderrForwarder;

#[async_trait]
impl EventSubscriber for StdoutStderrForwarder {
    async fn handle_tailer_stderr(&mut self, stderr: &str) -> anyhow::Result<()> {
        crate::eprintln!("{}", stderr)?;
        Ok(())
    }
    async fn handle_output(&mut self, raw_output: &[u8]) -> anyhow::Result<()> {
        // Lossy conversion is a bit awkward here, but so is printing stdout to stderr anyway. If
        // that happens our output is going to be pretty busted anyway.
        crate::eprint!("{}", String::from_utf8_lossy(raw_output))?;
        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::sync::Arc;
use std::time::Duration;
use std::time::Instant;

use async_trait::async_trait;
use buck2_events::BuckEvent;
use dupe::Dupe;

use crate::subscribers::observer::ErrorObserver;

/// Information about tick timing.
#[derive(Debug, Clone, Dupe)]
pub struct Tick {
    /// The time that the ticker was started.
    pub start_time: Instant,
    /// Elapsed time since the ticker was started for this tick.
    pub(crate) elapsed_time: Duration,
}

impl Tick {
    pub(crate) fn now() -> Tick {
        Self {
            start_time: Instant::now(),
            elapsed_time: Duration::ZERO,
        }
    }
}

/// Visitor trait.  Implement this to subscribe to the event streams.
/// Each method will be called whenever an event occurs.
#[async_trait]
pub trait EventSubscriber: Send {
    /// Fired by the tailer for stdout, or by PartialResultHandler instances that wish to write to
    /// stdout.
    async fn handle_output(&mut self, _raw_output: &[u8]) -> anyhow::Result<()> {
        Ok(())
    }
    /// Fired by the tailer for stderr.
    async fn handle_tailer_stderr(&mut self, _stderr: &str) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_console_interaction(&mut self, _c: char) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_events(&mut self, _event: &[Arc<BuckEvent>]) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_command_result(
        &mut self,
        _result: &buck2_cli_proto::CommandResult,
    ) -> anyhow::Result<()> {
        Ok(())
    }

    /// Give the subscriber a chance to react to errors as we start trying to clean up.
    /// They may return another error, which will be incorporated into the end result.
    async fn handle_error(&mut self, _error: &anyhow::Error) -> anyhow::Result<()> {
        Ok(())
    }

    /// Allow the subscriber to do some sort of action once every render cycle.
    async fn tick(&mut self, _tick: &Tick) -> anyhow::Result<()> {
        Ok(())
    }

    /// No more events. Close files, flush buffers etc.
    async fn exit(&mut self) -> anyhow::Result<()> {
        Ok(())
    }

    fn as_error_observer(&self) -> Option<&dyn ErrorObserver> {
        None
    }
}


/*
 * 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::sync::Arc;

use async_trait::async_trait;
use buck2_cli_proto::CommandResult;
use buck2_data::buck_event;
use buck2_data::InstantEvent;
use buck2_data::SpanEndEvent;
use buck2_data::SpanStartEvent;
use buck2_event_observer::unpack_event::VisitorError;
use buck2_events::BuckEvent;

use crate::subscribers::observer::ErrorObserver;
use crate::subscribers::subscriber::EventSubscriber;
use crate::subscribers::subscriber::Tick;

/// Wrap an `UnpackingEventSubscriber` instance to provide an `EventSubscriber`.
pub(crate) struct UnpackingEventSubscriberAsEventSubscriber<U: UnpackingEventSubscriber>(
    pub(crate) U,
);

#[async_trait]
pub trait UnpackingEventSubscriber: Send {
    async fn handle_output(&mut self, _raw_output: &[u8]) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_stderr(&mut self, _stderr: &str) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_console_interaction(&mut self, _c: char) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_event(&mut self, event: &Arc<BuckEvent>) -> anyhow::Result<()> {
        self.handle_inner_event(event).await
    }
    async fn handle_inner_event(&mut self, event: &Arc<BuckEvent>) -> anyhow::Result<()> {
        match event.data() {
            buck_event::Data::SpanStart(ref start) => self.handle_event_start(start, event),
            buck_event::Data::SpanEnd(ref end) => self.handle_event_end(end, event),
            buck_event::Data::Instant(ref instant) => self.handle_instant(instant, event),
            // Not present in the event stream from the daemon to CLI.
            buck_event::Data::Record(_) => Box::pin(async { Ok(()) }),
        }
        .await
    }
    async fn handle_command_result(&mut self, _result: &CommandResult) -> anyhow::Result<()> {
        Ok(())
    }

    async fn exit(&mut self) -> anyhow::Result<()> {
        Ok(())
    }

    async fn handle_event_start(
        &mut self,
        start: &SpanStartEvent,
        event: &Arc<BuckEvent>,
    ) -> anyhow::Result<()> {
        match start
            .data
            .as_ref()
            .ok_or_else(|| VisitorError::MissingField((**event).clone()))?
        {
            buck2_data::span_start_event::Data::Command(command) => {
                self.handle_command_start(command, event).await
            }
            _ => Ok(()),
        }
    }

    async fn handle_event_end(
        &mut self,
        end: &SpanEndEvent,
        event: &Arc<BuckEvent>,
    ) -> anyhow::Result<()> {
        match end
            .data
            .as_ref()
            .ok_or_else(|| VisitorError::MissingField((**event).clone()))?
        {
            buck2_data::span_end_event::Data::Command(command) => {
                self.handle_command_end(command, event).await
            }
            buck2_data::span_end_event::Data::ActionExecution(action) => {
                self.handle_action_execution_end(action, event).await
            }
            buck2_data::span_end_event::Data::FileWatcher(file_watcher) => {
                self.handle_file_watcher_end(file_watcher, event).await
            }
            _ => Ok(()),
        }
    }

    async fn handle_instant(
        &mut self,
        instant: &InstantEvent,
        event: &Arc<BuckEvent>,
    ) -> anyhow::Result<()> {
        match instant
            .data
            .as_ref()
            .ok_or_else(|| VisitorError::MissingField((**event).clone()))?
        {
            buck2_data::instant_event::Data::ConsoleMessage(message) => {
                self.handle_console_message(message, event).await
            }
            buck2_data::instant_event::Data::ReSession(session) => {
                self.handle_re_session_created(session, event).await
            }
            buck2_data::instant_event::Data::StructuredError(err) => {
                self.handle_structured_error(err, event).await
            }
            buck2_data::instant_event::Data::TestDiscovery(discovery) => {
                self.handle_test_discovery(discovery, event).await
            }
            buck2_data::instant_event::Data::TestResult(result) => {
                self.handle_test_result(result, event).await
            }
            buck2_data::instant_event::Data::ConsolePreferences(preferences) => {
                self.handle_console_preferences(preferences, event).await
            }
            buck2_data::instant_event::Data::DebugAdapterSnapshot(snapshot) => {
                self.handle_debug_adapter_snapshot(snapshot).await
            }
            _ => Ok(()),
        }
    }

    async fn handle_command_start(
        &mut self,
        _command: &buck2_data::CommandStart,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_command_end(
        &mut self,
        _command: &buck2_data::CommandEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_action_execution_end(
        &mut self,
        _action: &buck2_data::ActionExecutionEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_file_watcher_end(
        &mut self,
        _watchman: &buck2_data::FileWatcherEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_console_message(
        &mut self,
        _message: &buck2_data::ConsoleMessage,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_re_session_created(
        &mut self,
        _session: &buck2_data::RemoteExecutionSessionCreated,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        Ok(())
    }
    async fn handle_structured_error(
        &mut self,
        _err: &buck2_data::StructuredError,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_test_discovery(
        &mut self,
        _test_info: &buck2_data::TestDiscovery,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_test_result(
        &mut self,
        _result: &buck2_data::TestResult,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_console_preferences(
        &mut self,
        _prefs: &buck2_data::ConsolePreferences,
        _event: &BuckEvent,
    ) -> anyhow::Result<()>;

    async fn handle_debug_adapter_snapshot(
        &mut self,
        _msg: &buck2_data::DebugAdapterSnapshot,
    ) -> anyhow::Result<()> {
        Ok(())
    }

    /// Give the subscriber a chance to react to errors as we start trying to clean up.
    /// They may return another error, which will be incorporated into the end result.
    async fn handle_error(&mut self, _error: &anyhow::Error) -> anyhow::Result<()>;

    /// Allow the subscriber to do some sort of action once every render cycle.
    async fn tick(&mut self, _tick: &Tick) -> anyhow::Result<()>;

    fn as_error_observer(&self) -> Option<&dyn ErrorObserver> {
        None
    }
}

#[async_trait]
impl<U: UnpackingEventSubscriber> EventSubscriber for UnpackingEventSubscriberAsEventSubscriber<U> {
    async fn handle_output(&mut self, raw_output: &[u8]) -> anyhow::Result<()> {
        self.0.handle_output(raw_output).await
    }

    async fn handle_tailer_stderr(&mut self, stderr: &str) -> anyhow::Result<()> {
        self.0.handle_stderr(stderr).await
    }

    async fn handle_console_interaction(&mut self, c: char) -> anyhow::Result<()> {
        self.0.handle_console_interaction(c).await
    }

    async fn handle_events(&mut self, events: &[Arc<BuckEvent>]) -> anyhow::Result<()> {
        for event in events {
            self.0.handle_event(event).await?;
        }
        Ok(())
    }

    async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
        self.0.handle_error(error).await
    }

    async fn tick(&mut self, tick: &Tick) -> anyhow::Result<()> {
        self.0.tick(tick).await
    }

    async fn handle_command_result(
        &mut self,
        result: &buck2_cli_proto::CommandResult,
    ) -> anyhow::Result<()> {
        self.0.handle_command_result(result).await
    }

    async fn exit(&mut self) -> anyhow::Result<()> {
        self.0.exit().await
    }

    fn as_error_observer(&self) -> Option<&dyn ErrorObserver> {
        self.0.as_error_observer()
    }
}


/*
 * 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::borrow::Cow;
use std::fmt::Debug;
use std::io::Write;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Context as _;
use async_trait::async_trait;
use buck2_core::fs::paths::file_name::FileNameBuf;
use buck2_data::CommandExecutionDetails;
use buck2_event_observer::display;
use buck2_event_observer::display::display_file_watcher_end;
use buck2_event_observer::display::TargetDisplayOptions;
use buck2_event_observer::event_observer::DebugEventObserverExtra;
use buck2_event_observer::session_info::SessionInfo;
use buck2_event_observer::verbosity::Verbosity;
use buck2_event_observer::what_ran;
use buck2_event_observer::what_ran::command_to_string;
use buck2_event_observer::what_ran::worker_command_as_fallback_to_string;
use buck2_event_observer::what_ran::WhatRanOptions;
use buck2_events::BuckEvent;
use buck2_wrapper_common::invocation_id::TraceId;
use dupe::Dupe;
use gazebo::prelude::*;
use superconsole::components::DrawVertical;
use superconsole::style::Attribute;
use superconsole::style::Color;
use superconsole::style::ContentStyle;
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;
pub(crate) use superconsole::SuperConsole;

use crate::subscribers::simpleconsole::SimpleConsole;
use crate::subscribers::subscriber::Tick;
use crate::subscribers::subscriber_unpack::UnpackingEventSubscriber;
use crate::subscribers::superconsole::commands::CommandsComponent;
use crate::subscribers::superconsole::debug_events::DebugEventsComponent;
use crate::subscribers::superconsole::debugger::StarlarkDebuggerComponent;
use crate::subscribers::superconsole::dice::DiceComponent;
use crate::subscribers::superconsole::io::IoHeader;
use crate::subscribers::superconsole::re::ReHeader;
use crate::subscribers::superconsole::session_info::SessionInfoComponent;
use crate::subscribers::superconsole::test::TestHeader;
use crate::subscribers::superconsole::timed_list::Cutoffs;
use crate::subscribers::superconsole::timed_list::TimedList;

mod commands;
mod common;
pub(crate) mod debug_events;
mod debugger;
pub(crate) mod dice;
mod io;
mod re;
pub mod session_info;
pub mod test;
pub mod timed_list;

const SUPERCONSOLE_WIDTH: usize = 150;

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

pub struct StatefulSuperConsole {
    header: String,
    state: SuperConsoleState,
    super_console: Option<SuperConsole>,
    verbosity: Verbosity,
}

#[derive(Copy, Clone, Dupe, Debug)]
struct TimeSpeed {
    speed: f64,
}

const TIMESPEED_DEFAULT: f64 = 1.0;

impl TimeSpeed {
    pub(crate) fn new(speed_value: Option<f64>) -> anyhow::Result<Self> {
        let speed = speed_value.unwrap_or(TIMESPEED_DEFAULT);

        if speed <= 0.0 {
            return Err(anyhow::anyhow!("Time speed cannot be negative!"));
        }
        Ok(TimeSpeed { speed })
    }

    pub(crate) fn speed(self) -> f64 {
        self.speed
    }
}

pub struct SuperConsoleState {
    pub current_tick: Tick,
    time_speed: TimeSpeed,
    /// This contains the SpanTracker, which is why it's part of the SuperConsoleState.
    simple_console: SimpleConsole<DebugEventObserverExtra>,
    config: SuperConsoleConfig,
}

#[derive(Clone)]
pub struct SuperConsoleConfig {
    pub enable_dice: bool,
    pub enable_debug_events: bool,
    pub enable_detailed_re: bool,
    pub enable_io: bool,
    pub enable_commands: bool,
    pub display_platform: bool,
    /// Two lines for root events with single child event.
    pub two_lines: bool,
    pub max_lines: usize,
}

impl Default for SuperConsoleConfig {
    fn default() -> Self {
        Self {
            enable_dice: false,
            enable_debug_events: false,
            enable_detailed_re: false,
            enable_io: false,
            enable_commands: false,
            display_platform: false,
            two_lines: false,
            max_lines: 10,
        }
    }
}

struct BuckRootComponent<'s> {
    header: &'s str,
    state: &'s SuperConsoleState,
}

impl<'s> Component for BuckRootComponent<'s> {
    fn draw_unchecked(&self, dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
        // bound all components to our recommended grapheme-width
        let dimensions = dimensions.intersect(Dimensions {
            width: SUPERCONSOLE_WIDTH,
            height: usize::MAX,
        });

        let mut draw = DrawVertical::new(dimensions);
        draw.draw(
            &SessionInfoComponent {
                session_info: self.state.session_info(),
            },
            mode,
        )?;
        draw.draw(
            &ReHeader {
                super_console_config: &self.state.config,
                re_state: self.state.simple_console.observer.re_state(),
            },
            mode,
        )?;
        draw.draw(
            &IoHeader {
                super_console_config: &self.state.config,
                io_state: self.state.simple_console.observer.io_state(),
            },
            mode,
        )?;
        draw.draw(
            &TestHeader {
                session_info: self.state.session_info(),
                test_state: self.state.simple_console.observer.test_state(),
            },
            mode,
        )?;
        draw.draw(
            &DebugEventsComponent {
                super_console_config: &self.state.config,
                debug_events_state: self.state.simple_console.observer.extra().debug_events(),
            },
            mode,
        )?;
        draw.draw(
            &DiceComponent {
                super_console_config: &self.state.config,
                dice_state: self.state.simple_console.observer.extra().dice_state(),
            },
            mode,
        )?;
        draw.draw(
            &StarlarkDebuggerComponent {
                starlark_debugger_state: self
                    .state
                    .simple_console
                    .observer
                    .starlark_debugger_state(),
            },
            mode,
        )?;
        draw.draw(
            &CommandsComponent {
                super_console_config: &self.state.config,
                action_stats: self.state.simple_console.observer.action_stats(),
            },
            mode,
        )?;
        draw.draw(&TimedList::new(&CUTOFFS, self.header, self.state), mode)?;

        Ok(draw.finish())
    }
}

impl StatefulSuperConsole {
    pub const FALLBACK_SIZE: Dimensions = Dimensions {
        width: 100,
        height: 40,
    };

    pub(crate) fn new_with_root_forced(
        trace_id: TraceId,
        command_name: &str,
        verbosity: Verbosity,
        show_waiting_message: bool,
        replay_speed: Option<f64>,
        stream: Option<Box<dyn Write + Send + 'static + Sync>>,
        config: SuperConsoleConfig,
        isolation_dir: FileNameBuf,
    ) -> anyhow::Result<Self> {
        let mut builder = Self::console_builder();
        if let Some(stream) = stream {
            builder.write_to(stream);
        }
        Self::new(
            command_name,
            trace_id,
            builder.build_forced(Self::FALLBACK_SIZE)?,
            verbosity,
            show_waiting_message,
            replay_speed,
            config,
            isolation_dir,
        )
    }

    pub(crate) fn new_with_root(
        trace_id: TraceId,
        command_name: &str,
        verbosity: Verbosity,
        show_waiting_message: bool,
        replay_speed: Option<f64>,
        config: SuperConsoleConfig,
        isolation_dir: FileNameBuf,
    ) -> anyhow::Result<Option<Self>> {
        match Self::console_builder().build()? {
            None => Ok(None),
            Some(sc) => Ok(Some(Self::new(
                command_name,
                trace_id,
                sc,
                verbosity,
                show_waiting_message,
                replay_speed,
                config,
                isolation_dir,
            )?)),
        }
    }

    pub(crate) fn new(
        command_name: &str,
        trace_id: TraceId,
        super_console: SuperConsole,
        verbosity: Verbosity,
        show_waiting_message: bool,
        replay_speed: Option<f64>,
        config: SuperConsoleConfig,
        isolation_dir: FileNameBuf,
    ) -> anyhow::Result<Self> {
        let header = format!("Command: {}.", command_name);
        Ok(Self {
            header,
            state: SuperConsoleState::new(
                replay_speed,
                trace_id,
                isolation_dir,
                verbosity,
                show_waiting_message,
                config,
            )?,
            super_console: Some(super_console),
            verbosity,
        })
    }

    /// Construct a console suitable for use by the Buck2 CLI. We use non-blocking output here
    /// because we do all our event processing on a single thread, so that if stderr is blocked
    /// (e.g.  because the client is using a resumable remote terminal and they've temporarily
    /// disconnected), we don't delay ingesting new events.
    ///
    /// This ensures we a) don't have to catch up when the client reconnects, b) don't buffer
    /// events (though we might buffer output), and c) can show an accurate "time elapsed" when the
    /// client returns (if we wait until the client returns to resume, we'll always report that the
    /// command finished "just now", because we'll have some events to catch up on).
    pub fn console_builder() -> ::superconsole::Builder {
        let mut builder = ::superconsole::Builder::new();
        builder.non_blocking();
        builder
    }

    pub fn render_result_errors(result: &buck2_cli_proto::CommandResult) -> Lines {
        let mut lines = Lines::new();
        if let buck2_cli_proto::CommandResult {
            result: Some(buck2_cli_proto::command_result::Result::Error(e)),
        } = result
        {
            let style = ContentStyle {
                foreground_color: Some(Color::DarkRed),
                ..Default::default()
            };
            for message in &e.messages {
                lines
                    .0
                    .extend(Lines::from_multiline_string(message, style).0);
            }
        }
        lines
    }
}

impl SuperConsoleState {
    pub fn new(
        replay_speed: Option<f64>,
        trace_id: TraceId,
        isolation_dir: FileNameBuf,
        verbosity: Verbosity,
        show_waiting_message: bool,
        config: SuperConsoleConfig,
    ) -> anyhow::Result<SuperConsoleState> {
        Ok(SuperConsoleState {
            current_tick: Tick::now(),
            time_speed: TimeSpeed::new(replay_speed)?,
            simple_console: SimpleConsole::with_tty(
                trace_id,
                isolation_dir,
                verbosity,
                show_waiting_message,
            ),
            config,
        })
    }

    pub fn update_event_observer(&mut self, event: &Arc<BuckEvent>) -> anyhow::Result<()> {
        self.simple_console.update_event_observer(event)
    }

    pub fn session_info(&self) -> &SessionInfo {
        self.simple_console.observer.session_info()
    }
}

impl StatefulSuperConsole {
    async fn toggle(
        &mut self,
        what: &str,
        key: char,
        var: impl FnOnce(&mut Self) -> &mut bool,
    ) -> anyhow::Result<()> {
        let var = var(self);
        *var = !*var;
        let on_off = match *var {
            true => "on",
            false => "off",
        };
        self.handle_stderr(&format!("{what}: {on_off}, press `{key}` to revert"))
            .await
    }
}

// TODO(brasselsprouts): after deprecating filetailers, simplify these code paths
#[async_trait]
impl UnpackingEventSubscriber for StatefulSuperConsole {
    async fn handle_event(&mut self, event: &Arc<BuckEvent>) -> anyhow::Result<()> {
        match &mut self.super_console {
            Some(_) => {
                self.handle_inner_event(event)
                    .await
                    .with_context(|| display::InvalidBuckEvent(event.clone()))?;
                self.state.update_event_observer(event)?;
            }
            None => {
                self.state.simple_console.handle_event(event).await?;
            }
        }

        if self.verbosity.print_all_commands() {
            // This is a bit messy. It would be better for this to go in the branch above, but we
            // can't do that, because we call a method on `self` in a branch that takes a mutable
            // borrow of the SuperConsole there. That works *only* if we don't use the console we
            // borrowed.
            if let Some(console) = &mut self.super_console {
                what_ran::emit_event_if_relevant(
                    event.parent_id().into(),
                    event.data(),
                    self.state.simple_console.observer().spans(),
                    console,
                    &WhatRanOptions::default(),
                )?;
            }
        }

        Ok(())
    }

    async fn handle_stderr(&mut self, msg: &str) -> anyhow::Result<()> {
        match &mut self.super_console {
            Some(super_console) => {
                super_console.emit(msg.lines().map(Line::sanitized).collect());
                Ok(())
            }
            None => self.state.simple_console.handle_stderr(msg).await,
        }
    }

    async fn handle_structured_error(
        &mut self,
        err: &buck2_data::StructuredError,
        event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if err.quiet {
            return Ok(());
        }
        match &mut self.super_console {
            Some(super_console) => {
                super_console.emit(
                    err.payload
                        .lines()
                        .map(|line| {
                            Line::from_iter([Span::new_colored_lossy(line, Color::DarkYellow)])
                        })
                        .collect(),
                );
                Ok(())
            }
            None => {
                self.state
                    .simple_console
                    .handle_structured_error(err, event)
                    .await
            }
        }
    }

    async fn handle_file_watcher_end(
        &mut self,
        file_watcher: &buck2_data::FileWatcherEnd,
        event: &BuckEvent,
    ) -> anyhow::Result<()> {
        match &mut self.super_console {
            Some(super_console) => {
                if self.verbosity.print_status() {
                    super_console.emit(Lines(
                        display_file_watcher_end(file_watcher).into_map(|x| Line::sanitized(&x)),
                    ));
                }
                Ok(())
            }
            None => {
                self.state
                    .simple_console
                    .handle_file_watcher_end(file_watcher, event)
                    .await
            }
        }
    }

    async fn handle_output(&mut self, raw_output: &[u8]) -> anyhow::Result<()> {
        if let Some(super_console) = self.super_console.take() {
            super_console.finalize(&BuckRootComponent {
                header: &self.header,
                state: &self.state,
            })?;
        }

        self.state.simple_console.handle_output(raw_output).await
    }

    async fn handle_console_interaction(&mut self, c: char) -> anyhow::Result<()> {
        if c == 'd' {
            self.toggle("DICE component", 'd', |s| &mut s.state.config.enable_dice)
                .await?;
        } else if c == 'e' {
            self.toggle("Debug events component", 'e', |s| {
                &mut s.state.config.enable_debug_events
            })
            .await?;
        } else if c == '2' {
            self.toggle("Two lines mode", '2', |s| &mut s.state.config.two_lines)
                .await?;
        } else if c == 'r' {
            self.toggle("Detailed RE", 'r', |s| {
                &mut s.state.config.enable_detailed_re
            })
            .await?;
        } else if c == 'i' {
            self.toggle("I/O counters", 'i', |s| &mut s.state.config.enable_io)
                .await?;
        } else if c == 'p' {
            self.toggle("Display target configurations", 'p', |s| {
                &mut s.state.config.display_platform
            })
            .await?;
        } else if c == 'c' {
            self.toggle("Commands", 'c', |s| &mut s.state.config.enable_commands)
                .await?;
        } else if c == '+' {
            self.state.config.max_lines = self.state.config.max_lines.saturating_add(1);
        } else if c == '-' {
            self.state.config.max_lines = self.state.config.max_lines.saturating_sub(1);
        } else if c == '?' || c == 'h' {
            self.handle_stderr(
                "Help:\n\
                `d` = toggle DICE\n\
                `e` = toggle debug events\n\
                `2` = toggle two lines mode\n\
                `r` = toggle detailed RE\n\
                `i` = toggle I/O counters\n\
                `p` = display target configurations\n\
                `+` = show more lines\n\
                `-` = show fewer lines\n\
                `h` = show this help",
            )
            .await?;
        }

        Ok(())
    }

    async fn handle_command_result(
        &mut self,
        result: &buck2_cli_proto::CommandResult,
    ) -> anyhow::Result<()> {
        match self.super_console.take() {
            Some(mut super_console) => {
                let lines = Self::render_result_errors(result);
                super_console.emit(lines);
                super_console.finalize(&BuckRootComponent {
                    header: &self.header,
                    state: &self.state,
                })
            }
            None => {
                self.state
                    .simple_console
                    .handle_command_result(result)
                    .await
            }
        }
    }

    async fn tick(&mut self, tick: &Tick) -> anyhow::Result<()> {
        self.state.simple_console.detect_hangs().await?;
        match &mut self.super_console {
            Some(super_console) => {
                self.state.current_tick = tick.dupe();
                super_console.render(&BuckRootComponent {
                    header: &self.header,
                    state: &self.state,
                })
            }
            None => Ok(()),
        }
    }

    async fn handle_error(&mut self, _error: &anyhow::Error) -> anyhow::Result<()> {
        match self.super_console.take() {
            Some(super_console) => super_console.finalize(&BuckRootComponent {
                header: &self.header,
                state: &self.state,
            }),
            None => Ok(()),
        }
    }

    async fn handle_console_message(
        &mut self,
        message: &buck2_data::ConsoleMessage,
        event: &BuckEvent,
    ) -> anyhow::Result<()> {
        // TODO(nmj): Maybe better handling of messages that have color data in them. Right now
        //            they're just stripped
        match &mut self.super_console {
            Some(super_console) => {
                super_console.emit(Lines::from_multiline_string(
                    &message.message,
                    ContentStyle::default(),
                ));
                Ok(())
            }
            None => {
                self.state
                    .simple_console
                    .handle_console_message(message, event)
                    .await
            }
        }
    }

    async fn handle_action_execution_end(
        &mut self,
        action: &buck2_data::ActionExecutionEnd,
        event: &BuckEvent,
    ) -> anyhow::Result<()> {
        let super_console = match &mut self.super_console {
            Some(super_console) => super_console,
            None => {
                return self
                    .state
                    .simple_console
                    .handle_action_execution_end(action, event)
                    .await;
            }
        };

        let mut lines = vec![];
        let display_platform = self.state.config.display_platform;
        match action.error.as_ref() {
            Some(error) => {
                let display::ActionErrorDisplay {
                    action_id,
                    reason,
                    command,
                } = display::display_action_error(
                    action,
                    error,
                    TargetDisplayOptions::for_console(display_platform),
                )?;

                lines.push(Line::from_iter([Span::new_styled_lossy(
                    StyledContent::new(
                        ContentStyle {
                            foreground_color: Some(Color::White),
                            attributes: Attribute::Bold.into(),
                            ..Default::default()
                        },
                        format!("Action failed: {}", action_id,),
                    ),
                )]));

                lines.push(Line::from_iter([Span::new_styled_lossy(
                    reason.with(Color::DarkRed),
                )]));

                if let Some(command) = command {
                    lines_for_command_details(&command, self.verbosity, &mut lines);
                }
            }
            None => {
                if let Some(stderr) = display::success_stderr(action, self.verbosity)? {
                    let action_id = StyledContent::new(
                        ContentStyle {
                            foreground_color: Some(Color::White),
                            attributes: Attribute::Bold.into(),
                            ..Default::default()
                        },
                        format!(
                            "stderr for {}:",
                            display::display_action_identity(
                                action.key.as_ref(),
                                action.name.as_ref(),
                                TargetDisplayOptions::for_console(display_platform),
                            )?
                        ),
                    );
                    lines.push(Line::from_iter([Span::new_styled_lossy(action_id)]));
                    lines.extend(Lines::from_colored_multiline_string(stderr));
                }
            }
        }

        super_console.emit(Lines(lines));

        Ok(())
    }

    async fn handle_test_result(
        &mut self,
        result: &buck2_data::TestResult,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        if let Some(super_console) = &mut self.super_console {
            if let Some(msg) = display::format_test_result(result)? {
                super_console.emit(msg);
            }
        }

        Ok(())
    }

    async fn handle_console_preferences(
        &mut self,
        prefs: &buck2_data::ConsolePreferences,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        self.state.config.max_lines = prefs.max_lines.try_into()?;

        Ok(())
    }

    // Our state snapshot handles those for us.

    async fn handle_command_start(
        &mut self,
        _command: &buck2_data::CommandStart,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        Ok(())
    }

    async fn handle_command_end(
        &mut self,
        _command: &buck2_data::CommandEnd,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        Ok(())
    }

    async fn handle_test_discovery(
        &mut self,
        _test_info: &buck2_data::TestDiscovery,
        _event: &BuckEvent,
    ) -> anyhow::Result<()> {
        Ok(())
    }
}

fn lines_for_command_details(
    command_failed: &CommandExecutionDetails,
    verbosity: Verbosity,
    lines: &mut Vec<Line>,
) {
    use buck2_data::command_execution_details::Command;

    match command_failed.command.as_ref() {
        Some(Command::LocalCommand(local_command)) => {
            let command = command_to_string(local_command);
            let command = command.as_str();
            let command = if verbosity.print_failure_full_command() {
                Cow::Borrowed(command)
            } else {
                match truncate(command) {
                    None => Cow::Borrowed(command),
                    Some(short) => Cow::Owned(format!(
                        "{} (run `buck2 log what-failed` to get the full command)",
                        short
                    )),
                }
            };

            lines.push(Line::from_iter([Span::new_styled_lossy(
                format!("Reproduce locally: `{}`", command).with(Color::DarkRed),
            )]));
        }
        Some(Command::RemoteCommand(remote_command)) => {
            if !buck2_core::is_open_source() {
                lines.push(Line::from_iter([Span::new_styled_lossy(
                    format!(
                        "Reproduce locally: `frecli cas download-action {}`",
                        remote_command.action_digest
                    )
                    .with(Color::DarkRed),
                )]));
            }
        }
        Some(Command::OmittedLocalCommand(..)) | None => {
            // Nothing to show in this case.
        }
        Some(Command::WorkerInitCommand(worker_init_command)) => {
            let command = command_to_string(worker_init_command);
            let command = command.as_str();
            let command = if verbosity.print_failure_full_command() {
                Cow::Borrowed(command)
            } else {
                match truncate(command) {
                    None => Cow::Borrowed(command),
                    Some(short) => Cow::Owned(format!(
                        "{} (run `buck2 log what-failed` to get the full command)",
                        short
                    )),
                }
            };

            lines.push(Line::from_iter([Span::new_styled_lossy(
                format!("Reproduce locally: `{}`", command).with(Color::DarkRed),
            )]));
        }
        Some(Command::WorkerCommand(worker_command)) => {
            let command = worker_command_as_fallback_to_string(worker_command);
            let command = command.as_str();
            let command = if verbosity.print_failure_full_command() {
                Cow::Borrowed(command)
            } else {
                match truncate(command) {
                    None => Cow::Borrowed(command),
                    Some(short) => Cow::Owned(format!(
                        "{} (run `buck2 log what-failed` to get the full command)",
                        short
                    )),
                }
            };

            lines.push(Line::from_iter([Span::new_styled_lossy(
                format!("Reproduce locally: `{}`", command).with(Color::DarkRed),
            )]));
        }
    };

    lines.push(Line::from_iter([Span::new_styled_lossy(
        "stdout:"
            .to_owned()
            .with(Color::DarkRed)
            .attribute(Attribute::Bold),
    )]));
    lines.extend(Lines::from_multiline_string(
        &command_failed.stdout,
        color(Color::DarkRed),
    ));
    lines.push(Line::from_iter([Span::new_styled_lossy(
        "stderr:"
            .to_owned()
            .with(Color::DarkRed)
            .attribute(Attribute::Bold),
    )]));
    lines.extend(Lines::from_colored_multiline_string(&command_failed.stderr));
}

// Truncates a string to a reasonable number characters, or returns None if it doesn't need truncating.
fn truncate(contents: &str) -> Option<String> {
    const MAX_LENGTH: usize = 200;
    const BUFFER: usize = " ...<omitted>... ".len();
    if contents.len() > MAX_LENGTH + BUFFER {
        Some(format!(
            "{} ...<omitted>... {}",
            &contents[0..MAX_LENGTH / 2],
            &contents[contents.len() - MAX_LENGTH / 2..contents.len()]
        ))
    } else {
        None
    }
}

fn color(color: Color) -> ContentStyle {
    ContentStyle {
        foreground_color: Some(color),
        ..Default::default()
    }
}

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

    use buck2_cli_proto::CommandResult;
    use buck2_cli_proto::GenericResponse;
    use buck2_data::LoadBuildFileEnd;
    use buck2_data::LoadBuildFileStart;
    use buck2_data::SpanEndEvent;
    use buck2_data::SpanStartEvent;
    use buck2_events::span::SpanId;
    use buck2_wrapper_common::invocation_id::TraceId;
    use superconsole::testing::frame_contains;
    use superconsole::testing::test_console;
    use superconsole::testing::SuperConsoleTestingExt;

    use super::*;

    #[tokio::test]
    async fn test_transfer_state_to_simpleconsole() {
        let trace_id = TraceId::new();
        let mut console = StatefulSuperConsole::new_with_root_forced(
            trace_id.dupe(),
            "test",
            Verbosity::Default,
            true,
            None,
            None,
            Default::default(),
            FileNameBuf::unchecked_new("placeholder"),
        )
        .unwrap();

        // start a new event.
        let id = SpanId::new();
        let event = Arc::new(BuckEvent::new(
            SystemTime::now(),
            trace_id,
            Some(id),
            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(),
                    },
                )),
            }),
        ));
        console.handle_event(&event).await.unwrap();

        // drop into simple console
        console
            .handle_command_result(&CommandResult {
                result: Some(buck2_cli_proto::command_result::Result::GenericResponse(
                    GenericResponse {},
                )),
            })
            .await
            .unwrap();

        // finish the event from before
        // expect to successfully close event.
        let event = Arc::new(BuckEvent::new(
            SystemTime::now(),
            TraceId::new(),
            Some(id),
            None,
            buck2_data::buck_event::Data::SpanEnd(SpanEndEvent {
                data: Some(buck2_data::span_end_event::Data::Load(LoadBuildFileEnd {
                    module_id: "foo".to_owned(),
                    cell: "bar".to_owned(),
                    error: None,
                })),
                stats: None,
                duration: None,
            }),
        ));
        assert!(console.handle_event(&event).await.is_ok());
    }

    #[tokio::test]
    async fn test_default_layout() -> anyhow::Result<()> {
        let trace_id = TraceId::new();
        let now = SystemTime::now();
        let tick = Tick::now();

        let mut console = StatefulSuperConsole::new(
            "build",
            trace_id.dupe(),
            test_console(),
            Verbosity::Default,
            true,
            Default::default(),
            Default::default(),
            FileNameBuf::unchecked_new("placeholder"),
        )?;

        console
            .handle_event(&Arc::new(BuckEvent::new(
                now,
                trace_id.dupe(),
                Some(SpanId::new()),
                None,
                buck2_data::buck_event::Data::SpanStart(SpanStartEvent {
                    data: Some(
                        buck2_data::CommandStart {
                            metadata: Default::default(),
                            data: Some(buck2_data::BuildCommandStart {}.into()),
                        }
                        .into(),
                    ),
                }),
            )))
            .await?;

        console
            .handle_event(&Arc::new(BuckEvent::new(
                now,
                trace_id.dupe(),
                None,
                None,
                buck2_data::InstantEvent {
                    data: Some(
                        buck2_data::RemoteExecutionSessionCreated {
                            session_id: "reSessionID-123".to_owned(),
                            experiment_name: "".to_owned(),
                        }
                        .into(),
                    ),
                }
                .into(),
            )))
            .await?;

        console
            .handle_event(&Arc::new(BuckEvent::new(
                now,
                trace_id.dupe(),
                Some(SpanId::new()),
                None,
                SpanStartEvent {
                    data: Some(
                        LoadBuildFileStart {
                            module_id: "foo".to_owned(),
                            cell: "bar".to_owned(),
                        }
                        .into(),
                    ),
                }
                .into(),
            )))
            .await?;

        console.tick(&tick).await?;

        let frame = console
            .super_console
            .as_mut()
            .context("Console was downgraded")?
            .test_output_mut()?
            .frames
            .pop()
            .context("No frame was emitted")?;

        // Verify we have the right output on intermediate frames
        if cfg!(fbcode_build) {
            assert!(frame_contains(&frame, "Buck UI:"));
        } else {
            assert!(frame_contains(&frame, "Build ID:"));
        }
        assert!(frame_contains(&frame, "Network:"));
        assert!(frame_contains(&frame, "(reSessionID-123)"));
        assert!(frame_contains(&frame, "Remaining"));

        console
            .handle_command_result(&buck2_cli_proto::CommandResult { result: None })
            .await?;

        Ok(())
    }

    #[test]
    fn test_session_info() -> anyhow::Result<()> {
        let info = SessionInfo {
            trace_id: TraceId::null(),
            test_session: Some(buck2_data::TestSessionInfo {
                info: (0..100).map(|_| "a").collect(),
            }),
        };

        let full = SessionInfoComponent {
            session_info: &info,
        }
        .draw_unchecked(
            Dimensions {
                // Enough to print everything on one line (we need 109 in fbcode and 110 in OSS)
                width: 110,
                height: 1,
            },
            DrawMode::Normal,
        )?;

        assert_eq!(full.len(), 2);

        let multiline = SessionInfoComponent {
            session_info: &info,
        }
        .draw_unchecked(
            Dimensions {
                // Just long enough to print each on one line.
                width: 100,
                height: 1,
            },
            DrawMode::Normal,
        )?;

        assert_eq!(multiline.len(), 4);

        let too_small = SessionInfoComponent {
            session_info: &info,
        }
        .draw_unchecked(
            Dimensions {
                width: 1,
                height: 1,
            },
            DrawMode::Normal,
        )?;

        assert_eq!(too_small.len(), 1);

        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 anyhow::Context;
use buck2_core::fs::fs_util;
use buck2_core::fs::paths::abs_norm_path::AbsNormPath;
use buck2_core::fs::paths::abs_norm_path::AbsNormPathBuf;
use buck2_core::fs::paths::file_name::FileNameBuf;
use buck2_events::BuckEvent;
use buck2_wrapper_common::invocation_id::TraceId;
use chrono::DateTime;
use chrono::Utc;
use futures::StreamExt;
use gazebo::prelude::VecExt;

use crate::client_ctx::ClientCommandContext;
use crate::subscribers::event_log::read::EventLogPathBuf;
use crate::subscribers::event_log::utils::Encoding;
use crate::subscribers::event_log::utils::EventLogErrors;

pub(crate) fn get_logfile_name(
    event: &BuckEvent,
    encoding: Encoding,
    command_name: &str,
) -> anyhow::Result<FileNameBuf> {
    let time_str = {
        let datetime: DateTime<Utc> = event.timestamp().into();
        datetime.format("%Y%m%d-%H%M%S").to_string()
    };

    let trace_id = event.trace_id()?;
    let extension = encoding.extensions[0];

    // Sort order matters here: earliest builds are lexicographically first and deleted first.
    FileNameBuf::try_from(format!(
        "{time_str}_{command_name}_{trace_id}_events{extension}"
    ))
}

pub(crate) async fn remove_old_logs(logdir: &AbsNormPath) {
    const N_LOGS_RETAINED: usize = 10;

    if let Ok(logfiles) = get_files_in_log_dir(logdir) {
        futures::stream::iter(logfiles.into_iter().rev().skip(N_LOGS_RETAINED - 1))
            .then(async move |file| {
                // The oldest logs might be open from another concurrent build, so suppress error.
                tokio::fs::remove_file(file).await.ok()
            })
            .collect::<Vec<_>>()
            .await;
    }
}

/// List files in logdir, ordered from oldest to newest.
fn get_files_in_log_dir(logdir: &AbsNormPath) -> anyhow::Result<Vec<AbsNormPathBuf>> {
    Ok(fs_util::read_dir_if_exists(logdir)?
        .map(sort_logs)
        .unwrap_or_default())
}

/// List logs in logdir, ordered from oldest to newest.
pub fn get_local_logs(logdir: &AbsNormPath) -> anyhow::Result<Vec<EventLogPathBuf>> {
    Ok(get_files_in_log_dir(logdir)?
        .into_iter()
        .filter_map(|path| EventLogPathBuf::infer(path.into_abs_path_buf()).ok())
        .collect())
}

fn sort_logs(dir: fs_util::ReadDir) -> Vec<AbsNormPathBuf> {
    let mut logfiles = dir.filter_map(Result::ok).collect::<Vec<_>>();
    logfiles.sort_by_cached_key(|file| {
        // Return Unix epoch if unable to get creation time.
        if let Ok(metadata) = file.metadata() {
            if let Ok(created) = metadata.created() {
                return (created, file.file_name());
            }
        }
        (std::time::UNIX_EPOCH, file.file_name())
    });
    logfiles.into_map(|entry| entry.path())
}

/// Find log file by trace id. Return `None` if log not found, error on other errors.
pub fn find_log_by_trace_id(
    log_dir: &AbsNormPath,
    trace_id: &TraceId,
) -> anyhow::Result<Option<EventLogPathBuf>> {
    let trace_id = trace_id.to_string();
    Ok(get_local_logs(log_dir)?.into_iter().rev().find(|log| {
        let log_name = log.path.file_name().unwrap();
        log_name.to_string_lossy().contains(&trace_id)
    }))
}

/// Find log file by trace id. Return error if log not found or on other errors.
pub fn do_find_log_by_trace_id(
    log_dir: &AbsNormPath,
    trace_id: &TraceId,
) -> anyhow::Result<EventLogPathBuf> {
    find_log_by_trace_id(log_dir, trace_id)?.context("Error finding log by trace id")
}

pub fn retrieve_nth_recent_log(
    ctx: &ClientCommandContext,
    n: usize,
) -> anyhow::Result<EventLogPathBuf> {
    let log_dir = ctx.paths().context("Error identifying log dir")?.log_dir();
    let mut logfiles = get_local_logs(&log_dir)?;
    logfiles.reverse(); // newest first
    let chosen = logfiles
        .get(n)
        .ok_or(EventLogErrors::RecentIndexOutOfBounds {
            idx: n,
            num_logfiles: logfiles.len(),
        })?;

    Ok(chosen.clone())
}

pub fn retrieve_all_logs(ctx: &ClientCommandContext) -> anyhow::Result<Vec<EventLogPathBuf>> {
    let log_dir = ctx.paths().context("Error identifying log dir")?.log_dir();
    get_local_logs(&log_dir)
}


/*
 * 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.
 */

pub mod file_names;
pub mod read;
pub mod subscriber;
pub mod user_event_types;
pub mod utils;
pub mod write;


/*
 * 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::pin::Pin;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::task::Context;
use std::task::Poll;
use std::time::SystemTime;

use anyhow::Context as _;
use async_compression::tokio::bufread::GzipDecoder;
use async_compression::tokio::bufread::ZstdDecoder;
use buck2_cli_proto::protobuf_util::ProtobufSplitter;
use buck2_cli_proto::*;
use buck2_core::fs::async_fs_util;
use buck2_core::fs::paths::abs_path::AbsPath;
use buck2_core::fs::paths::abs_path::AbsPathBuf;
use buck2_events::BuckEvent;
use buck2_wrapper_common::invocation_id::TraceId;
use futures::stream::BoxStream;
use futures::stream::Stream;
use futures::stream::TryStreamExt;
use futures::StreamExt;
use pin_project::pin_project;
use prost::Message;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncRead;
use tokio::io::BufReader;
use tokio::io::ReadBuf;
use tokio_stream::wrappers::LinesStream;
use tokio_util::codec::FramedRead;

use crate::stream_value::StreamValue;
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::EventLogInferenceError;
use crate::subscribers::event_log::utils::Invocation;
use crate::subscribers::event_log::utils::LogMode;
use crate::subscribers::event_log::utils::NoInference;
use crate::subscribers::event_log::utils::KNOWN_ENCODINGS;

type EventLogReader<'a> = Box<dyn AsyncRead + Send + Sync + Unpin + 'a>;

pub struct ReaderStats {
    compressed_bytes: AtomicUsize,
    decompressed_bytes: AtomicUsize,
}

impl ReaderStats {
    pub fn new() -> Self {
        Self {
            compressed_bytes: AtomicUsize::new(0),
            decompressed_bytes: AtomicUsize::new(0),
        }
    }

    pub fn compressed_bytes(&self) -> usize {
        self.compressed_bytes.load(Ordering::Relaxed)
    }

    pub fn decompressed_bytes(&self) -> usize {
        self.decompressed_bytes.load(Ordering::Relaxed)
    }
}

mod counting_reader {
    #![allow(clippy::ref_option_ref)] // for the projection

    use super::*;

    #[pin_project]
    pub struct CountingReader<'a, T> {
        #[pin]
        pub(super) inner: T,
        pub(super) stats: Option<&'a AtomicUsize>,
    }
}

use counting_reader::CountingReader;

impl<'a, T> CountingReader<'a, T> {
    fn new(inner: T, stats: Option<&'a AtomicUsize>) -> Self {
        Self { inner, stats }
    }
}

impl<'a, T> AsyncRead for CountingReader<'a, T>
where
    T: AsyncRead,
{
    fn poll_read(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        let before = buf.remaining();

        let this = self.project();
        futures::ready!(this.inner.poll_read(cx, buf))?;
        if let Some(stats) = this.stats.as_mut() {
            stats.fetch_add(before - buf.remaining(), Ordering::Relaxed);
        }

        Poll::Ready(Ok(()))
    }
}

#[derive(Clone)]
pub struct EventLogPathBuf {
    pub(crate) path: AbsPathBuf,
    pub(crate) encoding: Encoding,
}

pub struct EventLogSummary {
    pub trace_id: TraceId,
    pub timestamp: SystemTime,
    pub invocation: Invocation,
}

impl EventLogPathBuf {
    pub fn infer(path: AbsPathBuf) -> anyhow::Result<Self> {
        Self::infer_opt(path)?
            .map_err(|NoInference(path)| EventLogInferenceError::InvalidExtension(path).into())
    }

    pub fn path(&self) -> &AbsPath {
        &self.path
    }

    pub(crate) fn infer_opt(path: AbsPathBuf) -> anyhow::Result<Result<Self, NoInference>> {
        let name = path
            .file_name()
            .with_context(|| EventLogInferenceError::NoFilename(path.clone()))?
            .to_str()
            .with_context(|| EventLogInferenceError::InvalidFilename(path.clone()))?;

        for encoding in KNOWN_ENCODINGS {
            for extension in encoding.extensions {
                if name.ends_with(extension) {
                    return Ok(Ok(Self {
                        path,
                        encoding: *encoding,
                    }));
                }
            }
        }

        Ok(Err(NoInference(path)))
    }

    async fn unpack_stream_json<'a>(
        &self,
        stats: Option<&'a ReaderStats>,
    ) -> anyhow::Result<(Invocation, BoxStream<'a, anyhow::Result<StreamValue>>)> {
        assert_eq!(self.encoding.mode, LogMode::Json);

        let log_file = self.open(stats).await?;
        let log_file = BufReader::new(log_file);
        let mut log_lines = log_file.lines();

        // This one is not an event.
        let header = log_lines
            .next_line()
            .await
            .context("Error reading header line")?
            .context("No header line")?;
        let invocation = Invocation::parse_json_line(&header)?;

        let events = LinesStream::new(log_lines).map(|line| {
            let line = line.context("Error reading next line")?;
            serde_json::from_str::<StreamValue>(&line)
                .with_context(|| format!("Invalid line: {}", line.trim_end()))
        });

        Ok((invocation, events.boxed()))
    }

    async fn unpack_stream_protobuf<'a>(
        &self,
        stats: Option<&'a ReaderStats>,
    ) -> anyhow::Result<(Invocation, BoxStream<'a, anyhow::Result<StreamValue>>)> {
        assert_eq!(self.encoding.mode, LogMode::Protobuf);

        let log_file = self.open(stats).await?;
        let mut stream = FramedRead::new(log_file, ProtobufSplitter);

        let invocation = stream.try_next().await?.context("No invocation found")?;
        let invocation = buck2_data::Invocation::decode_length_delimited(invocation)
            .context("Invalid Invocation")?;
        let invocation = Invocation {
            command_line_args: invocation.command_line_args,
            expanded_command_line_args: invocation.expanded_command_line_args,
            working_dir: invocation.working_dir,
            trace_id: invocation
                .trace_id
                .map(|t| t.parse())
                .transpose()
                .context("Invalid TraceId")?
                .unwrap_or_else(TraceId::null),
        };

        let events = stream.and_then(|data| async move {
            let val = buck2_cli_proto::CommandProgress::decode_length_delimited(data)
                .context("Invalid CommandProgress")?;
            match val.progress {
                Some(command_progress::Progress::Event(event)) => Ok(StreamValue::Event(event)),
                Some(command_progress::Progress::Result(result)) => Ok(StreamValue::Result(result)),
                Some(command_progress::Progress::PartialResult(result)) => {
                    Ok(StreamValue::PartialResult(result))
                }
                None => Err(anyhow::anyhow!("Event type not recognized")),
            }
        });

        Ok((invocation, events.boxed()))
    }

    async fn unpack_stream_inner<'a>(
        &self,
        stats: Option<&'a ReaderStats>,
    ) -> anyhow::Result<(
        Invocation,
        impl Stream<Item = anyhow::Result<StreamValue>> + 'a,
    )> {
        match self.encoding.mode {
            LogMode::Json => self.unpack_stream_json(stats).await,
            LogMode::Protobuf => self.unpack_stream_protobuf(stats).await,
        }
    }

    /// Read the invocation line then the event stream.
    pub async fn unpack_stream_with_stats<'a>(
        &self,
        stats: &'a ReaderStats,
    ) -> anyhow::Result<(
        Invocation,
        impl Stream<Item = anyhow::Result<StreamValue>> + 'a,
    )> {
        self.unpack_stream_inner(Some(stats)).await
    }

    pub async fn unpack_stream(
        &self,
    ) -> anyhow::Result<(
        Invocation,
        impl Stream<Item = anyhow::Result<StreamValue>> + 'static,
    )> {
        self.unpack_stream_inner(None).await
    }

    async fn open<'a>(&self, stats: Option<&'a ReaderStats>) -> anyhow::Result<EventLogReader<'a>> {
        tracing::info!(
            "Open {} using encoding {:?}",
            self.path.display(),
            self.encoding
        );

        let (compressed_bytes, decompressed_bytes) = match stats {
            Some(stats) => (
                Some(&stats.compressed_bytes),
                Some(&stats.decompressed_bytes),
            ),
            None => (None, None),
        };

        let file = async_fs_util::open(&self.path).await?;
        let file = CountingReader::new(file, compressed_bytes);
        let file = match self.encoding.compression {
            Compression::None => {
                Box::new(CountingReader::new(file, decompressed_bytes)) as EventLogReader
            }
            Compression::Gzip => Box::new(CountingReader::new(
                GzipDecoder::new(BufReader::new(file)),
                decompressed_bytes,
            )) as EventLogReader,
            Compression::Zstd => Box::new(CountingReader::new(
                ZstdDecoder::new(BufReader::new(file)),
                decompressed_bytes,
            )) as EventLogReader,
        };

        Ok(file)
    }

    pub async fn get_summary(&self) -> anyhow::Result<EventLogSummary> {
        let (invocation, events) = self.unpack_stream().await?;
        let buck_event: BuckEvent = events
            .try_filter_map(|log| {
                let maybe_buck_event = match log {
                    StreamValue::Result(_) | StreamValue::PartialResult(_) => None,
                    StreamValue::Event(buck_event) => Some(buck_event),
                };
                futures::future::ready(Ok(maybe_buck_event))
            })
            .try_next()
            .await?
            .ok_or_else(|| {
                anyhow::anyhow!(EventLogErrors::EndOfFile(
                    self.path.to_str().unwrap().to_owned()
                ))
            })?
            .try_into()?;
        Ok(EventLogSummary {
            trace_id: buck_event.trace_id()?,
            timestamp: buck_event.timestamp(),
            invocation,
        })
    }

    pub fn extension(&self) -> &str {
        self.encoding.extensions[0]
    }
}


/*
 * 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::sync::atomic::AtomicU64;
use std::sync::Arc;

use async_trait::async_trait;
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 crate::argv::SanitizedArgv;
use crate::cleanup_ctx::AsyncCleanupContext;
use crate::subscribers::event_log::write::WriteEventLog;
use crate::subscribers::subscriber::EventSubscriber;
use crate::subscribers::subscriber::Tick;

/// This EventLog lets us to events emitted by Buck and log them to a file. The events are
/// serialized as JSON and logged one per line.
pub(crate) struct EventLog<'a> {
    writer: WriteEventLog<'a>,
}

impl<'a> EventLog<'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<EventLog> {
        Ok(Self {
            writer: WriteEventLog::new(
                logdir,
                working_dir,
                extra_path,
                extra_user_event_log_path,
                sanitized_argv,
                async_cleanup_context,
                command_name,
                log_size_counter_bytes,
            )?,
        })
    }
}

#[async_trait]
impl<'a> EventSubscriber for EventLog<'a> {
    async fn handle_events(&mut self, events: &[Arc<BuckEvent>]) -> anyhow::Result<()> {
        self.writer.write_events(events).await
    }

    async fn handle_command_result(
        &mut self,
        result: &buck2_cli_proto::CommandResult,
    ) -> anyhow::Result<()> {
        self.writer.write_result(result).await
    }

    /// Flush all log files during on tick to avoid buffering data in memory which we might lose if
    /// we hit an error.
    async fn tick(&mut self, _tick: &Tick) -> anyhow::Result<()> {
        self.writer.flush_files().await
    }

    async fn exit(&mut self) -> anyhow::Result<()> {
        self.writer.exit().await
    }
}


/*
 * 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 allocative::Allocative;
use anyhow::Context;
use buck2_common::convert::ProstDurationExt;
use buck2_data::ActionExecutionKind;
use buck2_data::ActionKind;
use buck2_data::ActionName;
use buck2_data::BuckEvent;
use buck2_data::StarlarkUserEvent;
use buck2_event_observer::display::display_action_owner;
use buck2_event_observer::display::TargetDisplayOptions;
use serde::Deserialize;
use serde::Serialize;

use crate::stream_value::StreamValue;

#[derive(Debug, thiserror::Error)]
pub(crate) enum SerializeUserEventError {
    #[error("Internal error: Missing `data` in `{0}`")]
    MissingData(String),
    #[error("Internal error: Missing `timestamp` in `BuckEvent`")]
    MissingTimestamp,
    #[error(
        "Internal error: Missing `input_materialization_duration` in `CommandExecutionDetails`"
    )]
    MissingInputMaterializationDuration,
    #[error("Internal error: Missing `name` in `ActionExecutionEnd`")]
    MissingName,
    #[error("Internal error: `ActionKey` is malformed in `ActionExecutionEnd`")]
    MalformedActionKey,
}

/// Wrapper around StarlarkUserEvent so we discard the rest of BuckEvent fields.
#[derive(Serialize, Deserialize, Allocative, Clone)]
pub struct UserEvent {
    #[serde(flatten)]
    data: UserEventData,
    epoch_millis: u64,
}

/// BuckEvent types that are allowed in the user event log.
#[derive(Serialize, Deserialize, Allocative, Clone)]
pub enum UserEventData {
    StarlarkUserEvent(StarlarkUserEvent),
    ActionExecutionEvent(ActionExecutionEndSimple),
    BxlEnsureArtifactsEvent(BxlEnsureArtifactsEvent),
}

/// Simplified version of ActionExecutionEnd.
#[derive(Serialize, Deserialize, Allocative, Clone)]
pub struct ActionExecutionEndSimple {
    kind: ActionKind,
    name: ActionName,
    duration_millis: u64,
    output_size: u64,
    input_materialization_duration_millis: u64,
    execution_kind: ActionExecutionKind,
    owner: String,
}

#[derive(Serialize, Deserialize, Allocative, Clone)]
pub struct BxlEnsureArtifactsEvent {
    duration_millis: u64,
}

pub fn try_get_user_event_for_read(
    stream_value: &StreamValue,
) -> anyhow::Result<Option<UserEvent>> {
    match stream_value {
        StreamValue::Event(buck_event) => try_get_user_event(buck_event.as_ref()),
        _ => Ok(None),
    }
}

pub(crate) fn try_get_user_event(buck_event: &BuckEvent) -> anyhow::Result<Option<UserEvent>> {
    let timestamp = buck_event
        .timestamp
        .as_ref()
        .context(SerializeUserEventError::MissingTimestamp)?;
    let epoch_millis = timestamp.seconds as u64 * 1000 + timestamp.nanos as u64 / 1_000_000;

    match buck_event
        .data
        .as_ref()
        .context(SerializeUserEventError::MissingData("BuckEvent".to_owned()))?
    {
        buck2_data::buck_event::Data::Instant(ref instant) => {
            match instant
                .data
                .as_ref()
                .context(SerializeUserEventError::MissingData(
                    "InstantEvent".to_owned(),
                ))? {
                buck2_data::instant_event::Data::StarlarkUserEvent(event) => Ok(Some(UserEvent {
                    data: UserEventData::StarlarkUserEvent(event.clone()),
                    epoch_millis,
                })),
                _ => Ok(None),
            }
        }
        buck2_data::buck_event::Data::SpanEnd(ref span_end_event) => {
            let duration_millis = span_end_event
                .duration
                .as_ref()
                .context(SerializeUserEventError::MissingTimestamp)?
                .try_into_duration()?
                .as_millis() as u64;

            match span_end_event
                .data
                .as_ref()
                .context(SerializeUserEventError::MissingData(
                    "SpanEndEvent".to_owned(),
                ))? {
                buck2_data::span_end_event::Data::ActionExecution(action_execution) => {
                    let mut input_materialization_duration_millis = 0;

                    // Take the last command report's input materialization duration
                    if let Some(command) = action_execution.commands.last() {
                        if let Some(details) = &command.details {
                            input_materialization_duration_millis = details
                                .input_materialization_duration
                                .as_ref()
                                .context(
                                    SerializeUserEventError::MissingInputMaterializationDuration,
                                )?
                                .try_into_duration()?
                                .as_millis()
                                as u64;
                        }
                    }

                    let owner = action_execution
                        .key
                        .as_ref()
                        .context(SerializeUserEventError::MalformedActionKey)?
                        .owner
                        .as_ref()
                        .context(SerializeUserEventError::MalformedActionKey)?;

                    // Let's just show the unconfigured label for simplicity
                    let owner = display_action_owner(owner, TargetDisplayOptions::for_log())?;

                    let action_event = ActionExecutionEndSimple {
                        kind: action_execution.kind(),
                        name: action_execution
                            .name
                            .as_ref()
                            .context(SerializeUserEventError::MissingName)?
                            .clone(),
                        duration_millis,
                        output_size: action_execution.output_size,
                        input_materialization_duration_millis,
                        execution_kind: action_execution.execution_kind(),
                        owner,
                    };

                    Ok(Some(UserEvent {
                        data: UserEventData::ActionExecutionEvent(action_event),
                        epoch_millis,
                    }))
                }
                buck2_data::span_end_event::Data::BxlEnsureArtifacts(_) => {
                    let bxl_ensure_artifacts = BxlEnsureArtifactsEvent { duration_millis };

                    Ok(Some(UserEvent {
                        data: UserEventData::BxlEnsureArtifactsEvent(bxl_ensure_artifacts),
                        epoch_millis,
                    }))
                }
                _ => Ok(None),
            }
        }
        _ => Ok(None),
    }
}

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

    use buck2_data::starlark_user_metadata_value::Value;
    use buck2_data::StarlarkUserEvent;
    use buck2_data::StarlarkUserMetadataValue;

    #[test]
    fn test_serialize_starlark_user_event() {
        let val1 = StarlarkUserMetadataValue {
            value: Some(Value::BoolValue(true)),
        };

        let mut metadata = HashMap::new();
        metadata.insert("bool_value".to_owned(), val1);

        let starlark_user_event = StarlarkUserEvent {
            id: "foo".to_owned(),
            metadata,
        };

        let serialized = serde_json::to_string_pretty(&starlark_user_event).unwrap();
        let expected = r#"{
  "id": "foo",
  "metadata": {
    "bool_value": true
  }
}"#;
        assert_eq!(expected, serialized);
    }
}