//! 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,
    action_impl::{debian::repo::Needed, Custom, NpmError},
    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),
    DebGet {
        packages: Vec<Needed>,
    },
    NpmGetSucceded,
    NpmGetFailed1 {
        error: String,
    },
    CustomActionStarts {
        source: PathBuf,
        custom: Custom,
        exe: PathBuf,
        exe_exists: bool,
    },
    CustomActionOutput {
        stdout: Vec<u8>,
        stderr: Vec<u8>,
    },
    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,
    },
}

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

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

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

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

    pub fn to_json(&self) -> String {
        match serde_json::to_string(self) {
            Ok(json) => json,
            Err(err) => {
                format!(r#"{{"error":"failed to convert RunLogMessage to JSON: {err}""#)
            }
        }
    }
}

/// 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, source: RunLogSource, msg: impl Into<String>) {
        self.write(&RunLogMessage::new(
            source,
            RunLogMessageDetail::Debug { debug: msg.into() },
        ));
    }

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

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

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

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

    pub fn run_ci(&mut self, source: RunLogSource, 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(
            source,
            RunLogMessageDetail::RunCi { project_name },
        ))
    }

    pub fn skip_ci(&mut self, source: RunLogSource, 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(
            source,
            RunLogMessageDetail::SkipCi { project_name },
        ))
    }

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

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

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

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

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

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

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

    // `deb_get` action succeeded.
    pub fn deb_get(&mut self, source: RunLogSource, packages: &[Needed]) {
        self.write(&RunLogMessage::new(
            source,
            RunLogMessageDetail::DebGet {
                packages: packages.to_vec(),
            },
        ));
    }

    /// `npm_get` action succeeded.
    pub fn npm_get_succeeded(&mut self, source: RunLogSource) {
        self.write(&RunLogMessage::new(
            source,
            RunLogMessageDetail::NpmGetSucceded,
        ));
    }

    /// `npm_get` action failed.
    pub fn npm_get_failed(&mut self, log_source: RunLogSource, err: &NpmError) {
        self.write(&RunLogMessage::new(
            log_source,
            RunLogMessageDetail::NpmGetFailed1 {
                error: err.to_string(),
            },
        ));
    }

    /// Custom action starts.
    pub fn custom_action_starts(
        &mut self,
        log_source: RunLogSource,
        source: PathBuf,
        custom: Custom,
        exe: PathBuf,
        exe_exists: bool,
    ) {
        self.write(&RunLogMessage::new(
            log_source,
            RunLogMessageDetail::CustomActionStarts {
                source,
                custom,
                exe,
                exe_exists,
            },
        ));
    }

    /// CUstom action succeeded.
    pub fn custom_action_output(&mut self, source: RunLogSource, stdout: Vec<u8>, stderr: Vec<u8>) {
        self.write(&RunLogMessage::new(
            source,
            RunLogMessageDetail::CustomActionOutput { stdout, stderr },
        ));
    }

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

    /// Start a program.
    pub fn start_program(&mut self, source: RunLogSource, 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(
            source,
            RunLogMessageDetail::StartProgram { argv },
        ));
    }

    /// Program succeeded.
    pub fn program_succeeded(&mut self, source: RunLogSource, output: &Output) {
        self.write(&RunLogMessage::new(
            source,
            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, source: RunLogSource, 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(
            source,
            RunLogMessageDetail::ProgramFailed {
                exit_code: exit_code.copied(),
                stdout,
                stderr,
            },
        ));
    }

    /// Start a program.
    pub fn start_qemu(&mut self, source: RunLogSource, 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(
            source,
            RunLogMessageDetail::StartQemu { argv },
        ));
    }

    /// Program succeeded.
    pub fn qemu_succeeded(&mut self, source: RunLogSource, output: &Output) {
        self.write(&RunLogMessage::new(
            source,
            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, source: RunLogSource, 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(
            source,
            RunLogMessageDetail::QemuFailed {
                exit_code: exit_code.copied(),
                stdout,
                stderr,
            },
        ));
    }
}

