//! Run logs for Ambient.

#![allow(dead_code)]
#![allow(missing_docs)]

use std::{
    ffi::{OsStr, OsString},
    fs::OpenOptions,
    path::{Path, PathBuf},
    process::{Command, Output},
    time::{Duration, SystemTime},
};

use clingwrap::runner::CommandError;
use html_page::{Element, HtmlPage, Tag};
use serde::{Deserialize, Serialize};

use crate::{action::RunnableAction, config::Config, plan::RunnablePlan, util::format_timestamp};

const CSS: &str = include_str!("style.css");

// A run log message without metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum RunLogMessageDetail {
    AmbientStarts {
        name: String,
        version: String,
    },
    AmbientEndsSuccssfully,
    AmbientEndsInFailure,
    AmbientRuntimeConfig(Config),
    ExecutorStarts {
        name: String,
        version: String,
    },
    ExecutorEndsSuccessfully,
    ExecutorEndsInFailure {
        exit_code: i32,
    },
    RunnablePlan(RunnablePlan),
    RunCi {
        project_name: String,
    },
    SkipCi {
        project_name: String,
    },
    Debug {
        debug: String,
    },
    ExecuteAction(RunnableAction),
    ActionSucceeded(RunnableAction),
    ActionFailed(RunnableAction),
    PlanSucceeded,
    StartProgram {
        argv: Vec<OsString>,
    },
    ProgramSucceeded {
        exit_code: i32,
        stdout: String,
        stderr: String,
    },
    ProgramFailed {
        exit_code: Option<i32>,
        stdout: String,
        stderr: String,
    },
    StartQemu {
        argv: Vec<OsString>,
    },
    QemuSucceeded {
        exit_code: i32,
        stdout: String,
        stderr: String,
    },
    QemuFailed {
        exit_code: Option<i32>,
        stdout: String,
        stderr: String,
    },

    // We construct this one after reading from JSON.
    #[serde(skip)]
    RunProgramSucceeded {
        argv: Vec<OsString>,
        exit_code: i32,
        stdout: String,
        stderr: String,
    },

    // We construct this one after reading from JSON.
    RunProgramFailed {
        argv: Vec<OsString>,
        exit_code: Option<i32>,
        stdout: String,
        stderr: String,
    },

    // We construct this one after reading from JSON.
    #[serde(skip)]
    RunQemuSucceeded {
        argv: Vec<OsString>,
        exit_code: i32,
        stdout: String,
        stderr: String,
    },

    // We construct this one after reading from JSON.
    RunQemuFailed {
        argv: Vec<OsString>,
        exit_code: Option<i32>,
        stdout: String,
        stderr: String,
    },
}

impl RunLogMessageDetail {
    fn was_successful(&self) -> bool {
        !matches!(
            self,
            Self::ProgramFailed { .. }
                | Self::RunProgramFailed { .. }
                | Self::RunQemuFailed { .. }
                | Self::ActionFailed(_)
                | Self::AmbientEndsInFailure
                | Self::QemuFailed { .. }
        )
    }
}

/// A run log message with metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunLogMessage {
    #[serde(flatten)]
    detail: RunLogMessageDetail,
    timestamp: SystemTime,
}

#[allow(missing_docs)]
impl RunLogMessage {
    fn new(detail: RunLogMessageDetail) -> Self {
        Self {
            detail,
            timestamp: SystemTime::now(),
        }
    }

    fn format_timestamp(&self) -> String {
        format_timestamp(self.timestamp).unwrap_or("time stamp error".into())
    }

    pub fn to_json(&self) -> String {
        if let Ok(json) = serde_json::to_string(self) {
            json
        } else {
            format!(
                r#"{{"error":"failed to convert RunLogMessage to JSON","debug":{:?}}}"#,
                self
            )
        }
    }

