//! Run logs for Ambient.

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

use std::{
    ffi::{OsStr, OsString},
    fs::OpenOptions,
    path::{Path, PathBuf},
    process::{Command, Output},
    time::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,
    project::{LogKind, State},
    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),
    ActionSkipped {
        summary: String,
        reason: String,
    },
    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 { .. }
                | Self::ExecutorEndsInFailure { .. }
        )
    }
}

/// 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}""#)
            }
        }
    }

    pub fn to_html(&self, prefix: &str) -> Element {
        fn yaml<Value>(value: &Value) -> String
        where
            Value: Serialize + std::fmt::Debug,
        {
            serde_norway::to_string(value).unwrap_or_else(|_| format!("{value:#?}"))
        }

        fn pre<S: AsRef<str>>(s: S) -> Element {
            Element::new(Tag::Pre).with_text(s.as_ref())
        }

        fn code<S: AsRef<str>>(s: S, class: &'static str) -> Element {
            Element::new(Tag::Code)
                .with_text(s.as_ref())
                .with_class(class)
        }

        fn span<S: Into<String>>(s: S) -> Element {
            Element::new(Tag::Span).with_text(s)
        }

        fn li(child: Element) -> Element {
            Element::new(Tag::Li).with_child(child)
        }

        fn ul(children: &[Element]) -> Element {
            let mut e = Element::new(Tag::Ul);
            e.push_children(children);
            e
        }

        fn details<S: Into<String>>(prefix: &str, summary: S, child: Element) -> Element {
            Element::new(Tag::Details)
                .with_child(
                    Element::new(Tag::Summary)
                        .with_text(prefix)
                        .with_text(summary),
                )
                .with_child(child)
        }

        fn string<O: AsRef<OsStr>>(os: O) -> String {
            String::from_utf8_lossy(os.as_ref().as_encoded_bytes()).to_string()
        }

        fn argv0(argv: &[OsString]) -> String {
            argv.first()
                .map(string)
                .unwrap_or_else(|| "unknown program".to_string())
        }

        fn argv_list(argv: &[OsString]) -> Element {
            let argv_strings: Vec<Element> =
                argv.iter().map(|os| li(code(string(os), "argv"))).collect();
            ul(&argv_strings)
        }

        fn program_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> Element {
            let exit_code = if let Some(i) = exit_code {
                i.to_string()
            } else {
                "killed by signal".to_string()
            };
            ul(&[
                li(span("Exit code: ").with_text(exit_code)),
                li(span("Stdout").with_child(pre(stdout))),
                li(span("Stderr").with_child(pre(stderr))),
            ])
        }

        let mut e = match &self.detail {
            RunLogMessageDetail::AmbientStarts { name, version } => details(
                prefix,
                "Ambient starts",
                ul(&[
                    li(span("Name: ").with_child(code(name, "ambient-name"))),
                    li(span("Version: ").with_child(code(version, "ambient-version"))),
                ]),
            ),
            RunLogMessageDetail::AmbientEndsSuccssfully => details(
                prefix,
                "Ambient ends successfully",
                span("Phew, nothing seems to have gone wrong."),
            ),
            RunLogMessageDetail::AmbientEndsInFailure => details(
                prefix,
                "Executor ends in failure",
                span("See above for why"),
            ),
            RunLogMessageDetail::AmbientRuntimeConfig(config) => {
                details(prefix, "Ambient configuration", pre(yaml(config)))
            }

            RunLogMessageDetail::RunCi { project_name } => {
                span(format!("Run CI for {project_name}"))
            }
            RunLogMessageDetail::SkipCi { project_name } => {
                span(format!("Skip CI for {project_name}"))
            }

            RunLogMessageDetail::StartQemu { argv } => details(
                prefix,
                format!("Start QEMU: {}", argv0(argv)),
                argv_list(argv),
            ),
            RunLogMessageDetail::QemuSucceeded {
                exit_code,
                stdout,
                stderr,
            } => details(
                prefix,
                "QEMU succeeded",
                program_result(Some(*exit_code), stdout, stderr),
            ),
            RunLogMessageDetail::QemuFailed {
                exit_code,
                stdout,
                stderr,
            } => details(
                prefix,
                "QEMU failed",
                program_result(*exit_code, stdout, stderr),
            ),

            RunLogMessageDetail::ExecutorStarts { name, version } => details(
                prefix,
                "Executor starts",
                ul(&[
                    li(span("Name: ").with_child(code(name, "executor-name"))),
                    li(span("Version: ").with_child(code(version, "executor-version"))),
                ]),
            ),
            RunLogMessageDetail::ExecutorEndsSuccessfully => details(
                prefix,
                "Executor ends successfully",
                span("Phew, nothing seems to have gone wrong."),
            ),
            RunLogMessageDetail::ExecutorEndsInFailure { exit_code } => details(
                prefix,
                "Executor ends in failure",
                ul(&[li(span("Exit code: ").with_text(format!("{exit_code}")))]),
            ),
            RunLogMessageDetail::RunnablePlan(plan) => {
                details(prefix, "Runnable plan", pre(yaml(&plan)))
            }

            RunLogMessageDetail::ExecuteAction(action) => details(
                prefix,
                format!("Execute action: {}", action.summary()),
                pre(yaml(&vec![action])),
            ),
            RunLogMessageDetail::ActionSkipped { summary, reason } => details(
                prefix,
                format!("Action was skipped: {}", summary),
                span(reason),
            ),
            RunLogMessageDetail::ActionSucceeded(action) => details(
                prefix,
                format!("Action succeeded: {}", action.summary()),
                pre(yaml(&vec![action])),
            ),
            RunLogMessageDetail::ActionFailed(action) => details(
                prefix,
                format!("Action failed: {}", action.summary()),
                pre(yaml(&vec![action])),
            ),

            RunLogMessageDetail::StartProgram { argv } => details(
                prefix,
                format!("Start program: {}", argv0(argv)),
                argv_list(argv),
            ),
            RunLogMessageDetail::ProgramSucceeded {
                exit_code,
                stdout,
                stderr,
            } => details(
                prefix,
                "Program succeeded",
                program_result(Some(*exit_code), stdout, stderr),
            ),
            RunLogMessageDetail::ProgramFailed {
                exit_code,
                stdout,
                stderr,
            } => details(
                prefix,
                "Program failed",
                program_result(*exit_code, stdout, stderr),
            ),

            RunLogMessageDetail::DebGet { packages } => {
                let packages: Vec<Element> = packages
                    .iter()
                    .map(|needed| li(span(needed.package_name())))
                    .collect();
                details(prefix, "Download deb packges", ul(&packages))
            }
            RunLogMessageDetail::NpmGetSucceded => span("NPM get succeeded"),
            RunLogMessageDetail::NpmGetFailed1 { error } => {
                details(prefix, "NPM get failed", code(error, "error-message"))
            }

            RunLogMessageDetail::CustomActionStarts {
                source,
                custom,
                exe,
                exe_exists,
            } => details(
                prefix,
                format!("Custom action starts: {}", custom.name),
                ul(&[
                    li(span("Source: ").with_text(source.to_string_lossy())),
                    li(pre(yaml(&vec![custom]))),
                    li(span("Executable: ").with_text(exe.to_string_lossy())),
                    li(span("Executable exists? ").with_text(exe_exists.to_string())),
                ]),
            ),
            RunLogMessageDetail::CustomActionOutput { stdout, stderr } => details(
                prefix,
                "Custom action output",
                ul(&[
                    li(span("Stdout").with_child(pre(String::from_utf8_lossy(stdout)))),
                    li(span("Stderr").with_child(pre(String::from_utf8_lossy(stderr)))),
                ]),
            ),

            RunLogMessageDetail::PlanSucceeded => {
                details(prefix, "Plan succeded", span("There was much rejoicing!"))
            }

            RunLogMessageDetail::Debug { debug } => details(prefix, "Debug", pre(yaml(debug))),
        };

        if self.detail.was_successful() {
            e.set_attribute("class", "succeeded");
        } else {
            e.set_attribute("class", "failed");
            e.set_attribute("id", "failed");
        }

        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 from in-memory raw data. This parses the output to extract
    /// JSON Lihes.
    pub fn from_raw(filename: &Path, 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(filename.to_path_buf()))
        }
    }

    /// 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 was skipped.
    pub fn action_skipped<S: Into<String>>(
        &mut self,
        source: RunLogSource,
        summary: String,
        reason: S,
    ) {
        self.write(&RunLogMessage::new(
            source,
            RunLogMessageDetail::ActionSkipped {
                summary,
                reason: reason.into(),
            },
        ));
    }

    /// 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
        }
    }
}