/// Where does run log message come from?
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum RunLogSource {
    Prelude,
    PrePlan,
    Plan,
    PostPlan,
    Epilog,
}

impl RunLogSource {
    fn class(&self) -> &'static str {
        match self {
            Self::Prelude => "prelude",
            Self::PrePlan => "pre-plan",
            Self::Plan => "plan",
            Self::PostPlan => "post-plan",
            Self::Epilog => "epilog",
        }
    }
}

#[derive(Debug)]
struct Cursor<'a, T> {
    data: &'a [T],
}

impl<'a, T> Cursor<'a, T> {
    fn new(data: &'a [T]) -> Self {
        Self { data }
    }

    fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    fn skip(&mut self, n: usize) {
        if n >= self.data.len() {
            self.data = &[]
        } else {
            self.data = &self.data[n..];
        }
    }

    fn peek(&self) -> Option<&T> {
        self.data.first()
    }

    fn take_one(&mut self) -> Option<&T> {
        let item = self.data.first();
        self.skip(1);
        item
    }

    fn take_if<F, U>(&mut self, func: F) -> Option<U>
    where
        F: Fn(&[T]) -> Option<(usize, Option<U>)>,
    {
        if let Some((n, maybe_item)) = func(self.data) {
            self.skip(n);
            maybe_item
        } else {
            None
        }
    }
}

pub struct SynthLog {
    msgs: Vec<SynthMessage>,
    raw: Vec<RunLogMessage>,
    console: Option<Vec<u8>>,
}