    pub fn to_html_element(&self, offset: Duration) -> Element {
        let mut e = Element::new(Tag::Details);
        if !self.detail.was_successful() {
            e.set_boolean_attribute("open");
            e.set_attribute("class", "failed");
        } else {
            e.set_attribute("class", "succeeded");
        }
        let mut summary = Element::new(Tag::Summary);

        let mut more = Element::new(Tag::Div)
            .with_class("more")
            .with_child(
                Element::new(Tag::Span)
                    .with_class("timestamp")
                    .with_text("At: ")
                    .with_child(
                        Element::new(Tag::Span)
                            .with_class("exact-timestamp")
                            .with_text(&self.format_timestamp()),
                    ),
            )
            .with_child(Element::new(Tag::Br))
            .with_child(
                Element::new(Tag::Span)
                    .with_class("duration")
                    .with_text("After: ")
                    .with_child(
                        Element::new(Tag::Span)
                            .with_class("time-offset")
                            .with_text(&format!("{:.02} seconds", offset.as_secs_f64())),
                    ),
            )
            .with_child(Element::new(Tag::Br));

        match &self.detail {
            RunLogMessageDetail::AmbientStarts { name, version } => {
                summary.push_text("Ambient starts");
                more.push_child(
                    Element::new(Tag::Span).with_text("Program: ").with_child(
                        Element::new(Tag::Span)
                            .with_class("program-name")
                            .with_text(name),
                    ),
                );
                more.push_child(Element::new(Tag::Br));
                more.push_child(
                    Element::new(Tag::Span).with_text("Version: ").with_child(
                        Element::new(Tag::Span)
                            .with_class("program-version")
                            .with_text(version),
                    ),
                );
            }

            RunLogMessageDetail::AmbientEndsSuccssfully => {
                summary.push_text("Ambient ends, success");
                more.push_text("Everything is fine.");
            }

            RunLogMessageDetail::AmbientEndsInFailure => {
                summary.push_text("Ambient ends, failure");
                more.push_text("Woe be us!");
            }

            RunLogMessageDetail::AmbientRuntimeConfig(config) => {
                summary.push_text("Ambient configuration");
                let config = serde_norway::to_string(config)
                    .unwrap_or("Error serializing configuration to YAML".to_string());
                more.push_child(
                    Element::new(Tag::Pre)
                        .with_class("ambient-config")
                        .with_text(&config),
                );
            }

            RunLogMessageDetail::RunCi { project_name } => {
                summary.push_text("Will run CI for project ");
                summary.push_child(
                    Element::new(Tag::Span)
                        .with_class("project-name")
                        .with_text(project_name),
                );
                more.push_child(Element::new(Tag::Span).with_text("Hoping for the best"));
            }

            RunLogMessageDetail::SkipCi { project_name } => {
                summary.push_text("Will skip running CI for ");
                summary.push_child(
                    Element::new(Tag::Span)
                        .with_class("project-name")
                        .with_text(project_name),
                );
                more.push_child(
                    Element::new(Tag::Span)
                        .with_text(&format!("project {project_name}: NOT running CI")),
                );
            }

            RunLogMessageDetail::ExecutorStarts { name, version } => {
                summary.push_text("Executor starts");
                more.push_child(
                    Element::new(Tag::Span).with_text("Program: ").with_child(
                        Element::new(Tag::Span)
                            .with_class("program-name")
                            .with_text(name),
                    ),
                );
                more.push_child(Element::new(Tag::Br));
                more.push_child(
                    Element::new(Tag::Span).with_text("Version: ").with_child(
                        Element::new(Tag::Span)
                            .with_class("program-version")
                            .with_text(version),
                    ),
                );
            }

            RunLogMessageDetail::ExecutorEndsSuccessfully => {
                summary.push_text("Executor ends, success");
                more.push_text("Everything is fine.");
            }

            RunLogMessageDetail::ExecutorEndsInFailure { exit_code } => {
                summary.push_text("Executor ends, failure");
                more.push_text(&format!("Exit codd {exit_code}"));
            }

            RunLogMessageDetail::RunnablePlan(plan) => {
                summary.push_text("Runnable plan");

                #[allow(clippy::unwrap_used)]
                let yaml = serde_norway::to_string(plan).unwrap();
                more.push_child(
                    Element::new(Tag::Pre)
                        .with_class("runnable-plan")
                        .with_text(&yaml),
                );
            }

            RunLogMessageDetail::Debug { debug } => {
                summary.push_text("Debug output");
                more.push_child(
                    Element::new(Tag::Span)
                        .with_class("debug-output")
                        .with_text(debug),
                );
            }

            RunLogMessageDetail::ExecuteAction(action) => {
                summary.push_text("Start action ");
                summary.push_child(
                    Element::new(Tag::Span)
                        .with_class("action-name")
                        .with_text(action.name()),
                );
                more.push_child(
                    Element::new(Tag::Pre)
                        .with_class("action")
                        .with_text(&format!("{action:#?}")),
                );
            }

            RunLogMessageDetail::ActionSucceeded(action) => {
                summary.push_text("End action ");
                summary.push_text(action.name());
            }

            RunLogMessageDetail::ActionFailed(_) => {
                summary.push_text("Action failed");
            }

            RunLogMessageDetail::PlanSucceeded => {
                summary.push_text("Plan succeeded");
                more.push_text("Hopefully all is good.");
            }

            RunLogMessageDetail::StartProgram { argv } => {
                summary.push_text("Program started");

                let mut args = Element::new(Tag::Ul).with_class("argv");
                for arg in argv.iter() {
                    args.push_child(
                        Element::new(Tag::Li).with_class("arg").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-arg")
                                .with_text(&String::from_utf8_lossy(arg.as_encoded_bytes())),
                        ),
                    );
                }
                more.push_child(args);
            }

            RunLogMessageDetail::ProgramSucceeded {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("Program succeeded");
                more.push_child(
                    Element::new(Tag::Span)
                        .with_class("program-ok")
                        .with_text(&format!("exit: {exit_code}")),
                );
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::ProgramFailed {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("Program failed");
                if let Some(exit_code) = exit_code {
                    more.push_child(
                        Element::new(Tag::Span)
                            .with_class("program-failed")
                            .with_text(&format!("exit: {exit_code}")),
                    );
                }
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::StartQemu { argv } => {
                summary.push_text("QEMU started");

                let mut args = Element::new(Tag::Ul).with_class("argv");
                for arg in argv.iter() {
                    args.push_child(
                        Element::new(Tag::Li).with_class("arg").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-arg")
                                .with_text(&String::from_utf8_lossy(arg.as_encoded_bytes())),
                        ),
                    );
                }
                more.push_child(args);
            }

            RunLogMessageDetail::QemuSucceeded {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("QEMU succeeded");
                more.push_child(
                    Element::new(Tag::Span)
                        .with_class("program-ok")
                        .with_text(&format!("exit: {exit_code}")),
                );
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::QemuFailed {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("QEMU failed");
                if let Some(exit_code) = exit_code {
                    more.push_child(
                        Element::new(Tag::Span)
                            .with_class("program-failed")
                            .with_text(&format!("exit: {exit_code}")),
                    );
                }
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::RunProgramSucceeded {
                argv,
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("OK: ");
                let mut args = Element::new(Tag::Span).with_class("args");
                for arg in argv.iter() {
                    args.push_text(&String::from_utf8_lossy(arg.as_encoded_bytes()));
                    args.push_text(" ");
                }
                summary.push_child(args);

                let mut args = Element::new(Tag::Ul).with_class("argv");
                for arg in argv.iter() {
                    args.push_child(
                        Element::new(Tag::Li).with_class("arg").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-arg")
                                .with_text(&String::from_utf8_lossy(arg.as_encoded_bytes())),
                        ),
                    );
                }
                more.push_child(args);
                more.push_child(
                    Element::new(Tag::Span)
                        .with_class("program-ok")
                        .with_text(&format!("exit: {exit_code}")),
                );
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::RunProgramFailed {
                argv,
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("FAILED: ");
                let mut args = Element::new(Tag::Span).with_class("args");
                for arg in argv.iter() {
                    args.push_text(&String::from_utf8_lossy(arg.as_encoded_bytes()));
                    args.push_text(" ");
                }
                summary.push_child(args);

                let mut args = Element::new(Tag::Ul).with_class("argv");
                for arg in argv.iter() {
                    args.push_child(
                        Element::new(Tag::Li).with_class("arg").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-arg")
                                .with_text(&String::from_utf8_lossy(arg.as_encoded_bytes())),
                        ),
                    );
                }
                more.push_child(args);
                if let Some(exit_code) = exit_code {
                    more.push_child(
                        Element::new(Tag::Span)
                            .with_class("program-ok")
                            .with_text(&format!("exit: {exit_code}")),
                    );
                }
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::RunQemuSucceeded {
                argv,
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("QEMU OK");

                let mut args = Element::new(Tag::Ul).with_class("argv");
                for arg in argv.iter() {
                    args.push_child(
                        Element::new(Tag::Li).with_class("arg").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-arg")
                                .with_text(&String::from_utf8_lossy(arg.as_encoded_bytes())),
                        ),
                    );
                }
                more.push_child(args);
                more.push_child(
                    Element::new(Tag::Span)
                        .with_class("program-ok")
                        .with_text(&format!("exit: {exit_code}")),
                );
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }

            RunLogMessageDetail::RunQemuFailed {
                argv,
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("QEMU FAILED");

                let mut args = Element::new(Tag::Ul).with_class("argv");
                for arg in argv.iter() {
                    args.push_child(
                        Element::new(Tag::Li).with_class("arg").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-arg")
                                .with_text(&String::from_utf8_lossy(arg.as_encoded_bytes())),
                        ),
                    );
                }
                more.push_child(args);
                if let Some(exit_code) = exit_code {
                    more.push_child(
                        Element::new(Tag::Span)
                            .with_class("program-ok")
                            .with_text(&format!("exit: {exit_code}")),
                    );
                }
                if !stdout.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stdout:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stdout")
                                .with_text(stdout),
                        ),
                    );
                }
                if !stderr.is_empty() {
                    more.push_child(
                        Element::new(Tag::Div).with_text("Stderr:").with_child(
                            Element::new(Tag::Pre)
                                .with_class("stderr")
                                .with_text(stderr),
                        ),
                    );
                }
            }
        };

        e.push_child(more);
        e.push_child(summary);
        e
    }
}