#[derive(Debug)]
pub struct VmRunLogFile {
    raw: Vec<u8>,
    json: RunLog,
    console: Vec<u8>,
}

impl VmRunLogFile {
    pub fn new(raw: PathBuf, console: PathBuf) -> Result<Self, RunLogError> {
        fn read(filename: &Path) -> Result<Vec<u8>, RunLogError> {
            std::fs::read(filename)
                .map_err(|err| RunLogError::ReadLogFile(filename.to_path_buf(), err))
        }

        let raw_data = read(&raw)?;
        let json = if raw_data.is_empty() {
            RunLog::default()
        } else {
            RunLog::from_raw(&raw, raw_data.clone())?
        };
        Ok(Self {
            raw: raw_data,
            json,

            console: read(&console)?,
        })
    }

    fn to_html(&self) -> Element {
        fn msg_list<'a>(
            started: SystemTime,
            msgs: impl Iterator<Item = &'a RunLogMessage>,
        ) -> Element {
            let mut list = Element::new(Tag::Ol);
            for msg in msgs {
                let offset = format!(
                    "{} ms: ",
                    msg.timestamp
                        .duration_since(started)
                        .unwrap_or_default()
                        .as_millis()
                );
                let msg = msg.to_html(&offset);
                let li = Element::new(Tag::Li).with_child(msg);
                list.push_child(li);
            }
            list
        }