impl SynthLog {
    pub fn new(log_msgs: &[RunLogMessage]) -> Self {
        fn log_messages(n: usize, data: &[RunLogMessage]) -> Vec<SynthMessage> {
            data.iter()
                .take(n)
                .map(|m| SynthMessage::from_run_log_message(m, Duration::default()))
                .collect()
        }

        fn successful_action(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
            match &data {
                &[RunLogMessage {
                    detail: RunLogMessageDetail::ExecuteAction(action),
                    timestamp,
                    log_source: source,
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ActionSucceeded(_),
                    ..
                }, ..] => {
                    let n = 2;
                    let msg = SynthMessage {
                        detail: SynthDetail::SuccessfulAction {
                            action: action.clone(),
                            log_messages: log_messages(n, data),
                        },
                        timestamp: *timestamp,
                        offset: Duration::default(),
                        source: *source,
                    };
                    Some((n, Some(msg)))
                }
                _ => None,
            }
        }

        fn successful_action_runs_program(
            data: &[RunLogMessage],
        ) -> Option<(usize, Option<SynthMessage>)> {
            match &data {
                &[RunLogMessage {
                    detail: RunLogMessageDetail::ExecuteAction(action),
                    timestamp,
                    log_source: source,
                }, RunLogMessage {
                    detail: RunLogMessageDetail::StartProgram { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ActionSucceeded(_),
                    ..
                }, ..] => {
                    let n = 4;
                    let msg = SynthMessage {
                        detail: SynthDetail::SuccessfulAction {
                            action: action.clone(),
                            log_messages: log_messages(n, data),
                        },
                        timestamp: *timestamp,
                        offset: Duration::default(),
                        source: *source,
                    };
                    Some((n, Some(msg)))
                }
                _ => None,
            }
        }

        fn successful_action_runs_four_programs(
            data: &[RunLogMessage],
        ) -> Option<(usize, Option<SynthMessage>)> {
            match &data {
                &[RunLogMessage {
                    detail: RunLogMessageDetail::ExecuteAction(action),
                    timestamp,
                    log_source: source,
                }, RunLogMessage {
                    detail: RunLogMessageDetail::StartProgram { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::StartProgram { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::StartProgram { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::StartProgram { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ProgramSucceeded { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ActionSucceeded(_),
                    ..
                }, ..] => {
                    let n = 10;
                    let msg = SynthMessage {
                        detail: SynthDetail::SuccessfulAction {
                            action: action.clone(),
                            log_messages: log_messages(n, data),
                        },
                        timestamp: *timestamp,
                        offset: Duration::default(),
                        source: *source,
                    };
                    Some((n, Some(msg)))
                }
                _ => None,
            }
        }

        fn successful_custom_action(
            data: &[RunLogMessage],
        ) -> Option<(usize, Option<SynthMessage>)> {
            match &data {
                &[RunLogMessage {
                    detail: RunLogMessageDetail::ExecuteAction(action),
                    timestamp,
                    log_source: source,
                }, RunLogMessage {
                    detail: RunLogMessageDetail::CustomActionStarts { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::CustomActionOutput { .. },
                    ..
                }, RunLogMessage {
                    detail: RunLogMessageDetail::ActionSucceeded(_),
                    ..
                }, ..] => {
                    let n = 4;
                    let msg = SynthMessage {
                        detail: SynthDetail::SuccessfulAction {
                            action: action.clone(),
                            log_messages: log_messages(n, data),
                        },
                        timestamp: *timestamp,
                        offset: Duration::default(),
                        source: *source,
                    };
                    Some((n, Some(msg)))
                }
                _ => None,
            }
        }

        fn skip_uninteresting(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
            let first = data.first()?;

            #[allow(clippy::match_like_matches_macro)]
            let skip = match &first.detail {
                &RunLogMessageDetail::RunCi { .. }
                | RunLogMessageDetail::ExecutorEndsSuccessfully
                | RunLogMessageDetail::ExecutorEndsInFailure { .. } => true,
                _ => false,
            };

            if skip {
                Some((1, None))
            } else {
                None
            }
        }

        fn just_convert(data: &[RunLogMessage]) -> Option<(usize, Option<SynthMessage>)> {
            if let Some(orig) = data.first() {
                let synth = match &orig.detail {
                    RunLogMessageDetail::StartQemu { .. } => {
                        SynthMessage::from_run_log_message(orig, Duration::default())
                    }
                    _ => return None,
                };
                Some((1, Some(synth)))
            } else {
                None
            }
        }

        let mut msgs: Vec<SynthMessage> = vec![];

        if let Some(first) = log_msgs.first() {
            let mut cursor = Cursor::new(log_msgs);

            while !cursor.is_empty() {
                let offset: Duration = cursor
                    .peek()
                    .map(|m| {
                        m.timestamp
                            .duration_since(first.timestamp)
                            .unwrap_or_default()
                    })
                    .unwrap_or_default();

                if cursor.take_if(skip_uninteresting).is_some() {
                    // Do nothing.
                } else if let Some(mut msg) = cursor.take_if(just_convert) {
                    msg.offset = offset;
                    msgs.push(msg);
                } else if let Some(mut msg) = cursor.take_if(successful_custom_action) {
                    msg.offset = offset;
                    msgs.push(msg);
                } else if let Some(mut msg) = cursor.take_if(successful_action) {
                    msg.offset = offset;
                    msgs.push(msg);
                } else if let Some(mut msg) = cursor.take_if(successful_action_runs_program) {
                    msg.offset = offset;
                    msgs.push(msg);
                } else if let Some(mut msg) = cursor.take_if(successful_action_runs_four_programs) {
                    msg.offset = offset;
                    msgs.push(msg);
                } else if let Some(log_msg) = cursor.take_one() {
                    msgs.push(SynthMessage::from_run_log_message(log_msg, offset));
                }
            }
        }

        Self {
            msgs,
            raw: log_msgs.to_vec(),
            console: None,
        }
    }

    pub fn set_console_log(&mut self, console_log: impl Into<Vec<u8>>) {
        self.console = Some(console_log.into());
    }

    pub fn to_html(&self) -> HtmlPage {
        let title = "Ambient run log";
        let mut page = HtmlPage::default()
            .with_head_element(Element::new(Tag::Meta).with_attribute("charset", "utf-8"))
            .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.to_html_messages());
        page
    }

    pub fn to_html_messages(&self) -> Element {
        let mut e = Element::new(Tag::Div);
        let mut prev_source = None;
        let mut first_failure = true;
        for (i, msg) in self.msgs.iter().enumerate() {
            if Some(msg.source) != prev_source {
                let h = match msg.source {
                    RunLogSource::Prelude => "Prelude, before pre-plan starts",
                    RunLogSource::PrePlan => "Pre-plan, before VM starts",
                    RunLogSource::Plan => "Plan, inside VM without network",
                    RunLogSource::PostPlan => "Post-plan, after VM stops",
                    RunLogSource::Epilog => "Epilog, aft3er post-plan is done",
                };
                e.push_child(Element::new(Tag::H2).with_text(h));
                prev_source = Some(msg.source);
            }
            let child: Element = msg.into();
            let id = if !msg.detail.is_successful() && first_failure {
                first_failure = false;
                "failed".to_string()
            } else {
                format!("msg{i}")
            };
            // child.set_attribute("id", &id);
            e.push_child(
                Element::new(Tag::Div).with_attribute("id", &id).with_child(
                    Element::new(Tag::A)
                        .with_attribute("href", format!("#{id}"))
                        .with_class("anchor")
                        .with_child(child),
                ),
            );
        }

        let mut raw = Element::new(Tag::Ol);
        for m in self.raw.iter() {
            raw.push_child(
                Element::new(Tag::Li).with_child(
                    Element::new(Tag::Pre)
                        .with_text(serde_json::to_string_pretty(m).unwrap_or(format!("{m:#?}"))),
                ),
            );
        }

        e.push_child(
            Element::new(Tag::H2)
                .with_attribute("id", "json-log")
                .with_child(
                    Element::new(Tag::A)
                        .with_attribute("href", "#json-log")
                        .with_text("Raw log messages for Ambient troubleshooting"),
                ),
        );
        e.push_child(
            Element::new(Tag::Details)
                .with_child(Element::new(Tag::Summary).with_text("Raw log messages"))
                .with_child(Element::new(Tag::P).with_text(
            "These raw log messages are meant to help Ambient developers figure out problems. You can ignore them.",
        ))
                .with_child(Element::new(Tag::Div).with_child(raw)),
        );

        if let Some(console_log) = &self.console {
            e.push_child(
                Element::new(Tag::H2)
                    .with_attribute("id", "console-log")
                    .with_child(
                        Element::new(Tag::A)
                            .with_attribute("href", "#console-log")
                            .with_text("Raw console log from virtual machine, for troubleshoting"),
                    ),
            );
            e.push_child(
            Element::new(Tag::Details)
                .with_child(Element::new(Tag::Summary).with_text("Raw log messages"))
                .with_child(Element::new(Tag::P).with_text(
            "This raw console log from the Ambient virtual machine is meant to help you and Ambient developers figure out problems. You can ignore this if everything works.",
        ))
                .with_child(Element::new(Tag::Pre).with_text(String::from_utf8_lossy(console_log).to_string())),
        );
        }

        e
    }
}

#[derive(Debug, Serialize)]
struct SynthMessage {
    #[serde(flatten)]
    detail: SynthDetail,
    timestamp: SystemTime,
    offset: Duration,
    source: RunLogSource,
}

impl SynthMessage {
    fn from_run_log_message(log_msg: &RunLogMessage, offset: Duration) -> Self {
        if let Ok(detail) = SynthDetail::try_from(&log_msg.detail) {
            Self {
                detail,
                timestamp: log_msg.timestamp,
                offset,
                source: log_msg.log_source,
            }
        } else {
            Self {
                detail: SynthDetail::Other(log_msg.clone()),
                timestamp: log_msg.timestamp,
                offset,
                source: log_msg.log_source,
            }
        }
    }
}

#[derive(Debug, Serialize)]
#[allow(clippy::large_enum_variant)]
enum SynthDetail {
    AmbientStarts {
        name: String,
        version: String,
    },
    AmbientEndsSuccssfully,
    AmbientEndsInFailure,
    AmbientRuntimeConfig(Config),
    StartQemu {
        argv: Vec<OsString>,
    },
    QemuSucceeded {
        exit_code: i32,
        stdout: String,
        stderr: String,
    },
    QemuFailed {
        exit_code: Option<i32>,
        stdout: String,
        stderr: String,
    },
    ExecutorStarts {
        name: String,
        version: String,
    },
    RunnablePlan(RunnablePlan),
    SuccessfulAction {
        action: RunnableAction,
        log_messages: Vec<SynthMessage>,
    },
    ExecuteAction(RunnableAction),
    ActionSucceeded(RunnableAction),
    ActionFailed(RunnableAction),
    DebGet {
        packages: Vec<Needed>,
    },
    NpmGetSucceded,
    NpmGetFailed2(String),
    SuccessfulCustomAction {
        custom: Custom,
        log_messages: Vec<SynthMessage>,
    },
    CustomActionStarts {
        source: PathBuf,
        custom: Custom,
        exe: PathBuf,
        exe_exists: bool,
    },
    CustomActionOutput {
        stdout: Vec<u8>,
        stderr: Vec<u8>,
    },
    StartProgram {
        argv: Vec<OsString>,
    },
    ProgramSucceeded {
        exit_code: i32,
        stdout: String,
        stderr: String,
    },
    ProgramFailed {
        exit_code: Option<i32>,
        stdout: String,
        stderr: String,
    },
    PlanSucceeded,
    Other(RunLogMessage),
}

impl SynthDetail {
    fn is_successful(&self) -> bool {
        !matches!(
            self,
            Self::AmbientEndsInFailure
                | Self::QemuFailed { .. }
                | Self::ActionFailed(_)
                | Self::ProgramFailed { .. }
        )
    }
}

impl TryFrom<&RunLogMessageDetail> for SynthDetail {
    type Error = ();
    fn try_from(log_detail: &RunLogMessageDetail) -> Result<Self, Self::Error> {
        let detail = match log_detail {
            RunLogMessageDetail::AmbientStarts { name, version } => SynthDetail::AmbientStarts {
                name: name.to_string(),
                version: version.to_string(),
            },
            RunLogMessageDetail::AmbientRuntimeConfig(config) => {
                SynthDetail::AmbientRuntimeConfig(config.clone())
            }
            RunLogMessageDetail::AmbientEndsSuccssfully => SynthDetail::AmbientEndsSuccssfully,
            RunLogMessageDetail::AmbientEndsInFailure => SynthDetail::AmbientEndsInFailure,
            RunLogMessageDetail::StartQemu { argv } => SynthDetail::StartQemu {
                argv: argv.to_vec(),
            },
            RunLogMessageDetail::QemuSucceeded {
                exit_code,
                stdout,
                stderr,
            } => SynthDetail::QemuSucceeded {
                exit_code: *exit_code,
                stdout: stdout.to_string(),
                stderr: stderr.to_string(),
            },
            RunLogMessageDetail::ExecutorStarts { name, version } => SynthDetail::ExecutorStarts {
                name: name.to_string(),
                version: version.to_string(),
            },
            RunLogMessageDetail::RunnablePlan(plan) => SynthDetail::RunnablePlan(plan.clone()),
            RunLogMessageDetail::PlanSucceeded => SynthDetail::PlanSucceeded,
            RunLogMessageDetail::ExecuteAction(action) => {
                SynthDetail::ExecuteAction(action.clone())
            }
            RunLogMessageDetail::ActionSucceeded(action) => {
                SynthDetail::ActionSucceeded(action.clone())
            }
            RunLogMessageDetail::ActionFailed(action) => SynthDetail::ActionFailed(action.clone()),
            RunLogMessageDetail::StartProgram { argv } => SynthDetail::StartProgram {
                argv: argv.to_vec(),
            },
            RunLogMessageDetail::ProgramSucceeded {
                exit_code,
                stdout,
                stderr,
            } => SynthDetail::ProgramSucceeded {
                exit_code: *exit_code,
                stdout: stdout.to_string(),
                stderr: stderr.to_string(),
            },
            RunLogMessageDetail::ProgramFailed {
                exit_code,
                stdout,
                stderr,
            } => SynthDetail::ProgramFailed {
                exit_code: *exit_code,
                stdout: stdout.to_string(),
                stderr: stderr.to_string(),
            },
            RunLogMessageDetail::DebGet { packages } => SynthDetail::DebGet {
                packages: packages.clone(),
            },
            RunLogMessageDetail::NpmGetSucceded => SynthDetail::NpmGetSucceded,
            RunLogMessageDetail::NpmGetFailed1 { error } => {
                SynthDetail::NpmGetFailed2(error.to_string())
            }
            RunLogMessageDetail::CustomActionStarts {
                source,
                custom,
                exe,
                exe_exists,
            } => SynthDetail::CustomActionStarts {
                source: source.into(),
                custom: custom.clone(),
                exe: exe.to_path_buf(),
                exe_exists: *exe_exists,
            },
            RunLogMessageDetail::CustomActionOutput { stdout, stderr } => {
                SynthDetail::CustomActionOutput {
                    stdout: stdout.clone(),
                    stderr: stderr.clone(),
                }
            }
            RunLogMessageDetail::RunCi { .. }
            | RunLogMessageDetail::SkipCi { .. }
            | RunLogMessageDetail::Debug { .. }
            | RunLogMessageDetail::QemuFailed { .. }
            | RunLogMessageDetail::ExecutorEndsSuccessfully
            | &RunLogMessageDetail::ExecutorEndsInFailure { .. } => return Err(()),
        };
        Ok(detail)
    }
}

impl From<&SynthMessage> for Element {
    fn from(msg: &SynthMessage) -> Self {
        fn stream(label: &'static str, text: &str) -> Element {
            Element::new(Tag::Div)
                .with_text(label)
                .with_child(Element::new(Tag::Pre).with_text(text))
        }

        fn program_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> Element {
            let mut e = Element::new(Tag::Div).with_text(format!(
                "Exit code: {}",
                exit_code
                    .map(|code| code.to_string())
                    .unwrap_or("Killed by signal".to_string())
            ));

            if !stdout.is_empty() {
                e.push_child(stream("Stdout:", stdout));
            }
            if !stderr.is_empty() {
                e.push_child(stream("Stderr:", stderr));
            }

            e
        }

        let mut e = Element::new(Tag::Details).with_class(msg.source.class());

        let mut summary = Element::new(Tag::Summary)
            .with_text(msg.source.class())
            .with_text(": ");

        let mut more = Element::new(Tag::Div)
            .with_class("more")
            .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", msg.offset.as_secs_f64())),
                    )
                    .with_text(" "),
            )
            .with_child(
                Element::new(Tag::Span)
                    .with_class("timestamp")
                    .with_text(" at ")
                    .with_child(
                        Element::new(Tag::Span)
                            .with_class("exact-timestamp")
                            .with_text(
                                format_timestamp(msg.timestamp)
                                    .unwrap_or("broken timestamp".to_string()),
                            ),
                    ),
            );

        match &msg.detail {
            SynthDetail::SuccessfulAction {
                action,
                log_messages,
            } => {
                summary.push_text("Successful action ");
                summary.push_child(
                    Element::new(Tag::Span)
                        .with_class("action-name")
                        .with_text(action.summary()),
                );
                let mut details = Element::new(Tag::Ul);
                for underlying in log_messages {
                    details.push_child(Element::new(Tag::Li).with_child(underlying.into()));
                }
                more.push_child(
                    Element::new(Tag::Div)
                        .with_class("action-details")
                        .with_child(details),
                );
            }
            SynthDetail::DebGet { packages } => {
                summary.push_text("Download deb packages");
                let mut list = Element::p();
                for needed in packages.iter() {
                    list.push_child(Element::new(Tag::Li).with_text(needed.package_name()));
                }
                more.push_child(Element::div().with_child(list));
            }
            SynthDetail::NpmGetSucceded => {
                summary.push_text("Download of npm packages succeeded");
            }
            SynthDetail::NpmGetFailed2(msg) => {
                summary.push_text("Download of npm packages failed");
                more.push_child(Element::new(Tag::Pre).with_text(msg));
            }
            SynthDetail::SuccessfulCustomAction {
                custom,
                log_messages,
            } => {
                summary.push_text("Successful action ");
                summary.push_child(
                    Element::new(Tag::Span)
                        .with_class("action-name")
                        .with_text("custom: ")
                        .with_text(&custom.name),
                );
                let mut details = Element::new(Tag::Ul);
                for underlying in log_messages {
                    details.push_child(Element::new(Tag::Li).with_child(underlying.into()));
                }
                more.push_child(
                    Element::new(Tag::Div)
                        .with_class("action-details")
                        .with_child(details),
                );
            }
            SynthDetail::AmbientStarts { name, version } => {
                summary.push_text("Ambient starts");
                more.push_child(
                    Element::new(Tag::P).with_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::P).with_child(
                        Element::new(Tag::Span).with_text("Version: ").with_child(
                            Element::new(Tag::Span)
                                .with_class("program-version")
                                .with_text(version),
                        ),
                    ),
                );
            }
            SynthDetail::AmbientEndsSuccssfully => {
                summary.push_text("Ambient ends, success");
                more.push_text("Everything is fine.");
            }
            SynthDetail::AmbientEndsInFailure => {
                summary.push_text("Ambient ends, failure");
                more.push_text("Woe be us!");
            }
            SynthDetail::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),
                );
            }
            SynthDetail::StartQemu { argv } => {
                summary.push_text("Start QEMU");

                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);
            }
            SynthDetail::QemuSucceeded {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("QEMU succeeded");
                more.push_child(program_result(Some(*exit_code), stdout, stderr));
            }
            SynthDetail::QemuFailed {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("QEMU failed");
                more.push_child(program_result(*exit_code, stdout, stderr));
            }
            SynthDetail::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),
                    ),
                );
            }
            SynthDetail::RunnablePlan(plan) => {
                summary.push_text("Runnable plan");

                let yaml =
                    serde_norway::to_string(plan).unwrap_or("plan to YAML failed".to_string());
                more.push_child(
                    Element::new(Tag::Pre)
                        .with_class("runnable-plan")
                        .with_text(&yaml),
                );
            }
            SynthDetail::PlanSucceeded => {
                summary.push_text("Plan succeeded");
                more.push_text("Hopefully all is good.");
            }
            SynthDetail::ExecuteAction(action) => {
                summary.push_text("Start action ");
                summary.push_text(action.summary());
                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
            }
            SynthDetail::ActionSucceeded(action) => {
                summary.push_text("Action succeeded ");
                summary.push_text(action.summary());
                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
            }
            SynthDetail::ActionFailed(action) => {
                summary.push_text("Action failed: ");
                summary.push_text(action.summary());
                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", action)));
            }
            SynthDetail::CustomActionStarts { custom, .. } => {
                summary.push_text("Start action custom: ");
                summary.push_text(&custom.name);
                more.push_child(Element::new(Tag::Pre).with_text(format!("{:#?}", custom)));
            }
            SynthDetail::CustomActionOutput { stdout, stderr } => {
                summary.push_text("Custom action output");
                more.push_child(stream("Stdout:", &String::from_utf8_lossy(stdout)));
                more.push_child(stream("Stderr:", &String::from_utf8_lossy(stderr)));
            }
            SynthDetail::StartProgram { argv } => {
                summary.push_text("Start program ");
                summary.push_text(
                    argv.first()
                        .map(|s| String::from_utf8_lossy(s.as_encoded_bytes()).to_string())
                        .unwrap_or("unrepresentable string".to_string()),
                );

                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);
            }
            SynthDetail::ProgramSucceeded {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("Program succeeded");
                more.push_child(program_result(Some(*exit_code), stdout, stderr));
            }
            SynthDetail::ProgramFailed {
                exit_code,
                stdout,
                stderr,
            } => {
                summary.push_text("ERROR: Program failed");
                more.push_child(program_result(*exit_code, stdout, stderr));
            }
            SynthDetail::Other(log_msg) => {
                summary.push_text(format!("{log_msg:?}"));
            }
        }
        if msg.detail.is_successful() {
            e.add_class("succeeded");
        } else {
            e.add_class("failed");
        }
        e.push_child(summary);
        e.push_child(more);
        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),
}