/// All the log messages for a CI run.
///
/// The default version collects messages, but doesn't write them. If the output
/// is set later, the collected messages get written there.
#[derive(Default)]
pub struct RunLog {
    // We collect messages here via the `push` and `write` methods.
    msgs: Vec<RunLogMessage>,

    output: Option<Box<dyn std::io::Write>>,
}

impl std::fmt::Debug for RunLog {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RunLog.msgs={:?}", self.msgs)
    }
}

impl RunLog {
    fn flush(&mut self) {
        if !self.msgs.is_empty() {
            while !self.msgs.is_empty() {
                let msg = self.msgs.remove(0);
                self.write_to_output(&msg);
            }
        }
    }

    fn write_to_output(&mut self, msg: &RunLogMessage) {
        if let Some(output) = &mut self.output {
            let buf = format!("{}\n", msg.to_json());
            output.write_all(buf.as_bytes()).ok();
        }
    }

    /// Write messages to stdout. Flush any collected messages.
    pub fn stdout(&mut self) {
        self.output = Some(Box::new(std::io::stdout()));
        self.flush();
    }

    /// Change `RunLog` so that further log messages are written to a
    /// named file.
    pub fn to_named_file(&mut self, filename: impl AsRef<Path>) -> Result<(), RunLogError> {
        let filename = filename.as_ref();
        let file = OpenOptions::new()
            .append(true)
            .open(filename)
            .map_err(|err| RunLogError::Create(filename.to_path_buf(), err))?;
        self.output = Some(Box::new(file));
        self.flush();
        Ok(())
    }

