use std::{
    io::Write,
    path::{Path, PathBuf},
};

use clap::{Parser, ValueEnum};

use super::{AmbientDriverError, Config, Leaf};
use ambient_ci::{
    project::{ProjectError, State},
    runlog::{RunLog, RunLogError},
};

/// Show run log.
///
/// Output can be the raw run log from the VM, a JSON Lines log with
/// everything that Ambient did durig the CI run (including outside
/// the VM) or an HTML version of that.
#[derive(Debug, Parser)]
pub struct Log {
    /// What format should log be?
    #[clap(long, default_value = "raw")]
    format: Format,

    /// Which project log should be written out?
    #[clap(conflicts_with = "filename")]
    project: Option<String>,

    /// Read run log from this file.
    #[clap(long)]
    filename: Option<PathBuf>,

    /// Write log to this file, not stdout.
    #[clap(long)]
    output: Option<PathBuf>,
}

impl Log {
    fn read(&self, filename: &Path) -> Result<Vec<u8>, LogError> {
        std::fs::read(filename).map_err(|err| LogError::Read(filename.to_path_buf(), err))
    }

    fn write(&self, data: &[u8]) -> Result<(), LogError> {
        if let Some(output) = &self.output {
            std::fs::write(output, data)
                .map_err(|err| LogError::Write(output.to_path_buf(), err))?;
        } else {
            std::io::stdout()
                .write_all(data)
                .map_err(LogError::WriteStdout)?;
        }
        Ok(())
    }
}

impl Leaf for Log {
    fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientDriverError> {
        let filename = match (&self.project, &self.filename) {
            (Some(project), None) => {
                let statedir = config.state();

                if !statedir.exists() {
                    return Err(LogError::NoStateDir(project.clone(), statedir.into()))?;
                }

                let state = State::from_file(statedir, project).map_err(LogError::Project)?;

                if self.format == Format::Raw {
                    state.raw_log_filename()
                } else {
                    state.run_log_filename()
                }
            }
            (None, Some(filename)) => filename.to_path_buf(),
            _ => panic!("programming error in CLI parsing"),
        };

        let data = self.read(&filename)?;

        match self.format {
            Format::Raw | Format::Json => {
                self.write(&data)?;
            }
            Format::Html => {
                let runlog = RunLog::parse_jsonl(data)
                    .map_err(|err| LogError::LoadJson(filename.to_path_buf(), err))?;
                self.write(runlog.to_html().to_string().as_bytes())?;
            }
        }

        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
    Raw,
    Json,
    Html,
}

#[derive(Debug, thiserror::Error)]
pub enum LogError {
    #[error("state directory for project {0} does not exist: {1}")]
    NoStateDir(String, PathBuf),

    #[error(transparent)]
    Project(#[from] ProjectError),

    #[error("failed to load Ambient JSON run log file {0}")]
    LoadJson(PathBuf, #[source] RunLogError),

    #[error("failed to read log file {0}")]
    Read(PathBuf, #[source] std::io::Error),

    #[error("failed to write output to {0}")]
    Write(PathBuf, #[source] std::io::Error),

    #[error("failed to write output to stdout")]
    WriteStdout(#[source] std::io::Error),
}