        fn split_messages(msgs: &[RunLogMessage]) -> (&[RunLogMessage], &[RunLogMessage]) {
            let mut first_failed = None;
            for (i, m) in msgs.iter().enumerate() {
                if !m.detail.was_successful() {
                    first_failed = Some(i);
                    break;
                }
            }

            if let Some(i) = first_failed {
                msgs.split_at(i)
            } else {
                msgs.split_at(msgs.len())
            }
        }

        fn messages(started: SystemTime, msgs: &[RunLogMessage], title: &str) -> Element {
            assert!(!msgs.is_empty());
            let mut structured =
                Element::new(Tag::Details).with_child(Element::new(Tag::Summary).with_text(title));

            let list = msg_list(started, msgs.iter());
            structured.push_child(list);

            structured
        }

        fn raw_to_string(mut raw: &[u8]) -> String {
            let mut s = vec![];
            while !raw.is_empty() {
                if let Some(rest) = raw.strip_prefix(b"\r\n") {
                    s.push(b'\n');
                    raw = rest;
                } else {
                    s.push(raw[0]);
                    raw = &raw[1..];
                }
            }
            let s = strip_ansi_escapes::strip(&s);
            String::from_utf8_lossy(&s).to_string()
        }

        let mut e = Element::new(Tag::Div);

        if let Some(first) = self.json.msgs().first() {
            let (hidden, shown) = split_messages(self.json.msgs());
            let child = match (hidden.is_empty(), shown.is_empty()) {
                (true, true) => Element::new(Tag::P).with_text("No structured log actions"),
                (true, false) => {
                    messages(first.timestamp, shown, "Structured log: FAILURE at start")
                        .with_boolean_attribute("open")
                }
                (false, true) => messages(first.timestamp, hidden, "Structured log: all OK"),
                (false, false) => Element::new(Tag::Div)
                    .with_child(messages(
                        first.timestamp,
                        hidden,
                        "Structured log: successful actions",
                    ))
                    .with_child(
                        messages(first.timestamp, shown, "FAILURE").with_boolean_attribute("open"),
                    ),
            };
            e.push_child(child);

            if shown.is_empty() {
                e.set_attribute("class", "all_succeeded");
            } else {
                e.set_attribute("class", "at_least_one_failed");
            }

            // Add console log and raw run log.

            e.push_child(
                Element::new(Tag::Details)
                    .with_child(Element::new(Tag::Summary).with_text("Console log"))
                    .with_child(Element::new(Tag::Pre).with_text(raw_to_string(&self.console))),
            );

            e.push_child(
                Element::new(Tag::Details)
                    .with_child(Element::new(Tag::Summary).with_text("Raw run log"))
                    .with_child(Element::new(Tag::Pre).with_text(raw_to_string(&self.raw))),
            );

            e
        } else {
            Element::new(Tag::P).with_text("No log")
        }
    }
}

#[derive(Debug)]
pub struct HtmlRunLog {
    pre_files: VmRunLogFile,
    plan_files: VmRunLogFile,
    post_files: VmRunLogFile,
    title: String,
    css: String,
    prelude: Option<Vec<Element>>,
}

impl HtmlRunLog {
    pub fn new(state: &State) -> Result<Self, RunLogError> {
        Ok(Self {
            pre_files: VmRunLogFile::new(
                state.log_filename(LogKind::PrePlanRaw),
                state.log_filename(LogKind::PrePlanConsole),
            )?,
            plan_files: VmRunLogFile::new(
                state.log_filename(LogKind::PlanRaw),
                state.log_filename(LogKind::PlanConsole),
            )?,
            post_files: VmRunLogFile::new(
                state.log_filename(LogKind::PostPlanRaw),
                state.log_filename(LogKind::PostPlanConsole),
            )?,
            title: "Ambient CI run log".into(),
            css: CSS.into(),
            prelude: None,
        })
    }

    pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
        self.title = title.into();
        self
    }

    pub fn with_css<S: Into<String>>(mut self, css: S) -> Self {
        self.css = css.into();
        self
    }

    pub fn with_prelude(mut self, prelude: Vec<Element>) -> Self {
        self.prelude = Some(prelude);
        self
    }

    pub fn to_html(&self) -> HtmlPage {
        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(&self.title))
            .with_head_element(Element::new(Tag::Style).with_text(&self.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(&self.title));

        if let Some(prelude) = &self.prelude {
            for e in prelude {
                page.push_to_body(e);
            }
        }
        page.push_to_body(self.to_html_messages());
        page
    }

    pub fn to_html_messages(&self) -> Element {
        let mut e = Element::new(Tag::Div).with_class("messages");

        e.push_child(Element::new(Tag::H2).with_text("Pre-plan VM"));
        e.push_child(self.pre_files.to_html());

        e.push_child(Element::new(Tag::H2).with_text("Plan VM"));
        e.push_child(self.plan_files.to_html());

        e.push_child(Element::new(Tag::H2).with_text("Post-plan VM"));
        e.push_child(self.post_files.to_html());

        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 {0}")]
    ReadLogFile(PathBuf, #[source] std::io::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: {0}")]
    NoBegin(PathBuf),

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