    /// All messages.
    pub fn msgs(&self) -> &[RunLogMessage] {
        &self.msgs
    }

    /// Append a message to the run log.
    pub fn push(&mut self, msg: RunLogMessage) {
        assert!(self.output.is_none());
        self.msgs.push(msg);
    }

    /// Output a message to stderr.
    #[allow(clippy::unwrap_used)]
    pub fn write(&mut self, msg: &RunLogMessage) {
        if self.output.is_none() {
            self.push(msg.clone());
        } else {
            self.write_to_output(msg);
        }
    }

    /// Load run log file a reader. This reads the output from the "run log"
    /// serial port of QEMU and extracts the JSON lines from that for parsing.
    pub fn read_raw(mut reader: impl std::io::Read) -> Result<Self, RunLogError> {
        let mut buf = vec![];
        reader.read_to_end(&mut buf).map_err(RunLogError::ReadLog)?;
        Self::from_raw(buf)
    }

    /// Load run log from in-memory raw data. This parses the output to extract
    /// JSON Lihes.
    pub fn from_raw(buf: Vec<u8>) -> Result<Self, RunLogError> {
        let buf = String::from_utf8(buf).map_err(RunLogError::Utf8)?;

        const BEGIN: &str = "====================== BEGIN ======================";
        if let Some((_, suffix)) = buf.split_once(BEGIN) {
            Self::from_lines(suffix.lines().filter(|line| line.starts_with("{")))
        } else {
            Err(RunLogError::NoBegin)
        }
    }

    /// Load run log file a reader. This reads pure JSON Lines run log.
    pub fn read_jsonl(mut reader: impl std::io::Read) -> Result<Self, RunLogError> {
        let mut buf = vec![];
        reader.read_to_end(&mut buf).map_err(RunLogError::ReadLog)?;
        Self::parse_jsonl(buf)
    }

    /// Parse JSON run log from memory.
    pub fn parse_jsonl(data: Vec<u8>) -> Result<Self, RunLogError> {
        let buf = String::from_utf8(data).map_err(RunLogError::Utf8)?;
        Self::from_lines(buf.lines())
    }

    fn from_lines<'a>(lines: impl Iterator<Item = &'a str>) -> Result<Self, RunLogError> {
        let mut runlog = Self::default();
        for line in lines {
            let msg: RunLogMessage = serde_json::from_str(line).map_err(|err| {
                let prefix = line.split_at_checked(40).map(|(a, _)| a).unwrap_or("");
                RunLogError::Json(prefix.to_string(), err)
            })?;
            runlog.push(msg);
        }

        Ok(runlog)
    }
}

// Methods to create and write run log messages.
impl RunLog {
    pub fn debug(&mut self, msg: impl Into<String>) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::Debug {
            debug: msg.into(),
        }));
    }

    pub fn ambient_starts<N: Into<String>, V: Into<String>>(&mut self, name: N, version: V) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::AmbientStarts {
            name: name.into(),
            version: version.into(),
        }))
    }

    pub fn ambient_ends_successfully(&mut self) {
        self.write(&RunLogMessage::new(
            RunLogMessageDetail::AmbientEndsSuccssfully,
        ))
    }

    pub fn ambient_ends_in_failure(&mut self) {
        self.write(&RunLogMessage::new(
            RunLogMessageDetail::AmbientEndsInFailure,
        ))
    }

    pub fn ambient_runtime_config(&mut self, config: &Config) {
        self.write(&RunLogMessage::new(
            RunLogMessageDetail::AmbientRuntimeConfig(config.clone()),
        ))
    }

    pub fn run_ci(&mut self, project_name: impl Into<String>) {
        let project_name = project_name.into();

        // We output this to stderr for the test suite.
        eprintln!("run CI for {project_name}");

        self.write(&RunLogMessage::new(RunLogMessageDetail::RunCi {
            project_name,
        }))
    }

    pub fn skip_ci(&mut self, project_name: impl Into<String>) {
        let project_name = project_name.into();

        // We output this to stderr for the test suite.
        eprintln!("skip CI for {project_name}");

        self.write(&RunLogMessage::new(RunLogMessageDetail::SkipCi {
            project_name,
        }))
    }

    pub fn executor_starts(&mut self, name: impl Into<String>, version: impl Into<String>) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::ExecutorStarts {
            name: name.into(),
            version: version.into(),
        }))
    }

    pub fn executor_ends_successfully(&mut self) {
        self.write(&RunLogMessage::new(
            RunLogMessageDetail::ExecutorEndsSuccessfully,
        ))
    }

    pub fn executor_ends_in_failure(&mut self, exit_code: i32) {
        self.write(&RunLogMessage::new(
            RunLogMessageDetail::ExecutorEndsInFailure { exit_code },
        ))
    }

    /// Log runnable plan at start of execution.
    pub fn runnable_plan(&mut self, plan: &RunnablePlan) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::RunnablePlan(
            plan.clone(),
        )))
    }

    /// Execute an action.
    pub fn execute_action(&mut self, action: &RunnableAction) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::ExecuteAction(
            action.clone(),
        )));
    }

    /// Action succeded.
    pub fn action_succeeded(&mut self, action: &RunnableAction) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::ActionSucceeded(
            action.clone(),
        )));
    }

    /// Action failed.
    pub fn action_failed(&mut self, action: &RunnableAction) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::ActionFailed(
            action.clone(),
        )));
    }

    /// All actions in plan succeded.
    pub fn plan_succeeded(&mut self) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::PlanSucceeded));
    }

    /// Start a program.
    pub fn start_program(&mut self, cmd: &Command) {
        fn oss(os: &OsStr) -> OsString {
            os.to_os_string()
        }

        let mut argv = vec![oss(cmd.get_program())];
        for arg in cmd.get_args() {
            argv.push(oss(arg));
        }

        self.write(&RunLogMessage::new(RunLogMessageDetail::StartProgram {
            argv,
        }));
    }

    /// Program succeeded.
    pub fn program_succeeded(&mut self, output: &Output) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::ProgramSucceeded {
            #[allow(clippy::unwrap_used)]
            exit_code: output.status.code().unwrap(),
            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        }));
    }

    /// Program failed.
    pub fn program_failed(&mut self, err: &CommandError) {
        let (exit_code, stdout, stderr) = match err {
            CommandError::CommandFailed {
                exit_code, output, ..
            } => {
                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
                (Some(exit_code), stdout, stderr)
            }
            CommandError::KilledBySignal { .. }
            | CommandError::NoSuchCommand(_)
            | CommandError::NoPermission(_)
            | CommandError::Other { .. }
            | CommandError::Stdin(_)
            | CommandError::PipeCapture(_)
            | CommandError::PipeClone(_)
            | CommandError::ReadCombined(_) => {
                let stdout = String::new();
                let stderr = err.to_string();
                (None, stdout, stderr)
            }
        };
        self.write(&RunLogMessage::new(RunLogMessageDetail::ProgramFailed {
            exit_code: exit_code.copied(),
            stdout,
            stderr,
        }));
    }

    /// Start a program.
    pub fn start_qemu(&mut self, cmd: &Command) {
        fn oss(os: &OsStr) -> OsString {
            os.to_os_string()
        }

        let mut argv = vec![oss(cmd.get_program())];
        for arg in cmd.get_args() {
            argv.push(oss(arg));
        }

        self.write(&RunLogMessage::new(RunLogMessageDetail::StartQemu { argv }));
    }

    /// Program succeeded.
    pub fn qemu_succeeded(&mut self, output: &Output) {
        self.write(&RunLogMessage::new(RunLogMessageDetail::QemuSucceeded {
            #[allow(clippy::unwrap_used)]
            exit_code: output.status.code().unwrap(),
            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        }));
    }

    /// Program failed.
    pub fn qemu_failed(&mut self, err: &CommandError) {
        let (exit_code, stdout, stderr) = match err {
            CommandError::CommandFailed {
                exit_code, output, ..
            } => {
                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
                (Some(exit_code), stdout, stderr)
            }
            CommandError::KilledBySignal { .. }
            | CommandError::NoSuchCommand(_)
            | CommandError::NoPermission(_)
            | CommandError::Other { .. }
            | CommandError::Stdin(_)
            | CommandError::PipeCapture(_)
            | CommandError::PipeClone(_)
            | CommandError::ReadCombined(_) => {
                let stdout = String::new();
                let stderr = err.to_string();
                (None, stdout, stderr)
            }
        };
        self.write(&RunLogMessage::new(RunLogMessageDetail::QemuFailed {
            exit_code: exit_code.copied(),
            stdout,
            stderr,
        }));
    }
}

// Methods to produce HTML from run log.
impl RunLog {
    /// Produce HTML from run log.
    pub fn to_html(&self) -> HtmlPage {
        let title = "Ambient run log";
        let mut page = HtmlPage::default()
            .with_head_element(Element::new(Tag::Title).with_text(title))
            .with_head_element(Element::new(Tag::Style).with_text(CSS))
            .with_head_element(
                Element::new(Tag::Link)
                    .with_attribute("href", "local.css")
                    .with_attribute("rel", "stylesheet")
                    .with_attribute("type", "text/css"),
            )
            .with_body_element(Element::new(Tag::H1).with_text(title));

        page.push_to_body(self.messages_to_html());

        page
    }

    /// Prouce an HTML element with the run log messages. The caller can,
    /// if needed, put it into an HTML page themselves.
    pub fn messages_to_html(&self) -> Element {
        let mut msgs = vec![];
        let mut prev_argv = None;
        for msg in self.msgs.iter().cloned() {
            match msg.detail {
                RunLogMessageDetail::StartProgram { argv } => {
                    prev_argv = Some(argv.to_vec());
                }
                RunLogMessageDetail::ProgramSucceeded {
                    exit_code,
                    stdout,
                    stderr,
                } => {
                    msgs.push(RunLogMessage::new(
                        RunLogMessageDetail::RunProgramSucceeded {
                            argv: prev_argv.unwrap_or_default().to_vec(),
                            exit_code,
                            stdout,
                            stderr,
                        },
                    ));
                    prev_argv = None;
                }
                RunLogMessageDetail::ProgramFailed {
                    exit_code,
                    stdout,
                    stderr,
                } => {
                    msgs.push(RunLogMessage::new(RunLogMessageDetail::RunProgramFailed {
                        argv: prev_argv.unwrap_or_default().to_vec(),
                        exit_code,
                        stdout,
                        stderr,
                    }));
                    prev_argv = None;
                }

                RunLogMessageDetail::StartQemu { argv } => {
                    prev_argv = Some(argv.to_vec());
                }
                RunLogMessageDetail::QemuSucceeded {
                    exit_code,
                    stdout,
                    stderr,
                } => {
                    msgs.push(RunLogMessage::new(RunLogMessageDetail::RunQemuSucceeded {
                        argv: prev_argv.unwrap_or_default().to_vec(),
                        exit_code,
                        stdout,
                        stderr,
                    }));
                    prev_argv = None;
                }
                RunLogMessageDetail::QemuFailed {
                    exit_code,
                    stdout,
                    stderr,
                } => {
                    msgs.push(RunLogMessage::new(RunLogMessageDetail::RunQemuFailed {
                        argv: prev_argv.unwrap_or_default().to_vec(),
                        exit_code,
                        stdout,
                        stderr,
                    }));
                    prev_argv = None;
                }
                // RunLogMessageDetail::ExecuteAction(_)
                // | RunLogMessageDetail::ActionSucceeded(_)
                // | RunLogMessageDetail::ActionFailed(_)
                // | RunLogMessageDetail::RunCi { .. }
                // | RunLogMessageDetail::PlanSucceeded => (),
                _ => msgs.push(msg),
            }
        }

        let mut e = Element::new(Tag::Div).with_class("run-log-messages");
        if let Some(first) = self.msgs.first() {
            for msg in msgs {
                let offset = msg
                    .timestamp
                    .duration_since(first.timestamp)
                    .unwrap_or_default();
                e.push_child(msg.to_html_element(offset));
            }
        }
        e
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RunLogError {
    #[error("line in log is not JSON: {0:?}")]
    Json(String, #[source] serde_json::Error),

    #[error("failed to read log file")]
    ReadLog(#[source] std::io::Error),

    #[error("log file is not UTF8")]
    Utf8(#[source] std::string::FromUtf8Error),

    #[error("run log does not contain BEGIN marker")]
    NoBegin,

    #[error("failed to create file or open it for writing: {0}")]
    Create(PathBuf, #[source] std::io::Error),
}
